blob: 0015e2fe5f3dbae91b2d9b524bbfb56b76cb2a5a [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.post_process import MustRun
from PB.recipes.fuchsia.tricium.tricium import InputProperties
DEPS = [
"fuchsia/build",
"fuchsia/buildbucket_util",
"fuchsia/checkout",
"fuchsia/git",
"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 = InputProperties
# 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, props):
checkout = api.checkout.fuchsia_with_options(
manifest=props.manifest,
remote=props.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=(),
use_incremental_cache=props.incremental_build,
)
project_name = api.buildbucket.build.input.gerrit_changes[0].project
# If specified, download CIPD packages.
if props.cipd_packages:
with api.step.nest("ensure_packages"):
with api.context(infra_steps=True):
cipd_dir = checkout.root_dir.join("cipd")
pkgs = api.cipd.EnsureFile()
for package in props.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 = api.path.abs_to_path(checkout.project(project_name)["path"])
api.tricium_analyze.check_commit_message()
if props.fint_params_path:
api.tricium_analyze.build_results = api.build.with_options(
checkout=checkout,
fint_params_path=props.fint_params_path,
incremental=props.incremental_build,
)
with api.context(cwd=project_dir):
paths = api.git.get_changed_files(
"get changed files",
deleted=False,
# If we include submodules then the output will include the
# directory paths of those submodules, but tricium_analyze operates
# on files, not directories. If we ever want to analyze changes to
# files within submodules, we can instead do a submodule-aware diff
# to get the paths to changed files within the submodule.
ignore_submodules=True,
)
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_dir.join("scripts", "fx")
)
api.tricium_analyze.checkout = checkout
api.tricium_analyze(
paths,
enabled_analyzers=props.analyses,
enabled_luci_analyzers=props.luci_analyzers,
)
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_text(),
# 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_text, 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_text(DIFF.format(filename)),
)
def formatted_diff_data(filename):
return api.step_data(
"analyze %s.get formatted diff" % filename,
api.raw_io.stream_output_text(DIFF.format(filename)),
)
def properties(cipd_packages=(), **kwargs):
defaults = dict(
manifest="flower",
remote="https://fuchsia.googlesource.com/integration",
)
# 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(fint_params_path="fint_params/tricium.textproto")
defaults.update(kwargs)
return api.properties(**defaults)
yield (
api.buildbucket_util.test("default", tryjob=True)
+ properties(
analyses=["ClangFormat", "GNFormat"],
luci_analyzers=["Spellchecker"],
incremental_build=True,
)
+ changed_files_data(["BUILD.gn", "hello.go"])
+ change_diff_data("BUILD.gn")
+ formatted_diff_data("BUILD.gn")
)
yield (
api.buildbucket_util.test("with_cipd_packages", tryjob=True)
+ 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.buildbucket_util.test("attribute_disable", tryjob=True)
+ 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_text(
"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.buildbucket_util.test("one_analysis_fails", tryjob=True, status="failure")
+ 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")
)