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/src/compile_commands.bzl b/src/compile_commands.bzl index 198a419a..bd8b8eed 100644 --- a/src/compile_commands.bzl +++ b/src/compile_commands.bzl @@ -383,14 +383,30 @@ platforms_transition = transition( ], ) -def _check_source_files(source_files, compilation_db): +def check_source_files(source_files, compilation_db): + """Check that all files in compilation_db are present in source_files. + + Args: + source_files: list of source file objects. + compilation_db: list of compilation database entries. + Returns: + None if all files are present, or an error message string if a file is missing. + """ available_sources = [src.path for src in source_files] checking_sources = [item.file for item in compilation_db] for src in checking_sources: if src not in available_sources: - fail("File: %s\nNot available in collected source files" % src) + return "File: %s\nNot available in collected source files" % src + return None -def _compile_commands_json(compilation_db): +def compile_commands_json(compilation_db): + """Generate a compile_commands.json string from a compilation database. + + Args: + compilation_db: list of structs with file, command, directory fields. + Returns: + A JSON string representing the compilation database. + """ json_file = "[\n" entries = [json.encode(entry) for entry in compilation_db] json_file += ",\n".join(entries) @@ -426,10 +442,12 @@ def compile_commands_impl(ctx): fail("Compilation database is empty!") # Check that we collect all required source files - _check_source_files(source_files, compilation_db) + error = check_source_files(source_files, compilation_db) + if error: + fail(error) # Generate compile_commands.json from compilation database info - compile_db_json = _compile_commands_json(compilation_db) + compile_db_json = compile_commands_json(compilation_db) # Save compile_commands.json file ctx.actions.write( diff --git a/test/unit/compile_commands/BUILD b/test/unit/compile_commands/BUILD new file mode 100644 index 00000000..30e7c44d --- /dev/null +++ b/test/unit/compile_commands/BUILD @@ -0,0 +1,219 @@ +# 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", + "cc_compiler_info_test_suite", + "collect_headers_test_suite", + "custom_ccinfo", + "get_compile_flags_no_duplicates_test", + "get_compile_flags_quote_includes_from_deps_test", + "get_compile_flags_test_suite", + "get_sources_test_suite", +) +load( + ":compile_commands_unit_test.bzl", + "compile_commands_test_suite", +) + +# ============================================================================= +# Unit tests +# ============================================================================= + +compile_commands_test_suite(name = "compile_commands_tests") + +# ============================================================================= +# Analysis tests +# ============================================================================= + +# Shared test targets +# ------------------- + +cc_library( + name = "collect_headers_tests_foo", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + tags = ["manual"], +) + +cc_library( + name = "collect_headers_tests_bar", + srcs = ["testdata/bar.cc"], + hdrs = ["testdata/bar.h"], + tags = ["manual"], +) + +cc_library( + name = "collect_headers_tests_with_dep", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + tags = ["manual"], + deps = [":collect_headers_tests_bar"], +) + +cc_library( + name = "collect_headers_tests_with_impl_dep", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + implementation_deps = [":collect_headers_tests_bar"], + tags = ["manual"], +) + +cc_library( + name = "collect_headers_tests_no_hdrs", + srcs = ["testdata/foo.cc"], + tags = ["manual"], +) + +# collect_headers +# --------------- + +collect_headers_test_suite(name = "collect_headers_tests") + +# get_sources +# ----------- + +get_sources_test_suite(name = "get_sources_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"], +) + +cc_library( + name = "compile_flags_tests_with_local_defines", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + local_defines = ["LOCAL_DEF=1"], + tags = ["manual"], +) + +cc_library( + name = "compile_flags_tests_with_includes", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + includes = ["my/include/path"], + tags = ["manual"], +) + +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"], +) + +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"], +) + +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", +) + +# 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", +) + +# _cc_compiler_info +# ----------------- + +cc_library( + name = "cc_compiler_info_tests_cpp_target", + srcs = ["testdata/foo.cc"], + hdrs = ["testdata/foo.h"], + tags = ["manual"], +) + +cc_library( + name = "cc_compiler_info_tests_c_target", + srcs = ["testdata/bar.c"], + hdrs = ["testdata/bar.h"], + tags = ["manual"], +) + +cc_compiler_info_test_suite(name = "cc_compiler_info_tests") 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..549dcdec --- /dev/null +++ b/test/unit/compile_commands/compile_commands_analysis_test.bzl @@ -0,0 +1,663 @@ +# 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.""" + compilation_context = cc_common.create_compilation_context( + 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 = { + "includes": attr.string_list(default = []), + "quote_includes": attr.string_list(default = []), + "system_includes": attr.string_list(default = []), + }, +) + +# ============================================================================= +# Helpers +# ============================================================================= + +def _get_header_basenames(source_files_info): + """Flatten SourceFilesInfo.headers into a list of basename strings.""" + basenames = [] + for h in source_files_info.headers.to_list(): + if hasattr(h, "basename"): + basenames.append(h.basename) + elif hasattr(h, "to_list"): + for f in h.to_list(): + if hasattr(f, "basename"): + basenames.append(f.basename) + return basenames + +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()] + +def _get_source_basenames(source_files_info): + """Extract basenames from SourceFilesInfo.transitive_source_files.""" + return [f.basename for f in source_files_info.transitive_source_files.to_list()] + +# ============================================================================= +# collect_headers +# ============================================================================= + +def _collect_headers_direct_test_impl(ctx): + """Direct hdrs are collected.""" + env = analysistest.begin(ctx) + header_basenames = _get_header_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.h" in header_basenames, + "collect_headers should find foo.h, got: %s" % header_basenames, + ) + + return analysistest.end(env) + +collect_headers_direct_test = analysistest.make( + _collect_headers_direct_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _collect_headers_transitive_deps_test_impl(ctx): + """Headers from deps are included transitively.""" + env = analysistest.begin(ctx) + header_basenames = _get_header_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.h" in header_basenames, + "Should find direct foo.h, got: %s" % header_basenames, + ) + asserts.true( + env, + "bar.h" in header_basenames, + "Should find transitive bar.h from deps, got: %s" % header_basenames, + ) + + return analysistest.end(env) + +collect_headers_transitive_deps_test = analysistest.make( + _collect_headers_transitive_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _collect_headers_implementation_deps_test_impl(ctx): + """Headers from implementation_deps are included.""" + env = analysistest.begin(ctx) + header_basenames = _get_header_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.h" in header_basenames, + "Should find direct foo.h, got: %s" % header_basenames, + ) + asserts.true( + env, + "bar.h" in header_basenames, + "Should find bar.h from implementation_deps, got: %s" % header_basenames, + ) + + return analysistest.end(env) + +collect_headers_implementation_deps_test = analysistest.make( + _collect_headers_implementation_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _collect_headers_no_hdrs_test_impl(ctx): + """Target with no hdrs produces no custom headers.""" + env = analysistest.begin(ctx) + header_basenames = _get_header_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.false( + env, + "foo.h" in header_basenames, + "Should not contain foo.h, got: %s" % header_basenames, + ) + asserts.false( + env, + "bar.h" in header_basenames, + "Should not contain bar.h, got: %s" % header_basenames, + ) + + return analysistest.end(env) + +collect_headers_no_hdrs_test = analysistest.make( + _collect_headers_no_hdrs_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +# ============================================================================= +# get_sources +# ============================================================================= + +def _get_sources_srcs_and_hdrs_test_impl(ctx): + """Both srcs and hdrs are collected.""" + env = analysistest.begin(ctx) + basenames = _get_source_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.cc" in basenames, + "Should collect foo.cc from srcs, got: %s" % basenames, + ) + asserts.true( + env, + "foo.h" in basenames, + "Should collect foo.h from hdrs, got: %s" % basenames, + ) + + return analysistest.end(env) + +get_sources_srcs_and_hdrs_test = analysistest.make( + _get_sources_srcs_and_hdrs_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_sources_only_srcs_test_impl(ctx): + """Only srcs collected when no hdrs defined.""" + env = analysistest.begin(ctx) + basenames = _get_source_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.cc" in basenames, + "Should collect foo.cc, got: %s" % basenames, + ) + asserts.false( + env, + "foo.h" in basenames, + "Should not collect foo.h when not in hdrs, got: %s" % basenames, + ) + + return analysistest.end(env) + +get_sources_only_srcs_test = analysistest.make( + _get_sources_only_srcs_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_sources_transitive_test_impl(ctx): + """Sources from deps are accumulated transitively.""" + env = analysistest.begin(ctx) + basenames = _get_source_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.cc" in basenames, + "Should collect foo.cc, got: %s" % basenames, + ) + asserts.true( + env, + "bar.cc" in basenames, + "Should collect bar.cc from dep, got: %s" % basenames, + ) + asserts.true( + env, + "bar.h" in basenames, + "Should collect bar.h from dep's hdrs, got: %s" % basenames, + ) + + return analysistest.end(env) + +get_sources_transitive_test = analysistest.make( + _get_sources_transitive_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _get_sources_implementation_deps_test_impl(ctx): + """Sources from implementation_deps are accumulated.""" + env = analysistest.begin(ctx) + basenames = _get_source_basenames(analysistest.target_under_test(env)[SourceFilesInfo]) + + asserts.true( + env, + "foo.cc" in basenames, + "Should collect foo.cc, got: %s" % basenames, + ) + asserts.true( + env, + "bar.cc" in basenames, + "Should collect bar.cc from implementation_deps, got: %s" % basenames, + ) + asserts.true( + env, + "bar.h" in basenames, + "Should collect bar.h from implementation_deps' hdrs, got: %s" % basenames, + ) + + return analysistest.end(env) + +get_sources_implementation_deps_test = analysistest.make( + _get_sources_implementation_deps_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +# ============================================================================= +# 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_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_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_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_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 fixed 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], +) + +# ============================================================================= +# _cc_compiler_info (C vs C++ language mode) +# ============================================================================= + +def _cc_compiler_info_cpp_language_mode_test_impl(ctx): + """C++ files get -x c++ in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + cpp_commands = [c for c in commands if "foo.cc" in c] + asserts.true(env, len(cpp_commands) > 0, "Should have a command for foo.cc") + asserts.true( + env, + "-x c++" in cpp_commands[0], + "C++ file should get -x c++, got: %s" % cpp_commands[0], + ) + + return analysistest.end(env) + +cc_compiler_info_cpp_gets_language_mode_test = analysistest.make( + _cc_compiler_info_cpp_language_mode_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +def _cc_compiler_info_c_no_language_mode_test_impl(ctx): + """C files do NOT get -x c++ in the compile command.""" + env = analysistest.begin(ctx) + commands = _get_compile_commands(analysistest.target_under_test(env)[SourceFilesInfo]) + + c_commands = [c for c in commands if "bar.c" in c] + asserts.true(env, len(c_commands) > 0, "Should have a command for bar.c") + asserts.false( + env, + "-x c++" in c_commands[0], + "C file should NOT get -x c++, got: %s" % c_commands[0], + ) + + return analysistest.end(env) + +cc_compiler_info_c_no_language_mode_test = analysistest.make( + _cc_compiler_info_c_no_language_mode_test_impl, + extra_target_under_test_aspects = [compile_commands_aspect], +) + +# ============================================================================= +# Test suites +# ============================================================================= + +def collect_headers_test_suite(name): + """Analysis tests for collect_headers. + + Args: + name: the name prefix for the test suite. + """ + collect_headers_direct_test( + name = name + "_direct_test", + target_under_test = ":" + name + "_foo", + ) + collect_headers_transitive_deps_test( + name = name + "_transitive_test", + target_under_test = ":" + name + "_with_dep", + ) + collect_headers_implementation_deps_test( + name = name + "_impl_dep_test", + target_under_test = ":" + name + "_with_impl_dep", + ) + collect_headers_no_hdrs_test( + name = name + "_no_hdrs_test", + target_under_test = ":" + name + "_no_hdrs", + ) + + native.test_suite( + name = name, + tests = [ + ":" + name + "_direct_test", + ":" + name + "_transitive_test", + ":" + name + "_impl_dep_test", + ":" + name + "_no_hdrs_test", + ], + ) + +def get_sources_test_suite(name): + """Analysis tests for get_sources. + + Args: + name: the name prefix for the test suite. + """ + get_sources_srcs_and_hdrs_test( + name = name + "_srcs_and_hdrs_test", + target_under_test = ":collect_headers_tests_foo", + ) + get_sources_only_srcs_test( + name = name + "_only_srcs_test", + target_under_test = ":collect_headers_tests_no_hdrs", + ) + get_sources_transitive_test( + name = name + "_transitive_test", + target_under_test = ":collect_headers_tests_with_dep", + ) + get_sources_implementation_deps_test( + name = name + "_impl_deps_test", + target_under_test = ":collect_headers_tests_with_impl_dep", + ) + + native.test_suite( + name = name, + tests = [ + ":" + name + "_srcs_and_hdrs_test", + ":" + name + "_only_srcs_test", + ":" + name + "_transitive_test", + ":" + name + "_impl_deps_test", + ], + ) + +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", + ], + ) + +def cc_compiler_info_test_suite(name): + """Analysis tests for _cc_compiler_info (C vs C++ language mode). + + Args: + name: the name prefix for the test suite. + """ + cc_compiler_info_cpp_gets_language_mode_test( + name = name + "_cpp_language_mode_test", + target_under_test = ":" + name + "_cpp_target", + ) + cc_compiler_info_c_no_language_mode_test( + name = name + "_c_no_language_mode_test", + target_under_test = ":" + name + "_c_target", + ) + + native.test_suite( + name = name, + tests = [ + ":" + name + "_cpp_language_mode_test", + ":" + name + "_c_no_language_mode_test", + ], + ) diff --git a/test/unit/compile_commands/compile_commands_unit_test.bzl b/test/unit/compile_commands/compile_commands_unit_test.bzl new file mode 100644 index 00000000..e03b237e --- /dev/null +++ b/test/unit/compile_commands/compile_commands_unit_test.bzl @@ -0,0 +1,185 @@ +# 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. + +""" +Unit tests for compile_commands.bzl pure Starlark functions. + +Tests the following functions directly (no analysis phase needed): + - compile_commands_json + - check_source_files +""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load( + "//src:compile_commands.bzl", + "check_source_files", + "compile_commands_json", +) + +# ============================================================================= +# compile_commands_json +# ============================================================================= + +def _compile_commands_json_single_entry_test_impl(ctx): + """Single entry produces valid JSON array.""" + env = unittest.begin(ctx) + + compilation_db = [struct( + file = "src/main.cc", + command = "clang++ -c src/main.cc", + directory = ".", + )] + + result = compile_commands_json(compilation_db) + + asserts.true(env, result.startswith("[\n"), "Should start with '[\\n'") + asserts.true(env, result.endswith("]\n"), "Should end with ']\\n'") + asserts.true(env, "src/main.cc" in result, "Should contain file path") + asserts.true(env, "clang++ -c src/main.cc" in result, "Should contain command") + + return unittest.end(env) + +compile_commands_json_single_entry_test = unittest.make( + _compile_commands_json_single_entry_test_impl, +) + +def _compile_commands_json_multiple_entries_test_impl(ctx): + """Multiple entries are comma-separated without trailing comma.""" + env = unittest.begin(ctx) + + compilation_db = [ + struct(file = "src/main.cc", command = "clang++ -c src/main.cc", directory = "."), + struct(file = "src/util.c", command = "clang -c src/util.c", directory = "."), + struct(file = "src/lib.cc", command = "clang++ -c src/lib.cc", directory = "/ws"), + ] + + result = compile_commands_json(compilation_db) + + asserts.true(env, "src/main.cc" in result, "Should contain first file") + asserts.true(env, "src/util.c" in result, "Should contain second file") + asserts.true(env, "src/lib.cc" in result, "Should contain third file") + asserts.true(env, ",\n" in result, "Entries should be comma-separated") + asserts.false(env, ",\n]\n" in result, "Should not have trailing comma") + + return unittest.end(env) + +compile_commands_json_multiple_entries_test = unittest.make( + _compile_commands_json_multiple_entries_test_impl, +) + +def _compile_commands_json_empty_list_test_impl(ctx): + """Empty list produces empty JSON array.""" + env = unittest.begin(ctx) + + result = compile_commands_json([]) + + asserts.equals(env, "[\n]\n", result, "Empty list should produce empty JSON array") + + return unittest.end(env) + +compile_commands_json_empty_list_test = unittest.make( + _compile_commands_json_empty_list_test_impl, +) + +# ============================================================================= +# check_source_files +# ============================================================================= + +def _check_source_files_all_present_test_impl(ctx): + """All compilation DB files found in source list → no error.""" + env = unittest.begin(ctx) + + source_files = [struct(path = "src/main.cc"), struct(path = "src/util.c")] + compilation_db = [struct(file = "src/main.cc"), struct(file = "src/util.c")] + + result = check_source_files(source_files, compilation_db) + asserts.equals(env, None, result, "Should return None when all files present") + + return unittest.end(env) + +check_source_files_all_present_test = unittest.make( + _check_source_files_all_present_test_impl, +) + +def _check_source_files_missing_file_test_impl(ctx): + """Missing file in source list → error mentioning the file.""" + env = unittest.begin(ctx) + + source_files = [struct(path = "src/main.cc")] + compilation_db = [struct(file = "src/main.cc"), struct(file = "src/missing.cc")] + + result = check_source_files(source_files, compilation_db) + asserts.true(env, result != None, "Should return an error message") + asserts.true(env, "src/missing.cc" in result, "Error should mention the missing file") + asserts.true(env, "Not available" in result, "Error should indicate file is not available") + + return unittest.end(env) + +check_source_files_missing_file_test = unittest.make( + _check_source_files_missing_file_test_impl, +) + +def _check_source_files_empty_sources_test_impl(ctx): + """Empty source list with non-empty db → error.""" + env = unittest.begin(ctx) + + source_files = [] + compilation_db = [struct(file = "src/main.cc")] + + result = check_source_files(source_files, compilation_db) + asserts.true(env, result != None, "Should return error when sources empty but db has entries") + asserts.true(env, "src/main.cc" in result, "Error should mention the missing file") + + return unittest.end(env) + +check_source_files_empty_sources_test = unittest.make( + _check_source_files_empty_sources_test_impl, +) + +def _check_source_files_empty_db_test_impl(ctx): + """Empty compilation database → no error.""" + env = unittest.begin(ctx) + + source_files = [struct(path = "src/main.cc")] + compilation_db = [] + + result = check_source_files(source_files, compilation_db) + asserts.equals(env, None, result, "Should return None for empty compilation db") + + return unittest.end(env) + +check_source_files_empty_db_test = unittest.make( + _check_source_files_empty_db_test_impl, +) + +# ============================================================================= +# Test suite +# ============================================================================= + +def compile_commands_test_suite(name): + """Unit test suite for compile_commands.bzl pure functions. + + Args: + name: the name of the test suite target. + """ + unittest.suite( + name, + compile_commands_json_single_entry_test, + compile_commands_json_multiple_entries_test, + compile_commands_json_empty_list_test, + check_source_files_all_present_test, + check_source_files_missing_file_test, + check_source_files_empty_db_test, + check_source_files_empty_sources_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