diff --git a/services/report/tests/unit/test_transplant.py b/services/report/tests/unit/test_transplant.py new file mode 100644 index 000000000..41d1706ee --- /dev/null +++ b/services/report/tests/unit/test_transplant.py @@ -0,0 +1,49 @@ +import pytest +from shared.django_apps.core.tests.factories import ( + CommitFactory, + CommitWithReportFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import ReportLevelTotals + +from services.archive import ArchiveService +from services.report.transplant import transplant_commit_report + + +@pytest.mark.django_db() +def test_transplanting_commit(mock_storage): + repo = RepositoryFactory() + commit_from = CommitWithReportFactory(repository=repo) + + archive_service = ArchiveService(repo) + with open("tasks/tests/samples/sample_chunks_1.txt", "rb") as f: + chunks = f.read() + archive_service.write_chunks(commit_from.commitid, chunks) + + commit_to = CommitFactory(repository=repo) + + transplant_commit_report( + repo_id=repo.repoid, from_sha=commit_from.commitid, to_sha=commit_to.commitid + ) + commit_to.refresh_from_db() + + from_totals = commit_from.commitreport.reportleveltotals + to_totals = commit_to.commitreport.reportleveltotals + + def totals_tuple(totals: ReportLevelTotals): + return ( + totals.branches, + totals.coverage, + totals.hits, + totals.lines, + totals.methods, + totals.misses, + totals.partials, + totals.files, + ) + + assert totals_tuple(from_totals) == totals_tuple(to_totals) + + report = commit_to.full_report + file = report.get("tests/__init__.py") + assert file.get(1).coverage == 1 diff --git a/services/report/transplant.py b/services/report/transplant.py new file mode 100644 index 000000000..aaa17de6f --- /dev/null +++ b/services/report/transplant.py @@ -0,0 +1,52 @@ +from shared.django_apps.core.models import Commit +from shared.django_apps.reports.models import CommitReport, ReportType + +from services.archive import ArchiveService + + +def transplant_commit_report(repo_id: int, from_sha: str, to_sha: str): + """ + This copies a `Report` from one commit to another commit. + + The `to_sha` commit has to exist already (being auto-created using a git provider sync). + + It does so by creating a copy of the underlying `report_json` and `chunks` storage files, + as well as some related DB models. + """ + + from_commit = Commit.objects.select_related("repository").get( + repository=repo_id, commitid=from_sha + ) + to_commit = Commit.objects.get(repository=repo_id, commitid=to_sha) + + archive_service = ArchiveService(from_commit.repository) + + chunks = archive_service.read_chunks(from_commit.commitid) + report_json = from_commit.report + totals = from_commit.totals + + archive_service.write_chunks(to_commit.commitid, chunks) + + to_commit.report = report_json + to_commit.totals = totals + to_commit.state = "complete" + to_commit.save() + + if old_commit_report := from_commit.commitreport: + commit_report = CommitReport( + commit=to_commit, report_type=ReportType.COVERAGE.value + ) + commit_report.save() + + if totals := old_commit_report.reportleveltotals: + # See + totals.pk = None + totals.id = None + totals._state.adding = True + + totals.report = commit_report + totals.save() + + # TODO: + # We might also have to create copies of all of `Upload` (aka `Reportsession`), + # `UploadLevelTotals`, `UploadError` and `UploadFlagMembership` diff --git a/tasks/__init__.py b/tasks/__init__.py index 04c246658..7270e6d2d 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -56,6 +56,7 @@ timeseries_backfill_dataset_task, ) from tasks.timeseries_delete import timeseries_delete_task +from tasks.transplant_report import transplant_report_task from tasks.trial_expiration import trial_expiration_task from tasks.trial_expiration_cron import trial_expiration_cron_task from tasks.update_branches import update_branches_task_name diff --git a/tasks/compute_comparison.py b/tasks/compute_comparison.py index 8c0058107..fcbff0c98 100644 --- a/tasks/compute_comparison.py +++ b/tasks/compute_comparison.py @@ -13,7 +13,7 @@ from app import celery_app from database.enums import CompareCommitError, CompareCommitState from database.models import CompareCommit, CompareComponent, CompareFlag -from database.models.reports import ReportLevelTotals, RepositoryFlag +from database.models.reports import RepositoryFlag from helpers.comparison import minimal_totals from helpers.github_installation import get_installation_name_for_owner_for_task from rollouts import PARALLEL_COMPONENT_COMPARISON @@ -216,7 +216,7 @@ def store_flag_comparison( db_session, comparison: CompareCommit, repositoryflag: RepositoryFlag, - totals: ReportLevelTotals, + totals, ): flag_comparison = CompareFlag( commit_comparison=comparison, diff --git a/tasks/transplant_report.py b/tasks/transplant_report.py new file mode 100644 index 000000000..0f25aebfa --- /dev/null +++ b/tasks/transplant_report.py @@ -0,0 +1,12 @@ +from app import celery_app +from services.report.transplant import transplant_commit_report +from tasks.base import BaseCodecovTask + + +class TransplantReportTask(BaseCodecovTask, name="app.tasks.reports.transplant_report"): + def run_impl(self, db_session, repo_id: int, from_sha: str, to_sha: str): + transplant_commit_report(repo_id, from_sha, to_sha) + + +RegisteredTransplantReportTask = celery_app.register_task(TransplantReportTask()) +transplant_report_task = celery_app.tasks[RegisteredTransplantReportTask.name]