본문 바로가기

building system/bazel

bzlMod 기반 GoogleTest 기본 예제 구현하기

반응형

개요

이번 게시글에서는 새로운 의존성 모델인 bzlMod는 BCR(Bazel Centeral Registry)를 중심으로 다양한 라이브러리에 접근할 수 있도록 설계했다. 이미 많은 라이브러리를 사용할 수 있고, 빠른 속도로 다양한 라이브러리를 추가하고 있다.

그 중에 GoogleTest 라이브러리를 사용해 unittest 기본 예제를 만들어보는 것이 목표다.

실제 구체적인 GTest에 대한 구체적인 사용법은 관련 사이트를 통해 얻어낼 수 있고, 여기서는 단순히 unittest 빌드 시스템 구축이 목표다.

풀어야 할 문제

폴더 구조는 아래와 같다. maic.cc 파일을 컴파일할 수 있도록 나머지 파일 내용을 완성하는 것이 풀어야 할 문제다.

│  .bazelrc
│  MODULE.bazel
│
└─project
        BUILD.bazel
        main.cc

maic.cc

아래 코딩은 fmt 라이브러리의 unitest 샘플 중 하나다. 일단 코딩을 보면 gtest/gtest.h 부분을 문제없이 작동하도록 googletest 라이브러리가 참여할 수 있도록 빌드 환경을 구축해야 한다.
다음으로 전체 코딩에서 main 함수가 없다. 따라서 unittest 과정에서 사용하도록 만들어진 googlemock/src/gmock_main.cc 파일을 코딩에 참여시켜야 한다. googletest 라이브러리는 bazel 빌드 시스템을 사용하고 BUILD 파일을 확인해보면, @googletest//:gtest_main에 의존하면 간단히 해결된다. BCR에서 googletest가 이미 bazel 기반임으로 포워딩하도록 코딩되어 있다.
마지막 fmt 라이브러리에 대한 unittest 파일이기 때문에, fmt 라이브러리를 BCR에서 참조해야 한다. fmt 자체는 cmake 빌드 시스템이지만, BCR에서 추가 정보를 제공함으로써 bazel 환경에서 사용할 수 있다.

// Formatting library for C++ - dynamic argument store tests
//
// Copyright (c) 2012 - present, Victor Zverovich
// All rights reserved.
//
// For the license information refer to format.h.

#include "fmt/args.h"
#include <memory>
#include "gtest/gtest.h"

TEST(args_test, basic) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    store.push_back(42);
    store.push_back("abc1");
    store.push_back(1.5f);
    EXPECT_EQ("42 and abc1 and 1.5", fmt::vformat("{} and {} and {}", store));
}

TEST(args_test, strings_and_refs) {
    // Unfortunately the tests are compiled with old ABI so strings use COW.
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    char str[] = "1234567890";
    store.push_back(str);
    store.push_back(std::cref(str));
    store.push_back(fmt::string_view{ str });
    str[0] = 'X';

    auto result = fmt::vformat("{} and {} and {}", store);
    EXPECT_EQ("1234567890 and X234567890 and X234567890", result);
}

struct custom_type {
    int i = 0;
};

FMT_BEGIN_NAMESPACE
template <> struct formatter<custom_type> {
    auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
        return ctx.begin();
    }

    template <typename FormatContext>
    auto format(const custom_type& p, FormatContext& ctx) const
        -> decltype(ctx.out()) {
        return fmt::format_to(ctx.out(), "cust={}", p.i);
    }
};
FMT_END_NAMESPACE

TEST(args_test, custom_format) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    auto c = custom_type();
    store.push_back(c);
    ++c.i;
    store.push_back(c);
    ++c.i;
    store.push_back(std::cref(c));
    ++c.i;
    auto result = fmt::vformat("{} and {} and {}", store);
    EXPECT_EQ("cust=0 and cust=1 and cust=3", result);
}

struct to_stringable {
    friend fmt::string_view to_string_view(to_stringable) { return {}; }
};

FMT_BEGIN_NAMESPACE
template <> struct formatter<to_stringable> {
    auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
        return ctx.begin();
    }

    auto format(to_stringable, format_context& ctx) const -> decltype(ctx.out()) {
        return ctx.out();
    }
};
FMT_END_NAMESPACE

TEST(args_test, to_string_and_formatter) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    auto s = to_stringable();
    store.push_back(s);
    store.push_back(std::cref(s));
    fmt::vformat("", store);
}

TEST(args_test, named_int) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    store.push_back(fmt::arg("a1", 42));
    EXPECT_EQ("42", fmt::vformat("{a1}", store));
}

TEST(args_test, named_strings) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    char str[] = "1234567890";
    store.push_back(fmt::arg("a1", str));
    store.push_back(fmt::arg("a2", std::cref(str)));
    str[0] = 'X';
    EXPECT_EQ("1234567890 and X234567890", fmt::vformat("{a1} and {a2}", store));
}

TEST(args_test, named_arg_by_ref) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    char band[] = "Rolling Stones";
    store.push_back(fmt::arg("band", std::cref(band)));
    band[9] = 'c';  // Changing band affects the output.
    EXPECT_EQ(fmt::vformat("{band}", store), "Rolling Scones");
}

TEST(args_test, named_custom_format) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    auto c = custom_type();
    store.push_back(fmt::arg("c1", c));
    ++c.i;
    store.push_back(fmt::arg("c2", c));
    ++c.i;
    store.push_back(fmt::arg("c_ref", std::cref(c)));
    ++c.i;
    auto result = fmt::vformat("{c1} and {c2} and {c_ref}", store);
    EXPECT_EQ("cust=0 and cust=1 and cust=3", result);
}

TEST(args_test, clear) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    store.push_back(42);

    auto result = fmt::vformat("{}", store);
    EXPECT_EQ("42", result);

    store.push_back(43);
    result = fmt::vformat("{} and {}", store);
    EXPECT_EQ("42 and 43", result);

    store.clear();
    store.push_back(44);
    result = fmt::vformat("{}", store);
    EXPECT_EQ("44", result);
}

TEST(args_test, reserve) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    store.reserve(2, 1);
    store.push_back(1.5f);
    store.push_back(fmt::arg("a", 42));
    auto result = fmt::vformat("{} and {a}", store);
    EXPECT_EQ("1.5 and 42", result);
}

struct copy_throwable {
    copy_throwable() {}
    copy_throwable(const copy_throwable&) { throw "deal with it"; }
};

FMT_BEGIN_NAMESPACE
template <> struct formatter<copy_throwable> {
    auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
        return ctx.begin();
    }
    auto format(copy_throwable, format_context& ctx) const
        -> decltype(ctx.out()) {
        return ctx.out();
    }
};
FMT_END_NAMESPACE

TEST(args_test, throw_on_copy) {
    fmt::dynamic_format_arg_store<fmt::format_context> store;
    store.push_back(std::string("foo"));
    try {
        store.push_back(copy_throwable());
    }
    catch (...) {
    }
    EXPECT_EQ(fmt::vformat("{}", store), "foo");
}

TEST(args_test, move_constructor) {
    using store_type = fmt::dynamic_format_arg_store<fmt::format_context>;
    auto store = std::unique_ptr<store_type>(new store_type());
    store->push_back(42);
    store->push_back(std::string("foo"));
    store->push_back(fmt::arg("a1", "foo"));
    auto moved_store = std::move(*store);
    store.reset();
    EXPECT_EQ(fmt::vformat("{} {} {a1}", moved_store), "42 foo foo");
}

.bazelrc

모든 컴파일 과정이 bzlMod 기반으로 작동하도록 다음 옵션을 추가한다.

common --enable_bzlmod

MODULE.bazel

BCR의 googletest, fmt, platforms repository를 사용하기 위해 다음과 같이 코딩을 작성한다. platforms는 컴파일되는 환경 정보을 기반으로 플랫폼별 다른 옵션을 설정한다. 하부에 os, cpu config를 포함하고 있어, 사용하기 편하다.
사용 가능한 라이브러리 목록 검색

bazel_dep(name = "googletest", version = "1.14.0")
bazel_dep(name = "fmt", version = "10.2.1")
bazel_dep(name = "platforms", version = "0.0.8")

project/ BUILD.bazel

cc_test rule를 사용하고 srcs, deps 항목을 추가한다. 일단 @fmt//:fmt 타겟을 의존해야 하고, unittest의 main 함수를 제공받기 위해 @googletest//:gtest_main 타겟에 의존한다.
컴파일 옵션을 정의하는 copts에서 select rule를 적용해 windows 환경 기반과 나머지 환경 기반에 따라, 다른 컴파일 옵션을 적용한다. c++20 표준 기반으로 컴파일하기 위해 관련 옵션을 추가한다.

cc_test(
    name  = "test",
    srcs    = [ "main.cc"],
    deps   = ["@fmt//:fmt","@googletest//:gtest_main"],
    copts  = select({
        "@platforms//os:windows": ["/std:c++20"],
        "//conditions:default": ["-std=c++20"],        
    }),
)

빌드하기

MODULE.bazel 파일이 있는 위치에서 console 프로그램을 수행해 다음 명령을 수행한다.

bazelisk build //...

프로그램 실행하기

bazelisk run //project:test

결과 화면

테스트 성공 화면

INFO: Analyzed target //project:test (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //project:test up-to-date:
  bazel-bin/project/test.exe
INFO: Elapsed time: 0.181s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: external/bazel_tools/tools/test/tw.exe project/test.exe
Executing tests from //project:test
-----------------------------------------------------------------------------
Running main() from gmock_main.cc
[==========] Running 12 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 12 tests from args_test
[ RUN      ] args_test.basic
[       OK ] args_test.basic (0 ms)
[ RUN      ] args_test.strings_and_refs
[       OK ] args_test.strings_and_refs (0 ms)
[ RUN      ] args_test.custom_format
[       OK ] args_test.custom_format (0 ms)
[ RUN      ] args_test.to_string_and_formatter
[       OK ] args_test.to_string_and_formatter (0 ms)
[ RUN      ] args_test.named_int
[       OK ] args_test.named_int (0 ms)
[ RUN      ] args_test.named_strings
[       OK ] args_test.named_strings (0 ms)
[ RUN      ] args_test.named_arg_by_ref
[       OK ] args_test.named_arg_by_ref (0 ms)
[ RUN      ] args_test.named_custom_format
[       OK ] args_test.named_custom_format (0 ms)
[ RUN      ] args_test.clear
[       OK ] args_test.clear (0 ms)
[ RUN      ] args_test.reserve
[       OK ] args_test.reserve (0 ms)
[ RUN      ] args_test.throw_on_copy
[       OK ] args_test.throw_on_copy (0 ms)
[ RUN      ] args_test.move_constructor
[       OK ] args_test.move_constructor (0 ms)
[----------] 12 tests from args_test (0 ms total)

[----------] Global test environment tear-down
[==========] 12 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 12 tests.

테스트 실패 화면

INFO: Analyzed target //project:test (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //project:test up-to-date:
  bazel-bin/project/test.exe
INFO: Elapsed time: 0.156s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: external/bazel_tools/tools/test/tw.exe project/test.exe
Executing tests from //project:test
-----------------------------------------------------------------------------
Running main() from gmock_main.cc
[==========] Running 12 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 12 tests from args_test
[ RUN      ] args_test.basic
[       OK ] args_test.basic (0 ms)
[ RUN      ] args_test.strings_and_refs
project/main.cc(32): error: Expected equality of these values:
  "1234567890 and X234567890 and X234567890"
  result
    Which is: "12345678904 and X2345678904 and X2345678904"

[  FAILED  ] args_test.strings_and_refs (0 ms)
[ RUN      ] args_test.custom_format
[       OK ] args_test.custom_format (0 ms)
[ RUN      ] args_test.to_string_and_formatter
[       OK ] args_test.to_string_and_formatter (0 ms)
[ RUN      ] args_test.named_int
[       OK ] args_test.named_int (0 ms)
[ RUN      ] args_test.named_strings
[       OK ] args_test.named_strings (0 ms)
[ RUN      ] args_test.named_arg_by_ref
[       OK ] args_test.named_arg_by_ref (0 ms)
[ RUN      ] args_test.named_custom_format
[       OK ] args_test.named_custom_format (0 ms)
[ RUN      ] args_test.clear
[       OK ] args_test.clear (0 ms)
[ RUN      ] args_test.reserve
[       OK ] args_test.reserve (0 ms)
[ RUN      ] args_test.throw_on_copy
[       OK ] args_test.throw_on_copy (0 ms)
[ RUN      ] args_test.move_constructor
[       OK ] args_test.move_constructor (0 ms)
[----------] 12 tests from args_test (0 ms total)

[----------] Global test environment tear-down
[==========] 12 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 11 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] args_test.strings_and_refs

 1 FAILED TEST

test 옵션으로 테스트하기

bazelisk test //project:test

결과 화면

INFO: Analyzed target //project:test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //project:test up-to-date:
  bazel-bin/project/test.exe
INFO: Elapsed time: 0.217s, Critical Path: 0.07s
INFO: 2 processes: 1 internal, 1 local.
INFO: Build completed successfully, 2 total actions
//project:test                                                           PASSED in 0.1s

Executed 1 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.

예제 파일

GTest.zip
0.00MB

728x90
반응형