Build unit test for C with mocked dependencies using bazel
Asked Answered
N

3

8

I have a C codebase I'm trying to build with Bazel. This codebase is covered with unit tests that use the fff library for generating function mocks in C. The actual library is not important though, I have a problem with the whole concept of function mocks.

Right now I have a makefile where I link and run my tests. When I build a test, I compile and link the library under test and the test source itself. This test also defines mocks for library's dependencies. When linked, mocked symbols get resolved to mock implementations and everything's working as expected. But the reason it does so is that I don't link the actual dependency library, I only link the mock symbols defined in the test source.

The main question is: how do I do this with Bazel? When linking the binary for a cc_test target, Bazel compilis and links all transitive dependencies. Since the library under test depends (via deps) on the real implementation of a symbol, this real definition is linked alongside the mock one and naturally I get this error: multiple definition of XXX.

Example:

cc_library(
  name = "a",
  # a.cc has the real version of "void some_function()".
  srcs = ["a.cc"],
  hdrs = ["a.h"],
)

# This test is working just fine.
cc_test(
  name = "a_test",
  srcs = ["a_test.cpp"],
  deps = [":a"],
)

cc_library(
  name = "b",
   # b.cc includes a.h and uses "void some_function()".
  srcs = ["b.cc"],
  hdrs = ["b.h"],
  deps = [":a"],
)

# This test has two definitions for "void some_function()":
# the real one and the mock one.
cc_test(
  name = "b_test",
  # b_test.cpp has the mock version of "void some_function()".
  srcs = ["b_test.cpp"],
  deps = [":b"],
)

I'm not exactly new to Bazel, but I'm not an expert either and after spending many hours trying things I have failed. Any advice on how do I make it work?

Newton answered 27/12, 2018 at 20:20 Comment(2)
why do you tag c++ for a C question?Collarbone
as far as I see the whole project is in c++ and not c. you use the suffix cpp and also the c++ connector of bazel (they don't have one for c I think). Don't know if usage of fff is possible then but that's another problem.Diandrous
D
0

Your problem is probably a very basic one, that is related to namespaces and/or extending classes.

c has no namespaces by definition but they can be emulated somehow:
https://mcmap.net/q/167379/-why-doesn-39-t-ansi-c-have-namespaces

But as you probably use c++ and never c, namespaces could be used directly.

The concrete problem is that you've two times the same function which would be expressed with namespaces a.some_function() and b.some_function().

In Bazel there exist several options how to solve it, I post just short copied snippets, details you've to read on the linked pages.

deps = [
    "@gtest//:main",
    "//lib:hello-greet",
],

Link: https://docs.bazel.build/versions/master/cpp-use-cases.html
There the snippet is used also for tests but I think that's not important related to your problem.

Then there exist toolchains in Bazel, here is an example:

config_setting(
    name = "on_linux",
    constraint_values = [
        "@bazel_tools//platforms:linux",
    ],
)

config_setting(
    name = "on_windows",
    constraint_values = [
        "@bazel_tools//platforms:windows",
    ],
)

bar_binary(
    name = "myprog",
    srcs = ["mysrc.bar"],
    compiler = select({
        ":on_linux": "//bar_tools:barc_linux",
        ":on_windows": "//bar_tools:barc_windows",
    }),
)

Link: https://docs.bazel.build/versions/master/toolchains.html

As I'm not programming in c or c++ usually it would take me quite some time to dig enough in it to post the solution, so I've to keep it without a real solution but I hope the problem and the way to solve it got a bit clearer.
I'd try it with the second link first, below the first snippet above.

Diandrous answered 10/1, 2019 at 0:1 Comment(2)
Thanks, but this is not the problem I'm trying to solve. And yes, I do, in fact, use C, not C++. I do use C++ library GoogleTest for unit tests, but code under test is pure C. In any case, the question is about linking, not compiling.Newton
why you use cc and cpp file-suffix? your problem is related to these files. The recommended way in my answer never mentions compiling, but different ways how to link the 'deps'Diandrous
W
0

I don't see any good solution in the Bazel itself. In case of a C++ the Bazel try to create a simple and clear dependency management system to reduce all shenanigans, which can be done in C/C++ compiling/linking system. And your case is rather unusual.

Only solution I see is to use weak symbols to move that burden to the linker. You can use __attribute__((weak)) to annotate you "normal" function. Your "fake" functions should be chosen over the "normal" one in all targets, where strong symbols for functions (fake function) are present

Walkling answered 3/3, 2021 at 19:9 Comment(0)
I
0

I had the same problem and wrote a bazel rule to add or remove link dependencies from CC targets. It's crude and doesn't handle any non-linker dependencies:

def _replace_cc_link_deps_impl(ctx):
    remove_labels = [target.label for target in ctx.attr.remove]
    remove_linker_inputs = []
    if ctx.attr.remove_deps:
        remove_cc_info = cc_common.merge_cc_infos(cc_infos = [target[CcInfo] for target in ctx.attr.remove])
        remove_linker_inputs = remove_cc_info.linking_context.linker_inputs.to_list()

    actual_cc_info = ctx.attr.actual[CcInfo]
    linker_inputs = [
        linker_input
        for linker_input in actual_cc_info.linking_context.linker_inputs.to_list()
        if linker_input.owner not in remove_labels and linker_input not in remove_linker_inputs
    ]
    linking_context = cc_common.create_linking_context(linker_inputs = depset(linker_inputs, order = "topological"))

    cc_info = cc_common.merge_cc_infos(cc_infos = [
        CcInfo(compilation_context = actual_cc_info.compilation_context, linking_context = linking_context),
    ] + [target[CcInfo] for target in ctx.attr.add])
    return [cc_info]

replace_cc_link_deps = rule(
    implementation = _replace_cc_link_deps_impl,
    attrs = {
        "actual": attr.label(mandatory = True, providers = [CcInfo], doc = "The target whose dependencies you want to modify"),
        "remove": attr.label_list(mandatory = True, providers = [CcInfo], doc = "The link dependencies of these targets are removed."),
        "add": attr.label_list(providers = [CcInfo], doc = "List of targets that are added as link dependencies."),
        "remove_deps": attr.bool(
            doc = "If true, also remove all dependencies of removed targets. " +
                  "This may inadvertently remove common dependencies! " +
                  "You can either try to re-add those, or explicitly remove transitive dependencies instead.",
        ),
    },
    fragments = ["cpp"],
    provides = [CcInfo],
)

Given your example, you could then define:

replace_cc_link_deps(
    name = "b_without_a",
    actual = ":b",
    remove = [":a"],
)

cc_test(
  name = "b_test",
  srcs = ["b_test.cpp"],
  deps = [":b_without_a"],
)
Ingenuity answered 21/7, 2023 at 13:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.