| # Copyright 2020 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. |
| """Recipe for testing Pigweed using presubmit_checks.py script.""" |
| |
| import collections |
| import re |
| |
| from PB.recipes.pigweed.cipd_roller import InputProperties |
| |
| DEPS = [ |
| 'fuchsia/auto_roller', |
| 'fuchsia/buildbucket_util', |
| 'fuchsia/status_check', |
| 'pigweed/checkout', |
| 'pigweed/roll_util', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/cipd', |
| 'recipe_engine/file', |
| 'recipe_engine/json', |
| 'recipe_engine/path', |
| 'recipe_engine/properties', |
| 'recipe_engine/step', |
| ] |
| |
| PROPERTIES = InputProperties |
| |
| PYTHON_VERSION_COMPATIBILITY = "PY3" |
| |
| COMMIT_MESSAGE = """ |
| roll: {package_name} |
| |
| From: {old_version} |
| To: {new_version} |
| |
| CQ-Do-Not-Cancel-Tryjobs: true |
| """.strip() |
| |
| |
| def _is_platform(part): |
| if '{' in part: |
| return True |
| os = part.split('-')[0] |
| return os in ('linux', 'mac', 'windows') |
| |
| |
| def find_shared_tags(api, package_tags, tag): |
| """Attempts to find a tag shared by all packages. |
| |
| This function can be used if the intersection of the sets of tags |
| associated with different-platform packages with the same 'ref' is empty. |
| It finds a tag shared by all packages, with as many of them as possible |
| matching 'ref'. |
| """ |
| # Find the most common tags. We use the sorted dict keys for determinism. |
| package_paths = sorted(package_tags.keys()) |
| counter = collections.Counter() |
| for path in package_paths: |
| counter.update(package_tags[path]) |
| most_common_tags = counter.most_common() |
| |
| with api.step.nest("find shared tag"): |
| for tag_candidate, _ in most_common_tags: |
| # There is at least one package for which the version with the |
| # specified 'ref' does not have this tag. See if there exists a |
| # version of this package that *does* have this tag. If so, use |
| # that version. |
| updated_tags = dict() |
| for package_path in package_paths: |
| if tag_candidate in package_tags[package_path]: |
| # For this package we already have a version with this tag, |
| # nothing to do. |
| continue |
| try: |
| package_data = api.cipd.describe( |
| package_path, tag_candidate |
| ) |
| except api.step.StepFailure: |
| # No luck: there exists no version with this tag. |
| break |
| updated_tags[package_path] = set( |
| x.tag |
| for x in package_data.tags |
| if x.tag.startswith(tag + ':') |
| ) |
| else: |
| # We found a version of each package with the tag_candidate. |
| merged_tags = dict() |
| merged_tags.update(package_tags) |
| merged_tags.update(updated_tags) |
| tags = set.intersection(*merged_tags.values()) |
| # Should always succeed. |
| assert len(tags) > 0 |
| # Update package_tags to be consistent with the returned tags. |
| package_tags.update(updated_tags) |
| return tags |
| |
| # We failed to find any tag that meets our criteria. |
| return set() |
| |
| |
| def RunSteps(api, props): |
| cipd_json_path = props.cipd_json_path |
| package_name = props.package_name |
| package_spec = props.package_spec |
| ref = props.ref |
| tag = props.tag |
| dry_run = props.dry_run |
| allow_mismatched_refs = props.allow_mismatched_refs |
| |
| api.checkout(use_trigger=False) |
| |
| cipd_json_path = api.checkout.root.join( |
| *re.split(r'[\\/]+', cipd_json_path) |
| ) |
| |
| if not package_name: |
| # Turn foo/bar/baz/${platform} and foo/bar/baz/${os=mac}-${arch} into |
| # 'baz'. |
| package_name = [ |
| part for part in package_spec.split('/') if not _is_platform(part) |
| ][-1] |
| |
| basename = api.path.basename(cipd_json_path) |
| packages = api.file.read_json('read {}'.format(basename), cipd_json_path) |
| old_version = None |
| package = None |
| for package in packages: |
| if package['path'] == package_spec: |
| old_version = package['tags'][0] |
| break |
| else: |
| raise api.step.StepFailure( |
| "couldn't find package {} in {}".format( |
| package_spec, cipd_json_path |
| ) |
| ) |
| |
| assert package.get('platforms'), 'platforms empty in json' |
| platforms = package.get('platforms') |
| base, name = package_spec.rstrip('/').rsplit('/', 1) |
| if _is_platform(name): |
| package_paths = ['{}/{}'.format(base, x) for x in platforms] |
| else: |
| package_paths = [package_spec] |
| |
| package_tags = {} |
| tags = None |
| for package_path in package_paths: |
| try: |
| package_data = api.cipd.describe(package_path, ref) |
| |
| except api.step.StepFailure: |
| # If we got here this package doesn't have the correct ref. This is |
| # likely because it's a new platform for an existing package. In |
| # that case ignore this platform when checking that refs agree on |
| # package versions. We still need at least one platform to have the |
| # ref or the checks below will fail. |
| pass |
| |
| else: |
| package_tags[package_path] = set( |
| x.tag for x in package_data.tags if x.tag.startswith(tag + ':') |
| ) |
| if tags is None: |
| tags = set(package_tags[package_path]) |
| else: |
| tags.intersection_update(package_tags[package_path]) |
| |
| if not tags and allow_mismatched_refs: |
| # The package with the requested ref has non-overlapping tag values for |
| # different platforms. Try relaxing the requirement that all packages |
| # come from the same ref, and see if this allows us to find a set with |
| # shared tag values. |
| tags = find_shared_tags(api, package_tags, tag) |
| |
| with api.step.nest('common tags') as presentation: |
| presentation.step_summary_text = '\n'.join(sorted(tags)) |
| |
| if not tags: |
| err_lines = ['no common tags across "{}" refs of packages'.format(ref)] |
| for package_path, package_tags in sorted(package_tags.items()): |
| err_lines.append('') |
| err_lines.append(package_path) |
| for tag in package_tags: |
| err_lines.append(tag) |
| |
| raise api.step.StepFailure('<br>'.join(err_lines)) |
| # Deterministically pick one of the common tags. |
| new_version = sorted(tags)[0] |
| package['tags'] = [new_version] |
| |
| # Verify there's only one instance of each platform package with this tag. |
| with api.step.nest('check number of instances'): |
| for package_path in package_paths: |
| api.cipd.describe(package_path, new_version) |
| |
| with api.step.nest('new_version') as presentation: |
| presentation.step_summary_text = new_version |
| |
| if old_version in tags: |
| with api.step.nest('already up-to-date') as presentation: |
| presentation.step_summary_text = ( |
| 'current version {} in common tags' |
| ).format(old_version) |
| return |
| |
| api.file.write_text( |
| 'write {}'.format(basename), |
| cipd_json_path, |
| api.json.dumps(packages, indent=2, separators=(',', ': ')) + '\n', |
| ) |
| |
| change = api.auto_roller.attempt_roll( |
| gerrit_host=api.checkout.gerrit_host(), |
| gerrit_project=api.checkout.gerrit_project(), |
| upstream_ref=api.checkout.branch, |
| repo_dir=api.path.dirname(cipd_json_path), |
| commit_message=COMMIT_MESSAGE.format( |
| package_name=package_name, |
| package_spec=package_spec, |
| old_version=old_version, |
| new_version=new_version, |
| builder=api.buildbucket.builder_name, |
| build_id=api.buildbucket_util.id, |
| ), |
| dry_run=dry_run, |
| labels_to_set=api.roll_util.labels_to_set, |
| labels_to_wait_on=api.roll_util.labels_to_wait_on, |
| bot_commit=props.bot_commit, |
| ) |
| |
| return api.auto_roller.raw_result(change) |
| |
| |
| def GenTests(api): # pylint: disable=invalid-name |
| """Create tests.""" |
| |
| def package(cipd_path, old_version, platforms=None): |
| result = { |
| 'path': cipd_path, |
| 'tags': [old_version], |
| '_comment': 'comments should be preserved', |
| } |
| |
| if platforms is not None: |
| result['platforms'] = list(platforms) |
| |
| return result |
| |
| def describe(package_path, *tags, **kwargs): |
| return api.step_data( |
| 'cipd describe {}'.format(package_path), |
| api.cipd.example_describe( |
| package_path, |
| test_data_tags=[ |
| '{}:{}'.format(name, value) for name, value in tags |
| ], |
| ), |
| **kwargs |
| ) |
| |
| def no_ref(package_paths): |
| res = None |
| for package_path in package_paths: |
| step = describe(package_path, retcode=1) |
| res = res + step if res else step |
| return res |
| |
| def no_common_tags(package_paths, tagname='git_revision'): |
| res = None |
| for i, package_path in enumerate(package_paths): |
| step = describe(package_path, (tagname, i)) |
| res = res + step if res else step |
| return res |
| |
| def multiple_common_tags(package_paths, tagname='git_revision'): |
| res = None |
| for package_path in package_paths: |
| step = describe( |
| package_path, (tagname, 1), (tagname, 2), (tagname, 3) |
| ) |
| res = res + step if res else step |
| return res |
| |
| def relaxing_works(package_paths, tagname='git_revision'): |
| return api.step_data( |
| 'find shared tag.cipd describe {}'.format(package_paths[1]), |
| api.cipd.example_describe( |
| package_paths[1], test_data_tags=['{}:{}'.format(tagname, 0)], |
| ), |
| ) |
| |
| def relaxing_does_not_work(package_paths, tagname='git_revision'): |
| # No version of package_paths[1] with hoped-for tag. |
| return api.step_data( |
| 'find shared tag.cipd describe {}'.format(package_paths[0]), |
| api.cipd.example_describe(package_paths[0]), |
| retcode=1, |
| ) + api.step_data( |
| 'find shared tag.cipd describe {}'.format(package_paths[1]), |
| api.cipd.example_describe(package_paths[1]), |
| retcode=1, |
| ) |
| |
| def read_file_step_data(package_json_name, *packages): |
| return api.step_data( |
| 'read {}'.format(package_json_name), api.file.read_json(packages), |
| ) |
| |
| spec = 'pigweed/host_tools/${platform}' |
| paths = ( |
| 'pigweed/host_tools/linux-amd64', |
| 'pigweed/host_tools/windows-amd64', |
| ) |
| |
| def properties(package_spec=spec, dry_run=True, **kwargs): |
| new_kwargs = { |
| 'package_spec': package_spec, |
| 'dry_run': dry_run, |
| 'ref': 'latest', |
| 'tag': 'git_revision', |
| 'cipd_json_path': 'pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json', |
| } |
| new_kwargs.update(api.checkout.git_properties()) |
| new_kwargs.update(kwargs) |
| return api.properties(**new_kwargs) |
| |
| yield ( |
| api.status_check.test('success') |
| + properties() |
| + api.checkout.ci_test_data() |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| spec, |
| 'git_revision:123', |
| platforms=['linux-amd64', 'windows-amd64'], |
| ), |
| ) |
| + api.auto_roller.dry_run_success() |
| ) |
| |
| yield ( |
| api.status_check.test('bad_package_spec', status='failure') |
| + properties(package_spec='bad-{}'.format(spec)) |
| + api.checkout.ci_test_data() |
| + read_file_step_data('pigweed.json', package(spec, 'git_revision:123')) |
| ) |
| |
| yield ( |
| api.status_check.test('no_common_tags', status='failure') |
| + properties() |
| + api.checkout.ci_test_data() |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| spec, |
| 'git_revision:123', |
| platforms=('linux-amd64', 'windows-amd64'), |
| ), |
| ) |
| + no_common_tags(paths) |
| ) |
| |
| yield ( |
| api.status_check.test('no_common_tags_but_relaxing_ref_mismatch_helps') |
| + properties(allow_mismatched_refs=True) |
| + api.checkout.ci_test_data() |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| spec, |
| 'git_revision:123', |
| platforms=('linux-amd64', 'windows-amd64'), |
| ), |
| ) |
| # Package 0 exists at git_revision:0, package 1 at git_revision:1 |
| + no_common_tags(paths) |
| # However, package 1 also exists at git_revision:0, so that revision is |
| # good to use. |
| + relaxing_works(paths) |
| + api.auto_roller.dry_run_success() |
| ) |
| |
| yield ( |
| api.status_check.test( |
| 'no_common_tags_and_relaxing_ref_mismatch_does_not_help', |
| status='failure', |
| ) |
| + properties(allow_mismatched_refs=True) |
| + api.checkout.ci_test_data() |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| spec, |
| 'git_revision:123', |
| platforms=('linux-amd64', 'windows-amd64'), |
| ), |
| ) |
| # Package 0 exists at git_revision:0, package 1 at git_revision:1 |
| + no_common_tags(paths) |
| # However, package 1 does not exist at git_revision:0. |
| + relaxing_does_not_work(paths) |
| ) |
| |
| yield ( |
| api.status_check.test('multiple_common_tags') |
| + properties() |
| + api.checkout.ci_test_data() |
| + multiple_common_tags(paths) |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| spec, |
| 'git_revision:2', |
| platforms=('linux-amd64', 'windows-amd64'), |
| ), |
| ) |
| ) |
| |
| yield ( |
| api.status_check.test('missing_tag') |
| + properties() |
| + api.checkout.ci_test_data() |
| + multiple_common_tags(paths) |
| + no_ref(['pigweed/host_tools/fake-amd64']) |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| spec, |
| 'git_revision:2', |
| platforms=('linux-amd64', 'windows-amd64', 'fake-amd64'), |
| ), |
| ) |
| ) |
| |
| no_curly_spec = 'pigweed/host_tools/linux-amd64' |
| yield ( |
| api.status_check.test('no_curlies_in_spec') |
| + properties(package_spec=no_curly_spec) |
| + api.checkout.ci_test_data() |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| no_curly_spec, |
| 'git_revision:123', |
| platforms=('linux-amd64', 'windows-amd64'), |
| ), |
| ) |
| + api.auto_roller.dry_run_success() |
| ) |
| |
| yield ( |
| api.status_check.test('platform-independent') |
| + properties(package_spec='foo/bar/baz') |
| + api.checkout.ci_test_data() |
| + read_file_step_data( |
| 'pigweed.json', |
| package( |
| 'foo/bar/baz', |
| 'git_revision:123', |
| platforms=['linux-amd64', 'mac-amd64'], |
| ), |
| ) |
| + api.auto_roller.dry_run_success() |
| ) |