blob: 5b18e287ff2dae1c9d87b660c20aaef3f1dadc6b [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Toolchain service tests."""
from collections import defaultdict
import os
from pathlib import Path
from typing import Dict, List, NamedTuple, Text
from unittest import mock
from chromite.lib import cros_test_lib
from chromite.lib.parser import package_info
from chromite.scripts import tricium_clang_tidy
from chromite.service import toolchain
class MockArtifact(NamedTuple):
"""Data for a Mocked Artifact."""
linter: Text
package: Text
file_name: Text
contents: Text
class MockBuildLinter(toolchain.BuildLinter):
"""Mocked version of Build Linters class."""
def __init__(self, tempdir: Text, packages: List[Text] = None):
super().__init__([], "")
self.tempdir = tempdir
self.packages = [] if packages is None else packages
self.package_atoms = packages
self.artifacts = defaultdict(lambda: defaultdict(list))
self.artifacts_base = os.path.join(self.tempdir, "artifacts")
def add_artifact(self, artifact: MockArtifact):
"""Adds a mock artifact and writes it to tempdir."""
tmp_path = os.path.join(
self.artifacts_base,
artifact.linter,
artifact.package,
artifact.file_name,
)
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
with open(tmp_path, "w", encoding="utf-8") as tmp_artifact_file:
tmp_artifact_file.write(artifact.contents)
self.artifacts[artifact.linter][artifact.package].append(tmp_path)
def _fetch_from_linting_artifacts(self, subdir) -> Dict[Text, List[Text]]:
"""Get file from emerge artifact directory."""
artifacts = {}
for package, package_artifacts in self.artifacts[subdir].items():
if not self.packages or package in self.package_atoms:
if package not in artifacts:
artifacts[package] = []
artifacts[package].extend(package_artifacts)
return artifacts
class BuildLinterTests(cros_test_lib.MockTempDirTestCase):
"""Unit tests for Build Linter Class."""
def checkArtifacts(
self,
expected_artifacts: List[MockArtifact],
retrieved_artifact_paths: Dict[Text, List[Text]],
):
"""Asserts that artifact paths match the list of expected results."""
actual_artifacts = []
for paths in retrieved_artifact_paths.values():
actual_artifacts.extend(paths)
self.assertEqual(len(actual_artifacts), len(expected_artifacts))
for artifact in expected_artifacts:
for artifact_path in retrieved_artifact_paths:
if artifact_path.endswith(
f"{artifact.linter}/{artifact.package}/{artifact.file_name}"
):
with open(
artifact_path, "r", encoding="utf-8"
) as artifact_file:
contents = artifact_file.read()
self.assertEqual(contents, artifact.contents)
def testMockBuildLinter(self):
mbl = MockBuildLinter(self.tempdir, ["pkg_1", "pkg_2", "pkg_3"])
relevant_artifacts = [
MockArtifact("linter_1", "pkg_1", "out.txt", "Hello world"),
MockArtifact("linter_1", "pkg_2", "out2.txt", "Hello world 2"),
MockArtifact("linter_1", "pkg_3", "out.txt", "Hello\nWorld"),
]
irrelevant_artifacts = [
MockArtifact("linter_2", "pkg_2", "out.txt", "Goodbye World"),
MockArtifact("linter_1", "pkg_4", "out.txt", "Goodbye world"),
MockArtifact("linter_2", "pkg_5", "out.txt", "Goodbye world 2"),
MockArtifact("linter_3", "pkg_6", "out.txt", "Goodbye\nWorld"),
MockArtifact("linter_4", "pkg_1", "out.txt", "Goodbye\nWorld"),
]
for artifact in relevant_artifacts + irrelevant_artifacts:
mbl.add_artifact(artifact)
# pylint: disable=protected-access
retrieved_artifact_paths = mbl._fetch_from_linting_artifacts("linter_1")
self.checkArtifacts(relevant_artifacts, retrieved_artifact_paths)
def testMockBuildLinterNoPackages(self):
mbl = MockBuildLinter(self.tempdir, [])
relevant_artifacts = [
MockArtifact("linter_1", "pkg_1", "out.txt", "Hello world"),
MockArtifact("linter_1", "pkg_2", "out2.txt", "Hello world 2"),
MockArtifact("linter_1", "pkg_3", "out.txt", "Hello\nWorld"),
MockArtifact("linter_1", "pkg_4", "out.txt", "Hello world"),
]
irrelevant_artifacts = [
MockArtifact("linter_2", "pkg_2", "out.txt", "Goodbye World"),
MockArtifact("linter_2", "pkg_5", "out.txt", "Goodbye world 2"),
MockArtifact("linter_3", "pkg_6", "out.txt", "Goodbye\nWorld"),
MockArtifact("linter_4", "pkg_1", "out.txt", "Goodbye\nWorld"),
]
for artifact in relevant_artifacts + irrelevant_artifacts:
mbl.add_artifact(artifact)
# pylint: disable=protected-access
retrieved_artifact_paths = mbl._fetch_from_linting_artifacts("linter_1")
self.checkArtifacts(relevant_artifacts, retrieved_artifact_paths)
def testFetchFromLintingArtifacts(self):
bl = toolchain.BuildLinter(
[
package_info.parse("category0/package0"),
package_info.parse("category1/package1"),
package_info.parse("category1/package2"),
package_info.parse("category2/package3"),
],
self.tempdir,
)
bl_no_pkg = toolchain.BuildLinter([], self.tempdir)
relevant_cases = [
"category1/package1/linting-output/linter1/a.out",
"category1/package1/linting-output/linter1/b.json",
"category1/package1/linting-output/linter1/c",
"category1/package2/linting-output/linter1/a.out",
"category1/package2/linting-output/linter1/b.json",
"category1/package2/linting-output/linter1/c",
"category2/package3/linting-output/linter1/a.out",
"category2/package3/linting-output/linter1/b.json",
"category2/package3/linting-output/linter1/c",
]
irrelevant_cases = [
"category3/package1/linting-output/linter1/a.out",
"category4/package2/linting-output/linter1/a.out",
"category2/package5/linting-output/linter1/a.out",
"category2/package6/linting-output/linter1/a.out",
"category1/package1/linting-output/linter2/a.out",
"category1/package2/linting-output/linter2/a.out",
"category2/package3/linting-output/linter3/a.out",
"category2/package4/linting-output/linter4/a.out",
]
root = Path(self.tempdir) / "var/lib/chromeos/package-artifacts"
expected_results = {
"category1/package1": [
f"{str(root)}/category1/package1/linting-output/linter1/a.out",
f"{str(root)}/category1/package1/linting-output/linter1/b.json",
f"{str(root)}/category1/package1/linting-output/linter1/c",
],
"category1/package2": [
f"{str(root)}/category1/package2/linting-output/linter1/a.out",
f"{str(root)}/category1/package2/linting-output/linter1/b.json",
f"{str(root)}/category1/package2/linting-output/linter1/c",
],
"category2/package3": [
f"{str(root)}/category2/package3/linting-output/linter1/a.out",
f"{str(root)}/category2/package3/linting-output/linter1/b.json",
f"{str(root)}/category2/package3/linting-output/linter1/c",
],
}
additional_no_pkg_results = {
"category3/package1": [
f"{str(root)}/category3/package1/linting-output/linter1/a.out"
],
"category4/package2": [
f"{str(root)}/category4/package2/linting-output/linter1/a.out"
],
"category2/package5": [
f"{str(root)}/category2/package5/linting-output/linter1/a.out"
],
"category2/package6": [
f"{str(root)}/category2/package6/linting-output/linter1/a.out"
],
}
expected_no_pkg_results = dict(
expected_results, **additional_no_pkg_results
)
for case in relevant_cases + irrelevant_cases:
test_path = root / case
test_path.parent.mkdir(parents=True, exist_ok=True)
test_path.touch()
# pylint: disable=protected-access
actual_results = bl._fetch_from_linting_artifacts("linter1")
self.assertDictEqual(expected_results, actual_results)
no_pkg_results = bl_no_pkg._fetch_from_linting_artifacts("linter1")
self.assertDictEqual(expected_no_pkg_results, no_pkg_results)
def testGetPackageForArtifactDir(self):
bl = toolchain.BuildLinter([], "")
test_cases = [
("category1", "package1", "linter1", "category1/package1"),
("category1", "package2", "linter2", "category1/package2"),
("category2", "package1", "linter3", "category2/package1"),
("category3", "package3", "linter4", "category3/package3"),
]
root = Path(self.tempdir) / "var/lib/chromeos/package-artifacts"
for case in test_cases:
test_path = root / case[0] / case[1] / "linting-output" / case[2]
expected_result = case[3]
# pylint: disable=protected-access
actual_result = bl._get_package_for_artifact_dir(test_path)
self.assertEqual(expected_result, actual_result)
def testFetchTidyLints(self):
mock_calls = 0
diagnostics = [
tricium_clang_tidy.TidyDiagnostic("", 0, "lint1", "body1", [], []),
tricium_clang_tidy.TidyDiagnostic("", 0, "lint2", "body2", [], []),
tricium_clang_tidy.TidyDiagnostic("", 0, "lint3", "body3", [], []),
tricium_clang_tidy.TidyDiagnostic("", 0, "lint4", "body4", [], []),
]
expected_findings = [
toolchain.LinterFinding(
name="lint1",
message="body1",
locations=tuple(
[toolchain.CodeLocation("", "", 0, 0, None, None)]
),
linter="clang_tidy",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
toolchain.LinterFinding(
name="lint2",
message="body2",
locations=tuple(
[toolchain.CodeLocation("", "", 0, 0, None, None)]
),
linter="clang_tidy",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
toolchain.LinterFinding(
name="lint3",
message="body3",
locations=tuple(
[toolchain.CodeLocation("", "", 0, 0, None, None)]
),
linter="clang_tidy",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
toolchain.LinterFinding(
name="lint4",
message="body4",
locations=tuple(
[toolchain.CodeLocation("", "", 0, 0, None, None)]
),
linter="clang_tidy",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
]
def mock_parse_tidy_invocation(_):
nonlocal mock_calls
mock_calls += 1
if mock_calls == 1:
meta = tricium_clang_tidy.InvocationMetadata(0, [], "", "", "")
return meta, diagnostics[:2]
if mock_calls == 2:
meta = tricium_clang_tidy.InvocationMetadata(0, [], "", "", "")
return meta, diagnostics[2:]
if mock_calls == 3:
meta = tricium_clang_tidy.InvocationMetadata(0, [], "", "", "")
return meta, []
elif mock_calls == 4:
return tricium_clang_tidy.ExceptionData()
elif mock_calls == 5:
meta = tricium_clang_tidy.InvocationMetadata(1, [], "", "", "")
return meta, diagnostics
else:
self.fail("Too many calls to parse_tidy_invocation")
def mock_filter_findings(_, __, findings):
if mock_calls == 1:
self.assertCountEqual(findings, diagnostics[:2])
elif mock_calls == 2:
self.assertCountEqual(findings, diagnostics[2:])
elif mock_calls == 3:
self.assertEqual(findings, [])
return findings
mbl = MockBuildLinter(self.tempdir)
artifacts = [
MockArtifact("clang-tidy", "cat/pkg", "a.json", "content"),
MockArtifact("clang-tidy", "cat/pkg", "b.json", "content"),
MockArtifact("clang-tidy", "cat/pkg", "c.json", "content"),
MockArtifact("clang-tidy", "cat/pkg", "d.json", "content"),
MockArtifact("clang-tidy", "cat/pkg", "e.out", "content"),
]
for artifact in artifacts:
mbl.add_artifact(artifact)
tmp_parse_tidy_invocation = tricium_clang_tidy.parse_tidy_invocation
tricium_clang_tidy.parse_tidy_invocation = mock_parse_tidy_invocation
tmp_filter_tidy_lints = tricium_clang_tidy.filter_tidy_lints
tricium_clang_tidy.filter_tidy_lints = mock_filter_findings
# pylint: disable=protected-access
lints = mbl._fetch_tidy_lints()
tricium_clang_tidy.parse_tidy_invocation = tmp_parse_tidy_invocation
tricium_clang_tidy.filter_tidy_lints = tmp_filter_tidy_lints
self.assertCountEqual(lints, expected_findings)
def testParseIWYUFiles(self):
mbl = MockBuildLinter(self.tempdir)
artifacts = [
MockArtifact(
"iwyu",
"cat/pkg",
"iwyu.out",
(
"\n".join(
[
"(/a/good/file.c has correct #includes/fwd-decls)",
"",
"/path/to/some/file.c should add these lines:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"",
"/path/to/some/file.c should remove these lines:",
"- #include <unwanted.h> // lines 11-11",
"",
"The full include-list for /path/to/some/file.c:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"#include <stdint.h>",
"#include <stdlib.h>",
'#include "lib/foo.h" // for func7, func8',
"---",
]
)
),
),
MockArtifact(
"iwyu",
"cat/pkg",
"duplicate.out",
(
"\n".join(
[
"/path/to/some/file.c should add these lines:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"",
"/path/to/some/file.c should remove these lines:",
"- #include <unwanted.h> // lines 11-11",
"",
"The full include-list for /path/to/some/file.c:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"#include <stdint.h>",
"#include <stdlib.h>",
'#include "lib/foo.h" // for func7, func8',
"---",
]
)
),
),
MockArtifact("IWYU", "cat/pkg", "empty.out", ""),
]
expected_findings = [
toolchain.LinterFinding(
name="add",
message="\n".join(
[
"Include list is missing:",
"\t#include <missing1.h>",
"Which is required for func1, func2, func3",
"",
"The full suggested include-list for this file:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"#include <stdint.h>",
"#include <stdlib.h>",
'#include "lib/foo.h" // for func7, func8',
"",
"Note: Suggestions from IWYU are not always correct and"
" thus require human supervision.",
]
),
locations=(
toolchain.CodeLocation(
"/path/to/some/file.c",
"",
line_start=1,
line_end=1,
col_start=None,
col_end=None,
),
),
linter="iwyu",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
toolchain.LinterFinding(
name="add",
message="\n".join(
[
"Include list is missing:",
"\t#include <missing2.h>",
"Which is required for func4, func5, func6",
"",
"The full suggested include-list for this file:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"#include <stdint.h>",
"#include <stdlib.h>",
'#include "lib/foo.h" // for func7, func8',
"",
"Note: Suggestions from IWYU are not always correct and"
" thus require human supervision.",
]
),
locations=(
toolchain.CodeLocation(
"/path/to/some/file.c",
"",
line_start=1,
line_end=1,
col_start=None,
col_end=None,
),
),
linter="iwyu",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
toolchain.LinterFinding(
name="remove",
message="\n".join(
[
"Remove from include list:",
"\t#include <unwanted.h>",
"",
"The full suggested include-list for this file:",
"#include <missing1.h> // for func1, func2, func3",
"#include <missing2.h> // for func4, func5, func6",
"#include <stdint.h>",
"#include <stdlib.h>",
'#include "lib/foo.h" // for func7, func8',
"",
"Note: Suggestions from IWYU are not always correct and"
" thus require human supervision.",
]
),
locations=(
toolchain.CodeLocation(
"/path/to/some/file.c",
"",
line_start=11,
line_end=11,
col_start=None,
col_end=None,
),
),
linter="iwyu",
suggested_fixes=tuple(),
package=package_info.parse("cat/pkg"),
),
]
for artifact in artifacts:
mbl.add_artifact(artifact)
# pylint: disable=protected-access
parses = mbl._fetch_iwyu_lints()
self.assertCountEqual(parses, expected_findings)
class TestEmergeAndUploadLints(cros_test_lib.RunCommandTestCase):
"""Unit tests for emerge_and_upload_lints"""
def setUp(self):
self.rc.AddCmdResult(
[
"lint_package",
"--fetch-only",
"--json",
"--no-clippy",
"--no-golint",
],
stdout="linting output",
)
@mock.patch.object(toolchain.gs.GSContext, "CreateWithContents")
def testEmergeAndUploadLints(self, copy_mock):
used_gs_path = toolchain.emerge_and_upload_lints("atlas", 9999)
target_gs_path = (
"gs://chromeos-toolchain-artifacts/code-health/9999/atlas.json"
)
self.assertEqual(used_gs_path, target_gs_path)
copy_mock.assert_called_with(target_gs_path, b"linting output")