From e49042af6cca6cd474313f8c76f4702aae37bc24 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 19 Dec 2024 16:15:42 -0500 Subject: [PATCH 1/2] feat: create new TA tasks and add TA storage this commit essentially does 3 things: - creates the new ta_processor and ta_finisher tasks - the difference between these tasks and the old ones is that these ones use upload states differently - these ones also use the TA storage module to persist data to BQ - updates the version of the test results parser being used - we've gone from parsing individual JUnit XML files to parsing the entire raw upload at once - creates the ta_storage module - the ta_storage module serves as an abstraction for persisting data to both PG and BQ --- requirements.in | 3 +- requirements.txt | 240 +++++----- rollouts/__init__.py | 2 + services/processing/flake_processing.py | 3 +- services/test_results.py | 10 + ta_storage/base.py | 6 +- ta_storage/bq.py | 6 +- ta_storage/pg.py | 23 +- ta_storage/tests/test_bq.py | 6 +- tasks/__init__.py | 2 + tasks/ta_finisher.py | 415 +++++++++++++++++ tasks/ta_processor.py | 170 +++++++ tasks/test_results_processor.py | 113 ++--- .../ta_finisher_task__analytics__0.json | 144 ++++++ ...orTask__ta_processor_task_bad_file__0.json | 5 + ...cessorTask__ta_processor_task_call__0.json | 142 ++++++ ...ocessorTask__ta_processor_task_call__1.bin | 9 + ...cessorTask__ta_processor_task_call__2.json | 51 ++ tasks/tests/unit/test_process_flakes.py | 8 +- tasks/tests/unit/test_ta_finisher_task.py | 281 +++++++++++ tasks/tests/unit/test_ta_processor_task.py | 437 ++++++++++++++++++ .../tests/unit/test_test_results_finisher.py | 13 +- .../unit/test_test_results_processor_task.py | 153 +----- tasks/tests/unit/test_upload_task.py | 109 ++++- tasks/upload.py | 69 ++- 25 files changed, 2048 insertions(+), 372 deletions(-) create mode 100644 tasks/ta_finisher.py create mode 100644 tasks/ta_processor.py create mode 100644 tasks/tests/unit/snapshots/ta_finisher_task__analytics__0.json create mode 100644 tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_bad_file__0.json create mode 100644 tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__0.json create mode 100644 tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__1.bin create mode 100644 tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__2.json create mode 100644 tasks/tests/unit/test_ta_finisher_task.py create mode 100644 tasks/tests/unit/test_ta_processor_task.py diff --git a/requirements.in b/requirements.in index 7782f3afa..c1fd6cf08 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,4 @@ -https://github.com/codecov/test-results-parser/archive/996ecb2aaf7767bf4c2944c75835c1ee1eb2b566.tar.gz#egg=test-results-parser +https://github.com/codecov/test-results-parser/archive/190bbc8a911099749928e13d5fe57f6027ca1e74.tar.gz#egg=test-results-parser https://github.com/codecov/shared/archive/609e56d2aa30b26d44cddaba0e1ebd79ba954ac9.tar.gz#egg=shared https://github.com/codecov/timestring/archive/d37ceacc5954dff3b5bd2f887936a98a668dda42.tar.gz#egg=timestring asgiref>=3.7.2 @@ -37,6 +37,7 @@ pytest-celery pytest-cov pytest-django pytest-freezegun +pytest-insta pytest-mock pytest-sqlalchemy python-dateutil diff --git a/requirements.txt b/requirements.txt index a8c9acd69..26ba4c927 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,20 @@ # This file was autogenerated by uv via the following command: # uv pip compile requirements.in -o requirements.txt -amqp==5.2.0 +amqp==5.3.1 # via kombu analytics-python==1.3.0b1 # via -r requirements.in -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==3.6.1 +anyio==4.8.0 # via - # httpcore + # httpx # openai -asgiref==3.7.2 +argon2-cffi==23.1.0 + # via minio +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +asgiref==3.8.1 # via # -r requirements.in # django @@ -20,52 +24,52 @@ billiard==4.2.1 # via # -r requirements.in # celery -boto3==1.34.73 +boto3==1.35.97 # via # -r requirements.in # shared -botocore==1.34.73 +botocore==1.35.97 # via # boto3 # s3transfer -cachetools==4.2.1 +cachetools==5.5.0 # via # google-auth # shared -celery==5.3.6 +celery==5.4.0 # via # -r requirements.in # pytest-celery # sentry-sdk -cerberus==1.3.5 +cerberus==1.3.7 # via shared -certifi==2024.7.4 +certifi==2024.12.14 # via # httpcore # httpx # minio # requests # sentry-sdk -cffi==1.14.5 +cffi==1.17.1 # via + # argon2-cffi-bindings # cryptography - # google-crc32c cfgv==3.4.0 # via pre-commit -charset-normalizer==2.0.12 +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # -r requirements.in # celery # click-didyoumean # click-plugins # click-repl -click-didyoumean==0.3.0 +click-didyoumean==0.3.1 # via celery click-plugins==1.1.1 # via celery -click-repl==0.2.0 +click-repl==0.3.0 # via celery codecov-ribs==0.1.18 # via @@ -73,21 +77,23 @@ codecov-ribs==0.1.18 # shared colour==0.1.5 # via shared -coverage==7.5.0 +coverage==7.6.10 # via # -r requirements.in # pytest-cov -cryptography==43.0.1 +cryptography==44.0.0 # via shared +debugpy==1.8.11 + # via pytest-celery deprecated==1.2.15 # via # opentelemetry-api # opentelemetry-semantic-conventions -distlib==0.3.7 +distlib==0.3.9 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via openai -django==4.2.16 +django==4.2.17 # via # -r requirements.in # django-model-utils @@ -103,22 +109,26 @@ django-postgres-extra==2.0.8 # shared django-prometheus==2.3.1 # via shared -factory-boy==3.2.0 +docker==7.1.0 + # via + # pytest-celery + # pytest-docker-tools +factory-boy==3.3.1 # via -r requirements.in -faker==8.8.2 +faker==33.3.1 # via factory-boy -filelock==3.12.4 +filelock==3.16.1 # via virtualenv -freezegun==1.5.0 +freezegun==1.5.1 # via pytest-freezegun -google-api-core==2.23.0 +google-api-core==2.24.0 # via # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-core # google-cloud-pubsub # google-cloud-storage -google-auth==2.36.0 +google-auth==2.37.0 # via # google-api-core # google-cloud-bigquery @@ -135,15 +145,15 @@ google-cloud-core==2.4.1 # via # google-cloud-bigquery # google-cloud-storage -google-cloud-pubsub==2.27.1 +google-cloud-pubsub==2.27.2 # via # -r requirements.in # shared -google-cloud-storage==2.18.2 +google-cloud-storage==2.19.0 # via # -r requirements.in # shared -google-crc32c==1.1.2 +google-crc32c==1.6.0 # via # google-cloud-storage # google-resumable-media @@ -158,7 +168,7 @@ googleapis-common-protos==1.66.0 # grpcio-status grpc-google-iam-v1==0.14.0 # via google-cloud-pubsub -grpcio==1.68.1 +grpcio==1.69.0 # via # -r requirements.in # google-api-core @@ -166,63 +176,65 @@ grpcio==1.68.1 # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.58.0 +grpcio-status==1.69.0 # via # google-api-core # google-cloud-pubsub h11==0.14.0 # via httpcore -httpcore==0.16.3 +httpcore==1.0.7 # via httpx -httpx==0.23.1 +httpx==0.28.1 # via # -r requirements.in # openai # respx # shared -identify==2.5.30 +identify==2.6.5 # via pre-commit -idna==3.7 +idna==3.10 # via # anyio + # httpx # requests - # rfc3986 # yarl -ijson==3.2.3 +ijson==3.3.0 # via shared importlib-metadata==8.5.0 # via opentelemetry-api -iniconfig==1.1.1 +iniconfig==2.0.0 # via pytest -jinja2==3.1.4 +jinja2==3.1.5 # via -r requirements.in -jmespath==0.10.0 +jiter==0.8.2 + # via openai +jmespath==1.0.1 # via # boto3 # botocore -kombu==5.3.5 +kombu==5.4.2 # via celery lxml==5.3.0 # via -r requirements.in -markupsafe==2.1.3 +markupsafe==3.0.2 # via jinja2 -minio==7.1.13 +minio==7.2.14 # via shared -mmh3==4.0.1 +mmh3==5.0.1 # via shared -mock==4.0.3 +mock==5.1.0 # via -r requirements.in -monotonic==1.5 +monotonic==1.6 # via analytics-python multidict==6.1.0 # via # -r requirements.in # yarl -nodeenv==1.8.0 +nodeenv==1.9.1 # via pre-commit -oauthlib==3.1.0 +oauthlib==3.2.2 # via shared -openai==1.2.4 +openai==1.59.6 # via -r requirements.in opentelemetry-api==1.29.0 # via @@ -233,35 +245,37 @@ opentelemetry-sdk==1.29.0 # via google-cloud-pubsub opentelemetry-semantic-conventions==0.50b0 # via opentelemetry-sdk -orjson==3.10.11 +orjson==3.10.14 # via # -r requirements.in # shared -packaging==24.1 +packaging==24.2 # via # google-cloud-bigquery # pytest -platformdirs==3.11.0 +platformdirs==4.3.6 # via virtualenv pluggy==1.5.0 # via pytest polars==1.12.0 # via -r requirements.in -pre-commit==3.4.0 +pre-commit==4.0.1 # via -r requirements.in -prometheus-client==0.17.1 +prometheus-client==0.21.1 # via # django-prometheus # shared -prompt-toolkit==3.0.28 +prompt-toolkit==3.0.48 # via click-repl +propcache==0.2.1 + # via yarl proto-plus==1.25.0 # via # -r requirements.in # google-api-core # google-cloud-bigquery-storage # google-cloud-pubsub -protobuf==5.29.2 +protobuf==5.29.3 # via # -r requirements.in # google-api-core @@ -271,49 +285,59 @@ protobuf==5.29.2 # grpc-google-iam-v1 # grpcio-status # proto-plus +psutil==6.1.1 + # via pytest-celery psycopg2==2.9.10 # via -r requirements.in -pyasn1==0.4.8 +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.2.8 +pyasn1-modules==0.4.1 # via google-auth -pycparser==2.20 +pycparser==2.22 # via cffi -pydantic==2.10.4 +pycryptodome==3.21.0 + # via minio +pydantic==2.10.5 # via # -r requirements.in # openai # shared pydantic-core==2.27.2 # via pydantic -pyjwt==2.10.0 +pyjwt==2.10.1 # via # -r requirements.in # shared -pyparsing==2.4.7 +pyparsing==3.2.1 # via shared -pytest==8.1.1 +pytest==8.3.4 # via # -r requirements.in # pytest-asyncio # pytest-cov # pytest-django + # pytest-docker-tools # pytest-freezegun + # pytest-insta # pytest-mock # pytest-sqlalchemy -pytest-asyncio==0.14.0 +pytest-asyncio==0.25.2 # via -r requirements.in -pytest-celery==0.0.0 +pytest-celery==1.1.3 # via -r requirements.in -pytest-cov==5.0.0 +pytest-cov==6.0.0 # via -r requirements.in -pytest-django==4.7.0 +pytest-django==4.9.0 # via -r requirements.in +pytest-docker-tools==3.1.3 + # via pytest-celery pytest-freezegun==0.4.2 # via -r requirements.in -pytest-mock==1.13.0 +pytest-insta==0.3.0 + # via -r requirements.in +pytest-mock==3.14.0 # via -r requirements.in pytest-sqlalchemy==0.2.1 # via -r requirements.in @@ -328,122 +352,124 @@ python-dateutil==2.9.0.post0 # freezegun # google-cloud-bigquery # time-machine -python-json-logger==0.1.11 +python-json-logger==3.2.1 # via -r requirements.in python-redis-lock==4.0.0 # via # -r requirements.in # shared -pytz==2022.1 +pytz==2024.2 # via timestring -pyyaml==6.0.1 +pyyaml==6.0.2 # via # -r requirements.in # pre-commit # shared # vcrpy -redis==4.5.4 +redis==5.2.1 # via # -r requirements.in # python-redis-lock # shared -regex==2023.12.25 +regex==2024.11.6 # via -r requirements.in requests==2.32.3 # via # -r requirements.in # analytics-python + # docker # google-api-core # google-cloud-bigquery # google-cloud-storage # shared # stripe -respx==0.20.2 +respx==0.22.0 # via -r requirements.in -rfc3986==1.4.0 - # via httpx -rsa==4.7.2 +rsa==4.9 # via google-auth -s3transfer==0.10.1 +s3transfer==0.10.4 # via boto3 -sentry-sdk==2.13.0 +sentry-sdk==2.19.2 # via # -r requirements.in # shared -setuptools==75.7.0 - # via nodeenv +setuptools==75.8.0 + # via pytest-celery shared @ https://github.com/codecov/shared/archive/609e56d2aa30b26d44cddaba0e1ebd79ba954ac9.tar.gz#egg=shared # via -r requirements.in -six==1.16.0 +six==1.17.0 # via # analytics-python - # click-repl # python-dateutil - # sqlalchemy-utils - # vcrpy -sniffio==1.2.0 +sniffio==1.3.1 # via # anyio - # httpcore - # httpx -sqlalchemy==1.3.23 + # openai +sqlalchemy==1.4.54 # via # -r requirements.in # pytest-sqlalchemy # shared # sqlalchemy-utils -sqlalchemy-utils==0.36.8 +sqlalchemy-utils==0.41.2 # via # -r requirements.in # pytest-sqlalchemy -sqlparse==0.5.0 +sqlparse==0.5.3 # via django -statsd==3.3.0 +statsd==4.0.1 # via -r requirements.in stripe==11.4.1 # via -r requirements.in -test-results-parser @ https://github.com/codecov/test-results-parser/archive/996ecb2aaf7767bf4c2944c75835c1ee1eb2b566.tar.gz#egg=test-results-parser +tenacity==9.0.0 + # via pytest-celery +test-results-parser @ https://github.com/codecov/test-results-parser/archive/190bbc8a911099749928e13d5fe57f6027ca1e74.tar.gz#egg=test-results-parser # via -r requirements.in -text-unidecode==1.3 - # via faker -time-machine==2.14.1 +time-machine==2.16.0 # via -r requirements.in timestring @ https://github.com/codecov/timestring/archive/d37ceacc5954dff3b5bd2f887936a98a668dda42.tar.gz#egg=timestring # via -r requirements.in -tqdm==4.66.1 +tqdm==4.67.1 # via openai typing-extensions==4.12.2 # via + # faker + # minio # openai # opentelemetry-sdk # pydantic # pydantic-core # stripe -tzdata==2024.1 - # via celery -urllib3==1.26.19 +tzdata==2024.2 + # via + # celery + # kombu +urllib3==2.3.0 # via # -r requirements.in # botocore + # docker # minio # requests # sentry-sdk -vcrpy==4.1.1 + # vcrpy +vcrpy==7.0.0 # via -r requirements.in vine==5.1.0 # via # amqp # celery # kombu -virtualenv==20.24.5 +virtualenv==20.28.1 # via pre-commit -wcwidth==0.2.5 +wcwidth==0.2.13 # via prompt-toolkit -wrapt==1.16.0 +wrapt==1.17.0 # via # deprecated + # pytest-insta # vcrpy -yarl==1.9.4 +yarl==1.18.3 # via vcrpy zipp==3.21.0 # via importlib-metadata diff --git a/rollouts/__init__.py b/rollouts/__init__.py index 574b77056..a8f9af829 100644 --- a/rollouts/__init__.py +++ b/rollouts/__init__.py @@ -12,3 +12,5 @@ SHOW_IMPACT_ANALYSIS_DEPRECATION_MSG = Feature( "show_impact_analysis_deprecation_message" ) + +NEW_TA_TASKS = Feature("new_ta_tasks") diff --git a/services/processing/flake_processing.py b/services/processing/flake_processing.py index 88765b8ca..7d231b744 100644 --- a/services/processing/flake_processing.py +++ b/services/processing/flake_processing.py @@ -22,8 +22,9 @@ def process_flake_for_repo_commit( ): uploads = ReportSession.objects.filter( report__report_type=CommitReport.ReportType.TEST_RESULTS.value, + report__commit__repository__repoid=repo_id, report__commit__commitid=commit_id, - state="processed", + state__in=["processed", "v2_finished"], ).all() curr_flakes = fetch_curr_flakes(repo_id) diff --git a/services/test_results.py b/services/test_results.py index 18ee6f4ff..e9b67abe3 100644 --- a/services/test_results.py +++ b/services/test_results.py @@ -13,6 +13,7 @@ from database.models import ( Commit, CommitReport, + Flake, Repository, RepositoryFlag, TestInstance, @@ -410,3 +411,12 @@ def should_do_flaky_detection(repo: Repository, commit_yaml: UserYaml) -> bool: ) has_valid_plan_repo_or_owner = not_private_and_free_or_team(repo) return has_flaky_configured and (feature_enabled or has_valid_plan_repo_or_owner) + + +def get_flake_set(db_session: Session, repoid: int) -> set[str]: + repo_flakes: list[Flake] = ( + db_session.query(Flake.testid) + .filter(Flake.repoid == repoid, Flake.end_date.is_(None)) + .all() + ) + return {flake.testid for flake in repo_flakes} diff --git a/ta_storage/base.py b/ta_storage/base.py index f8b3a4629..ddeb5558b 100644 --- a/ta_storage/base.py +++ b/ta_storage/base.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from abc import ABC, abstractmethod -from test_results_parser import Testrun +import test_results_parser from database.models.reports import Upload @@ -15,6 +17,6 @@ def write_testruns( branch_name: str, upload: Upload, framework: str | None, - testruns: list[Testrun], + testruns: list[test_results_parser.Testrun], ): pass diff --git a/ta_storage/bq.py b/ta_storage/bq.py index 6617e31f4..609256f70 100644 --- a/ta_storage/bq.py +++ b/ta_storage/bq.py @@ -1,8 +1,10 @@ +from __future__ import annotations + from datetime import datetime from typing import Literal, TypedDict, cast +import test_results_parser from shared.config import get_config -from test_results_parser import Testrun import generated_proto.testrun.ta_testrun_pb2 as ta_testrun_pb2 from database.models.reports import Upload @@ -52,7 +54,7 @@ def write_testruns( branch_name: str, upload: Upload, framework: str | None, - testruns: list[Testrun], + testruns: list[test_results_parser.Testrun], ): bq_service = get_bigquery_service() diff --git a/ta_storage/pg.py b/ta_storage/pg.py index becf4fd7b..2a6634a7a 100644 --- a/ta_storage/pg.py +++ b/ta_storage/pg.py @@ -1,9 +1,11 @@ +from __future__ import annotations + from datetime import date, datetime from typing import Any, Literal, TypedDict +import test_results_parser from sqlalchemy.dialects.postgresql import insert from sqlalchemy.orm import Session -from test_results_parser import Testrun from database.models import ( DailyTestRollup, @@ -51,7 +53,7 @@ def modify_structures( test_instances_to_write: list[dict[str, Any]], test_flag_bridge_data: list[dict], daily_totals: dict[str, DailyTotals], - testrun: Testrun, + testrun: test_results_parser.Testrun, upload: Upload, repoid: int, branch: str | None, @@ -104,7 +106,7 @@ def modify_structures( def generate_test_dict( test_id: str, repoid: int, - testrun: Testrun, + testrun: test_results_parser.Testrun, flags_hash: str, framework: str | None, ) -> dict[str, Any]: @@ -123,7 +125,7 @@ def generate_test_dict( def generate_test_instance_dict( test_id: str, upload: Upload, - testrun: Testrun, + testrun: test_results_parser.Testrun, commit_sha: str, branch: str | None, repoid: int, @@ -142,13 +144,11 @@ def generate_test_instance_dict( def update_daily_totals( - daily_totals: dict, + daily_totals: dict[str, DailyTotals], test_id: str, duration_seconds: float | None, outcome: Literal["pass", "failure", "error", "skip"], ): - daily_totals[test_id]["last_duration_seconds"] = duration_seconds - # logic below is a little complicated but we're basically doing: # (old_avg * num of values used to compute old avg) + new value @@ -192,8 +192,8 @@ def create_daily_totals( daily_totals[test_id] = { "test_id": test_id, "repoid": repoid, - "last_duration_seconds": duration_seconds, - "avg_duration_seconds": duration_seconds, + "last_duration_seconds": duration_seconds or 0.0, + "avg_duration_seconds": duration_seconds or 0.0, "pass_count": 1 if outcome == "pass" else 0, "fail_count": 1 if outcome == "failure" or outcome == "error" else 0, "skip_count": 1 if outcome == "skip" else 0, @@ -290,7 +290,7 @@ def write_testruns( branch_name: str, upload: Upload, framework: str | None, - testruns: list[Testrun], + testruns: list[test_results_parser.Testrun], ): tests_to_write: dict[str, dict[str, Any]] = {} test_instances_to_write: list[dict[str, Any]] = [] @@ -326,6 +326,3 @@ def write_testruns( if len(test_instances_to_write) > 0: save_test_instances(self.db_session, test_instances_to_write) - - upload.state = "v2_persisted" - self.db_session.commit() diff --git a/ta_storage/tests/test_bq.py b/ta_storage/tests/test_bq.py index 43f20e83a..d78840c25 100644 --- a/ta_storage/tests/test_bq.py +++ b/ta_storage/tests/test_bq.py @@ -1,8 +1,10 @@ +from __future__ import annotations + from datetime import datetime from unittest.mock import MagicMock, patch import pytest -from test_results_parser import Testrun +import test_results_parser import generated_proto.testrun.ta_testrun_pb2 as ta_testrun_pb2 from database.tests.factories import RepositoryFlagFactory, UploadFactory @@ -38,7 +40,7 @@ def test_bigquery_driver(dbsession, mock_bigquery_service): upload.flags.append(repo_flag_2) dbsession.flush() - test_data: list[Testrun] = [ + test_data: list[test_results_parser.Testrun] = [ { "name": "test_name", "classname": "test_class", diff --git a/tasks/__init__.py b/tasks/__init__.py index 7c9c39823..e9315bd32 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -49,6 +49,8 @@ from tasks.sync_repo_languages_gql import sync_repo_languages_gql_task from tasks.sync_repos import sync_repos_task from tasks.sync_teams import sync_teams_task +from tasks.ta_finisher import ta_finisher_task +from tasks.ta_processor import ta_processor_task from tasks.test_results_finisher import test_results_finisher_task from tasks.test_results_processor import test_results_processor_task from tasks.timeseries_backfill import ( diff --git a/tasks/ta_finisher.py b/tasks/ta_finisher.py new file mode 100644 index 000000000..40f31802f --- /dev/null +++ b/tasks/ta_finisher.py @@ -0,0 +1,415 @@ +import logging +from dataclasses import dataclass +from typing import Any + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.reports.types import UploadType +from shared.typings.torngit import AdditionalData +from shared.yaml import UserYaml +from sqlalchemy.orm import Session + +from app import celery_app +from database.enums import FlakeSymptomType, ReportType, TestResultsProcessingError +from database.models import ( + Commit, + CommitReport, + Flake, + Repository, + TestResultReportTotals, + Upload, +) +from helpers.checkpoint_logger.flows import TestResultsFlow +from helpers.notifier import NotifierResult +from helpers.string import EscapeEnum, Replacement, StringEscaper, shorten_file_paths +from services.activation import activate_user +from services.lock_manager import LockManager, LockRetry, LockType +from services.repository import ( + EnrichedPull, + TorngitBaseAdapter, + fetch_and_update_pull_request_information_from_commit, + get_repo_provider_service, +) +from services.seats import ShouldActivateSeat, determine_seat_activation +from services.test_results import ( + FlakeInfo, + TestResultsNotificationFailure, + TestResultsNotificationPayload, + TestResultsNotifier, + get_test_summary_for_commit, + latest_failures_for_commit, + should_do_flaky_detection, +) +from tasks.base import BaseCodecovTask +from tasks.cache_test_rollups import cache_test_rollups_task +from tasks.notify import notify_task +from tasks.process_flakes import process_flakes_task + +log = logging.getLogger(__name__) + +ta_finisher_task_name = "app.tasks.test_results.TAFinisher" + +ESCAPE_FAILURE_MESSAGE_DEFN = [ + Replacement(["\r"], "", EscapeEnum.REPLACE), +] + + +@dataclass +class FlakeUpdateInfo: + new_flake_ids: list[str] + old_flake_ids: list[str] + newly_calculated_flakes: dict[str, set[FlakeSymptomType]] + + +def get_uploads(db_session: Session, commit: Commit) -> dict[int, Upload]: + return { + upload.id: upload + for upload in ( + db_session.query(Upload) + .join(CommitReport) + .filter( + CommitReport.commit_id == commit.id, + CommitReport.report_type == ReportType.TEST_RESULTS.value, + Upload.state == "v2_processed", + ) + .all() + ) + } + + +def queue_optional_tasks( + repo: Repository, + commit: Commit, + commit_yaml: UserYaml, + branch: str | None, +): + if should_do_flaky_detection(repo, commit_yaml): + if commit.merged is True or branch == repo.branch: + process_flakes_task_sig = process_flakes_task.s( + repo_id=repo.repoid, + commit_id=commit.commitid, + ) + process_flakes_task_sig.apply_async() + + if branch is not None: + cache_task_sig = cache_test_rollups_task.s( + repoid=repo.repoid, + branch=branch, + ) + cache_task_sig.apply_async() + + +def get_totals( + commit_report: CommitReport, db_session: Session +) -> TestResultReportTotals: + totals = commit_report.test_result_totals + if totals is None: + totals = TestResultReportTotals( + report_id=commit_report.id, + ) + totals.passed = 0 + totals.skipped = 0 + totals.failed = 0 + totals.error = str(TestResultsProcessingError.NO_SUCCESS) + db_session.add(totals) + db_session.flush() + + return totals + + +def handle_processor_fail( + totals: TestResultReportTotals, commit: Commit, commit_yaml: UserYaml +) -> dict[str, bool]: + # every processor errored, nothing to notify on + queue_notify = False + + # if error is None this whole process should be a noop + if totals.error is not None: + # make an attempt to make test results comment + notifier = TestResultsNotifier(commit, commit_yaml) + success, reason = notifier.error_comment() + if not success and reason == "torngit_error": + sentry_sdk.capture_message( + "Error posting error comment in test results finisher due to torngit error" + ) + + # also make attempt to make coverage comment + queue_notify = True + + return { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": queue_notify, + } + + +def populate_failures( + failures: list[TestResultsNotificationFailure], + db_session: Session, + repoid: int, + commitid: str, + shorten_paths: bool, + uploads: dict[int, Upload], + escaper: StringEscaper, +) -> None: + failed_test_instances = latest_failures_for_commit(db_session, repoid, commitid) + + for test_instance in failed_test_instances: + failure_message = test_instance.failure_message + if failure_message is not None: + if shorten_paths: + failure_message = shorten_file_paths(failure_message) + failure_message = escaper.replace(failure_message) + + upload = uploads[test_instance.upload_id] + + failures.append( + TestResultsNotificationFailure( + display_name=test_instance.test.computed_name + if test_instance.test.computed_name is not None + else test_instance.test.name, + failure_message=failure_message, + test_id=test_instance.test_id, + envs=upload.flag_names, + duration_seconds=test_instance.duration_seconds, + build_url=upload.build_url, + ) + ) + + +def get_flaky_tests( + db_session: Session, + repoid: int, + failures: list[TestResultsNotificationFailure], +) -> dict[str, FlakeInfo]: + failure_test_ids = [failure.test_id for failure in failures] + + matching_flakes = list( + db_session.query(Flake) + .filter( + Flake.repoid == repoid, + Flake.testid.in_(failure_test_ids), + Flake.end_date.is_(None), + Flake.count != (Flake.recent_passes_count + Flake.fail_count), + ) + .limit(100) + .all() + ) + + flaky_test_ids = { + flake.testid: FlakeInfo(flake.fail_count, flake.count) + for flake in matching_flakes + } + return flaky_test_ids + + +class TAFinisherTask(BaseCodecovTask, name=ta_finisher_task_name): + def run_impl( + self, + db_session: Session, + chord_result: list[bool], + *, + repoid: int, + commitid: str, + commit_yaml: dict, + **kwargs, + ): + repoid = int(repoid) + + self.extra_dict: dict[str, Any] = {"commit_yaml": commit_yaml} + log.info("Starting test results finisher task", extra=self.extra_dict) + + lock_manager = LockManager( + repoid=repoid, + commitid=commitid, + report_type=ReportType.COVERAGE, + lock_timeout=max(80, self.hard_time_limit_task), + ) + + try: + # this needs to be the coverage notification lock + # since both tests post/edit the same comment + with lock_manager.locked( + LockType.NOTIFICATION, + retry_num=self.request.retries, + ): + finisher_result = self.process_impl_within_lock( + db_session=db_session, + repoid=repoid, + commitid=commitid, + commit_yaml=UserYaml.from_dict(commit_yaml), + previous_result=chord_result, + **kwargs, + ) + if finisher_result["queue_notify"]: + notify_task_sig = notify_task.s( + repoid=repoid, + commitid=commitid, + current_yaml=commit_yaml, + ) + notify_task_sig.apply_async() + + return finisher_result + + except LockRetry as retry: + self.retry(max_retries=5, countdown=retry.countdown) + + def process_impl_within_lock( + self, + *, + db_session: Session, + repoid: int, + commitid: str, + commit_yaml: UserYaml, + previous_result: list[bool], + **kwargs, + ): + log.info("Running test results finishers", extra=self.extra_dict) + TestResultsFlow.log(TestResultsFlow.TEST_RESULTS_FINISHER_BEGIN) + + commit: Commit = ( + db_session.query(Commit).filter_by(repoid=repoid, commitid=commitid).first() + ) + assert commit, "commit not found" + + commit_report = commit.commit_report(ReportType.TEST_RESULTS) + + totals = get_totals(commit_report, db_session) + + if not any(previous_result): + return handle_processor_fail(totals, commit, commit_yaml) + + repo = commit.repository + branch = commit.branch + + uploads = get_uploads(db_session, commit) + + # if we succeed once, error should be None for this commit forever + if totals.error is not None: + totals.error = None + db_session.flush() + + test_summary = get_test_summary_for_commit(db_session, repoid, commitid) + totals.failed = test_summary.get("error", 0) + test_summary.get("failure", 0) + totals.skipped = test_summary.get("skip", 0) + totals.passed = test_summary.get("pass", 0) + db_session.flush() + if not totals.failed: + return { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": True, + } + + escaper = StringEscaper(ESCAPE_FAILURE_MESSAGE_DEFN) + shorten_paths = commit_yaml.read_yaml_field( + "test_analytics", "shorten_paths", _else=True + ) + + failures = [] + populate_failures( + failures, + db_session, + repoid, + commitid, + shorten_paths, + uploads, + escaper, + ) + + additional_data: AdditionalData = {"upload_type": UploadType.TEST_RESULTS} + repo_service = get_repo_provider_service(repo, additional_data=additional_data) + pull = async_to_sync(fetch_and_update_pull_request_information_from_commit)( + repo_service, commit, commit_yaml + ) + + if pull: + seat_activation_result = self.seat_activation( + db_session, pull, commit, commit_yaml, repo_service + ) + if seat_activation_result: + return seat_activation_result + + flaky_tests = dict() + if should_do_flaky_detection(repo, commit_yaml): + flaky_tests = get_flaky_tests(db_session, repoid, failures) + + failures = sorted(failures, key=lambda x: x.duration_seconds)[:3] + payload = TestResultsNotificationPayload( + totals.failed, totals.passed, totals.skipped, failures, flaky_tests + ) + notifier = TestResultsNotifier( + commit, commit_yaml, payload=payload, _pull=pull, _repo_service=repo_service + ) + notifier_result = notifier.notify() + + for upload in uploads.values(): + upload.state = "v2_finished" + db_session.commit() + + queue_optional_tasks(repo, commit, commit_yaml, branch) + + success = True if notifier_result is NotifierResult.COMMENT_POSTED else False + TestResultsFlow.log(TestResultsFlow.TEST_RESULTS_NOTIFY) + + self.extra_dict["success"] = success + self.extra_dict["notifier_result"] = notifier_result.value + log.info("Finished test results notify", extra=self.extra_dict) + + return { + "notify_attempted": True, + "notify_succeeded": success, + "queue_notify": False, + } + + def seat_activation( + self, + db_session: Session, + pull: EnrichedPull, + commit: Commit, + commit_yaml: UserYaml, + repo_service: TorngitBaseAdapter, + ) -> dict[str, bool] | None: + activate_seat_info = determine_seat_activation(pull) + + should_show_upgrade_message = True + + match activate_seat_info.should_activate_seat: + case ShouldActivateSeat.AUTO_ACTIVATE: + assert activate_seat_info.owner_id + assert activate_seat_info.author_id + successful_activation = activate_user( + db_session=db_session, + org_ownerid=activate_seat_info.owner_id, + user_ownerid=activate_seat_info.author_id, + ) + if successful_activation: + self.schedule_new_user_activated_task( + activate_seat_info.owner_id, + activate_seat_info.author_id, + ) + should_show_upgrade_message = False + case ShouldActivateSeat.MANUAL_ACTIVATE: + pass + case ShouldActivateSeat.NO_ACTIVATE: + should_show_upgrade_message = False + + if should_show_upgrade_message: + notifier = TestResultsNotifier( + commit, commit_yaml, _pull=pull, _repo_service=repo_service + ) + success, reason = notifier.upgrade_comment() + + self.extra_dict["success"] = success + self.extra_dict["reason"] = reason + log.info("Made upgrade comment", extra=self.extra_dict) + + return { + "notify_attempted": True, + "notify_succeeded": success, + "queue_notify": False, + } + + +RegisteredTAFinisherTask = celery_app.register_task(TAFinisherTask()) +ta_finisher_task = celery_app.tasks[RegisteredTAFinisherTask.name] diff --git a/tasks/ta_processor.py b/tasks/ta_processor.py new file mode 100644 index 000000000..ac1e925e0 --- /dev/null +++ b/tasks/ta_processor.py @@ -0,0 +1,170 @@ +import logging +from typing import Any + +import sentry_sdk +from shared.config import get_config +from shared.yaml import UserYaml +from sqlalchemy.orm import Session +from test_results_parser import parse_raw_upload + +from app import celery_app +from database.models import ( + Commit, + Repository, + Upload, +) +from services.archive import ArchiveService +from services.processing.types import UploadArguments +from services.test_results import get_flake_set +from services.yaml import read_yaml_field +from ta_storage.bq import BQDriver +from ta_storage.pg import PGDriver +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +ta_processor_task_name = "app.tasks.test_results.TAProcessor" + + +class TAProcessorTask(BaseCodecovTask, name=ta_processor_task_name): + __test__ = False + + def run_impl( + self, + db_session: Session, + *args, + repoid: int, + commitid: str, + commit_yaml: dict[str, Any], + argument: UploadArguments, + **kwargs, + ) -> bool: + log.info("Received TA processing task") + + user_yaml: UserYaml = UserYaml(commit_yaml) + repoid = int(repoid) + + repository = ( + db_session.query(Repository) + .filter(Repository.repoid == int(repoid)) + .first() + ) + + should_delete_archive = self.should_delete_archive(user_yaml) + archive_service = ArchiveService(repository) + successful = False + + commit = db_session.query(Commit).filter_by(commitid=commitid).first() + branch = commit.branch + + # process each report session's test information + upload = db_session.query(Upload).filter_by(id_=argument["upload_id"]).first() + result = self.process_individual_upload( + db_session, + archive_service, + repoid, + commitid, + branch, + upload, + should_delete_archive, + ) + if result: + successful = True + + return successful + + def process_individual_upload( + self, + db_session, + archive_service: ArchiveService, + repoid: int, + commitid: str, + branch: str, + upload: Upload, + should_delete_archive: bool, + ) -> bool: + upload_id = upload.id + + log.info("Processing individual upload", extra=dict(upload_id=upload_id)) + if upload.state == "v2_processed": + # don't need to process again because the intermediate result should already be in redis + return False + + payload_bytes = archive_service.read_file(upload.storage_path) + + try: + parsing_infos, readable_file = parse_raw_upload(payload_bytes) + except RuntimeError as exc: + log.error( + "Error parsing raw test results upload", + extra=dict( + repoid=upload.report.commit.repoid, + commitid=upload.report.commit_id, + uploadid=upload.id, + parser_err_msg=str(exc), + ), + ) + sentry_sdk.capture_exception(exc, tags={"upload_state": upload.state}) + upload.state = "v2_processed" + db_session.commit() + return False + else: + flaky_test_set = get_flake_set(db_session, upload.report.commit.repoid) + pg = PGDriver(db_session, flaky_test_set) + bq = BQDriver() + + for parsing_info in parsing_infos: + framework = parsing_info["framework"] + testruns = parsing_info["testruns"] + pg.write_testruns( + None, + repoid, + commitid, + branch, + upload, + framework, + testruns, + ) + + bq.write_testruns( + None, + repoid, + commitid, + branch, + upload, + framework, + testruns, + ) + + upload.state = "v2_processed" + db_session.commit() + + if should_delete_archive: + self.delete_archive(archive_service, upload) + else: + archive_service.write_file(upload.storage_path, bytes(readable_file)) + + return True + + def should_delete_archive(self, user_yaml: UserYaml): + if get_config("services", "minio", "expire_raw_after_n_days"): + return True + return not read_yaml_field( + user_yaml, ("codecov", "archive", "uploads"), _else=True + ) + + def delete_archive(self, archive_service: ArchiveService, upload: Upload): + archive_url = upload.storage_path + if archive_url and not archive_url.startswith("http"): + log.info( + "Deleting uploaded file as requested", + extra=dict( + archive_url=archive_url, + upload=upload.external_id, + ), + ) + archive_service.delete_file(archive_url) + + +RegisteredTAProcessorTask = celery_app.register_task(TAProcessorTask()) +ta_processor_task = celery_app.tasks[RegisteredTAProcessorTask.name] diff --git a/tasks/test_results_processor.py b/tasks/test_results_processor.py index 7d6457d5e..a01b1a96b 100644 --- a/tasks/test_results_processor.py +++ b/tasks/test_results_processor.py @@ -1,18 +1,19 @@ +from __future__ import annotations + import base64 -import json import logging import zlib from dataclasses import dataclass from datetime import date, datetime -from typing import TypedDict +from typing import Literal, TypedDict import sentry_sdk +import test_results_parser from shared.celery_config import test_results_processor_task_name from shared.config import get_config from shared.yaml import UserYaml from sqlalchemy.dialects.postgresql import insert from sqlalchemy.orm import Session -from test_results_parser import Outcome, ParserError, ParsingInfo, parse_junit_xml from app import celery_app from database.models import ( @@ -60,7 +61,7 @@ def create_daily_totals( test_id: str, repoid: int, duration_seconds: float, - outcome: Outcome, + outcome: Literal["pass", "failure", "error", "skip"], branch: str, commitid: str, flaky_test_set: set[str], @@ -70,20 +71,17 @@ def create_daily_totals( "repoid": repoid, "last_duration_seconds": duration_seconds, "avg_duration_seconds": duration_seconds, - "pass_count": 1 if outcome == str(Outcome.Pass) else 0, - "fail_count": 1 - if outcome == str(Outcome.Failure) or outcome == str(Outcome.Error) - else 0, - "skip_count": 1 if outcome == str(Outcome.Skip) else 0, + "pass_count": 1 if outcome == "pass" else 0, + "fail_count": 1 if outcome == "failure" or outcome == "error" else 0, + "skip_count": 1 if outcome == "skip" else 0, "flaky_fail_count": 1 - if test_id in flaky_test_set - and (outcome == str(Outcome.Failure) or outcome == str(Outcome.Error)) + if test_id in flaky_test_set and (outcome == "failure" or outcome == "error") else 0, "branch": branch, "date": date.today(), "latest_run": datetime.now(), "commits_where_fail": [commitid] - if (outcome == str(Outcome.Failure) or outcome == str(Outcome.Error)) + if outcome == "failure" or outcome == "error" else [], } @@ -92,7 +90,7 @@ def update_daily_totals( daily_totals: dict, test_id: str, duration_seconds: float, - outcome: Outcome, + outcome: Literal["pass", "failure", "error", "skip"], ): daily_totals[test_id]["last_duration_seconds"] = duration_seconds @@ -107,11 +105,11 @@ def update_daily_totals( + duration_seconds ) / (daily_totals[test_id]["pass_count"] + daily_totals[test_id]["fail_count"] + 1) - if outcome == str(Outcome.Pass): + if outcome == "pass": daily_totals[test_id]["pass_count"] += 1 - elif outcome == str(Outcome.Failure) or outcome == str(Outcome.Error): + elif outcome == "failure" or outcome == "error": daily_totals[test_id]["fail_count"] += 1 - elif outcome == str(Outcome.Skip): + elif outcome == "skip": daily_totals[test_id]["skip_count"] += 1 @@ -199,7 +197,7 @@ def _bulk_write_tests_to_db( commitid: str, upload_id: int, branch: str, - parsing_results: list[ParsingInfo], + parsing_results: list[test_results_parser.ParsingInfo], flaky_test_set: set[str], flags: list[str], ): @@ -213,21 +211,20 @@ def _bulk_write_tests_to_db( repo_flag_ids = get_repo_flag_ids(db_session, repoid, flags) for p in parsing_results: - framework = str(p.framework) if p.framework else None + framework = p["framework"] - for testrun in p.testruns: + for testrun in p["testruns"]: # Build up the data for bulk insert - name: str = f"{testrun.classname}\x1f{testrun.name}" - testsuite: str = testrun.testsuite - outcome: str = str(testrun.outcome) + name: str = f"{testrun['classname']}\x1f{testrun['name']}" + testsuite: str = testrun["testsuite"] + outcome = testrun["outcome"] duration_seconds: float = ( - testrun.duration if testrun.duration is not None else 0.0 + testrun["duration"] if testrun["duration"] is not None else 0.0 ) - failure_message: str | None = testrun.failure_message + failure_message: str | None = testrun["failure_message"] test_id: str = generate_test_id(repoid, testsuite, name, flags_hash) - computed_name = testrun.computed_name - - filename: str | None = testrun.filename + computed_name = testrun["computed_name"] + filename: str | None = testrun["filename"] test_data[(repoid, name, testsuite, flags_hash)] = dict( id=test_id, @@ -260,7 +257,7 @@ def _bulk_write_tests_to_db( ) ) - if outcome != str(Outcome.Skip): + if outcome != "skip": if test_id in daily_totals: update_daily_totals( daily_totals, test_id, duration_seconds, outcome @@ -331,7 +328,9 @@ def save_tests(self, db_session: Session, test_data: list[dict]): db_session.execute(insert_on_conflict_do_update) db_session.commit() - def save_daily_test_rollups(self, db_session: Session, daily_rollups: list[dict]): + def save_daily_test_rollups( + self, db_session: Session, daily_rollups: list[DailyTotals] + ): rollup_table = DailyTestRollup.__table__ stmt = insert(rollup_table).values(daily_rollups) stmt = stmt.on_conflict_do_update( @@ -386,12 +385,16 @@ def decode_raw_file(self, file: bytes) -> bytes: return file_bytes def parse_file( - self, file_bytes: bytes, upload: Upload, parsing_results: list[ParsingInfo] - ) -> bool: + self, + file_bytes: bytes, + upload: Upload, + ) -> tuple[list[test_results_parser.ParsingInfo], bytes] | None: try: - parsing_results.append(parse_junit_xml(file_bytes)) - return True - except ParserError as exc: + parsing_infos, readable_files = test_results_parser.parse_raw_upload( + file_bytes + ) + return parsing_infos, readable_files + except RuntimeError as exc: log.error( "Error parsing file", extra=dict( @@ -402,7 +405,7 @@ def parse_file( ), ) sentry_sdk.capture_exception(exc, tags={"upload_state": upload.state}) - return False + return None def process_individual_upload( self, @@ -423,38 +426,19 @@ def process_individual_upload( return {"successful": False} payload_bytes = archive_service.read_file(upload.storage_path) - try: - data = json.loads(payload_bytes) - except json.JSONDecodeError as e: - with sentry_sdk.new_scope() as scope: - scope.set_tag("upload_state", upload.state) - sentry_sdk.capture_exception(e, scope) - - upload.state = "not parsed" - db_session.flush() - return {"successful": False} - - parsing_results: list[ParsingInfo] = [] - network: list[str] | None = data.get("network_files") + parsing_results: list[test_results_parser.ParsingInfo] = [] report_contents: list[ReadableFile] = [] - all_succesful = True - for file_dict in data["test_results_files"]: - file = file_dict["data"] - file_bytes = self.decode_raw_file(file) - report_contents.append( - ReadableFile(path=file_dict["filename"], contents=file_bytes) - ) - success = self.parse_file(file_bytes, upload, parsing_results) - if not success: - all_succesful = False - - if all_succesful: - upload.state = "processed" - else: + result = self.parse_file(payload_bytes, upload) + if result is None: upload.state = "has_failed" + return {"successful": False} + + parsing_results, readable_files = result + + upload.state = "processed" - if all(len(result.testruns) == 0 for result in parsing_results): + if all(len(result["testruns"]) == 0 for result in parsing_results): successful = False log.error( "No test result files were successfully parsed for this upload", @@ -483,8 +467,7 @@ def process_individual_upload( if should_delete_archive: self.delete_archive(archive_service, upload) else: - readable_report = self.rewrite_readable(network, report_contents) - archive_service.write_file(upload.storage_path, readable_report) + archive_service.write_file(upload.storage_path, readable_files) return {"successful": successful} diff --git a/tasks/tests/unit/snapshots/ta_finisher_task__analytics__0.json b/tasks/tests/unit/snapshots/ta_finisher_task__analytics__0.json new file mode 100644 index 000000000..f7c6014fe --- /dev/null +++ b/tasks/tests/unit/snapshots/ta_finisher_task__analytics__0.json @@ -0,0 +1,144 @@ +{ + "tests": [ + { + "repoid": 1, + "name": "tests.test_parsers.TestParsers\u001ftest_subtract", + "testsuite": "hello_world", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": null, + "computed_name": null, + "filename": null + }, + { + "repoid": 1, + "name": "tests.test_parsers.TestParsers\u001ftest_divide", + "testsuite": "hello_world", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": null, + "computed_name": null, + "filename": null + }, + { + "repoid": 1, + "name": "tests.test_parsers.TestParsers\u001ftest_multiply", + "testsuite": "hello_world", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": null, + "computed_name": null, + "filename": null + }, + { + "repoid": 1, + "name": "tests.test_parsers.TestParsers\u001ftest_add", + "testsuite": "hello_world", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": null, + "computed_name": null, + "filename": null + } + ], + "test_instances": [ + { + "test_id": "7741ad6f22bd0b1e1f0dee0978162ffeed170807b08e926b2ea7518cb214f667", + "duration_seconds": 0.001, + "outcome": "failure", + "upload_id": 1, + "failure_message": "hello world", + "branch": "main", + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + }, + { + "test_id": "cdc5500179182edbe26b3b0934408a2f0a015592e4311ae709bcedd3235b1486", + "duration_seconds": 0.002, + "outcome": "pass", + "upload_id": 1, + "failure_message": null, + "branch": "main", + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + }, + { + "test_id": "e0f2e4b242b5dcb8b7ddc273936d217ca755ef3a47a9824ec025c4ce1b418ef7", + "duration_seconds": 0.003, + "outcome": "skip", + "upload_id": 1, + "failure_message": null, + "branch": "main", + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + }, + { + "test_id": "11b8beeee314d9674013cacdddc40d56d9ec3eb0c4bc38d75e74312888e31306", + "duration_seconds": 0.004, + "outcome": "error", + "upload_id": 1, + "failure_message": null, + "branch": "main", + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + } + ], + "rollups": [ + { + "test_id": "11b8beeee314d9674013cacdddc40d56d9ec3eb0c4bc38d75e74312888e31306", + "date": "2025-01-01", + "repoid": 1, + "branch": "main", + "fail_count": 1, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 0, + "last_duration_seconds": 0.004, + "avg_duration_seconds": 0.004, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [ + "cd76b0821854a780b60012aed85af0a8263004ad" + ] + }, + { + "test_id": "7741ad6f22bd0b1e1f0dee0978162ffeed170807b08e926b2ea7518cb214f667", + "date": "2025-01-01", + "repoid": 1, + "branch": "main", + "fail_count": 1, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 0, + "last_duration_seconds": 0.001, + "avg_duration_seconds": 0.001, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [ + "cd76b0821854a780b60012aed85af0a8263004ad" + ] + }, + { + "test_id": "cdc5500179182edbe26b3b0934408a2f0a015592e4311ae709bcedd3235b1486", + "date": "2025-01-01", + "repoid": 1, + "branch": "main", + "fail_count": 0, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 1, + "last_duration_seconds": 0.002, + "avg_duration_seconds": 0.002, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [] + }, + { + "test_id": "e0f2e4b242b5dcb8b7ddc273936d217ca755ef3a47a9824ec025c4ce1b418ef7", + "date": "2025-01-01", + "repoid": 1, + "branch": "main", + "fail_count": 0, + "flaky_fail_count": 0, + "skip_count": 1, + "pass_count": 0, + "last_duration_seconds": 0.003, + "avg_duration_seconds": 0.003, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [] + } + ] +} diff --git a/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_bad_file__0.json b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_bad_file__0.json new file mode 100644 index 000000000..81177b878 --- /dev/null +++ b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_bad_file__0.json @@ -0,0 +1,5 @@ +{ + "tests": [], + "test_instances": [], + "rollups": [] +} diff --git a/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__0.json b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__0.json new file mode 100644 index 000000000..9a9c678ad --- /dev/null +++ b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__0.json @@ -0,0 +1,142 @@ +{ + "tests": [ + { + "repoid": 1, + "name": "api.temp.calculator.test_calculator\u001ftest_multiply", + "testsuite": "pytest", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": "Pytest", + "computed_name": "api.temp.calculator.test_calculator::test_multiply", + "filename": null + }, + { + "repoid": 1, + "name": "api.temp.calculator.test_calculator\u001ftest_divide", + "testsuite": "pytest", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": "Pytest", + "computed_name": "api.temp.calculator.test_calculator::test_divide", + "filename": null + }, + { + "repoid": 1, + "name": "api.temp.calculator.test_calculator\u001ftest_add", + "testsuite": "pytest", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": "Pytest", + "computed_name": "api.temp.calculator.test_calculator::test_add", + "filename": null + }, + { + "repoid": 1, + "name": "api.temp.calculator.test_calculator\u001ftest_subtract", + "testsuite": "pytest", + "flags_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "framework": "Pytest", + "computed_name": "api.temp.calculator.test_calculator::test_subtract", + "filename": null + } + ], + "test_instances": [ + { + "test_id": "6cbd65e47887eda0f4c0355b0a03e549284499e1acf80cb16bfd7797b546a3cb", + "duration_seconds": 0.001, + "outcome": "pass", + "upload_id": 1, + "failure_message": null, + "branch": null, + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + }, + { + "test_id": "80ee2bcfe6727cd875914bc1fa2995fd5feea6171803907dfd3383154f643038", + "duration_seconds": 0.001, + "outcome": "pass", + "upload_id": 1, + "failure_message": null, + "branch": null, + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + }, + { + "test_id": "277ea9e35f1a02d0a8b441b9b74ecd48d35168929ff5ee9fa3dce48a6c4ea0ea", + "duration_seconds": 0.0, + "outcome": "pass", + "upload_id": 1, + "failure_message": null, + "branch": null, + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + }, + { + "test_id": "3ffa89c17705abeeff8728d3cd030c563ac1d40c67ae5e6e84c1bc66d4d6e012", + "duration_seconds": 0.001, + "outcome": "failure", + "upload_id": 1, + "failure_message": "def test_divide():\n> assert Calculator.divide(1, 2) == 0.5\nE assert 1.0 == 0.5\nE + where 1.0 = (1, 2)\nE + where = Calculator.divide\n\napi/temp/calculator/test_calculator.py:30: AssertionError", + "branch": null, + "commitid": "cd76b0821854a780b60012aed85af0a8263004ad", + "repoid": 1 + } + ], + "rollups": [ + { + "test_id": "277ea9e35f1a02d0a8b441b9b74ecd48d35168929ff5ee9fa3dce48a6c4ea0ea", + "date": "2025-01-01", + "repoid": 1, + "branch": null, + "fail_count": 0, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 1, + "last_duration_seconds": 0.0, + "avg_duration_seconds": 0.0, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [] + }, + { + "test_id": "3ffa89c17705abeeff8728d3cd030c563ac1d40c67ae5e6e84c1bc66d4d6e012", + "date": "2025-01-01", + "repoid": 1, + "branch": null, + "fail_count": 1, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 0, + "last_duration_seconds": 0.001, + "avg_duration_seconds": 0.001, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [ + "cd76b0821854a780b60012aed85af0a8263004ad" + ] + }, + { + "test_id": "6cbd65e47887eda0f4c0355b0a03e549284499e1acf80cb16bfd7797b546a3cb", + "date": "2025-01-01", + "repoid": 1, + "branch": null, + "fail_count": 0, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 1, + "last_duration_seconds": 0.001, + "avg_duration_seconds": 0.001, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [] + }, + { + "test_id": "80ee2bcfe6727cd875914bc1fa2995fd5feea6171803907dfd3383154f643038", + "date": "2025-01-01", + "repoid": 1, + "branch": null, + "fail_count": 0, + "flaky_fail_count": 0, + "skip_count": 0, + "pass_count": 1, + "last_duration_seconds": 0.001, + "avg_duration_seconds": 0.001, + "latest_run": "2025-01-01T00:00:00+00:00", + "commits_where_fail": [] + } + ] +} diff --git a/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__1.bin b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__1.bin new file mode 100644 index 000000000..27801b9df --- /dev/null +++ b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__1.bin @@ -0,0 +1,9 @@ +# path=codecov-demo/temp.junit.xml +def test_divide(): +> assert Calculator.divide(1, 2) == 0.5 +E assert 1.0 == 0.5 +E + where 1.0 = <function Calculator.divide at 0x104c9eb90>(1, 2) +E + where <function Calculator.divide at 0x104c9eb90> = Calculator.divide + +api/temp/calculator/test_calculator.py:30: AssertionError +<<<<<< EOF diff --git a/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__2.json b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__2.json new file mode 100644 index 000000000..f3e4935f2 --- /dev/null +++ b/tasks/tests/unit/snapshots/ta_processor_task__TestUploadTestProcessorTask__ta_processor_task_call__2.json @@ -0,0 +1,51 @@ +[ + { + "timestamp": "1735689600000000", + "name": "test_add", + "classname": "api.temp.calculator.test_calculator", + "testsuite": "pytest", + "computed_name": "api.temp.calculator.test_calculator::test_add", + "outcome": "PASSED", + "duration_seconds": 0.001, + "repoid": "1", + "commit_sha": "cd76b0821854a780b60012aed85af0a8263004ad", + "framework": "Pytest" + }, + { + "timestamp": "1735689600000000", + "name": "test_subtract", + "classname": "api.temp.calculator.test_calculator", + "testsuite": "pytest", + "computed_name": "api.temp.calculator.test_calculator::test_subtract", + "outcome": "PASSED", + "duration_seconds": 0.001, + "repoid": "1", + "commit_sha": "cd76b0821854a780b60012aed85af0a8263004ad", + "framework": "Pytest" + }, + { + "timestamp": "1735689600000000", + "name": "test_multiply", + "classname": "api.temp.calculator.test_calculator", + "testsuite": "pytest", + "computed_name": "api.temp.calculator.test_calculator::test_multiply", + "outcome": "PASSED", + "duration_seconds": 0.0, + "repoid": "1", + "commit_sha": "cd76b0821854a780b60012aed85af0a8263004ad", + "framework": "Pytest" + }, + { + "timestamp": "1735689600000000", + "name": "test_divide", + "classname": "api.temp.calculator.test_calculator", + "testsuite": "pytest", + "computed_name": "api.temp.calculator.test_calculator::test_divide", + "outcome": "FAILED", + "failure_message": "def test_divide():\n> assert Calculator.divide(1, 2) == 0.5\nE assert 1.0 == 0.5\nE + where 1.0 = (1, 2)\nE + where = Calculator.divide\n\napi/temp/calculator/test_calculator.py:30: AssertionError", + "duration_seconds": 0.001, + "repoid": "1", + "commit_sha": "cd76b0821854a780b60012aed85af0a8263004ad", + "framework": "Pytest" + } +] diff --git a/tasks/tests/unit/test_process_flakes.py b/tasks/tests/unit/test_process_flakes.py index b77bb2e57..4b30d42ef 100644 --- a/tasks/tests/unit/test_process_flakes.py +++ b/tasks/tests/unit/test_process_flakes.py @@ -311,8 +311,10 @@ def test_it_handles_only_passes(transactional_db): def test_it_creates_flakes_from_processed_uploads(transactional_db): rs = RepoSimulator() c1 = rs.create_commit() - rs.add_test_instance(c1) - rs.add_test_instance(c1, outcome=TestInstance.Outcome.FAILURE.value) + rs.add_test_instance(c1, state="v2_finished") + rs.add_test_instance( + c1, outcome=TestInstance.Outcome.FAILURE.value, state="processed" + ) rs.run_task(rs.repo.repoid, c1.commitid) @@ -329,7 +331,7 @@ def test_it_creates_flakes_from_processed_uploads(transactional_db): def test_it_does_not_create_flakes_from_flake_processed_uploads(transactional_db): rs = RepoSimulator() c1 = rs.create_commit() - rs.add_test_instance(c1) + rs.add_test_instance(c1, state="v2_processed") rs.add_test_instance( c1, outcome=TestInstance.Outcome.FAILURE.value, state="flake_processed" ) diff --git a/tasks/tests/unit/test_ta_finisher_task.py b/tasks/tests/unit/test_ta_finisher_task.py new file mode 100644 index 000000000..22c0243ef --- /dev/null +++ b/tasks/tests/unit/test_ta_finisher_task.py @@ -0,0 +1,281 @@ +import base64 +import json +import zlib +from pathlib import Path +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from time_machine import travel + +from database.models import DailyTestRollup, Test, TestInstance +from database.tests.factories import ( + CommitFactory, + ReportFactory, + RepositoryFactory, + UploadFactory, +) +from services.urls import services_short_dict +from tasks.ta_finisher import TAFinisherTask +from tasks.ta_processor import TAProcessorTask + +here = Path(__file__) + + +@pytest.fixture(autouse=True) +def mock_bigquery_service(): + with patch("ta_storage.bq.get_bigquery_service") as mock: + service = MagicMock() + mock.return_value = service + yield service + + +def generate_junit_xml( + testruns: list[dict[str, Any]], + testsuite_name: str = "hello_world", +) -> str: + testcases = [] + + num_total = len(testruns) + num_fail = 0 + num_skip = 0 + num_error = 0 + total_time = 0 + + for testrun in testruns: + total_time += float(testrun["duration_seconds"]) + match testrun["outcome"]: + case "fail": + num_fail += 1 + testcases.append( + f'' + ) + case "skip": + num_skip += 1 + testcases.append( + f'' + ) + case "error": + num_error += 1 + testcases.append( + f'' + ) + case "pass": + testcases.append( + f'' + ) + + testsuite_section = [ + f'', + *testcases, + "", + ] + + header = [ + '', + "", + *testsuite_section, + "", + ] + + return "\n".join(header) + + +@travel("2025-01-01T00:00:00Z", tick=False) +def test_test_analytics(dbsession, mocker, mock_storage, celery_app, snapshot): + url = "literally/whatever" + + testruns = [ + { + "name": "test_divide", + "outcome": "fail", + "duration_seconds": 0.001, + "failure_message": "hello world", + }, + {"name": "test_multiply", "outcome": "pass", "duration_seconds": 0.002}, + {"name": "test_add", "outcome": "skip", "duration_seconds": 0.003}, + {"name": "test_subtract", "outcome": "error", "duration_seconds": 0.004}, + ] + + content: str = generate_junit_xml(testruns) + json_content: dict[str, Any] = { + "test_results_files": [ + { + "filename": "hello_world.junit.xml", + "data": base64.b64encode(zlib.compress(content.encode())).decode(), + } + ], + } + mock_storage.write_file("archive", url, json.dumps(json_content).encode()) + repo = RepositoryFactory.create( + repoid=1, + owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + owner__username="joseph-sentry", + owner__service="github", + name="codecov-demo", + ) + dbsession.add(repo) + dbsession.flush() + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository=repo, + branch="main", + ) + dbsession.add(commit) + dbsession.flush() + report = ReportFactory.create(commit=commit) + report.report_type = "test_results" + dbsession.add(report) + dbsession.flush() + upload = UploadFactory.create(storage_path=url, report=report) + dbsession.add(upload) + dbsession.flush() + upload.id_ = 1 + dbsession.flush() + + argument = {"url": url, "upload_id": upload.id_} + + mocker.patch.object(TAProcessorTask, "app", celery_app) + mocker.patch.object(TAFinisherTask, "app", celery_app) + + celery_app.tasks = { + "app.tasks.flakes.ProcessFlakesTask": mocker.MagicMock(), + "app.tasks.cache_rollup.CacheTestRollupsTask": mocker.MagicMock(), + } + + mock_repo_provider_service = AsyncMock() + mocker.patch( + "tasks.ta_finisher.get_repo_provider_service", + return_value=mock_repo_provider_service, + ) + mock_pull_request_information = AsyncMock() + mocker.patch( + "tasks.ta_finisher.fetch_and_update_pull_request_information_from_commit", + return_value=mock_pull_request_information, + ) + + result = TAProcessorTask().run_impl( + dbsession, + repoid=upload.report.commit.repoid, + commitid=upload.report.commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is True + + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + rollups = dbsession.query(DailyTestRollup).all() + + tests = [ + { + "repoid": test.repoid, + "name": test.name, + "testsuite": test.testsuite, + "flags_hash": test.flags_hash, + "framework": test.framework, + "computed_name": test.computed_name, + "filename": test.filename, + } + for test in dbsession.query(Test).all() + ] + test_instances = [ + { + "test_id": test_instance.test_id, + "duration_seconds": test_instance.duration_seconds, + "outcome": test_instance.outcome, + "upload_id": test_instance.upload_id, + "failure_message": test_instance.failure_message, + "branch": test_instance.branch, + "commitid": test_instance.commitid, + "repoid": test_instance.repoid, + } + for test_instance in dbsession.query(TestInstance).all() + ] + rollups = [ + { + "test_id": rollup.test_id, + "date": rollup.date.isoformat(), + "repoid": rollup.repoid, + "branch": rollup.branch, + "fail_count": rollup.fail_count, + "flaky_fail_count": rollup.flaky_fail_count, + "skip_count": rollup.skip_count, + "pass_count": rollup.pass_count, + "last_duration_seconds": rollup.last_duration_seconds, + "avg_duration_seconds": rollup.avg_duration_seconds, + "latest_run": rollup.latest_run.isoformat(), + "commits_where_fail": rollup.commits_where_fail, + } + for rollup in dbsession.query(DailyTestRollup).all() + ] + + assert snapshot("json") == { + "tests": tests, + "test_instances": test_instances, + "rollups": rollups, + } + + result = TAFinisherTask().run_impl( + dbsession, + chord_result=[result], + repoid=upload.report.commit.repoid, + commitid=upload.report.commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + assert result["notify_attempted"] is True + assert result["notify_succeeded"] is True + assert result["queue_notify"] is False + + mock_repo_provider_service.edit_comment.assert_called_once() + + short_form_service_name = services_short_dict.get( + upload.report.commit.repository.owner.service + ) + + mock_repo_provider_service.edit_comment.assert_called_once_with( + mock_pull_request_information.database_pull.pullid, + mock_pull_request_information.database_pull.commentid, + f"""### :x: 2 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 3 | 2 | 1 | 1 | +
View the top 2 failed tests by shortest run time + +> +> ```python +> tests.test_parsers.TestParsers test_divide +> ``` +> +>
Stack Traces | 0.001s run time +> +> > +> > ```python +> > hello world +> > ``` +> +>
+ + +> +> ```python +> tests.test_parsers.TestParsers test_subtract +> ``` +> +>
Stack Traces | 0.004s run time +> +> > +> > ```python +> > No failure message available +> > ``` +> +>
+ +
+ +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/{short_form_service_name}/{upload.report.commit.repository.owner.username}/{upload.report.commit.repository.name}/tests/{upload.report.commit.branch}) +:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/304)""", + ) diff --git a/tasks/tests/unit/test_ta_processor_task.py b/tasks/tests/unit/test_ta_processor_task.py new file mode 100644 index 000000000..4ff985ba6 --- /dev/null +++ b/tasks/tests/unit/test_ta_processor_task.py @@ -0,0 +1,437 @@ +import base64 +import json +import zlib +from pathlib import Path +from unittest.mock import ANY, MagicMock, patch + +import pytest +from google.protobuf.json_format import MessageToDict +from shared.storage.exceptions import FileNotInStorageError +from time_machine import travel + +import generated_proto.testrun.ta_testrun_pb2 as ta_testrun_pb2 +from database.models import CommitReport, DailyTestRollup, Test, TestInstance +from database.tests.factories import ( + CommitFactory, + ReportFactory, + RepositoryFactory, + UploadFactory, +) +from tasks.ta_processor import TAProcessorTask + +here = Path(__file__) + + +@pytest.fixture() +def mock_bigquery_service(): + with patch("ta_storage.bq.get_bigquery_service") as mock: + service = MagicMock() + mock.return_value = service + yield service + + +class TestUploadTestProcessorTask(object): + @pytest.mark.integration + @travel("2025-01-01T00:00:00Z", tick=False) + def test_ta_processor_task_call( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + mock_bigquery_service, + snapshot, + ): + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + assert len(tests) == 0 + assert len(test_instances) == 0 + + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + + repo = RepositoryFactory.create( + repoid=1, + owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + owner__username="joseph-sentry", + owner__service="github", + name="codecov-demo", + ) + dbsession.add(repo) + dbsession.flush() + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository=repo, + ) + dbsession.add(commit) + dbsession.flush() + report = ReportFactory.create(commit=commit) + dbsession.add(report) + dbsession.flush() + upload = UploadFactory.create(storage_path=url, report=report) + dbsession.add(upload) + dbsession.flush() + upload.id_ = 1 + dbsession.flush() + + argument = {"url": url, "upload_id": upload.id_} + mocker.patch.object(TAProcessorTask, "app", celery_app) + + result = TAProcessorTask().run_impl( + dbsession, + repoid=upload.report.commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is True + assert upload.state == "v2_processed" + + tests = [ + { + "repoid": test.repoid, + "name": test.name, + "testsuite": test.testsuite, + "flags_hash": test.flags_hash, + "framework": test.framework, + "computed_name": test.computed_name, + "filename": test.filename, + } + for test in dbsession.query(Test).all() + ] + test_instances = [ + { + "test_id": test_instance.test_id, + "duration_seconds": test_instance.duration_seconds, + "outcome": test_instance.outcome, + "upload_id": test_instance.upload_id, + "failure_message": test_instance.failure_message, + "branch": test_instance.branch, + "commitid": test_instance.commitid, + "repoid": test_instance.repoid, + } + for test_instance in dbsession.query(TestInstance).all() + ] + rollups = [ + { + "test_id": rollup.test_id, + "date": rollup.date.isoformat(), + "repoid": rollup.repoid, + "branch": rollup.branch, + "fail_count": rollup.fail_count, + "flaky_fail_count": rollup.flaky_fail_count, + "skip_count": rollup.skip_count, + "pass_count": rollup.pass_count, + "last_duration_seconds": rollup.last_duration_seconds, + "avg_duration_seconds": rollup.avg_duration_seconds, + "latest_run": rollup.latest_run.isoformat(), + "commits_where_fail": rollup.commits_where_fail, + } + for rollup in dbsession.query(DailyTestRollup).all() + ] + + assert snapshot("json") == { + "tests": tests, + "test_instances": test_instances, + "rollups": rollups, + } + assert snapshot("bin") == mock_storage.read_file("archive", url) + + mock_bigquery_service.write.assert_called_once_with( + "codecov_prod", "testruns", ta_testrun_pb2, ANY + ) + + # this gets the bytes argument to the write call + # it gets the first call + # then it gets the args because call is a tuple (name, args, kwargs) + # then it gets the 3rd item in the args tuple which is the bytes arg + testruns = [ + MessageToDict( + ta_testrun_pb2.TestRun.FromString(testrun_bytes), + preserving_proto_field_name=True, + ) + for testrun_bytes in mock_bigquery_service.mock_calls[0][1][3] + ] + assert snapshot("json") == testruns + + @pytest.mark.integration + def test_ta_processor_task_error_parsing_file( + self, + caplog, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + mock_bigquery_service, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + argument = {"url": url, "upload_id": upload.id_} + mocker.patch.object(TAProcessorTask, "app", celery_app) + mocker.patch( + "tasks.ta_processor.parse_raw_upload", + side_effect=RuntimeError, + ) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + + result = TAProcessorTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is False + assert upload.state == "v2_processed" + + @pytest.mark.integration + def test_ta_processor_task_delete_archive( + self, + caplog, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + mock_bigquery_service, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + argument = {"url": url, "upload_id": upload.id_} + mocker.patch.object(TAProcessorTask, "app", celery_app) + mocker.patch.object(TAProcessorTask, "should_delete_archive", return_value=True) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TAProcessorTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is True + with pytest.raises(FileNotInStorageError): + mock_storage.read_file("archive", url) + + @pytest.mark.integration + def test_ta_processor_task_bad_file( + self, + caplog, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + snapshot, + mock_bigquery_service, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + mock_storage.write_file( + "archive", + url, + b'{"test_results_files": [{"filename": "blah", "format": "blah", "data": "eJzLSM3JyVcozy/KSQEAGgsEXQ=="}]}', + ) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + argument = {"url": url, "upload_id": upload.id_} + mocker.patch.object(TAProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TAProcessorTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is True + + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + rollups = dbsession.query(DailyTestRollup).all() + + assert snapshot("json") == { + "tests": tests, + "test_instances": test_instances, + "rollups": rollups, + } + + @pytest.mark.integration + def test_ta_processor_task_call_already_processed( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + mock_bigquery_service, + ): + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + assert len(tests) == 0 + assert len(test_instances) == 0 + + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url, state="v2_processed") + dbsession.add(upload) + dbsession.flush() + argument = {"url": url, "upload_id": upload.id_} + mocker.patch.object(TAProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TAProcessorTask().run_impl( + dbsession, + repoid=upload.report.commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is False + + @pytest.mark.integration + def test_ta_processor_task_call_already_processed_with_junit( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + mock_bigquery_service, + ): + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + assert len(tests) == 0 + assert len(test_instances) == 0 + + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_ta_file.xml") as f: + content = f.read() + compressed_and_base64_encoded = base64.b64encode( + zlib.compress(content.encode("utf-8")) + ).decode("utf-8") + thing = { + "test_results_files": [ + { + "filename": "codecov-demo/temp.junit.xml", + "format": "base64+compressed", + "data": compressed_and_base64_encoded, + } + ] + } + mock_storage.write_file("archive", url, json.dumps(thing)) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + + argument = {"url": url, "upload_id": upload.id_} + mocker.patch.object(TAProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TAProcessorTask().run_impl( + dbsession, + repoid=upload.report.commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + argument=argument, + ) + + assert result is True + assert upload.state == "v2_processed" diff --git a/tasks/tests/unit/test_test_results_finisher.py b/tasks/tests/unit/test_test_results_finisher.py index fd5c93471..fa32e5366 100644 --- a/tasks/tests/unit/test_test_results_finisher.py +++ b/tasks/tests/unit/test_test_results_finisher.py @@ -5,7 +5,6 @@ from mock import AsyncMock from shared.billing import BillingPlan from shared.torngit.exceptions import TorngitClientError -from test_results_parser import Outcome from database.enums import ReportType from database.models import ( @@ -177,7 +176,7 @@ def test_results_setup(mocker, dbsession): test_instances = [ TestInstance( test_id=test_id1, - outcome=str(Outcome.Failure), + outcome="failure", failure_message="This should not be in the comment, it will get overwritten by the last test instance", duration_seconds=1.0, upload_id=uploads[0].id, @@ -186,7 +185,7 @@ def test_results_setup(mocker, dbsession): ), TestInstance( test_id=test_id2, - outcome=str(Outcome.Failure), + outcome="failure", failure_message="Shared \n\n\n\n
 ````````\n \r\n\r\n | test | test | test 
failure message", duration_seconds=2.0, upload_id=uploads[1].id, @@ -195,7 +194,7 @@ def test_results_setup(mocker, dbsession): ), TestInstance( test_id=test_id3, - outcome=str(Outcome.Failure), + outcome="failure", failure_message="Shared \n\n\n\n
 \n  ````````  \n \r\n\r\n | test | test | test 
failure message", duration_seconds=3.0, upload_id=uploads[2].id, @@ -204,7 +203,7 @@ def test_results_setup(mocker, dbsession): ), TestInstance( test_id=test_id1, - outcome=str(Outcome.Failure), + outcome="failure", failure_message="
Fourth \r\n\r\n
| test | instance |", duration_seconds=4.0, upload_id=uploads[3].id, @@ -213,7 +212,7 @@ def test_results_setup(mocker, dbsession): ), TestInstance( test_id=test_id4, - outcome=str(Outcome.Failure), + outcome="failure", failure_message=None, duration_seconds=5.0, upload_id=uploads[3].id, @@ -478,7 +477,7 @@ def test_upload_finisher_task_call_no_failures( repoid, commit, _, test_instances = test_results_setup for instance in test_instances: - instance.outcome = str(Outcome.Pass) + instance.outcome = "pass" dbsession.flush() result = TestResultsFinisherTask().run_impl( diff --git a/tasks/tests/unit/test_test_results_processor_task.py b/tasks/tests/unit/test_test_results_processor_task.py index 982e6f6c9..7461e0723 100644 --- a/tasks/tests/unit/test_test_results_processor_task.py +++ b/tasks/tests/unit/test_test_results_processor_task.py @@ -1,13 +1,9 @@ -import base64 -import json -import zlib from datetime import date, datetime, timedelta, timezone from itertools import chain from pathlib import Path import pytest from shared.storage.exceptions import FileNotInStorageError -from test_results_parser import Outcome from time_machine import travel from database.models import CommitReport, RepositoryFlag @@ -16,7 +12,6 @@ from database.tests.factories.reports import FlakeFactory from services.test_results import generate_flags_hash, generate_test_id from tasks.test_results_processor import ( - ParserError, TestResultsProcessorTask, ) @@ -74,9 +69,7 @@ def test_upload_processor_task_call( expected_result = True tests = dbsession.query(Test).all() test_instances = dbsession.query(TestInstance).all() - failures = ( - dbsession.query(TestInstance).filter_by(outcome=str(Outcome.Failure)).all() - ) + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() assert len(tests) == 4 assert len(test_instances) == 4 @@ -103,7 +96,6 @@ def test_upload_processor_task_call( api/temp/calculator/test_calculator.py:30: AssertionError <<<<<< EOF - """ ) @@ -130,8 +122,8 @@ def test_test_result_processor_task_error_parsing_file( redis_queue = [{"url": url, "upload_id": upload.id_}] mocker.patch.object(TestResultsProcessorTask, "app", celery_app) mocker.patch( - "tasks.test_results_processor.parse_junit_xml", - side_effect=ParserError, + "tasks.test_results_processor.test_results_parser.parse_raw_upload", + side_effect=RuntimeError("Error parsing file"), ) commit = CommitFactory.create( @@ -213,9 +205,7 @@ def test_test_result_processor_task_delete_archive( tests = dbsession.query(Test).all() test_instances = dbsession.query(TestInstance).all() - failures = ( - dbsession.query(TestInstance).filter_by(outcome=str(Outcome.Failure)).all() - ) + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() assert result == expected_result @@ -288,6 +278,7 @@ def test_test_result_processor_task_bad_file( ) @pytest.mark.integration + @travel("2025-01-01T00:00:00Z", tick=False) def test_upload_processor_task_call_existing_test( self, mocker, @@ -352,9 +343,7 @@ def test_upload_processor_task_call_existing_test( expected_result = True tests = dbsession.query(Test).all() test_instances = dbsession.query(TestInstance).all() - failures = ( - dbsession.query(TestInstance).filter_by(outcome=str(Outcome.Failure)).all() - ) + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() assert len(tests) == 4 assert len(test_instances) == 4 @@ -443,11 +432,7 @@ def test_upload_processor_task_call_existing_test_diff_flags_hash( expected_result = True tests = dbsession.query(Test).all() test_instances = dbsession.query(TestInstance).all() - failures = ( - dbsession.query(TestInstance) - .filter(TestInstance.outcome == str(Outcome.Failure)) - .all() - ) + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() test_flag_bridges = dbsession.query(TestFlagBridge).all() @@ -691,9 +676,7 @@ def test_upload_processor_task_call_network( expected_result = True tests = dbsession.query(Test).all() test_instances = dbsession.query(TestInstance).all() - failures = ( - dbsession.query(TestInstance).filter_by(outcome=str(Outcome.Failure)).all() - ) + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() assert len(tests) == 4 assert len(test_instances) == 4 @@ -720,123 +703,3 @@ def test_upload_processor_task_call_network( b"""# path=codecov-demo/temp.junit.xml """ ) - - @pytest.mark.integration - def test_upload_processor_task_call_already_processed( - self, - mocker, - mock_configuration, - dbsession, - codecov_vcr, - mock_storage, - mock_redis, - celery_app, - ): - tests = dbsession.query(Test).all() - test_instances = dbsession.query(TestInstance).all() - assert len(tests) == 0 - assert len(test_instances) == 0 - - url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" - with open(here.parent.parent / "samples" / "sample_test.json") as f: - content = f.read() - mock_storage.write_file("archive", url, content) - upload = UploadFactory.create(storage_path=url, state="processed") - dbsession.add(upload) - dbsession.flush() - redis_queue = [{"url": url, "upload_id": upload.id_}] - mocker.patch.object(TestResultsProcessorTask, "app", celery_app) - - commit = CommitFactory.create( - message="hello world", - commitid="cd76b0821854a780b60012aed85af0a8263004ad", - repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", - repository__owner__username="joseph-sentry", - repository__owner__service="github", - repository__name="codecov-demo", - ) - dbsession.add(commit) - dbsession.flush() - current_report_row = CommitReport(commit_id=commit.id_) - dbsession.add(current_report_row) - dbsession.flush() - result = TestResultsProcessorTask().run_impl( - dbsession, - previous_result=False, - repoid=upload.report.commit.repoid, - commitid=commit.commitid, - commit_yaml={"codecov": {"max_report_age": False}}, - arguments_list=redis_queue, - ) - expected_result = True - - assert expected_result == result - - @pytest.mark.integration - def test_upload_processor_task_call_already_processed_with_junit( - self, - mocker, - mock_configuration, - dbsession, - codecov_vcr, - mock_storage, - mock_redis, - celery_app, - ): - tests = dbsession.query(Test).all() - test_instances = dbsession.query(TestInstance).all() - assert len(tests) == 0 - assert len(test_instances) == 0 - - url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" - with open(here.parent.parent / "samples" / "sample_ta_file.xml") as f: - content = f.read() - compressed_and_base64_encoded = base64.b64encode( - zlib.compress(content.encode("utf-8")) - ).decode("utf-8") - thing = { - "test_results_files": [ - { - "filename": "codecov-demo/temp.junit.xml", - "format": "base64+compressed", - "data": compressed_and_base64_encoded, - } - ] - } - mock_storage.write_file("archive", url, json.dumps(thing)) - upload = UploadFactory.create(storage_path=url) - dbsession.add(upload) - dbsession.flush() - - redis_queue = [{"url": url, "upload_id": upload.id_}] - mocker.patch.object(TestResultsProcessorTask, "app", celery_app) - - commit = CommitFactory.create( - message="hello world", - commitid="cd76b0821854a780b60012aed85af0a8263004ad", - repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", - repository__owner__username="joseph-sentry", - repository__owner__service="github", - repository__name="codecov-demo", - ) - dbsession.add(commit) - dbsession.flush() - current_report_row = CommitReport(commit_id=commit.id_) - dbsession.add(current_report_row) - dbsession.flush() - result = TestResultsProcessorTask().run_impl( - dbsession, - previous_result=False, - repoid=upload.report.commit.repoid, - commitid=commit.commitid, - commit_yaml={"codecov": {"max_report_age": False}}, - arguments_list=redis_queue, - ) - expected_result = True - - tests = dbsession.query(Test).all() - test_instances = dbsession.query(TestInstance).all() - - assert expected_result == result - assert len(tests) == 4 - assert len(test_instances) == 4 diff --git a/tasks/tests/unit/test_upload_task.py b/tasks/tests/unit/test_upload_task.py index 57369c200..94375c064 100644 --- a/tasks/tests/unit/test_upload_task.py +++ b/tasks/tests/unit/test_upload_task.py @@ -6,7 +6,7 @@ import mock import pytest from celery.exceptions import Retry -from mock import call +from mock import AsyncMock, call from redis.exceptions import LockError from shared.reports.enums import UploadState, UploadType from shared.torngit import GitlabEnterprise @@ -32,6 +32,8 @@ from services.report import NotReadyToBuildReportYetError, ReportService from tasks.bundle_analysis_notify import bundle_analysis_notify_task from tasks.bundle_analysis_processor import bundle_analysis_processor_task +from tasks.ta_finisher import ta_finisher_task +from tasks.ta_processor import ta_processor_task from tasks.test_results_finisher import test_results_finisher_task from tasks.test_results_processor import test_results_processor_task from tasks.upload import UploadContext, UploadTask @@ -437,6 +439,111 @@ def test_upload_task_call_test_results( notify_sig = test_results_finisher_task.signature(kwargs=kwargs) chain.assert_called_with(*[processor_sig, notify_sig]) + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call_new_ta_tasks( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + chord = mocker.patch("tasks.upload.chord") + _ = mocker.patch("tasks.upload.NEW_TA_TASKS.check_value", return_value=True) + storage_path = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + redis_queue = [{"url": storage_path, "build_code": "some_random_build"}] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mocker.patch.object(UploadTask, "app", celery_app) + + mock_repo_provider_service = AsyncMock() + mock_repo_provider_service.get_commit.return_value = { + "author": { + "id": "123", + "username": "456", + "email": "789", + "name": "101", + }, + "message": "hello world", + "parents": [], + "timestamp": str(datetime.now()), + } + mock_repo_provider_service.get_ancestors_tree.return_value = {"parents": []} + mock_repo_provider_service.get_pull_request.return_value = { + "head": {"branch": "main"}, + "base": {}, + } + mock_repo_provider_service.list_top_level_files.return_value = [ + {"name": "codecov.yml", "path": "codecov.yml"} + ] + mock_repo_provider_service.get_source.return_value = { + "content": """ + codecov: + max_report_age: 1y ago + """ + } + + mocker.patch( + "tasks.upload.get_repo_provider_service", + return_value=mock_repo_provider_service, + ) + mocker.patch("tasks.upload.hasattr", return_value=False) + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__oauth_token="GHTZB+Mi+kbl/ubudnSKTJYb/fgN4hRJVJYSIErtidEsCLDJBb8DZzkbXqLujHAnv28aKShXddE/OffwRuwKug==", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + branch="main", + ) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}/test_results"] = ( + jsonified_redis_queue + ) + + UploadTask().run_impl( + dbsession, + commit.repoid, + commit.commitid, + report_type="test_results", + ) + commit_report = commit.commit_report(report_type=ReportType.TEST_RESULTS) + assert commit_report + uploads = commit_report.uploads + assert len(uploads) == 1 + upload = dbsession.query(Upload).filter_by(report_id=commit_report.id).first() + processor_sig = ta_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + argument={ + "url": storage_path, + "flags": [], + "build_code": "some_random_build", + "upload_id": upload.id, + "upload_pk": upload.id, + }, + ) + kwargs = dict( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + checkpoints_TestResultsFlow=None, + ) + + kwargs[_kwargs_key(TestResultsFlow)] = mocker.ANY + notify_sig = ta_finisher_task.signature(kwargs=kwargs) + chord.assert_called_with([processor_sig], notify_sig) + def test_upload_task_call_no_jobs( self, mocker, diff --git a/tasks/upload.py b/tasks/upload.py index c74e8c13a..14554f5e0 100644 --- a/tasks/upload.py +++ b/tasks/upload.py @@ -31,6 +31,7 @@ from helpers.exceptions import RepositoryWithoutValidBotError from helpers.github_installation import get_installation_name_for_owner_for_task from helpers.save_commit_error import save_commit_error +from rollouts import NEW_TA_TASKS from services.archive import ArchiveService from services.bundle_analysis.report import BundleAnalysisReportService from services.processing.state import ProcessingState @@ -52,6 +53,8 @@ from tasks.base import BaseCodecovTask from tasks.bundle_analysis_notify import bundle_analysis_notify_task from tasks.bundle_analysis_processor import bundle_analysis_processor_task +from tasks.ta_finisher import ta_finisher_task +from tasks.ta_processor import ta_processor_task from tasks.test_results_finisher import test_results_finisher_task from tasks.test_results_processor import test_results_processor_task from tasks.upload_finisher import upload_finisher_task @@ -601,7 +604,7 @@ def schedule_task( ) elif upload_context.report_type == ReportType.TEST_RESULTS: assert commit_report.report_type == ReportType.TEST_RESULTS.value - return self._schedule_test_results_processing_task( + return self._schedule_ta_processing_task( commit, commit_yaml, argument_list, commit_report ) @@ -672,36 +675,56 @@ def _schedule_bundle_analysis_processing_task( return chain(task_signatures).apply_async() - def _schedule_test_results_processing_task( + def _schedule_ta_processing_task( self, commit: Commit, commit_yaml: dict, argument_list: list[UploadArguments], commit_report: CommitReport, ): - task_group = [ - test_results_processor_task.s( - repoid=commit.repoid, - commitid=commit.commitid, - commit_yaml=commit_yaml, - arguments_list=list(chunk), - report_code=commit_report.code, - ) - for chunk in itertools.batched(argument_list, CHUNK_SIZE) - ] + if NEW_TA_TASKS.check_value(commit.repoid): + ta_proc_group = [ + ta_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + argument=argument, + ) + for argument in argument_list + ] + ta_finisher_kwargs = { + "repoid": commit.repoid, + "commitid": commit.commitid, + "commit_yaml": commit_yaml, + } + ta_finisher_kwargs = TestResultsFlow.save_to_kwargs(ta_finisher_kwargs) + ta_finisher_task_sig = ta_finisher_task.s(**ta_finisher_kwargs) + + return chord(ta_proc_group, ta_finisher_task_sig).apply_async() + else: + task_group = [ + test_results_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + arguments_list=list(chunk), + report_code=commit_report.code, + ) + for chunk in itertools.batched(argument_list, CHUNK_SIZE) + ] - task_group[0].args = (False,) + task_group[0].args = (False,) - finisher_kwargs = { - "repoid": commit.repoid, - "commitid": commit.commitid, - "commit_yaml": commit_yaml, - } - finisher_kwargs = TestResultsFlow.save_to_kwargs(finisher_kwargs) - task_group.append( - test_results_finisher_task.signature(kwargs=finisher_kwargs), - ) - return chain(*task_group).apply_async() + finisher_kwargs = { + "repoid": commit.repoid, + "commitid": commit.commitid, + "commit_yaml": commit_yaml, + } + finisher_kwargs = TestResultsFlow.save_to_kwargs(finisher_kwargs) + task_group.append( + test_results_finisher_task.signature(kwargs=finisher_kwargs), + ) + return chain(*task_group).apply_async() def possibly_carryforward_bundle_report( self, From 4159b65e105d403512d954694f53f16cdc3c17fe Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Mon, 13 Jan 2025 09:58:26 -0500 Subject: [PATCH 2/2] fix requirements.txt --- requirements.txt | 234 +++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 128 deletions(-) diff --git a/requirements.txt b/requirements.txt index 26ba4c927..c0db6b762 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,16 @@ # This file was autogenerated by uv via the following command: # uv pip compile requirements.in -o requirements.txt -amqp==5.3.1 +amqp==5.2.0 # via kombu analytics-python==1.3.0b1 # via -r requirements.in -annotated-types==0.7.0 +annotated-types==0.6.0 # via pydantic -anyio==4.8.0 +anyio==3.6.1 # via - # httpx + # httpcore # openai -argon2-cffi==23.1.0 - # via minio -argon2-cffi-bindings==21.2.0 - # via argon2-cffi -asgiref==3.8.1 +asgiref==3.7.2 # via # -r requirements.in # django @@ -24,52 +20,52 @@ billiard==4.2.1 # via # -r requirements.in # celery -boto3==1.35.97 +boto3==1.34.73 # via # -r requirements.in # shared -botocore==1.35.97 +botocore==1.34.73 # via # boto3 # s3transfer -cachetools==5.5.0 +cachetools==4.2.1 # via # google-auth # shared -celery==5.4.0 +celery==5.3.6 # via # -r requirements.in # pytest-celery # sentry-sdk -cerberus==1.3.7 +cerberus==1.3.5 # via shared -certifi==2024.12.14 +certifi==2024.7.4 # via # httpcore # httpx # minio # requests # sentry-sdk -cffi==1.17.1 +cffi==1.14.5 # via - # argon2-cffi-bindings # cryptography + # google-crc32c cfgv==3.4.0 # via pre-commit -charset-normalizer==3.4.1 +charset-normalizer==2.0.12 # via requests -click==8.1.8 +click==8.1.7 # via # -r requirements.in # celery # click-didyoumean # click-plugins # click-repl -click-didyoumean==0.3.1 +click-didyoumean==0.3.0 # via celery click-plugins==1.1.1 # via celery -click-repl==0.3.0 +click-repl==0.2.0 # via celery codecov-ribs==0.1.18 # via @@ -77,23 +73,21 @@ codecov-ribs==0.1.18 # shared colour==0.1.5 # via shared -coverage==7.6.10 +coverage==7.5.0 # via # -r requirements.in # pytest-cov -cryptography==44.0.0 +cryptography==43.0.1 # via shared -debugpy==1.8.11 - # via pytest-celery deprecated==1.2.15 # via # opentelemetry-api # opentelemetry-semantic-conventions -distlib==0.3.9 +distlib==0.3.7 # via virtualenv -distro==1.9.0 +distro==1.8.0 # via openai -django==4.2.17 +django==4.2.16 # via # -r requirements.in # django-model-utils @@ -109,26 +103,22 @@ django-postgres-extra==2.0.8 # shared django-prometheus==2.3.1 # via shared -docker==7.1.0 - # via - # pytest-celery - # pytest-docker-tools -factory-boy==3.3.1 +factory-boy==3.2.0 # via -r requirements.in -faker==33.3.1 +faker==8.8.2 # via factory-boy -filelock==3.16.1 +filelock==3.12.4 # via virtualenv -freezegun==1.5.1 +freezegun==1.5.0 # via pytest-freezegun -google-api-core==2.24.0 +google-api-core==2.23.0 # via # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-core # google-cloud-pubsub # google-cloud-storage -google-auth==2.37.0 +google-auth==2.36.0 # via # google-api-core # google-cloud-bigquery @@ -145,15 +135,15 @@ google-cloud-core==2.4.1 # via # google-cloud-bigquery # google-cloud-storage -google-cloud-pubsub==2.27.2 +google-cloud-pubsub==2.27.1 # via # -r requirements.in # shared -google-cloud-storage==2.19.0 +google-cloud-storage==2.18.2 # via # -r requirements.in # shared -google-crc32c==1.6.0 +google-crc32c==1.1.2 # via # google-cloud-storage # google-resumable-media @@ -168,7 +158,7 @@ googleapis-common-protos==1.66.0 # grpcio-status grpc-google-iam-v1==0.14.0 # via google-cloud-pubsub -grpcio==1.69.0 +grpcio==1.68.1 # via # -r requirements.in # google-api-core @@ -176,65 +166,63 @@ grpcio==1.69.0 # googleapis-common-protos # grpc-google-iam-v1 # grpcio-status -grpcio-status==1.69.0 +grpcio-status==1.58.0 # via # google-api-core # google-cloud-pubsub h11==0.14.0 # via httpcore -httpcore==1.0.7 +httpcore==0.16.3 # via httpx -httpx==0.28.1 +httpx==0.23.1 # via # -r requirements.in # openai # respx # shared -identify==2.6.5 +identify==2.5.30 # via pre-commit -idna==3.10 +idna==3.7 # via # anyio - # httpx # requests + # rfc3986 # yarl -ijson==3.3.0 +ijson==3.2.3 # via shared importlib-metadata==8.5.0 # via opentelemetry-api -iniconfig==2.0.0 +iniconfig==1.1.1 # via pytest -jinja2==3.1.5 +jinja2==3.1.4 # via -r requirements.in -jiter==0.8.2 - # via openai -jmespath==1.0.1 +jmespath==0.10.0 # via # boto3 # botocore -kombu==5.4.2 +kombu==5.3.5 # via celery lxml==5.3.0 # via -r requirements.in -markupsafe==3.0.2 +markupsafe==2.1.3 # via jinja2 -minio==7.2.14 +minio==7.1.13 # via shared -mmh3==5.0.1 +mmh3==4.0.1 # via shared -mock==5.1.0 +mock==4.0.3 # via -r requirements.in -monotonic==1.6 +monotonic==1.5 # via analytics-python multidict==6.1.0 # via # -r requirements.in # yarl -nodeenv==1.9.1 +nodeenv==1.8.0 # via pre-commit -oauthlib==3.2.2 +oauthlib==3.1.0 # via shared -openai==1.59.6 +openai==1.2.4 # via -r requirements.in opentelemetry-api==1.29.0 # via @@ -245,37 +233,35 @@ opentelemetry-sdk==1.29.0 # via google-cloud-pubsub opentelemetry-semantic-conventions==0.50b0 # via opentelemetry-sdk -orjson==3.10.14 +orjson==3.10.11 # via # -r requirements.in # shared -packaging==24.2 +packaging==24.1 # via # google-cloud-bigquery # pytest -platformdirs==4.3.6 +platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest polars==1.12.0 # via -r requirements.in -pre-commit==4.0.1 +pre-commit==3.4.0 # via -r requirements.in -prometheus-client==0.21.1 +prometheus-client==0.17.1 # via # django-prometheus # shared -prompt-toolkit==3.0.48 +prompt-toolkit==3.0.28 # via click-repl -propcache==0.2.1 - # via yarl proto-plus==1.25.0 # via # -r requirements.in # google-api-core # google-cloud-bigquery-storage # google-cloud-pubsub -protobuf==5.29.3 +protobuf==5.29.2 # via # -r requirements.in # google-api-core @@ -285,59 +271,52 @@ protobuf==5.29.3 # grpc-google-iam-v1 # grpcio-status # proto-plus -psutil==6.1.1 - # via pytest-celery psycopg2==2.9.10 # via -r requirements.in -pyasn1==0.6.1 +pyasn1==0.4.8 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.1 +pyasn1-modules==0.2.8 # via google-auth -pycparser==2.22 +pycparser==2.20 # via cffi -pycryptodome==3.21.0 - # via minio -pydantic==2.10.5 +pydantic==2.10.4 # via # -r requirements.in # openai # shared pydantic-core==2.27.2 # via pydantic -pyjwt==2.10.1 +pyjwt==2.10.0 # via # -r requirements.in # shared -pyparsing==3.2.1 +pyparsing==2.4.7 # via shared -pytest==8.3.4 +pytest==8.1.1 # via # -r requirements.in # pytest-asyncio # pytest-cov # pytest-django - # pytest-docker-tools # pytest-freezegun # pytest-insta # pytest-mock # pytest-sqlalchemy -pytest-asyncio==0.25.2 +pytest-asyncio==0.14.0 # via -r requirements.in -pytest-celery==1.1.3 +pytest-celery==0.0.0 # via -r requirements.in -pytest-cov==6.0.0 +pytest-cov==5.0.0 # via -r requirements.in -pytest-django==4.9.0 +pytest-django==4.7.0 # via -r requirements.in -pytest-docker-tools==3.1.3 - # via pytest-celery pytest-freezegun==0.4.2 # via -r requirements.in pytest-insta==0.3.0 # via -r requirements.in -pytest-mock==3.14.0 +pytest-mock==1.13.0 # via -r requirements.in pytest-sqlalchemy==0.2.1 # via -r requirements.in @@ -352,124 +331,123 @@ python-dateutil==2.9.0.post0 # freezegun # google-cloud-bigquery # time-machine -python-json-logger==3.2.1 +python-json-logger==0.1.11 # via -r requirements.in python-redis-lock==4.0.0 # via # -r requirements.in # shared -pytz==2024.2 +pytz==2022.1 # via timestring -pyyaml==6.0.2 +pyyaml==6.0.1 # via # -r requirements.in # pre-commit # shared # vcrpy -redis==5.2.1 +redis==4.5.4 # via # -r requirements.in # python-redis-lock # shared -regex==2024.11.6 +regex==2023.12.25 # via -r requirements.in requests==2.32.3 # via # -r requirements.in # analytics-python - # docker # google-api-core # google-cloud-bigquery # google-cloud-storage # shared # stripe -respx==0.22.0 +respx==0.20.2 # via -r requirements.in -rsa==4.9 +rfc3986==1.4.0 + # via httpx +rsa==4.7.2 # via google-auth -s3transfer==0.10.4 +s3transfer==0.10.1 # via boto3 -sentry-sdk==2.19.2 +sentry-sdk==2.13.0 # via # -r requirements.in # shared -setuptools==75.8.0 - # via pytest-celery +setuptools==75.7.0 + # via nodeenv shared @ https://github.com/codecov/shared/archive/609e56d2aa30b26d44cddaba0e1ebd79ba954ac9.tar.gz#egg=shared # via -r requirements.in -six==1.17.0 +six==1.16.0 # via # analytics-python + # click-repl # python-dateutil -sniffio==1.3.1 + # sqlalchemy-utils + # vcrpy +sniffio==1.2.0 # via # anyio - # openai -sqlalchemy==1.4.54 + # httpcore + # httpx +sqlalchemy==1.3.23 # via # -r requirements.in # pytest-sqlalchemy # shared # sqlalchemy-utils -sqlalchemy-utils==0.41.2 +sqlalchemy-utils==0.36.8 # via # -r requirements.in # pytest-sqlalchemy -sqlparse==0.5.3 +sqlparse==0.5.0 # via django -statsd==4.0.1 +statsd==3.3.0 # via -r requirements.in stripe==11.4.1 # via -r requirements.in -tenacity==9.0.0 - # via pytest-celery test-results-parser @ https://github.com/codecov/test-results-parser/archive/190bbc8a911099749928e13d5fe57f6027ca1e74.tar.gz#egg=test-results-parser # via -r requirements.in -time-machine==2.16.0 +text-unidecode==1.3 + # via faker +time-machine==2.14.1 # via -r requirements.in timestring @ https://github.com/codecov/timestring/archive/d37ceacc5954dff3b5bd2f887936a98a668dda42.tar.gz#egg=timestring # via -r requirements.in -tqdm==4.67.1 +tqdm==4.66.1 # via openai typing-extensions==4.12.2 # via - # faker - # minio # openai # opentelemetry-sdk # pydantic # pydantic-core # stripe -tzdata==2024.2 - # via - # celery - # kombu -urllib3==2.3.0 +tzdata==2024.1 + # via celery +urllib3==1.26.19 # via # -r requirements.in # botocore - # docker # minio # requests # sentry-sdk - # vcrpy -vcrpy==7.0.0 +vcrpy==4.1.1 # via -r requirements.in vine==5.1.0 # via # amqp # celery # kombu -virtualenv==20.28.1 +virtualenv==20.24.5 # via pre-commit -wcwidth==0.2.13 +wcwidth==0.2.5 # via prompt-toolkit -wrapt==1.17.0 +wrapt==1.16.0 # via # deprecated # pytest-insta # vcrpy -yarl==1.18.3 +yarl==1.9.4 # via vcrpy zipp==3.21.0 # via importlib-metadata