blob: 5716a8e37ac69a6f99e6ad8f6966edd82ea1c805 [file] [log] [blame]
# Copyright 2021 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.
"""Wrapper for 'pw presubmit' in the project source tree."""
import collections
import dataclasses
from typing import Any, Dict, Optional, Sequence
from PB.recipe_modules.pigweed.pw_presubmit import options as options_pb2
from recipe_engine import config_types, recipe_api
METADATA = {
'binary_sizes': (('target', 12345), ('target.budget', 12346)),
'test_runtimes': (('target', 200), ('target.max', 250)),
'output_properties': (),
}
@dataclasses.dataclass
class Step:
name: str
dir: config_types.Path
substeps: Sequence = dataclasses.field(default_factory=tuple)
export_dir_name: Optional[str] = dataclasses.field(default=None)
metadata: Dict = dataclasses.field(default_factory=dict)
@property
def export_dir(self):
if not self.export_dir_name:
return None # pragma: no cover
return self.dir.join(self.export_dir_name)
@dataclasses.dataclass
class PresubmitContext:
options: options_pb2.Options
root: config_types.Path
checkout_root: config_types.Path
time_rng_seed: int
_step_objects: Dict = dataclasses.field(
default_factory=collections.OrderedDict
)
list_steps_file: Optional[Any] = dataclasses.field(default=None)
def add_step(self, name, step):
self._step_objects[name] = step
@property
def steps(self):
return self._step_objects.values()
class PwPresubmitApi(recipe_api.RecipeApi):
"""Calls to checkout code."""
def _step(self, ctx, step):
return Step(
name=step['name'],
dir=ctx.root.join(step['name']),
substeps=step.get('substeps', ()),
export_dir_name=ctx.options.export_dir_name,
)
def init(self, checkout_root, options=None, root=None):
options.command_name = options.command_name or 'python -m pw_cli'
ctx = PresubmitContext(
options=options or self._options,
checkout_root=checkout_root,
root=root or checkout_root.join('p'),
time_rng_seed=self.m.time.ms_since_epoch(),
)
if not ctx.options.step and not ctx.options.program:
raise self.m.step.StepFailure('no step or program properties')
with self.m.step.nest('get steps from programs') as pres:
args = ['--only-list-steps']
for program in ctx.options.program:
args.extend(('--program', program))
for step in ctx.options.step:
args.extend(('--step', step))
test_steps = []
for program in ctx.options.program:
test_steps.append(f'{program}_0')
test_steps.append(f'{program}_1')
test_steps.extend(ctx.options.step)
list_steps_data = self._run(
ctx,
args,
name='get steps',
use_debug_log=False,
stdout=self.m.json.output(),
step_test_data=lambda: self.m.json.test_api.output_stream(
{
'all_files': ['foo.cc', 'foo.h'],
'steps': [{'name': x} for x in test_steps],
},
),
).stdout
if isinstance(list_steps_data, dict):
program_steps = list_steps_data['steps']
else:
program_steps = list_steps_data
# The 'list steps file' is only written if the above run command
# was successful. Whether 'pw presubmit' supports JSON output for
# '--only-list-steps' correlates with whether it supports the
# '--list-steps-file' argument (pwrev/116576).
if program_steps is not None:
ctx.list_steps_file = ctx.root.join('list_steps_file.json')
self.m.file.write_json(
'write list steps file',
ctx.list_steps_file,
list_steps_data,
)
# TODO(b/253021172) Remove this block. It's here until all
# projects use the new output format with --only-list-steps.
if program_steps is None:
raw_steps = (
self._run(
ctx,
args,
name=f'get steps text',
stdout=self.m.raw_io.output_text(),
step_test_data=lambda: self.m.raw_io.test_api.stream_output_text(
'\n'.join(test_steps) + '\n'
),
use_debug_log=False,
)
.stdout.strip()
.splitlines()
)
program_steps = [{'name': x} for x in raw_steps]
for step in program_steps:
ctx.add_step(
step['name'], self._step(ctx, step),
)
pres.step_summary_text = '\n'.join(x['name'] for x in program_steps)
return ctx
def _run(
self, ctx, args, name='run', use_debug_log=True, substep=None, **kwargs,
):
if ctx.options.do_not_use_debug_log or not use_debug_log:
logging_args = ('--loglevel', 'debug')
else:
logging_args = (
'--debug-log',
self.m.raw_io.output_text(
name='debug.log', add_output_log=True,
),
)
cmd = ctx.options.command_name.split()
cmd += [
'--directory',
ctx.checkout_root,
*logging_args,
'presubmit',
'--output-directory',
ctx.root,
]
if ctx.list_steps_file:
cmd += ['--list-steps-file', ctx.list_steps_file]
if ctx.options.only_on_changed_files:
args.extend(('--base', 'HEAD~1'))
elif not ctx.options.do_not_use_full_argument:
args.append('--full')
if ctx.options.continue_after_build_error:
cmd.append('--continue-after-build-error')
if ctx.options.use_time_for_rng_seed:
cmd.extend(('--rng-seed', ctx.time_rng_seed))
cmd.extend(args)
if substep:
cmd.extend(('--substep', substep))
with self.m.default_timeout():
if self.m.resultdb.enabled:
return self.m.step(
name,
self.m.resultdb.wrap(
cmd,
base_variant={
'builder': self.m.buildbucket.builder_name,
'step': name,
},
include=True,
),
**kwargs,
)
else:
return self.m.step(name, cmd, **kwargs)
def _process_metadata(self, step):
if not step.export_dir:
return # pragma: no cover
for name, test_data in METADATA.items():
step.metadata.setdefault(name, {})
json_path = step.export_dir.join(f'{name}.json')
self.m.path.mock_add_file(json_path)
if self.m.path.isfile(json_path):
with self.m.step.nest(self.m.path.basename(json_path)):
step.metadata[name] = self.m.file.read_json(
'read', json_path, test_data=dict(test_data)
)
def run(self, ctx, step, env=None, log_dir=None):
with self.m.step.nest(step.name) as pres:
args = ['--step', step.name]
if env and env.override_gn_args:
for key, value in env.override_gn_args.items():
args.append('--override-gn-arg')
if isinstance(value, str):
args.append(f'{key}="{value}"')
else:
args.append(f'{key}={value!r}')
for gn_arg in ctx.options.override_gn_arg:
args.extend(('--override-gn-arg', gn_arg))
with self.m.step.defer_results():
if step.substeps:
for substep in step.substeps:
result = self._run(
ctx, args, name=substep, substep=substep
)
if not result.is_ok:
break
else:
result = self._run(ctx, args, name=step.name)
self.m.gerrit_comment.maybe_post(
ctx.options.gerrit_comment, result
)
if log_dir:
step_log_dir = log_dir.join(step.name)
else:
log_dir = step.export_dir
if step.export_dir:
self.m.file.ensure_directory(
f'mkdir {ctx.options.export_dir_name}', step.export_dir,
)
if log_dir and log_dir != step.export_dir:
self.m.file.ensure_directory('create log dir', log_dir)
self.m.save_logs((step.dir,), log_dir, pres=pres)
self.m.file.listdir('ls out', step.dir, recursive=True)
self._process_metadata(step)
def build_id(self, ctx):
command = ctx.options.command_name.split()
command.extend(['--directory', ctx.checkout_root, 'build-id'])
step_data = self.m.step(
'get build id',
command,
stdout=self.m.raw_io.output_text(),
step_test_data=lambda: self.m.raw_io.test_api.stream_output_text(
'123-1234567890'
),
ok_ret='any',
)
namespace = None
if step_data.exc_result.retcode == 0:
namespace = step_data.stdout.strip()
if namespace == '0':
namespace = None
return namespace