Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add root_organization property to worker, clean up decoration service, change notification urls for GL #990

Merged
merged 5 commits into from
Jan 16, 2025

Conversation

nora-codecov
Copy link
Contributor

Decoration service was causing confusion for GitLab users. Cleaned it up and added some comments to clarify the current GL org strategy which is:

  • the root org owns the plan, so when dealing with a GL org's plan, look at the plan from the root org
  • membership and activation through plan_activated_user also happens on the root org instead of a subgroup org
  • if a notification has the plan or member url, give GL users the correct url (correct url is to root group, not subgroup)

more details on ticket codecov/engineering-team#2710

@nora-codecov nora-codecov requested a review from a team January 4, 2025 01:33
Copy link

github-actions bot commented Jan 4, 2025

This PR includes changes to shared. Please review them here: codecov/shared@2674ae9...3a445a9

.filter_by(service_id=service_id, service=self.service)
.one_or_none()
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not new - it's in shared, I'm just replicating it in worker.
I learned that in order to use shared's PlanService, which now calls .root_organization, sqlalchemy also needs to know about this property, so I had to add it.

@codecov-notifications
Copy link

codecov-notifications bot commented Jan 4, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Copy link

codecov bot commented Jan 4, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 97.75%. Comparing base (2c7bd18) to head (7e8797f).
Report is 1 commits behind head on main.

❌ We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #990      +/-   ##
==========================================
+ Coverage   97.74%   97.75%   +0.01%     
==========================================
  Files         451      451              
  Lines       36670    36806     +136     
==========================================
+ Hits        35843    35981     +138     
+ Misses        827      825       -2     
Flag Coverage Δ
integration 42.47% <24.44%> (-0.10%) ⬇️
unit 90.24% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

⚠️ Impact Analysis from Codecov is deprecated and will be sunset on Jan 31 2025. See more

Copy link

github-actions bot commented Jan 4, 2025

✅ All tests successful. No failed tests were found.

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

root = None
if self.service == "gitlab" and self.parent_service_id:
root = self
while root.parent_service_id is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (but could be wrong) if self._get_owner_by_service can return None (from the .one_or_none() it seems possible, there are no typehints in the functions) then you will have a NoneType has no property "parent_service_id" error.

It would be good to add explicit typehints to the functions too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, could we ever infinite loop because the parent_service_id is always None? Would we like to cap it after 10 iterations or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added typehints, and changed that query to .one() @giovanni-guidini

@adrian-codecov if parent_service_id is None the while breaks - I think you mean the opposite, it will infinite loop if it's never None. This condition would only happen if our data was bad and there was a set of Owners referencing back and forth at each other. We don't have anything to prevent this, but it really should never happen. We have GL webhooks that should keep this field updated with the state on GL. I think the risk is low :)

@@ -31,8 +29,8 @@ class DecorationDetails(object):
decoration_type: Decoration
reason: str
should_attempt_author_auto_activation: bool = False
activation_org_ownerid: int = None
activation_author_ownerid: int = None
activation_org_ownerid: int | None = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 ty

func.public.get_gitlab_root_group(org.ownerid)
).first()
# do not access plan directly - only through PlanService
org_plan = PlanService(current_org=org)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PlanService doesn't seem to go looking for the root org (looking at main)

I think I'm missing a code change... does this PR depend on an unmerged change to shared too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, codecov/shared#461 ok ok

@@ -50,7 +48,7 @@ def determine_uploads_used(plan_service: PlanService) -> int:

def determine_decoration_details(
enriched_pull: EnrichedPull, empty_upload=None
) -> dict:
) -> DecorationDetails:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏆

@@ -57,6 +58,16 @@ def get_dashboard_base_url() -> str:
return configured_dashboard_url or "https://app.codecov.io"


def _get_username_for_url(repository: Repository) -> str:
username = repository.owner.username
if repository.owner.service == Service.GITLAB.value:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk if we save service always as "gitlab" vs "GitLab" or something like that, might be worth lowercasing this just in case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it follows the enum values in Service, just checked metabase, it will only be gitlab

Copy link

github-actions bot commented Jan 8, 2025

This PR includes changes to shared. Please review them here: codecov/shared@609e56d...990c398

@codecov-qa
Copy link

codecov-qa bot commented Jan 8, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1786 1 1785 4
View the top 1 failed tests by shortest run time
tasks/tests/unit/test_ta_finisher_task.py::test_test_analytics
Stack Traces | 0.126s run time
dbsession = <sqlalchemy.orm.session.Session object at 0x7fe329fdeb70>
mocker = <pytest_mock.plugin.MockFixture object at 0x7fe322dfc290>
mock_storage = <shared.storage.memory.MemoryStorageService object at 0x7fe30d7e11f0>
celery_app = <Celery celery.tests at 0x7fe330067c50>, snapshot = snapshot

    @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}},
        )

.../tests/unit/test_ta_finisher_task.py:221: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tasks/ta_finisher.py:236: in run_impl
    finisher_result = self.process_impl_within_lock(
tasks/ta_finisher.py:327: in process_impl_within_lock
    seat_activation_result = self.seat_activation(
tasks/ta_finisher.py:373: in seat_activation
    activate_seat_info = determine_seat_activation(pull)
services/seats.py:62: in determine_seat_activation
    org_plan = PlanService(current_org=org)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.plan.service.PlanService object at 0x7fe30d613bf0>
current_org = <AsyncMock name='mock.database_pull.repository.owner' id='140613739966016'>

    def __init__(self, current_org: Owner):
        """
        Initializes a PlanService object for a specific organization.
    
        Args:
            current_org (Owner): The organization for which the plan service is being initialized.
    
        Raises:
            ValueError: If the organization's plan is unsupported.
        """
        if (
            current_org.service == Service.GITLAB.value
            and current_org.parent_service_id
        ):
            # for GitLab groups and subgroups, use the plan on the root org
            self.current_org = current_org.root_organization
        else:
            self.current_org = current_org
        if self.current_org.plan not in USER_PLAN_REPRESENTATIONS:
>           raise ValueError("Unsupported plan")
E           ValueError: Unsupported plan

.../local/lib/python3.13.../shared/plan/service.py:59: ValueError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Copy link

codecov-public-qa bot commented Jan 8, 2025

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1786 1 1785 4
View the top 1 failed tests by shortest run time
tasks/tests/unit/test_ta_finisher_task.py::::test_test_analytics
Stack Traces | 0.126s run time
dbsession = <sqlalchemy.orm.session.Session object at 0x7fe329fdeb70>
mocker = <pytest_mock.plugin.MockFixture object at 0x7fe322dfc290>
mock_storage = <shared.storage.memory.MemoryStorageService object at 0x7fe30d7e11f0>
celery_app = <Celery celery.tests at 0x7fe330067c50>, snapshot = snapshot

    @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}},
        )

.../tests/unit/test_ta_finisher_task.py:221: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tasks/ta_finisher.py:236: in run_impl
    finisher_result = self.process_impl_within_lock(
tasks/ta_finisher.py:327: in process_impl_within_lock
    seat_activation_result = self.seat_activation(
tasks/ta_finisher.py:373: in seat_activation
    activate_seat_info = determine_seat_activation(pull)
services/seats.py:62: in determine_seat_activation
    org_plan = PlanService(current_org=org)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.plan.service.PlanService object at 0x7fe30d613bf0>
current_org = <AsyncMock name='mock.database_pull.repository.owner' id='140613739966016'>

    def __init__(self, current_org: Owner):
        """
        Initializes a PlanService object for a specific organization.
    
        Args:
            current_org (Owner): The organization for which the plan service is being initialized.
    
        Raises:
            ValueError: If the organization's plan is unsupported.
        """
        if (
            current_org.service == Service.GITLAB.value
            and current_org.parent_service_id
        ):
            # for GitLab groups and subgroups, use the plan on the root org
            self.current_org = current_org.root_organization
        else:
            self.current_org = current_org
        if self.current_org.plan not in USER_PLAN_REPRESENTATIONS:
>           raise ValueError("Unsupported plan")
E           ValueError: Unsupported plan

.../local/lib/python3.13.../shared/plan/service.py:59: ValueError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

@adrian-codecov
Copy link
Contributor

Lgtm, waiting on the Shared update and can approve then 👌

@nora-codecov nora-codecov force-pushed the nora/2710 branch 2 times, most recently from b1ea58a to a6005df Compare January 8, 2025 20:06
@adrian-codecov
Copy link
Contributor

@nora-codecov lmk when this is ready for a 2nd pass 👍

@nora-codecov nora-codecov force-pushed the nora/2710 branch 2 times, most recently from 2a3069e to ad41e11 Compare January 14, 2025 23:16
Copy link

github-actions bot commented Jan 14, 2025

This PR includes changes to shared. Please review them here: codecov/shared@de4b37b...191837f

@nora-codecov nora-codecov added this pull request to the merge queue Jan 16, 2025
Merged via the queue into main with commit 40613b5 Jan 16, 2025
26 of 27 checks passed
@nora-codecov nora-codecov deleted the nora/2710 branch January 16, 2025 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants