blob: d7e024e3327c7443224edf499d53ad8e13752eaf [file] [log] [blame]
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Recipe for running Tricium analyzers."""
import functools
from recipe_engine.config import ConfigGroup, ConfigList, List, Single
from recipe_engine.post_process import MustRun
from recipe_engine.recipe_api import Property
DEPS = [
"fuchsia/checkout",
"fuchsia/git",
"fuchsia/status_check",
"fuchsia/tricium_analyze",
"recipe_engine/buildbucket",
"recipe_engine/cipd",
"recipe_engine/context",
"recipe_engine/json",
"recipe_engine/path",
"recipe_engine/platform",
"recipe_engine/properties",
"recipe_engine/raw_io",
"recipe_engine/step",
]
PROPERTIES = {
"manifest": Property(kind=str, help="Jiri manifest to use", default=None),
"remote": Property(kind=str, help="Manifest project remote", default=None),
"cipd_packages": Property(
help="CIPD packages containing necessary binaries.",
kind=ConfigList(
lambda: ConfigGroup(
name=Single(str),
version=Single(str),
# Language subdirectory (e.g. clang) in which to put the
# package. The Tricium modules assume a Fuchsia prebuilts-like
# layout.
subdir=Single(str),
)
),
default=(),
),
"board": Property(kind=str, help="Board to build", default=""),
"build_type": Property(kind=str, help="The build type", default=""),
"product": Property(kind=str, help="Product to build", default=""),
"fint_params_path": Property(
kind=str, help="Path to a fint params file", default=None
),
"target": Property(kind=str, help="Target to build", default=""),
}
# We'll skip running checks on any files for which this git attribute is
# explicitly unset. We *will* run checks on all files for which this attribute
# is unspecified, or explicitly set.
TRICIUM_GIT_ATTR = "tricium"
def RunSteps(
api,
manifest,
remote,
cipd_packages,
board,
build_type,
product,
fint_params_path,
target,
):
with api.context(infra_steps=True):
checkout_root = api.path["start_dir"]
checkout = api.checkout.fuchsia_with_options(
path=checkout_root,
manifest=manifest,
remote=remote,
# Tricium should always be triggered on CLs that affect projects in
# the checkout, so we shouldn't skip patching any CLs.
skip_patch_projects=(),
)
project_name = api.buildbucket.build.input.gerrit_changes[0].project
# If specified, download CIPD packages.
if cipd_packages:
with api.step.nest("ensure_packages"):
with api.context(infra_steps=True):
cipd_dir = checkout_root.join("cipd")
pkgs = api.cipd.EnsureFile()
for package in cipd_packages:
pkgs.add_package(
package["name"], package["version"], subdir=package["subdir"]
)
api.cipd.ensure(cipd_dir, pkgs)
platform = "%s-%s" % (
api.platform.name.replace("win", "windows"),
{
"intel": {
32: "386",
# Note that this is different from the CIPD norm (this is how
# //prebuilt/third_party is laid out).
64: "x64",
},
"arm": {32: "armv6", 64: "arm64",},
}[api.platform.arch][api.platform.bits],
)
# Only these tools are being downloaded from CIPD, so directly set the
# paths if cipd_packages is defined.
api.tricium_analyze.black = cipd_dir.join("black")
api.tricium_analyze.go = cipd_dir.join("go", platform, "bin", "go")
api.tricium_analyze.gofmt = cipd_dir.join(
"go", platform, "bin", "gofmt"
)
api.tricium_analyze.yapf = cipd_dir.join("yapf")
project_dir = checkout_root.join(checkout.project(project_name)["path"])
api.tricium_analyze.check_commit_message()
with api.context(cwd=project_dir):
paths = api.git.get_changed_files("get changed files", deleted=False)
with api.step.nest("filter tricium-disabled files"):
paths = filter_by_git_attrs(api, paths)
api.tricium_analyze.suggest_fx = api.path.exists(
checkout_root.join("scripts", "fx")
)
api.tricium_analyze.checkout = checkout
api.tricium_analyze(
paths,
board=board,
build_type=build_type,
product=product,
fint_params_path=fint_params_path,
target=target,
)
def filter_by_git_attrs(api, paths):
"""Remove files that have Tricium disabled by a git attribute."""
filtered_files = []
# Process long lists of files in chunks to avoid exceeding command length
# limits. Arbitrarily chosen chunk size that's likely to not exceed the
# limit.
chunk_size = 100
for i in range(0, len(paths), chunk_size):
chunk = paths[i : i + chunk_size]
mock_output = "\0".join(
["%s\0%s\0unspecified" % (f, TRICIUM_GIT_ATTR) for f in chunk]
)
step = api.git(
"check git attributes for files %d-%d" % (i, i + len(chunk)),
"check-attr",
# Separate records with nuls, to prevent ugly escaping of non-ASCII
# filenames.
"-z",
TRICIUM_GIT_ATTR,
"--",
*chunk,
stdout=api.raw_io.output(),
# Use functools.partial instead of a lambda to avoid Pylint's
# cell-var-from-loop warning.
step_test_data=functools.partial(
api.raw_io.test_api.stream_output, mock_output
)
)
raw_records = step.stdout.strip("\0").split("\0")
attr_statuses = [raw_records[i : i + 3] for i in range(0, len(raw_records), 3)]
step.presentation.logs["file attributes"] = api.json.dumps(
attr_statuses, indent=2
).splitlines()
for filename, _, status in attr_statuses:
if status != "unset":
filtered_files.append(filename)
return filtered_files
def GenTests(api):
DIFF = """diff --git a/{0} b/{0}
index e684c1e..a76a10e 100644
--- a/{0}
+++ b/{0}
@@ -1,2 +1,4 @@
+ foo
+ bar
"""
def changed_files_data(files):
return api.git.get_changed_files("get changed files", files)
def change_diff_data(filename):
return api.step_data(
"analyze %s.get change diff" % filename,
api.raw_io.stream_output(DIFF.format(filename)),
)
def formatted_diff_data(filename):
return api.step_data(
"analyze %s.get formatted diff" % filename,
api.raw_io.stream_output(DIFF.format(filename)),
)
try_build = api.buildbucket.try_build(
git_repo="https://fuchsia.googlesource.com/topaz"
)
def properties(cipd_packages=(), **kwargs):
defaults = dict(
manifest="flower",
project="integration",
remote="https://fuchsia.googlesource.com/integration",
ref="refs/changes/12345/2",
)
# Using CIPD packages implies that we don't need to do a build, and
# vice versa.
if cipd_packages:
defaults["cipd_packages"] = cipd_packages
else:
defaults.update(
dict(
fint_params_path="specs/tricium.fint.textproto",
target="x64",
build_type="release",
board="boards/x64.gni",
product="products/core.gni",
)
)
defaults.update(kwargs)
return api.properties(**defaults)
yield (
api.status_check.test("default")
+ try_build
+ properties(analyses=["ClangFormat", "GNFormat"])
+ changed_files_data(["BUILD.gn", "hello.go"])
+ change_diff_data("BUILD.gn")
+ formatted_diff_data("BUILD.gn")
)
yield (
api.status_check.test("with_cipd_packages")
+ try_build
+ properties(
analyses=["GoFmt"],
cipd_packages=[
{
"name": "fuchsia/go/${platform}",
"version": "integration",
"subdir": "go",
}
],
)
+ changed_files_data(["BUILD.gn", "hello.go"])
+ change_diff_data("hello.go")
+ formatted_diff_data("hello.go")
)
yield (
api.status_check.test("attribute_disable")
+ try_build
+ properties(analyses=["ClangFormat", "GNFormat"])
+ changed_files_data(["BUILD.gn", "hello.go"])
+ api.step_data(
"filter tricium-disabled files.check git attributes for files 0-2",
api.raw_io.stream_output(
"BUILD.gn\0tricium\0unset\0hello.go\0tricium\0unspecified\0"
),
)
)
# Even if running one analysis fails, we should still run the other
# analyses and write their results before failing the build.
yield (
api.status_check.test("one_analysis_fails", status="failure")
+ try_build
+ properties(analyses=["GNFormat", "GoFmt"])
+ changed_files_data(["hello.go", "BUILD.gn"])
+ change_diff_data("BUILD.gn")
+ formatted_diff_data("BUILD.gn")
+ change_diff_data("hello.go")
+ api.step_data("analyze hello.go.gofmt.run", retcode=1)
+ api.post_process(MustRun, "write results")
)