You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzTest/AzTest/AzTest.h

499 lines
22 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/PlatformDef.h>
#include <AzTest_Traits_Platform.h>
#include <list>
#include <array>
AZ_PUSH_DISABLE_WARNING(4389 4800, "-Wunknown-warning-option"); // 'int' : forcing value to bool 'true' or 'false' (performance warning).
#undef strdup // This define is required by googletest
#include <gtest/gtest.h>
#include <gmock/gmock.h>
AZ_POP_DISABLE_WARNING;
#if defined(HAVE_BENCHMARK)
#include <benchmark/benchmark.h>
#endif
#include <AzCore/Memory/OSAllocator.h>
#define AZTEST_DLL_PUBLIC AZ_DLL_EXPORT
#define AZTEST_EXPORT extern "C" AZTEST_DLL_PUBLIC
namespace AZ
{
namespace Test
{
//! Forward declarations
class Platform;
/*!
* Implement this interface to define the environment setup and teardown functions.
*/
class ITestEnvironment
: public ::testing::Environment
{
public:
virtual ~ITestEnvironment()
{}
void SetUp() final
{
SetupEnvironment();
}
void TearDown() final
{
TeardownEnvironment();
}
protected:
virtual void SetupEnvironment() = 0;
virtual void TeardownEnvironment() = 0;
};
extern ::testing::Environment* sTestEnvironment;
/*!
* Monolithic builds will have all the environments available. Keep a mapping to run the desired envs.
*/
class TestEnvironmentRegistry
{
public:
TestEnvironmentRegistry(std::vector<ITestEnvironment*> a_envs, const std::string& a_module_name, bool a_unit)
: m_module_name(a_module_name)
, m_envs(a_envs)
, m_unit(a_unit)
{
s_envs.push_back(this);
}
const std::string m_module_name;
std::vector<ITestEnvironment*> m_envs;
bool m_unit;
static std::vector<TestEnvironmentRegistry*> s_envs;
private:
TestEnvironmentRegistry& operator=(const TestEnvironmentRegistry& tmp);
};
/*!
* Empty implementation of ITestEnvironment.
*/
class EmptyTestEnvironment final
: public ITestEnvironment
{
public:
virtual ~EmptyTestEnvironment()
{}
protected:
void SetupEnvironment() override
{}
void TeardownEnvironment() override
{}
};
void addTestEnvironment(ITestEnvironment* env);
void addTestEnvironments(std::vector<ITestEnvironment*> envs);
//! A hook that can be used to read any other misc parameters and remove them before google sees them.
//! Note that this modifies argc and argv to delete the parameters it consumes.
void ApplyGlobalParameters(int* argc, char** argv);
void printUnusedParametersWarning(int argc, char** argv);
/*!
* Main method for running tests from an executable.
*/
class AzUnitTestMain final
{
public:
AzUnitTestMain(std::vector<AZ::Test::ITestEnvironment*> envs)
: m_returnCode(0)
, m_envs(envs)
{}
bool Run(int argc, char** argv);
bool Run(const char* commandLine);
int ReturnCode() const { return m_returnCode; }
private:
int m_returnCode;
std::vector<ITestEnvironment*> m_envs;
};
//! Run tests in a single library by loading it dynamically and executing the exported symbol,
//! passing main-like parameters (argc, argv) from the (real or artificial) command line.
int RunTestsInLib(Platform& platform, const std::string& lib, const std::string& symbol, int& argc, char** argv);
#if defined(HAVE_BENCHMARK)
static constexpr const char* s_benchmarkEnvironmentName = "BenchmarkEnvironment";
// BenchmarkEnvironment is a base that can be implemented to used to perform global initialization and teardown
// for a module
class BenchmarkEnvironmentBase
{
public:
virtual ~BenchmarkEnvironmentBase() = default;
virtual void SetUpBenchmark()
{
}
virtual void TearDownBenchmark()
{
}
};
class BenchmarkEnvironmentRegistry
{
public:
BenchmarkEnvironmentRegistry() = default;
BenchmarkEnvironmentRegistry(const BenchmarkEnvironmentRegistry&) = delete;
BenchmarkEnvironmentRegistry& operator=(const BenchmarkEnvironmentRegistry&) = delete;
void AddBenchmarkEnvironment(std::unique_ptr<BenchmarkEnvironmentBase> env)
{
m_envs.push_back(std::move(env));
}
// Remove a registered benchmark from the registry
void RemoveBenchmarkEnvironment(BenchmarkEnvironmentBase* env)
{
auto RemoveBenchmarkFunc = [env](const std::unique_ptr<BenchmarkEnvironmentBase>& envElement)
{
return envElement.get() == env;
};
m_envs.erase(std::remove_if(m_envs.begin(), m_envs.end(), std::move(RemoveBenchmarkFunc)));
}
std::vector<std::unique_ptr<BenchmarkEnvironmentBase>>& GetBenchmarkEnvironments()
{
return m_envs;
}
private:
std::vector<std::unique_ptr<BenchmarkEnvironmentBase>> m_envs;
};
/*
* Creates a BenchmarkEnvironment using the specified template type and registers it with the BenchmarkEnvironmentRegister
* @param T template argument that must have BenchmarkEnvironmentBase as a base class
* @return returns a reference to the created BenchmarkEnvironment
*/
template<typename T>
T& RegisterBenchmarkEnvironment()
{
static_assert(std::is_base_of<BenchmarkEnvironmentBase, T>::value, "Supplied benchmark environment must be derived from BenchmarkEnvironmentBase");
static AZ::EnvironmentVariable<AZ::Test::BenchmarkEnvironmentRegistry> s_benchmarkRegistry;
if (!s_benchmarkRegistry)
{
s_benchmarkRegistry = AZ::Environment::CreateVariable<AZ::Test::BenchmarkEnvironmentRegistry>(s_benchmarkEnvironmentName);
}
auto benchmarkEnv{ new T };
s_benchmarkRegistry->AddBenchmarkEnvironment(std::unique_ptr<BenchmarkEnvironmentBase>{ benchmarkEnv });
return *benchmarkEnv;
}
/*
* An RAII wrapper about registering a BenchmarkEnvironment with the BenchmarkRegistry
* It will unregister the BenchmarkEnvironment with the BenchmarkRegistry on destruction
*/
struct ScopedRegisterBenchmarkEnvironment
{
template<typename T>
ScopedRegisterBenchmarkEnvironment(T& benchmarkEnv)
: m_benchmarkEnv(benchmarkEnv)
{}
~ScopedRegisterBenchmarkEnvironment()
{
if (auto benchmarkRegistry = AZ::Environment::FindVariable<BenchmarkEnvironmentRegistry>(s_benchmarkEnvironmentName);
benchmarkRegistry != nullptr)
{
benchmarkRegistry->RemoveBenchmarkEnvironment(&m_benchmarkEnv);
}
}
BenchmarkEnvironmentBase& m_benchmarkEnv;
};
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//! listener class to capture and print test output for embedded platforms
class OutputEventListener : public ::testing::EmptyTestEventListener
{
public:
std::list<std::string> resultList;
void OnTestEnd(const ::testing::TestInfo& test_info) override
{
std::string result;
if (test_info.result()->Failed())
{
result = "Fail";
}
else
{
result = "Pass";
}
std::string formattedResult = "[GTEST][" + result + "] " + test_info.test_case_name() + " " + test_info.name() + "\n";
resultList.emplace_back(formattedResult);
}
void OnTestProgramEnd(const ::testing::UnitTest& unit_test) override
{
for (std::string testResults : resultList)
{
AZ_Printf("", testResults.c_str());
}
if (unit_test.current_test_info())
{
AZ_Printf("", "[GTEST] %s completed %u tests with u% failed test cases.", unit_test.current_test_info()->name(), unit_test.total_test_count(), unit_test.failed_test_case_count());
}
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ITestEnvironment* DefaultTestEnv();
} // Test
} // AZ
#define AZ_UNIT_TEST_HOOK_NAME AzRunUnitTests
#if !defined(AZ_MONOLITHIC_BUILD)
// Environments should be declared dynamically, framework will handle deletion of resources
#define AZ_UNIT_TEST_HOOK_ENV(TEST_ENV) \
AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv) \
{ \
::testing::InitGoogleMock(&argc, argv); \
if (AZ_TRAIT_AZTEST_ATTACH_RESULT_LISTENER) \
{ \
::testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); \
listeners.Append(new AZ::Test::OutputEventListener); \
} \
AZ::Test::ApplyGlobalParameters(&argc, argv); \
AZ::Test::printUnusedParametersWarning(argc, argv); \
AZ::Test::addTestEnvironments({TEST_ENV}); \
int result = RUN_ALL_TESTS(); \
return result; \
}
#if defined(HAVE_BENCHMARK)
#define AZ_BENCHMARK_HOOK_ENV(TEST_ENV) \
AZTEST_EXPORT int AzRunBenchmarks(int argc, char** argv) \
{ \
AZ::Test::ScopedRegisterBenchmarkEnvironment scopedBenchmarkEnv(AZ::Test::RegisterBenchmarkEnvironment<TEST_ENV>()); \
auto benchmarkEnvRegistry = AZ::Environment::FindVariable<AZ::Test::BenchmarkEnvironmentRegistry>(AZ::Test::s_benchmarkEnvironmentName); \
std::vector<std::unique_ptr<AZ::Test::BenchmarkEnvironmentBase>>* benchmarkEnvs = benchmarkEnvRegistry ? &(benchmarkEnvRegistry->GetBenchmarkEnvironments()) : nullptr; \
if (benchmarkEnvs != nullptr) \
{ \
for (std::unique_ptr<AZ::Test::BenchmarkEnvironmentBase>& benchmarkEnv : *benchmarkEnvs) \
{ \
if (benchmarkEnv) \
{ \
benchmarkEnv->SetUpBenchmark(); \
} \
}\
} \
::benchmark::Initialize(&argc, argv); \
::benchmark::RunSpecifiedBenchmarks(); \
if (benchmarkEnvs != nullptr) \
{ \
for (auto benchmarkEnvIter = benchmarkEnvs->rbegin(); benchmarkEnvIter != benchmarkEnvs->rend(); ++benchmarkEnvIter) \
{ \
std::unique_ptr<AZ::Test::BenchmarkEnvironmentBase>& benchmarkEnv = *benchmarkEnvIter; \
if (benchmarkEnv) \
{ \
benchmarkEnv->TearDownBenchmark(); \
} \
}\
} \
return 0; \
}
#define AZ_BENCHMARK_HOOK() \
AZTEST_EXPORT int AzRunBenchmarks(int argc, char** argv) \
{ \
auto benchmarkEnvRegistry = AZ::Environment::FindVariable<AZ::Test::BenchmarkEnvironmentRegistry>(AZ::Test::s_benchmarkEnvironmentName); \
std::vector<std::unique_ptr<AZ::Test::BenchmarkEnvironmentBase>>* benchmarkEnvs = benchmarkEnvRegistry ? &(benchmarkEnvRegistry->GetBenchmarkEnvironments()) : nullptr; \
if (benchmarkEnvs != nullptr) \
{ \
for (std::unique_ptr<AZ::Test::BenchmarkEnvironmentBase>& benchmarkEnv : *benchmarkEnvs) \
{ \
if (benchmarkEnv) \
{ \
benchmarkEnv->SetUpBenchmark(); \
} \
}\
} \
::benchmark::Initialize(&argc, argv); \
::benchmark::RunSpecifiedBenchmarks(); \
if (benchmarkEnvs != nullptr) \
{ \
for (auto benchmarkEnvIter = benchmarkEnvs->rbegin(); benchmarkEnvIter != benchmarkEnvs->rend(); ++benchmarkEnvIter) \
{ \
std::unique_ptr<AZ::Test::BenchmarkEnvironmentBase>& benchmarkEnv = *benchmarkEnvIter; \
if (benchmarkEnv) \
{ \
benchmarkEnv->TearDownBenchmark(); \
} \
}\
} \
return 0; \
}
#else // !HAVE_BENCHMARK
#define AZ_BENCHMARK_HOOK_ENV(TEST_ENV) \
int AzRunBenchmarks(int argc, char** argv) \
{ \
std::cerr << "'AzRunBenchmarks' Not supported" << std::endl; \
return 1; \
}
#define AZ_BENCHMARK_HOOK() \
int AzRunBenchmarks(int argc, char** argv) \
{ \
std::cerr << "'AzRunBenchmarks' Not supported" << std::endl; \
return 1; \
}
#endif // HAVE_BENCHMARK
#if defined(AZ_TEST_EXECUTABLE)
#define IMPLEMENT_TEST_EXECUTABLE_MAIN() \
int main(int argc, char** argv) \
{ \
const bool isUnitTestCmd = (argc<2)?true:(strcmp(argv[1], AZ_STRINGIZE(AZ_UNIT_TEST_HOOK_NAME))==0); \
const bool isBenchmarkCmd = (argc<2)?false:(strcmp(argv[1], "AzRunBenchmarks")==0); \
if (isUnitTestCmd) \
{ \
if (argc<2) \
{ \
return AZ_UNIT_TEST_HOOK_NAME(argc, argv); \
} \
else \
{ \
argv[1] = argv[0]; \
return AZ_UNIT_TEST_HOOK_NAME(argc-1, &argv[1]); \
} \
} \
else if (isBenchmarkCmd) \
{ \
argv[1] = argv[0]; \
return AzRunBenchmarks(argc-1, &argv[1]); \
} \
else \
{ \
std::cerr << "Invalid arguments for test" << std::endl; \
return 1; \
} \
}
#else
#define IMPLEMENT_TEST_EXECUTABLE_MAIN()
#endif // defined(AZ_TEST_EXECUTABLE)
#else // monolithic build
#undef GTEST_MODULE_NAME_
#define GTEST_MODULE_NAME_ AZ_MODULE_NAME
#define AZTEST_CONCAT_(a, b) a ## _ ## b
#define AZTEST_CONCAT(a, b) AZTEST_CONCAT_(a, b)
#define AZ_UNIT_TEST_HOOK_REGISTRY_NAME AZTEST_CONCAT(AZ_UNIT_TEST_HOOK_NAME, Registry)
#define AZ_UNIT_TEST_HOOK_ENV(TEST_ENV) \
static AZ::Test::TestEnvironmentRegistry* AZ_UNIT_TEST_HOOK_REGISTRY_NAME =\
new( AZ_OS_MALLOC(sizeof(AZ::Test::TestEnvironmentRegistry), \
alignof(AZ::Test::TestEnvironmentRegistry))) \
AZ::Test::TestEnvironmentRegistry({ TEST_ENV }, AZ_MODULE_NAME, true);
#define AZ_BENCHMARK_HOOK_ENV(TEST_ENV)
#define AZ_BENCHMARK_HOOK()
#endif // AZ_MONOLITHIC_BUILD
// These macros are needed to implement unit test hooks necessary for running AzUnitTests or AzBenchmarks.
//
/* For unit test modules that implement AzUnitTests and AzBenchmarkTests with either a custom environment for AzUnitTests or a custom environment class for AzBenchmarks
the follow use the overloaded 'IMPLEMENT_AZ_UNIT_TEST_HOOKS' macro
AZ_UNIT_TEST_HOOK(UNIT_TEST_ENV, BENCHMARK_ENV_CLASS)
// Implement unit test hooks without a custom AzUnitTest or AzBenchmark environment,
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
// Implement unit test hooks with a custom AzUnitTest environment
AZ_UNIT_TEST_HOOK(new CustomEnvClass());
// Implement unit test hooks with a custom AzBenchmark environment class only
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV, CustomBenchmarkEnvClass);
// Implement unit test hooks with a custom AzUnitTest environment and a custom AzBenchmark environment class
AZ_UNIT_TEST_HOOK(new CustomEnvClass(), CustomBenchmarkEnvClass);
*/
#define DEFAULT_UNIT_TEST_ENV AZ::Test::DefaultTestEnv()
#define AZ_UNIT_TEST_HOOK_1(_1) \
AZ_UNIT_TEST_HOOK_ENV(_1) \
AZ_BENCHMARK_HOOK() \
IMPLEMENT_TEST_EXECUTABLE_MAIN()
#define AZ_UNIT_TEST_HOOK_2(_1, _2) \
AZ_UNIT_TEST_HOOK_ENV(_1) \
AZ_BENCHMARK_HOOK_ENV(_2) \
IMPLEMENT_TEST_EXECUTABLE_MAIN()
#define AZ_UNIT_TEST_HOOK(...) AZ_MACRO_SPECIALIZE(AZ_UNIT_TEST_HOOK_, AZ_VA_NUM_ARGS(__VA_ARGS__), (__VA_ARGS__))
// Declares a visible external symbol which identifies an executable as containing tests
#define DECLARE_AZ_UNIT_TEST_MAIN() AZTEST_EXPORT int ContainsAzUnitTestMain() { return 1; }
// Attempts to invoke the unit test main function if appropriate flags are present,
// otherwise simply continues launch as normal.
#define INVOKE_AZ_UNIT_TEST_MAIN(...) \
do { \
AZ::Test::AzUnitTestMain unitTestMain({__VA_ARGS__}); \
if (unitTestMain.Run(argc, argv)) \
{ \
return unitTestMain.ReturnCode(); \
} \
} while (0); // safe multi-line macro - creates a single statement
// Some implementations use a commandLine rather than argc/argv
#define INVOKE_AZ_UNIT_TEST_MAIN_COMMAND_LINE(...) \
do { \
AZ::Test::AzUnitTestMain unitTestMain({__VA_ARGS__}); \
if (unitTestMain.Run(commandLine)) \
{ \
return unitTestMain.ReturnCode(); \
} \
} while (0); // safe multi-line macro - creates a single statement
// Avoid problems with new/delete when AZ allocators are not ready or properly un/initialized.
#define AZ_TEST_CLASS_ALLOCATOR(Class_) \
void* operator new (size_t size) \
{ \
return AZ_OS_MALLOC(size, AZStd::alignment_of<Class_>::value); \
} \
void operator delete(void* ptr) \
{ \
AZ_OS_FREE(ptr); \
}