diff --git a/MODULE.bazel b/MODULE.bazel index e0659ac0..908c7314 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,6 +19,11 @@ module(name = "rules_codechecker") bazel_dep(name = "rules_cc", version = "0.2.3") bazel_dep(name = "rules_python", version = "0.38.0") +bazel_dep( + name = "bazel_skylib", + version = "1.7.1", + dev_dependency = True, +) bazel_dep( name = "buildifier_prebuilt", version = "7.3.1", diff --git a/test/unit/compile_commands/BUILD b/test/unit/compile_commands/BUILD new file mode 100644 index 00000000..042f4d1f --- /dev/null +++ b/test/unit/compile_commands/BUILD @@ -0,0 +1,230 @@ +# Copyright 2026 Ericsson AB +# +# 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 +# +# http://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. + +load("@rules_cc//cc:defs.bzl", "cc_library") +load( + ":compile_commands_analysis_test.bzl", + "custom_ccinfo", + "get_compile_flags_defines_from_impl_deps_test", + "get_compile_flags_includes_from_impl_deps_test", + "get_compile_flags_local_defines_from_impl_deps_test", + "get_compile_flags_no_duplicates_test", + "get_compile_flags_quote_includes_from_deps_test", + "get_compile_flags_system_includes_from_impl_deps_test", + "get_compile_flags_test_suite", +) + +# ============================================================================= +# Analysis tests +# ============================================================================= + +# get_compile_flags +# ----------------- + +cc_library( + name = "compile_flags_tests_with_defines", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + defines = ["MY_DEFINE=1"], + tags = ["manual"], +) + +# BUG (manual): defines from implementation_deps are not collected. +cc_library( + name = "compile_flags_tests_impl_dep_with_defines", + srcs = ["testdata/bar.cc"], + hdrs = ["testdata/bar.h"], + defines = ["IMPL_DEP_DEFINE=1"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_impl_dep_defines", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + implementation_deps = [":compile_flags_tests_impl_dep_with_defines"], + tags = ["manual"], +) + +get_compile_flags_defines_from_impl_deps_test( + name = "get_compile_flags_defines_from_impl_deps", + tags = ["manual"], + target_under_test = ":compile_flags_tests_with_impl_dep_defines", +) + +cc_library( + name = "compile_flags_tests_with_local_defines", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + local_defines = ["LOCAL_DEF=1"], + tags = ["manual"], +) + +# BUG (manual): local_defines from implementation_deps are not collected. +cc_library( + name = "compile_flags_tests_impl_dep_with_local_defines", + srcs = ["testdata/bar.cc"], + hdrs = ["testdata/bar.h"], + local_defines = ["IMPL_DEP_LOCAL_DEF=1"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_impl_dep_local_defines", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + implementation_deps = [":compile_flags_tests_impl_dep_with_local_defines"], + tags = ["manual"], +) + +get_compile_flags_local_defines_from_impl_deps_test( + name = "get_compile_flags_local_defines_from_impl_deps", + tags = ["manual"], + target_under_test = ":compile_flags_tests_with_impl_dep_local_defines", +) + +cc_library( + name = "compile_flags_tests_with_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + includes = ["my/include/path"], + tags = ["manual"], +) + +# includes from implementation_deps should be collected. +cc_library( + name = "compile_flags_tests_impl_dep_with_includes", + srcs = ["testdata/bar.cc"], + hdrs = ["testdata/bar.h"], + includes = ["impl_dep/include"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_impl_dep_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + implementation_deps = [":compile_flags_tests_impl_dep_with_includes"], + tags = ["manual"], +) + +get_compile_flags_includes_from_impl_deps_test( + name = "get_compile_flags_includes_from_impl_deps", + target_under_test = ":compile_flags_tests_with_impl_dep_includes", +) + +cc_library( + name = "compile_flags_tests_with_copts", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + copts = [ + "-Wall", + "-Wextra", + ], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_dep_with_includes", + srcs = ["testdata/bar.cc"], + hdrs = ["testdata/bar.h"], + includes = ["dep/include"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_dep_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + tags = ["manual"], + deps = [":compile_flags_tests_dep_with_includes"], +) + +custom_ccinfo( + name = "compile_flags_tests_sys_include_provider", + system_includes = ["my/sys/include"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_system_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + tags = ["manual"], + deps = [":compile_flags_tests_sys_include_provider"], +) + +# system_includes from implementation_deps should be collected. +custom_ccinfo( + name = "compile_flags_tests_impl_dep_sys_include_provider", + system_includes = ["impl_dep/sys/include"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_impl_dep_system_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + implementation_deps = [":compile_flags_tests_impl_dep_sys_include_provider"], + tags = ["manual"], +) + +get_compile_flags_system_includes_from_impl_deps_test( + name = "get_compile_flags_system_includes_from_impl_deps", + target_under_test = ":compile_flags_tests_with_impl_dep_system_includes", +) + +custom_ccinfo( + name = "compile_flags_tests_quote_include_provider", + quote_includes = ["my/quote/include"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_quote_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + tags = ["manual"], + deps = [":compile_flags_tests_quote_include_provider"], +) + +# BUG (manual): quote_includes from implementation_deps are not collected. +custom_ccinfo( + name = "compile_flags_tests_dep_quote_include_provider", + quote_includes = ["dep/quote/path"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_dep_quote_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + implementation_deps = [":compile_flags_tests_dep_quote_include_provider"], + tags = ["manual"], +) + +get_compile_flags_quote_includes_from_deps_test( + name = "get_compile_flags_quote_includes_from_deps", + tags = ["manual"], + target_under_test = ":compile_flags_tests_with_dep_quote_includes", +) + +get_compile_flags_test_suite(name = "compile_flags_tests") + +# BUG (manual): compile commands should not contain duplicate flags. +get_compile_flags_no_duplicates_test( + name = "get_compile_flags_no_duplicates", + tags = ["manual"], + target_under_test = ":compile_flags_tests_with_dep_includes", +) diff --git a/test/unit/compile_commands/compile_commands_analysis_test.bzl b/test/unit/compile_commands/compile_commands_analysis_test.bzl new file mode 100644 index 00000000..5c0b1a8a --- /dev/null +++ b/test/unit/compile_commands/compile_commands_analysis_test.bzl @@ -0,0 +1,407 @@ +# Copyright 2026 Ericsson AB +# +# 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 +# +# http://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. + +""" +Analysis-phase tests for compile_commands.bzl. + +Tests the following functions indirectly via compile_commands_aspect: + - collect_headers + - get_sources + - get_compile_flags + - _cc_compiler_info +""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load( + "//src:compile_commands.bzl", + "SourceFilesInfo", + "compile_commands_aspect", +) + +# ============================================================================= +# Helper rules +# ============================================================================= + +def _custom_ccinfo_impl(ctx): + """Rule that provides a CcInfo with custom include paths and defines.""" + compilation_context = cc_common.create_compilation_context( + defines = depset(ctx.attr.defines), + system_includes = depset(ctx.attr.system_includes), + quote_includes = depset(ctx.attr.quote_includes), + includes = depset(ctx.attr.includes), + ) + return [CcInfo(compilation_context = compilation_context)] + +custom_ccinfo = rule( + implementation = _custom_ccinfo_impl, + attrs = { + "defines": attr.string_list(default = []), + "includes": attr.string_list(default = []), + "quote_includes": attr.string_list(default = []), + "system_includes": attr.string_list(default = []), + }, +) + +# ============================================================================= +# Helpers +# ============================================================================= + +def _get_compile_commands(source_files_info): + """Extract command strings from SourceFilesInfo.compilation_db.""" + return [entry.command for entry in source_files_info.compilation_db.to_list()] + +# ============================================================================= +# get_compile_flags +# ============================================================================= + +def _get_compile_flags_defines_test_impl(ctx): + """defines appear as -D in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true(env, len(commands) > 0, "Should have at least one compile command") + asserts.true( + env, + "MY_DEFINE" in commands[0], + "Should contain define MY_DEFINE, got: %s" % commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_defines_test = analysistest.make( + _get_compile_flags_defines_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_defines_from_impl_deps_test_impl(ctx): + """BUG: defines from implementation_deps are missing in compile commands. + + get_compile_flags iterates over deps in SOURCE_ATTR and collects includes, + system_includes, and external_includes — but NOT defines. + """ + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + asserts.true( + env, + "IMPL_DEP_DEFINE" in foo_commands[0], + "Should contain define IMPL_DEP_DEFINE from implementation_dep, got: %s" % foo_commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_defines_from_impl_deps_test = analysistest.make( + _get_compile_flags_defines_from_impl_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_local_defines_test_impl(ctx): + """local_defines appear as -D in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true(env, len(commands) > 0, "Should have at least one compile command") + asserts.true( + env, + "LOCAL_DEF" in commands[0], + "Should contain local_define LOCAL_DEF, got: %s" % commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_local_defines_test = analysistest.make( + _get_compile_flags_local_defines_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_local_defines_from_impl_deps_test_impl(ctx): + """BUG: local_defines from implementation_deps are missing in compile commands. + + get_compile_flags iterates over deps in SOURCE_ATTR and collects includes, + system_includes, and external_includes — but NOT local_defines. + """ + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + asserts.true( + env, + "IMPL_DEP_LOCAL_DEF" in foo_commands[0], + "Should contain local_define IMPL_DEP_LOCAL_DEF from implementation_dep, got: %s" % foo_commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_local_defines_from_impl_deps_test = analysistest.make( + _get_compile_flags_local_defines_from_impl_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_includes_test_impl(ctx): + """includes appear as -I in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true(env, len(commands) > 0, "Should have at least one compile command") + asserts.true( + env, + "my/include/path" in commands[0], + "Should contain include path, got: %s" % commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_includes_test = analysistest.make( + _get_compile_flags_includes_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_includes_from_impl_deps_test_impl(ctx): + """includes from implementation_deps propagate to the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + asserts.true( + env, + "impl_dep/include" in foo_commands[0], + "Should contain impl_dep's include path, got: %s" % foo_commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_includes_from_impl_deps_test = analysistest.make( + _get_compile_flags_includes_from_impl_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_copts_test_impl(ctx): + """copts are passed through to the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true(env, len(commands) > 0, "Should have at least one compile command") + asserts.true( + env, + "-Wall" in commands[0], + "Should contain -Wall, got: %s" % commands[0], + ) + asserts.true( + env, + "-Wextra" in commands[0], + "Should contain -Wextra, got: %s" % commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_copts_test = analysistest.make( + _get_compile_flags_copts_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_dep_includes_test_impl(ctx): + """includes from deps propagate to the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + asserts.true( + env, + "dep/include" in foo_commands[0], + "Should contain dep's include path, got: %s" % foo_commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_dep_includes_test = analysistest.make( + _get_compile_flags_dep_includes_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_system_includes_test_impl(ctx): + """system_includes appear as -isystem in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true(env, len(commands) > 0, "Should have at least one compile command") + asserts.true( + env, + "-isystem my/sys/include" in commands[0], + "Should contain -isystem my/sys/include, got: %s" % commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_system_includes_test = analysistest.make( + _get_compile_flags_system_includes_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_system_includes_from_impl_deps_test_impl(ctx): + """system_includes from implementation_deps appear as -isystem in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + asserts.true( + env, + "-isystem impl_dep/sys/include" in foo_commands[0], + "Should contain -isystem impl_dep/sys/include from implementation_dep, got: %s" % foo_commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_system_includes_from_impl_deps_test = analysistest.make( + _get_compile_flags_system_includes_from_impl_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_quote_includes_test_impl(ctx): + """quote_includes appear as -iquote in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true(env, len(commands) > 0, "Should have at least one compile command") + asserts.true( + env, + "-iquote my/quote/include" in commands[0], + "Should contain -iquote my/quote/include, got: %s" % commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_quote_includes_test = analysistest.make( + _get_compile_flags_quote_includes_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_quote_includes_from_deps_test_impl(ctx): + """BUG: quote_includes from implementation_deps are missing in compile commands. + + get_compile_flags iterates over deps in SOURCE_ATTR and collects includes, + system_includes, and external_includes — but NOT quote_includes. + """ + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + + asserts.true( + env, + "-iquote dep/quote/path" in foo_commands[0], + "Should contain -iquote dep/quote/path from implementation_dep, got: %s" % foo_commands[0], + ) + + return analysistest.end(env) + +get_compile_flags_quote_includes_from_deps_test = analysistest.make( + _get_compile_flags_quote_includes_from_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_compile_flags_no_duplicates_test_impl(ctx): + """BUG: Compile flags should not contain duplicates. + + get_compile_flags may add the same include path multiple times — once + from the target's own CcInfo compilation_context, and again when iterating + over deps in SOURCE_ATTR. This test asserts the desired behavior: + no flag should appear more than once in a compile command. + """ + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + foo_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(foo_commands) > 0, "Should have a command for foo.cc") + + # Split command into flags and check for duplicates + flags = foo_commands[0].split(" ") + seen = [] + duplicates = [] + for f in flags: + if f == "": + continue + if f in seen and f not in duplicates: + duplicates.append(f) + seen.append(f) + asserts.true( + env, + len(duplicates) == 0, + "Compile command should not have duplicate flags, found: %s" % duplicates, + ) + + return analysistest.end(env) + +get_compile_flags_no_duplicates_test = analysistest.make( + _get_compile_flags_no_duplicates_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +# ============================================================================= +# Test suites +# ============================================================================= + +def get_compile_flags_test_suite(name): + """Analysis tests for get_compile_flags. + + Args: + name: the name prefix for the test suite. + """ + get_compile_flags_defines_test( + name = name + "_defines_test", + target_under_test = ":" + name + "_with_defines", + ) + get_compile_flags_local_defines_test( + name = name + "_local_defines_test", + target_under_test = ":" + name + "_with_local_defines", + ) + get_compile_flags_includes_test( + name = name + "_includes_test", + target_under_test = ":" + name + "_with_includes", + ) + get_compile_flags_copts_test( + name = name + "_copts_test", + target_under_test = ":" + name + "_with_copts", + ) + get_compile_flags_dep_includes_test( + name = name + "_dep_includes_test", + target_under_test = ":" + name + "_with_dep_includes", + ) + get_compile_flags_system_includes_test( + name = name + "_system_includes_test", + target_under_test = ":" + name + "_with_system_includes", + ) + get_compile_flags_quote_includes_test( + name = name + "_quote_includes_test", + target_under_test = ":" + name + "_with_quote_includes", + ) + + native.test_suite( + name = name, + tests = [ + ":" + name + "_defines_test", + ":" + name + "_local_defines_test", + ":" + name + "_includes_test", + ":" + name + "_copts_test", + ":" + name + "_dep_includes_test", + ":" + name + "_system_includes_test", + ":" + name + "_quote_includes_test", + ], + ) diff --git a/test/unit/compile_commands/testdata/bar.c b/test/unit/compile_commands/testdata/bar.c new file mode 100644 index 00000000..e194bebb --- /dev/null +++ b/test/unit/compile_commands/testdata/bar.c @@ -0,0 +1,2 @@ +#include "bar.h" +void bar(void) {} diff --git a/test/unit/compile_commands/testdata/bar.cc b/test/unit/compile_commands/testdata/bar.cc new file mode 100644 index 00000000..e194bebb --- /dev/null +++ b/test/unit/compile_commands/testdata/bar.cc @@ -0,0 +1,2 @@ +#include "bar.h" +void bar(void) {} diff --git a/test/unit/compile_commands/testdata/bar.h b/test/unit/compile_commands/testdata/bar.h new file mode 100644 index 00000000..2f848f6d --- /dev/null +++ b/test/unit/compile_commands/testdata/bar.h @@ -0,0 +1,4 @@ +#ifndef BAR_H +#define BAR_H +void bar(void); +#endif diff --git a/test/unit/compile_commands/testdata/foo.cc b/test/unit/compile_commands/testdata/foo.cc new file mode 100644 index 00000000..02ec45e8 --- /dev/null +++ b/test/unit/compile_commands/testdata/foo.cc @@ -0,0 +1,2 @@ +#include "foo.h" +void foo(void) {} diff --git a/test/unit/compile_commands/testdata/foo.h b/test/unit/compile_commands/testdata/foo.h new file mode 100644 index 00000000..518642bf --- /dev/null +++ b/test/unit/compile_commands/testdata/foo.h @@ -0,0 +1,4 @@ +#ifndef FOO_H +#define FOO_H +void foo(void); +#endif