tests: add ClangTypeParserTest

Currently there is no testing for ClangTypeParser even though it's used in
production. This is because adding integration tests is very hard: they require
testing the build time behaviour at runtime, or else they'd be build failures
intead of test failures. There's a PR available for integration tests but it's
incomplete.

In contrast ClangTypeParser can be sort of unit tested. This follows the
structure of `test/test_drgn_parser.cpp` with some differences. There is a
tonne of boilerplate for setting up the Clang tool, and this set of testing
operates on type names instead of OID functions. The new tests are also
incredibly slow as they compile the entire `integration_test_target.cpp` (which
is huge) for every test case. I don't think this is avoidable without
compromising the separation of the tests somewhat due to the way Clang tooling
forces the code to be structured.

Test plan:
- Tested locally
- CI
This commit is contained in:
Jake Hillion 2024-02-14 11:35:21 +00:00
parent 7103680894
commit 333447a736
2 changed files with 202 additions and 0 deletions

View File

@ -0,0 +1,177 @@
#include <gtest/gtest.h>
#include <clang/AST/Mangle.h>
#include <clang/AST/QualTypeNames.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/CompilerInvocation.h>
#include <clang/Frontend/FrontendAction.h>
#include <clang/Sema/Lookup.h>
#include <clang/Sema/Sema.h>
#include <clang/Tooling/Tooling.h>
#include <clang/Tooling/CompilationDatabase.h>
#include <range/v3/core.hpp>
#include <range/v3/view/drop.hpp>
#include <range/v3/view/filter.hpp>
#include <range/v3/view/for_each.hpp>
#include <range/v3/view/take.hpp>
#include <range/v3/view/transform.hpp>
#include <string>
#include <string_view>
#include "oi/type_graph/ClangTypeParser.h"
#include "oi/type_graph/Printer.h"
using namespace oi::detail;
using namespace oi::detail::type_graph;
class ClangTypeParserTest : public ::testing::Test {
public:
std::string run(std::string_view function, ClangTypeParserOptions opt);
void test(std::string_view function,
std::string_view expected,
ClangTypeParserOptions opts = {
.chaseRawPointers = true,
.readEnumValues = true,
});
};
// This stuff is a total mess. Set up factories as ClangTooling expects them.
class ConsumerContext;
class CreateTypeGraphConsumer;
class CreateTypeGraphAction : public clang::ASTFrontendAction {
public:
CreateTypeGraphAction(ConsumerContext& ctx_) : ctx{ctx_} {
}
void ExecuteAction() override;
std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
clang::CompilerInstance& CI, clang::StringRef file) override;
private:
ConsumerContext& ctx;
};
class CreateTypeGraphActionFactory
: public clang::tooling::FrontendActionFactory {
public:
CreateTypeGraphActionFactory(ConsumerContext& ctx_) : ctx{ctx_} {
}
std::unique_ptr<clang::FrontendAction> create() override {
return std::make_unique<CreateTypeGraphAction>(ctx);
}
private:
ConsumerContext& ctx;
};
class ConsumerContext {
public:
ConsumerContext(const std::vector<std::unique_ptr<ContainerInfo>>& containerInfos_) : containerInfos{containerInfos_} {}
std::string_view fullyQualifiedName;
ClangTypeParserOptions opts;
const std::vector<std::unique_ptr<ContainerInfo>>& containerInfos;
TypeGraph typeGraph;
Type* result = nullptr;
private:
clang::Sema* sema = nullptr;
friend CreateTypeGraphConsumer;
friend CreateTypeGraphAction;
};
class CreateTypeGraphConsumer : public clang::ASTConsumer {
private:
ConsumerContext& ctx;
public:
CreateTypeGraphConsumer(ConsumerContext& ctx_) : ctx{ctx_} {
}
void HandleTranslationUnit(clang::ASTContext& Context) override {
const clang::Type* type = nullptr;
for (const clang::Type* ty : Context.getTypes()) {
std::string fqnWithoutTemplateParams;
switch (ty->getTypeClass()) {
case clang::Type::Record:
fqnWithoutTemplateParams = llvm::cast<const clang::RecordType>(ty)->getDecl()->getQualifiedNameAsString();
break;
default:
continue;
}
if (fqnWithoutTemplateParams != ctx.fullyQualifiedName)
continue;
type = ty;
break;
}
EXPECT_NE(type, nullptr);
ClangTypeParser parser{ctx.typeGraph, ctx.containerInfos, ctx.opts};
ctx.result = &parser.parse(Context, *ctx.sema, *type);
}
};
void CreateTypeGraphAction::ExecuteAction() {
clang::CompilerInstance& CI = getCompilerInstance();
if (!CI.hasSema())
CI.createSema(clang::TU_Complete, nullptr);
ctx.sema = &CI.getSema();
clang::ASTFrontendAction::ExecuteAction();
}
std::unique_ptr<clang::ASTConsumer> CreateTypeGraphAction::CreateASTConsumer(
[[maybe_unused]] clang::CompilerInstance& CI,
[[maybe_unused]] clang::StringRef file) {
return std::make_unique<CreateTypeGraphConsumer>(ctx);
}
std::string ClangTypeParserTest::run(std::string_view type,
ClangTypeParserOptions opts) {
std::string err{"failed to load compilation database"};
auto db = clang::tooling::CompilationDatabase::loadFromDirectory(BUILD_DIR, err);
if (!db) {
throw std::runtime_error("failed to load compilation database");
}
std::vector<std::unique_ptr<ContainerInfo>> cis;
ConsumerContext ctx{ cis };
ctx.fullyQualifiedName = type;
ctx.opts = std::move(opts);
CreateTypeGraphActionFactory factory{ctx};
std::vector<std::string> sourcePaths{TARGET_CPP_PATH};
clang::tooling::ClangTool tool{*db, sourcePaths};
if (auto retCode = tool.run(&factory); retCode != 0) {
throw std::runtime_error("clang type parsing failed");
}
std::stringstream out;
NodeTracker tracker;
Printer printer{out, tracker, ctx.typeGraph.size()};
printer.print(*ctx.result);
return std::move(out).str();
}
void ClangTypeParserTest::test(std::string_view type,
std::string_view expected,
ClangTypeParserOptions opts) {
std::string actual = run(type, std::move(opts));
expected.remove_prefix(1); // Remove initial '\n'
EXPECT_EQ(expected, actual);
}
TEST_F(ClangTypeParserTest, SimpleStruct) {
test("ns_simple::SimpleStruct", R"(
[0] Struct: SimpleStruct [ns_simple::SimpleStruct] (size: 16, align: 8)
Member: a (offset: 0)
Primitive: int32_t
Member: b (offset: 4)
Primitive: int8_t
Member: c (offset: 8)
Primitive: int64_t
)");
}

View File

@ -69,6 +69,31 @@ target_link_libraries(test_type_graph
include(GoogleTest) include(GoogleTest)
gtest_discover_tests(test_type_graph) gtest_discover_tests(test_type_graph)
add_executable(test_clang_type_parser
main.cpp
../oi/type_graph/ClangTypeParserTest.cpp
)
add_dependencies(test_clang_type_parser integration_test_target)
target_compile_definitions(test_clang_type_parser PRIVATE
TARGET_CPP_PATH="${CMAKE_CURRENT_BINARY_DIR}/integration/integration_test_target.cpp"
BUILD_DIR="${CMAKE_BINARY_DIR}"
)
target_link_libraries(test_clang_type_parser
codegen
container_info
type_graph
range-v3
GTest::gmock_main
)
if (FORCE_LLVM_STATIC)
target_link_libraries(test_clang_type_parser clangTooling ${llvm_libs})
else()
target_link_libraries(test_clang_type_parser clang-cpp LLVM)
endif()
gtest_discover_tests(test_clang_type_parser)
cpp_unittest( cpp_unittest(
NAME test_parser NAME test_parser
SRCS test_parser.cpp SRCS test_parser.cpp