| # Copyright 2025 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Full test of limited_cq.""" |
| |
| from __future__ import annotations |
| |
| from collections.abc import Iterator |
| |
| from recipe_engine import post_process, recipe_api, recipe_test_api |
| |
| from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb |
| from PB.recipe_modules.pigweed.limited_cq.tests.full import InputProperties |
| |
| from RECIPE_MODULES.pigweed.checkout import api as checkout_api |
| |
| DEPS = [ |
| 'pigweed/checkout', |
| 'pigweed/limited_cq', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/cv', |
| 'recipe_engine/properties', |
| 'recipe_engine/step', |
| ] |
| |
| INLINE_PROPERTIES_PROTO = """ |
| import "recipe_modules/pigweed/checkout/options.proto"; |
| |
| message InputProperties { |
| recipe_modules.pigweed.checkout.Options checkout_options = 1; |
| } |
| """ |
| |
| PROPERTIES = InputProperties |
| |
| |
| def RunSteps( |
| api: recipe_api.RecipeApi, |
| props: InputProperties, |
| ) -> None: |
| checkout = api.checkout(options=props.checkout_options) |
| res = api.limited_cq.exit_early_if_not_approved( |
| checkout.changes, |
| verbose=True, |
| ) |
| if res: |
| with api.step.nest('status'): |
| status = { |
| common_pb.SUCCESS: 'SUCCESS', |
| common_pb.FAILURE: 'FAILURE', |
| common_pb.INFRA_FAILURE: 'INFRA_FAILURE', |
| }[res.status] |
| api.step.empty(str(status)) |
| |
| |
| def GenTests( |
| api: recipe_test_api.RecipeTestApi, |
| ) -> Iterator[recipe_test_api.TestData]: |
| def assert_public_project(): |
| return api.post_check( |
| post_process.MustRun, |
| 'checking change approvals.project is public', |
| ) |
| |
| def assert_not_tryjob(): |
| return api.post_check( |
| post_process.MustRun, |
| 'checking change approvals.not a tryjob', |
| ) |
| |
| def assert_continue(reason=None): |
| reason_part = rf'\.{reason}' if reason else '' |
| return ( |
| api.post_check( |
| post_process.MustRunRE, |
| rf'checking change approvals.*continue{reason_part}', |
| ) |
| + api.post_check( |
| post_process.DoesNotRun, |
| 'checking change approvals.exit early', |
| ) |
| + api.post_check( |
| post_process.DoesNotRun, |
| 'checking change approvals.exit early', |
| ) |
| + api.post_check( |
| post_process.PropertiesDoNotContain, |
| '$recipe_engine/cv/output', |
| ) |
| ) |
| |
| def assert_exit_early(): |
| def validate_reuse_properties(check, prop): |
| if check('reusability' in prop): |
| if check('modeAllowlist' in prop['reusability']): |
| check( |
| 'NEW_PATCHSET_RUN' |
| in prop['reusability']['modeAllowlist'], |
| ) |
| check(len(prop['reusability']['modeAllowlist']) == 1) |
| return True |
| |
| return ( |
| api.post_check( |
| post_process.MustRun, |
| 'checking change approvals.exit early', |
| ) |
| + api.post_check( |
| post_process.DoesNotRunRE, |
| r'checking change approvals\..*\.continue', |
| ) |
| + api.post_process( |
| post_process.PropertyMatchesCallable, |
| '$recipe_engine/cv/output', |
| validate_reuse_properties, |
| ) |
| ) |
| |
| def assert_status(status: str): |
| return api.post_check(post_process.MustRun, f'status.{status}') |
| |
| def drop_expectations_must_be_last(): |
| # No need for expectation files, everything of note here is tested by |
| # assertions. This must be the last thing added to the test. |
| return api.post_process(post_process.DropExpectation) |
| |
| def defaults( |
| project='pigweed-internal', |
| cv_active=True, |
| run_mode=api.cv.DRY_RUN, |
| bb_props=api.buildbucket.try_build, |
| tags=(), |
| created_by=None, |
| ): |
| bb_kwargs = {} |
| if created_by: |
| bb_kwargs['created_by'] = created_by |
| res = bb_props( |
| project=project, |
| git_repo=checkout_api.PIGWEED_REMOTE, |
| tags=tags, |
| **bb_kwargs, |
| ) |
| if cv_active: |
| res += api.cv(run_mode=run_mode) |
| res += api.properties(checkout_options=api.checkout.git_options()) |
| return res |
| |
| yield api.test( |
| 'skipped-project', |
| defaults(project='pigweed'), |
| api.cv(run_mode=api.cv.DRY_RUN), |
| assert_public_project(), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'skipped-ci', |
| defaults(bb_props=api.buildbucket.ci_build), |
| api.cv(run_mode=api.cv.DRY_RUN), |
| api.properties(checkout_options=api.checkout.git_options()), |
| assert_not_tryjob(), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'approved-owners', |
| defaults(), |
| api.checkout.cl_branch_parents( |
| submit_requirements={ |
| 'Code-Owners': 'SATISFIED', |
| 'Code-Review': 'ERROR', |
| 'Review-Enforcement': 'UNSATISFIED', |
| }, |
| ), |
| assert_continue('Code-Owners'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'approved-code-review', |
| defaults(), |
| api.checkout.cl_branch_parents( |
| submit_requirements={ |
| 'Code-Owners': 'ERROR', |
| 'Code-Review': 'OVERRIDDEN', |
| 'Review-Enforcement': 'UNSATISFIED', |
| }, |
| ), |
| assert_continue('Code-Review'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'approved-review-enforcement', |
| defaults(), |
| api.checkout.cl_branch_parents( |
| submit_requirements={ |
| 'Code-Owners': 'ERROR', |
| 'Code-Review': 'UNSATISFIED', |
| 'Review-Enforcement': 'SATISFIED', |
| }, |
| ), |
| assert_continue('Review-Enforcement'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'approved-uploader', |
| defaults(), |
| api.checkout.cl_branch_parents(uploader='noreply@google.com'), |
| assert_continue(), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'untrusted-uploader-dry-run', |
| defaults(), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_exit_early(), |
| assert_status('SUCCESS'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'untrusted-uploader-no-cv', |
| defaults(cv_active=False), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_exit_early(), |
| assert_status('FAILURE'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'untrusted-uploader-full_run', |
| defaults(run_mode=api.cv.FULL_RUN), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_exit_early(), |
| assert_status('FAILURE'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| def pair(key: str, value: str): |
| return common_pb.StringPair(key=key, value=value) |
| |
| yield api.test( |
| 'approved-triggerer', |
| defaults(tags=[pair('cq_triggerer', 'noreply@google.com')]), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_continue('triggerer'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'untrusted-triggerer', |
| defaults(tags=[pair('cq_triggerer', 'noreply@example.com')]), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_exit_early(), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'approved-created_by', |
| defaults(created_by='user:noreply@google.com'), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_continue('created by'), |
| drop_expectations_must_be_last(), |
| ) |
| |
| yield api.test( |
| 'untrusted-created_by', |
| defaults(created_by='user:noreply@example.com'), |
| api.checkout.cl_branch_parents(uploader='noreply@example.com'), |
| assert_exit_early(), |
| drop_expectations_must_be_last(), |
| ) |