diff --git a/.teamcity/MacOS/Project.kt b/.teamcity/MacOS/Project.kt index 249a19ed4..a91ad5913 100644 --- a/.teamcity/MacOS/Project.kt +++ b/.teamcity/MacOS/Project.kt @@ -117,7 +117,7 @@ class CarbonBuildMacOS(buildName: String, configType: String, preset: String, ag name = "Run Tests" workingDir = "%env.CMAKE_BUILD_FOLDER%" path = "ctest" - arguments = "-C %env.CMAKE_CONFIG_TYPE% -V --output-on-failure --output-junit %env.CTEST_JUNIT_OUTPUT_FILE%" + arguments = "-C %env.CMAKE_CONFIG_TYPE% -V --output-on-failure --timeout 30 --output-junit %env.CTEST_JUNIT_OUTPUT_FILE%" } exec { name = "Package artifact" diff --git a/.teamcity/Windows/Project.kt b/.teamcity/Windows/Project.kt index 0d4042fcf..368299420 100644 --- a/.teamcity/Windows/Project.kt +++ b/.teamcity/Windows/Project.kt @@ -114,7 +114,7 @@ class CarbonBuildWindows(buildName: String, configType: String, preset: String) name = "Run Tests" workingDir = "%env.CMAKE_BUILD_FOLDER%" path = "ctest" - arguments = "-C %env.CMAKE_CONFIG_TYPE% -V --output-on-failure --output-junit %env.CTEST_JUNIT_OUTPUT_FILE%" + arguments = "-C %env.CMAKE_CONFIG_TYPE% -V --output-on-failure --timeout 30 --output-junit %env.CTEST_JUNIT_OUTPUT_FILE%" } exec { name = "Package artifact" diff --git a/CMakeLists.txt b/CMakeLists.txt index 6105e9075..ce2b3970d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ if(BUILD_FOR_PYTHON_2) set(BUILD_METAL ON) set(BUILD_SHADER_COMPILER ON) set(INSTALL_TO_MONOLITH ON) + set(BUILD_TESTING OFF) else() # Feature options @@ -50,6 +51,14 @@ endif() project(carbon-trinity VERSION 1.0.0) +if(PROJECT_IS_TOP_LEVEL) + option(BUILD_TESTING "Build tests. Enabled by default." ON) +endif() + +if(BUILD_TESTING) + enable_testing() +endif() + if (BUILD_FOR_PYTHON_2) if(NOT DEFINED ENV{CCP_EVE_PERFORCE_BRANCH_PATH}) message(FATAL_ERROR "Missing required environment variable CCP_EVE_PERFORCE_BRANCH_PATH") diff --git a/shadercompiler/CMakeLists.txt b/shadercompiler/CMakeLists.txt index a21043de4..07e095dfe 100644 --- a/shadercompiler/CMakeLists.txt +++ b/shadercompiler/CMakeLists.txt @@ -306,18 +306,17 @@ else() # VCPKG install mode endif() -option(BUILD_TESTING "Build and run tests. Enabled by default." ON) -message(STATUS " BUILD_TESTING VALUE IS ${BUILD_TESTING}") if(BUILD_TESTING) - find_package(GTest REQUIRED) add_executable(ShaderCompilerTest) target_compile_definitions(ShaderCompilerTest PRIVATE SHADER_COMPILER_TEST=1) - target_link_libraries(ShaderCompilerTest PRIVATE GTest::GTest GTest::Main) + target_link_libraries(ShaderCompilerTest PRIVATE GTest::GTest) target_sources(ShaderCompilerTest PRIVATE ${_TEST_SOURCES}) source_group(Tests FILES ${_TEST_SOURCES}) configure_shader_compiler(ShaderCompilerTest) set_target_properties(ShaderCompilerTest PROPERTIES FOLDER "Tests") add_dependencies(ShaderCompilerTest ShaderCompiler) + + gtest_discover_tests(ShaderCompilerTest TEST_PREFIX ShaderCompiler.) endif() diff --git a/shadercompiler/EffectCompilerMetal.cpp b/shadercompiler/EffectCompilerMetal.cpp index 1c4e2b322..501c9c839 100644 --- a/shadercompiler/EffectCompilerMetal.cpp +++ b/shadercompiler/EffectCompilerMetal.cpp @@ -4254,7 +4254,7 @@ std::string MetalTool( const char* name ) size_t programFilesSize; getenv_s( &programFilesSize, programFiles, "PROGRAMFILES" ); - cmd << "\"" << programFiles << "\\Metal Developer Tools\\metal\\macos\\bin\\" << name << ".exe\""; + cmd << "\"" << std::string( programFiles ) << "\\Metal Developer Tools\\metal\\macos\\bin\\" << name << ".exe\""; } #else cmd << "xcrun -sdk macosx " << name; diff --git a/shadercompiler/tests/MetalConversionTest.cpp b/shadercompiler/tests/MetalConversionTest.cpp index abb5fe63c..c5288c6f8 100644 --- a/shadercompiler/tests/MetalConversionTest.cpp +++ b/shadercompiler/tests/MetalConversionTest.cpp @@ -8,6 +8,11 @@ TEST( MetalConversion, TextureIndexingWorks ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( Texture2D tex; @@ -38,6 +43,10 @@ technique t0 TEST( MetalConversion, AppliesPackedModifiersToCBuffers ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } const char* src = R"SRC( cbuffer cb0: register( b3 ) { @@ -72,6 +81,10 @@ technique t0 TEST( MetalConversion, AddsRowsToMatrixInitializers ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } const char* src = R"SRC( float4 vs(): SV_Position { @@ -105,6 +118,12 @@ technique t0 TEST( MetalConversion, AppliesPackedModifiersToRtLocalBuffers ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + + const char* src = R"SRC( struct HitInfo { @@ -140,6 +159,11 @@ technique t0 TEST( MetalConversion, AllowBindlessResources ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( Buffer HeapView_BufferFloat4[] @@ -186,6 +210,10 @@ technique t0 TEST( MetalConversion, AllowLocalBufferVars ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } const char* src = R"SRC( Buffer HeapView_BufferFloat4[] @@ -229,6 +257,11 @@ technique t0 TEST( MetalConversion, AllowManySrvsUavs ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( RWTexture2D Tex0; @@ -338,6 +371,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveVertexID ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -381,6 +419,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveVertexIDWithIAInputs ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -425,6 +468,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveNestedStructs ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -472,6 +520,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveNestedStructsWithSystemSemanticsWithIAInputs1 ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -522,6 +575,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveNestedStructsWithSystemSemanticsWithIAInputs2 ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -572,6 +630,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveInstanceID ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -615,6 +678,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveInstanceIDWithIAInputs ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn @@ -659,6 +727,11 @@ technique t0 TEST( MetalShaderPatching, CanHaveSystemSemanticMixedParameterAndIAInputs ) { + if( !g_metalCompilerAvailable ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct VSIn diff --git a/shadercompiler/tests/RayTracingTest.cpp b/shadercompiler/tests/RayTracingTest.cpp index 5c660bc9b..17b60d818 100644 --- a/shadercompiler/tests/RayTracingTest.cpp +++ b/shadercompiler/tests/RayTracingTest.cpp @@ -27,6 +27,11 @@ TYPED_TEST_SUITE( RayTracing, RayTracingCompilers ); TYPED_TEST( RayTracing, NumericInputsAreLocal ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -58,6 +63,11 @@ technique t0 TYPED_TEST( RayTracing, CanAccessLocalInputFromFunctions ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -92,6 +102,11 @@ technique t0 TYPED_TEST( RayTracing, SrvsAreGlobal ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -124,6 +139,11 @@ technique t0 TYPED_TEST( RayTracing, MissingSrvsInGlobalInputGeneratesError ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -153,6 +173,11 @@ technique t0 TYPED_TEST( RayTracing, AssignsRegistersBasedOnGlobalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -189,6 +214,11 @@ technique t0 TYPED_TEST( RayTracing, AllowsTextureArraysInGlobalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -227,6 +257,11 @@ technique t0 TYPED_TEST( RayTracing, AllowsBuffersInGlobalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -263,6 +298,11 @@ technique t0 TYPED_TEST( RayTracing, AllowsBufferArraysInGlobalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -301,6 +341,11 @@ technique t0 TYPED_TEST( RayTracing, PerFrameDataIsGlobal ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -339,6 +384,11 @@ technique t0 TYPED_TEST( RayTracing, IncludesScalarAnnotations ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -375,6 +425,11 @@ technique t0 TYPED_TEST( RayTracing, IncludesSrvAnnotations ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -412,6 +467,11 @@ technique t0 TYPED_TEST( RayTracing, AllowSamplersInGlobalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -456,6 +516,11 @@ technique t0 TYPED_TEST( RayTracing, AllowBindlessSamplersInLocalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -502,6 +567,11 @@ technique t0 TYPED_TEST( RayTracing, AllowMergedSamplersInGlobalInput ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -555,6 +625,11 @@ technique t0 TYPED_TEST( RayTracing, RealLifeTest ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( RaytracingAccelerationStructure Scene ; @@ -712,6 +787,11 @@ technique t1 TYPED_TEST( RayTracing, NumericConstantsAreInlined ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -743,6 +823,11 @@ technique t0 TYPED_TEST( RayTracing, CanProvideLocalCBuffers ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -781,6 +866,11 @@ technique t0 TYPED_TEST( RayTracing, CanProvideGlobalCBuffers ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -824,6 +914,11 @@ technique t0 TYPED_TEST( RayTracing, NotUsedGlobalInputsAreAccepted ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -864,6 +959,11 @@ technique t0 TYPED_TEST( RayTracing, NotUsedGlobalInputsAreExported ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( struct HitInfo { @@ -909,6 +1009,11 @@ technique t0 TYPED_TEST( RayTracing, NotUsedSamplerGlobalInputsAreExported ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( SamplerState TestMapSampler1 { @@ -917,6 +1022,7 @@ SamplerState TestMapSampler1 MipFilter = Point; AddressU = Clamp; AddressV = Clamp; + IsDynamic = true; }; struct HitInfo @@ -958,18 +1064,18 @@ technique t0 auto data = Compile( src ); ASSERT_EQ( data.techniques[0].libraries[0].globalInputs.textures.size(), 2 ); - ASSERT_EQ( data.techniques[0].libraries[0].globalInputs.registerInputs.size(), 3 ); - // on metal static samplers are not exposed - if( !std::is_same::value ) - { - ASSERT_FALSE( data.techniques[0].libraries[0].globalInputs.staticSamplers.empty() ); - } + ASSERT_EQ( data.techniques[0].libraries[0].globalInputs.registerInputs.size(), 4 ); } TYPED_TEST( RayTracing, CanCallTraceRayInFunction ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( RaytracingAccelerationStructure Scene ; @@ -1034,6 +1140,11 @@ technique t0 TYPED_TEST( RayTracing, CanCallTraceRayInFunctionChain ) { + if( !g_metalCompilerAvailable && std::is_same_v ) + { + GTEST_SKIP() << "Skipping test: Metal compiler is not available."; + } + const char* src = R"SRC( RaytracingAccelerationStructure Scene ; diff --git a/shadercompiler/tests/ShaderCompilerTest.cpp b/shadercompiler/tests/ShaderCompilerTest.cpp index 4a954d611..7cc053754 100644 --- a/shadercompiler/tests/ShaderCompilerTest.cpp +++ b/shadercompiler/tests/ShaderCompilerTest.cpp @@ -3,6 +3,7 @@ #include "gtest/gtest.h" extern std::string g_metalToolsPath; +bool g_metalCompilerAvailable = true; class ThrowListener : public testing::EmptyTestEventListener { @@ -47,6 +48,35 @@ int main( int argc, char** argv ) } } +#ifdef _WIN32 + std::ostringstream cmd; + if( !g_metalToolsPath.empty() ) + { + cmd << "\"" << g_metalToolsPath << "\\macos\\bin\\metal.exe\" --version"; + } + else + { + char programFiles[MAX_PATH] = { 0 }; + size_t programFilesSize; + getenv_s( &programFilesSize, programFiles, "PROGRAMFILES" ); + + cmd << "\"" << std::string( programFiles ) << "\\Metal Developer Tools\\metal\\macos\\bin\\metal2.exe\" --version"; + } + FILE* process = _popen( cmd.str().c_str(), "r" ); + if( process ) + { + char readBuffer[128]; + while( fgets( readBuffer, sizeof( readBuffer ), process ) ) + { + } + g_metalCompilerAvailable = _pclose( process ) == 0; + } + else + { + g_metalCompilerAvailable = false; + } +#endif + testing::InitGoogleTest( &argc, argv ); testing::UnitTest::GetInstance()->listeners().Append( new ThrowListener ); return RUN_ALL_TESTS(); diff --git a/shadercompiler/tests/TesingUtils.h b/shadercompiler/tests/TesingUtils.h index f2175b1c7..10aa9d314 100644 --- a/shadercompiler/tests/TesingUtils.h +++ b/shadercompiler/tests/TesingUtils.h @@ -8,6 +8,7 @@ #include "CompileMessageQueue.h" extern CompileMessageQueue g_messages; +extern bool g_metalCompilerAvailable; diff --git a/trinity/CMakeLists.txt b/trinity/CMakeLists.txt index 5f3f09c57..29f67f4ee 100644 --- a/trinity/CMakeLists.txt +++ b/trinity/CMakeLists.txt @@ -2112,3 +2112,98 @@ if (NOT INSTALL_TO_MONOLITH) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/carbon-trinityConfig.cmake DESTINATION share/carbon-trinity) endif() + + +if(BUILD_TESTING) + find_package(GTest CONFIG REQUIRED) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + # Find exefile tool, which is used to run the tests + if (DEFINED CMAKE_BUILD_TYPE AND NOT CMAKE_BUILD_TYPE MATCHES "Release") + string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYP_LOWER) + set(EXEFILE_TOOL_NAME exefile_${CMAKE_BUILD_TYP_LOWER}) + set(SCHEDULER_NAME _scheduler_${CMAKE_BUILD_TYP_LOWER}) + else() + set(EXEFILE_TOOL_NAME exefile) + set(SCHEDULER_NAME _scheduler) + endif() + find_program(exefile_tool ${EXEFILE_TOOL_NAME} NO_CACHE) + + # Get a list of Python test sources. These are only used to trigger a rebuild of the test target when they change + # and have nothing to do with test discovery or execution. + file(GLOB_RECURSE PYTHON_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/tests/Python/*.py") + # The TouchedPyFiles.h file is included in the test main cpp file and is used to trigger a rebuild of the test target + # when any of the Python test sources change, which in turn triggers test discovery. + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/TouchedPyFiles.h + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/TouchedPyFiles.h + DEPENDS ${PYTHON_TEST_SOURCES} + ) + + function(set_up_trinity_tests trinity_target platform) + + set(TEST_TARGET TrinityPythonTest_${platform}) + + # Set up a fake google test target that will run the python tests. + add_executable(${TEST_TARGET} ${CMAKE_CURRENT_SOURCE_DIR}/tests/Python/GTestAdapter/GTestAdapter.cpp ${CMAKE_CURRENT_BINARY_DIR}/TouchedPyFiles.h) + target_sources(${TEST_TARGET} PRIVATE ${PYTHON_TEST_SOURCES}) + source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}/tests/Python" FILES ${PYTHON_TEST_SOURCES}) + + if (WIN32) + add_custom_command(TARGET ${TEST_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory $ + ) + add_custom_command(TARGET ${TEST_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${exefile_tool} $ + ) + set(exefile_tool $/${EXEFILE_TOOL_NAME}.exe) + add_custom_command( + TARGET ${TEST_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ $ + COMMAND_EXPAND_LISTS + ) + add_custom_command( + TARGET ${TEST_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ $ + COMMAND_EXPAND_LISTS + ) + endif() + + target_compile_definitions(${TEST_TARGET} PRIVATE "EXECUTABLE_PATH=\"${exefile_tool}\"") + target_compile_definitions(${TEST_TARGET} PRIVATE "TEST_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/tests/Python/GTestAdapter/gtest_reporter.py\"") + target_compile_definitions(${TEST_TARGET} PRIVATE "BUILDFLAVOR=\"$\"") + target_compile_definitions(${TEST_TARGET} PRIVATE "TRINITYFLAVOR=\"$>\"") + target_compile_definitions(${TEST_TARGET} PRIVATE "TRINITYPLATFORM=\"${platform}\"") + target_include_directories(${TEST_TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(${TEST_TARGET} PRIVATE GTest::gtest) + add_dependencies(${TEST_TARGET} ${trinity_target}) + if(WIN32) + target_compile_definitions(${TEST_TARGET} PRIVATE "PYTHON_LIB_PATH=\"$\;$\;${CMAKE_CURRENT_SOURCE_DIR}/python\;${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin\;${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib\;${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/python\;${Python3_STDLIB}\"") + elseif (APPLE) + target_compile_definitions(${TEST_TARGET} PRIVATE "PYTHON_LIB_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/tests/python:$:$:${CMAKE_CURRENT_SOURCE_DIR}/python:${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin:${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib:${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/python3.12/lib-dynload:${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/python:${Python3_STDLIB}\"") + else() + message(FATAL_ERROR "Unsupported platform") + endif () + set_target_properties(${TEST_TARGET} PROPERTIES FOLDER "Tests") + gtest_discover_tests(${TEST_TARGET} DISCOVERY_MODE PRE_TEST TEST_PREFIX ${TEST_TARGET}.) + + endfunction() + + + + if(WIN32) + if (BUILD_DX11) + set_up_trinity_tests(trinity_dx11 dx11) + endif() + + if (BUILD_DX12) + set_up_trinity_tests(trinity_dx12 dx12) + endif() + set_up_trinity_tests(trinity_stub stub) + + elseif(APPLE) + if(BUILD_METAL) + set_up_trinity_tests(trinity_metal metal) + endif() + endif() +endif() \ No newline at end of file diff --git a/trinity/tests/Python/GTestAdapter/GTestAdapter.cpp b/trinity/tests/Python/GTestAdapter/GTestAdapter.cpp new file mode 100644 index 000000000..3d3ca4208 --- /dev/null +++ b/trinity/tests/Python/GTestAdapter/GTestAdapter.cpp @@ -0,0 +1,53 @@ +// Copyright © 2026 CCP ehf. + +#include +#include +#include +#include +#include + +#include "TouchedPyFiles.h" + + +// Including this test ensures that gtest is linked into the executable. +TEST( DummyTestSuite, DummyTest ) +{ + EXPECT_TRUE( true ); +} + +#ifdef _WIN32 +#define popen _popen +#define pclose _pclose +#endif + +int main( int argc, char* argv[] ) +{ + std::string command = std::string( EXECUTABLE_PATH ) + " /inherit /buildflavor=" BUILDFLAVOR " /py " + TEST_PATH; + for( int i = 1; i < argc; ++i ) + { + command += " "; + command += argv[i]; + } +#ifdef _WIN32 + _putenv_s( "PYTHONPATH", PYTHON_LIB_PATH ); + _putenv_s( "TRINITYPLATFORM", TRINITYPLATFORM ); + _putenv_s( "TRINITYFLAVOR", TRINITYFLAVOR ); +#else + setenv( "PYTHONPATH", PYTHON_LIB_PATH, 1 ); + setenv( "TRINITYPLATFORM", TRINITYPLATFORM, 1 ); + setenv( "TRINITYFLAVOR", TRINITYFLAVOR, 1 ); +#endif + FILE* pipe = popen( command.c_str(), "r" ); + if( !pipe ) + { + return -1; + } + + std::array buffer; + while( fgets( buffer.data(), int( buffer.size() ), pipe ) != nullptr ) + { + // output to stdout + printf( "%s", buffer.data() ); + } + return pclose( pipe ); +} \ No newline at end of file diff --git a/trinity/tests/Python/GTestAdapter/gtest_reporter.py b/trinity/tests/Python/GTestAdapter/gtest_reporter.py new file mode 100644 index 000000000..addba87ea --- /dev/null +++ b/trinity/tests/Python/GTestAdapter/gtest_reporter.py @@ -0,0 +1,503 @@ +# Copyright © 2026 CCP ehf. + +import argparse +import ctypes +import fnmatch +import json +import os +import random +import sys +import time +import unittest +from xml.sax.saxutils import escape as xml_escape + +GTEST_DESCRIPTION = ( + "Runs Python unittest tests and reports results using GoogleTest's " + "command-line interface and output format." +) + +GTEST_EPILOG = """\ +Supported GoogleTest flags: + --gtest_list_tests + --gtest_filter=PATTERN + --gtest_also_run_disabled_tests + --gtest_repeat=NUMBER + --gtest_shuffle + --gtest_random_seed=NUMBER + --gtest_color=(yes|no|auto) + --gtest_print_time=0|1 + --gtest_output=(xml|json)[:PATH] + --gtest_break_on_failure + --gtest_throw_on_failure + --gtest_fail_fast + --gtest_brief=0|1 + +Extra flags (not part of the GoogleTest CLI, needed to locate the Python +tests since this is a generic wrapper rather than a fixed test binary): + --test_path PATH directory to search for tests (default: '.') + --test_pattern GLOB unittest discovery filename pattern (default: 'test*.py') + test_names... explicit dotted test names/modules to load instead + of discovery (e.g. "my_module.MyCase.test_foo") +""" + + +class Colorizer: + def __init__(self, enabled): + self.enabled = enabled + + def _wrap(self, code, text): + if not self.enabled: + return text + return f"\033[0;{code}m{text}\033[0m" + + def green(self, text): + return self._wrap(32, text) + + def red(self, text): + return self._wrap(31, text) + + def yellow(self, text): + return self._wrap(33, text) + + +def _enable_windows_ansi(): + if os.name != "nt": + return + try: + kernel32 = ctypes.windll.kernel32 + handle = kernel32.GetStdHandle(-11) + mode = ctypes.c_uint32() + if kernel32.GetConsoleMode(handle, ctypes.byref(mode)): + kernel32.SetConsoleMode(handle, mode.value | 0x0004) + except Exception: + pass + + +def should_use_color(mode): + if mode == "yes": + return True + if mode == "no": + return False + return sys.stdout.isatty() and os.environ.get("TERM") != "dumb" + + +def _plural(n): + return "" if n == 1 else "s" + + +def build_arg_parser(): + parser = argparse.ArgumentParser( + prog="gtest_reporter.py", + description=GTEST_DESCRIPTION, + epilog=GTEST_EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("--gtest_list_tests", action="store_true") + parser.add_argument("--gtest_filter", default="*") + parser.add_argument("--gtest_also_run_disabled_tests", action="store_true") + parser.add_argument("--gtest_repeat", type=int, default=1) + parser.add_argument("--gtest_shuffle", action="store_true") + parser.add_argument("--gtest_random_seed", type=int, default=0) + parser.add_argument( + "--gtest_color", type=lambda s: s.lower(), choices=["yes", "no", "auto"], default="no" + ) + parser.add_argument("--gtest_print_time", type=int, choices=[0, 1], default=1) + parser.add_argument("--gtest_output", nargs="?", const="xml", default=None) + parser.add_argument("--gtest_break_on_failure", type=int, choices=[0, 1], default=0) + parser.add_argument("--gtest_throw_on_failure", type=int, choices=[0, 1], default=0) + parser.add_argument("--gtest_catch_exceptions", type=int, choices=[0, 1], default=0) + parser.add_argument("--gtest_fail_fast", type=int, choices=[0, 1], default=0) + parser.add_argument("--gtest_brief", type=int, choices=[0, 1], default=0) + parser.add_argument("--test_path", default=".") + parser.add_argument("--test_pattern", default="test*.py") + parser.add_argument("test_names", nargs="*") + return parser + + +def discover_tests(args): + loader = unittest.TestLoader() + if args.test_names: + return loader.loadTestsFromNames(args.test_names) + return loader.discover(args.test_path, pattern=args.test_pattern) + + +def flatten(suite): + tests = [] + for item in suite: + if isinstance(item, unittest.TestSuite): + tests.extend(flatten(item)) + else: + tests.append(item) + return tests + + +def is_disabled(test): + suite_name = type(test).__name__ + method_name = getattr(test, "_testMethodName", "") + return suite_name.startswith("DISABLED_") or method_name.startswith("DISABLED_") + + +def gtest_pattern_match(name, pattern): + positive, sep, negative = pattern.partition("-") + positive = positive or "*" + positive_patterns = [p for p in positive.split(":") if p] + negative_patterns = [p for p in negative.split(":") if p] if sep else [] + if not any(fnmatch.fnmatchcase(name, p) for p in positive_patterns): + return False + if any(fnmatch.fnmatchcase(name, p) for p in negative_patterns): + return False + return True + + +def group_by_suite(tests): + groups = {} + for test in tests: + groups.setdefault(type(test).__name__, []).append(test) + return groups + + +def print_test_list(tests, colorizer): + groups = group_by_suite(tests) + for suite_name, items in groups.items(): + print(f"{suite_name}.") + for test in items: + print(f" {getattr(test, '_testMethodName', test.id())}") + print() + + +class GTestResult(unittest.TestResult): + def __init__(self, suite_name, colorizer, args, out_results): + super().__init__() + self.suite_name = suite_name + self.colorizer = colorizer + self.args = args + self.out_results = out_results + self._start_time = 0.0 + + def _test_name(self, test): + method_name = getattr(test, "_testMethodName", None) + if method_name is None: + return f"{self.suite_name}.{test.id()}" + return f"{self.suite_name}.{method_name}" + + def startTest(self, test): + super().startTest(test) + self._start_time = time.time() + print(f"[{self.colorizer.green(' RUN ')}] {self._test_name(test)}") + + def _record(self, test, status, message=None): + elapsed_ms = int((time.time() - self._start_time) * 1000) + name = self._test_name(test) + method_name = getattr(test, "_testMethodName", test.id()) + self.out_results.append( + { + "suite": self.suite_name, + "name": method_name, + "full_name": name, + "status": status, + "time_ms": elapsed_ms, + "message": message, + } + ) + time_str = f" ({elapsed_ms} ms)" if self.args.gtest_print_time else "" + if status == "FAILED": + if message: + print(message.rstrip()) + print(f"[{self.colorizer.red(' FAILED ')}] {name}{time_str}") + if self.args.gtest_break_on_failure or self.args.gtest_fail_fast: + self.stop() + if self.args.gtest_throw_on_failure: + sys.exit(1) + elif status == "SKIPPED": + if message: + print(str(message).rstrip()) + print(f"[{self.colorizer.yellow(' SKIPPED ')}] {name}{time_str}") + else: + if self.args.gtest_brief: + return + print(f"[{self.colorizer.green(' OK ')}] {name}{time_str}") + + def addSuccess(self, test): + super().addSuccess(test) + self._record(test, "OK") + + def addFailure(self, test, err): + super().addFailure(test, err) + self._record(test, "FAILED", self._exc_info_to_string(err, test)) + + def addError(self, test, err): + super().addError(test, err) + self._record(test, "FAILED", self._exc_info_to_string(err, test)) + + def addSkip(self, test, reason): + super().addSkip(test, reason) + self._record(test, "SKIPPED", reason) + + def addExpectedFailure(self, test, err): + super().addExpectedFailure(test, err) + self._record(test, "OK") + + def addUnexpectedSuccess(self, test): + super().addUnexpectedSuccess(test) + self._record(test, "FAILED", "Test unexpectedly succeeded (marked as expectedFailure).") + + +def run_tests(groups, args, colorizer): + all_results = [] + total_tests = sum(len(v) for v in groups.values()) + total_suites = len(groups) + print( + f"[{colorizer.green('==========')}] Running {total_tests} test{_plural(total_tests)} " + f"from {total_suites} test suite{_plural(total_suites)}." + ) + print(f"[{colorizer.green('----------')}] Global test environment set-up.") + overall_start = time.time() + stopped_early = False + for suite_name, tests in groups.items(): + if stopped_early: + break + n = len(tests) + print(f"[{colorizer.green('----------')}] {n} test{_plural(n)} from {suite_name}") + suite_start = time.time() + result = GTestResult(suite_name, colorizer, args, all_results) + unittest.TestSuite(tests).run(result) + suite_elapsed_ms = int((time.time() - suite_start) * 1000) + time_str = f" ({suite_elapsed_ms} ms total)" if args.gtest_print_time else "" + print(f"[{colorizer.green('----------')}] {n} test{_plural(n)} from {suite_name}{time_str}") + print() + if result.shouldStop: + stopped_early = True + print(f"[{colorizer.green('----------')}] Global test environment tear-down") + overall_elapsed_ms = int((time.time() - overall_start) * 1000) + ran = len(all_results) + time_str = f" ({overall_elapsed_ms} ms total)" if args.gtest_print_time else "" + print( + f"[{colorizer.green('==========')}] {ran} test{_plural(ran)} from {total_suites} " + f"test suite{_plural(total_suites)} ran.{time_str}" + ) + + passed = [r for r in all_results if r["status"] == "OK"] + failed = [r for r in all_results if r["status"] == "FAILED"] + skipped = [r for r in all_results if r["status"] == "SKIPPED"] + + print(f"[{colorizer.green(' PASSED ')}] {len(passed)} test{_plural(len(passed))}.") + if skipped: + print( + f"[{colorizer.yellow(' SKIPPED ')}] {len(skipped)} test{_plural(len(skipped))}, " + f"listed below:" + ) + for r in skipped: + print(f"[{colorizer.yellow(' SKIPPED ')}] {r['full_name']}") + if failed: + print( + f"[{colorizer.red(' FAILED ')}] {len(failed)} test{_plural(len(failed))}, " + f"listed below:" + ) + for r in failed: + print(f"[{colorizer.red(' FAILED ')}] {r['full_name']}") + print() + print(f"{len(failed)} FAILED TEST{'S' if len(failed) != 1 else ''}") + + return all_results, overall_elapsed_ms + + +def write_xml(path, results, total_time_ms): + suites = {} + for r in results: + suites.setdefault(r["suite"], []).append(r) + total_failures = sum(1 for r in results if r["status"] == "FAILED") + lines = [''] + lines.append( + f'' + ) + for suite_name, items in suites.items(): + s_failures = sum(1 for r in items if r["status"] == "FAILED") + s_time = sum(r["time_ms"] for r in items) / 1000.0 + lines.append( + f' ' + ) + for r in items: + t_time = r["time_ms"] / 1000.0 + if r["status"] == "FAILED": + lines.append( + f' ' + ) + message = r["message"] or "Failed" + lines.append( + f' ' + f"" + ) + lines.append(" ") + elif r["status"] == "SKIPPED": + lines.append( + f' ' + ) + message = r["message"] or "Skipped" + lines.append( + f' ' + f"" + ) + lines.append(" ") + else: + lines.append( + f' ' + ) + lines.append(" ") + lines.append("") + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + + +def write_json(path, results, total_time_ms): + suites = {} + for r in results: + suites.setdefault(r["suite"], []).append(r) + total_failures = 0 + testsuites = [] + for suite_name, items in suites.items(): + s_failures = sum(1 for r in items if r["status"] == "FAILED") + total_failures += s_failures + s_time = sum(r["time_ms"] for r in items) / 1000.0 + testsuite = { + "name": suite_name, + "tests": len(items), + "failures": s_failures, + "disabled": 0, + "errors": 0, + "time": f"{s_time:.3f}s", + "testsuite": [], + } + for r in items: + testcase = { + "name": r["name"], + # Every entry here actually ran (disabled tests never reach this list), + # so status is always RUN; only the outcome ("result") differs. + "status": "RUN", + "result": "SKIPPED" if r["status"] == "SKIPPED" else "COMPLETED", + "time": f"{r['time_ms'] / 1000.0:.3f}s", + "classname": suite_name, + } + if r["status"] == "FAILED": + testcase["failures"] = [{"failure": r["message"] or "", "type": ""}] + testsuite["testsuite"].append(testcase) + testsuites.append(testsuite) + data = { + "tests": len(results), + "failures": total_failures, + "disabled": 0, + "errors": 0, + "time": f"{total_time_ms / 1000.0:.3f}s", + "name": "AllTests", + "testsuites": testsuites, + } + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +def write_output_file(spec, results, total_time_ms, working_dir): + fmt, sep, path = spec.partition(":") + if not sep: + fmt, path = "xml", "" + if not path: + path = "test_detail.xml" if fmt == "xml" else "test_detail.json" + wd = os.getcwd() + os.chdir(working_dir) + if os.path.isdir(path) or path.endswith(("/", "\\")): + path = os.path.join(path, "test_detail.xml" if fmt == "xml" else "test_detail.json") + directory = os.path.dirname(path) + if directory and not os.path.exists(directory): + os.makedirs(directory, exist_ok=True) + if fmt == "json": + write_json(path, results, total_time_ms) + else: + write_xml(path, results, total_time_ms) + os.chdir(wd) + return path + +main_exit_code = 0 + +def main(argv=None): + global main_exit_code + try: + args = build_arg_parser().parse_args(argv) + working_dir = os.getcwd() + os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + + color_enabled = should_use_color(args.gtest_color) + if color_enabled: + _enable_windows_ansi() + colorizer = Colorizer(color_enabled) + + all_tests = flatten(discover_tests(args)) + filtered = [ + t + for t in all_tests + if gtest_pattern_match(f"{type(t).__name__}.{getattr(t, '_testMethodName', t.id())}", args.gtest_filter) + ] + + if args.gtest_list_tests: + print_test_list(filtered, colorizer) + return 0 + + if args.gtest_filter != "*": + print(f"Note: Google Test filter = {args.gtest_filter}\n") + + disabled_tests = [t for t in filtered if is_disabled(t)] + if args.gtest_also_run_disabled_tests: + runnable = filtered + else: + runnable = [t for t in filtered if not is_disabled(t)] + + seed = args.gtest_random_seed + if args.gtest_shuffle: + if seed == 0: + seed = int(time.time() * 1000) % 100000 + random.Random(seed).shuffle(runnable) + print(f"Note: Randomizing tests' orders with a seed of {seed} .\n") + + repeat = args.gtest_repeat + infinite = repeat < 0 + iteration = 0 + exit_code = 0 + while infinite or iteration < repeat: + iteration += 1 + if repeat != 1 and iteration > 1: + print(f"Repeating all tests (iteration {iteration}) . . .\n") + + groups = group_by_suite(runnable) + results, elapsed_ms = run_tests(groups, args, colorizer) + + if not args.gtest_also_run_disabled_tests and disabled_tests: + n = len(disabled_tests) + print(f"\n YOU HAVE {n} DISABLED TEST{'S' if n != 1 else ''}") + + if args.gtest_output: + write_output_file(args.gtest_output, results, elapsed_ms, working_dir) + + if any(r["status"] == "FAILED" for r in results): + exit_code = 1 + if infinite: + break + except: + main_exit_code = 1 + raise + main_exit_code = exit_code + + +if __name__ == "__main__": + import blue + sys.modules['_scheduler'] = blue.LoadExtension('_scheduler') + import scheduler + + tasklet = scheduler.tasklet(main) + tasklet() + while tasklet.alive: + blue.os.Pump() + + sys.exit(main_exit_code) diff --git a/trinity/tests/Python/tests/__init__.py b/trinity/tests/Python/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/trinity/tests/Python/tests/test_childhierarchy.py b/trinity/tests/Python/tests/test_childhierarchy.py new file mode 100644 index 000000000..6a08d896a --- /dev/null +++ b/trinity/tests/Python/tests/test_childhierarchy.py @@ -0,0 +1,110 @@ +# Copyright © 2026 CCP ehf. + +import shutil +import tempfile + +import contextlib +import blue +import unittest + +import trinity + + +@contextlib.contextmanager +def TempRes(): + tempdir = tempfile.mkdtemp() + blue.paths.SetSearchPath('res', '%s;%s' % (tempdir, blue.paths.GetSearchPath('res'))) + try: + yield + finally: + shutil.rmtree(tempdir) + + +class TestChildHierarchy(unittest.TestCase): + def test_initValues(self): + obj = trinity.EveChildMesh() + self.assertIsNone(obj.GetOwner()) + self.assertIsNone(obj.GetParent()) + self.assertEqual(obj.partTag, 0) + + def test_ownerAssignment(self): + ship = trinity.EveShip2() + obj = trinity.EveChildMesh() + ship.effectChildren.append(obj) + self.assertEqual(obj.GetOwner(), ship) + + def test_transitiveOwnerAssignment(self): + ship = trinity.EveShip2() + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + ship.effectChildren.append(container) + self.assertEqual(container.GetOwner(), ship) + container.objects.append(mesh) + self.assertEqual(mesh.GetOwner(), ship) + + def test_ownerPropagation(self): + ship = trinity.EveShip2() + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + container.objects.append(mesh) + ship.effectChildren.append(container) + self.assertEqual(mesh.GetOwner(), ship) + + def test_parentAssignment(self): + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + container.objects.append(mesh) + self.assertEqual(mesh.GetParent(), container) + + def test_ownerRemoval(self): + ship = trinity.EveShip2() + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + ship.effectChildren.append(container) + container.objects.append(mesh) + + ship.effectChildren.remove(container) + self.assertIsNone(container.GetOwner()) + self.assertIsNone(mesh.GetOwner()) + + def test_grandChildOwnerRemoval(self): + ship = trinity.EveShip2() + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + ship.effectChildren.append(container) + container.objects.append(mesh) + + container.objects.remove(mesh) + self.assertIsNone(mesh.GetOwner()) + + def test_parentRemoval(self): + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + container.objects.append(mesh) + container.objects.remove(mesh) + self.assertIsNone(mesh.GetParent()) + + def test_parentClear(self): + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + container.objects.append(mesh) + container.objects.removeAt(-1) + self.assertIsNone(mesh.GetParent()) + + def test_loadedHierarchy(self): + ship = trinity.EveShip2() + container = trinity.EveChildContainer() + mesh = trinity.EveChildMesh() + container.objects.append(mesh) + ship.effectChildren.append(container) + + with TempRes(): + blue.resMan.SaveObject(ship, 'res:/test.red') + + ship2 = blue.resMan.LoadObject('res:/test.red') + container2 = ship2.effectChildren[0] + mesh2 = container2.objects[0] + self.assertEqual(mesh2.GetOwner(), ship2) + self.assertEqual(mesh2.GetParent(), container2) + self.assertEqual(container2.GetOwner(), ship2) + diff --git a/trinity/tests/Python/tests/test_modular.py b/trinity/tests/Python/tests/test_modular.py new file mode 100644 index 000000000..c11f27699 --- /dev/null +++ b/trinity/tests/Python/tests/test_modular.py @@ -0,0 +1,295 @@ +# Copyright © 2026 CCP ehf. + +import unittest +import blue +import trinity + + +def _CreateSof(): + data = trinity.EveSOFData() + data.generic = trinity.EveSOFDataGeneric() + shader = trinity.EveSOFDataGenericShader() + shader.shader = 'my_shader.fx' + data.generic.areaShaders.append(shader) + + hull = trinity.EveSOFDataHull() + hull.name = 'static_hull' + hull.geometryResFilePath = 'res:/mygeo.cmf' + hull.boundingSphere = (0.0, 0.0, 0.0, 50.0) + area = trinity.EveSOFDataHullArea() + area.name = 'area' + area.shader = shader.shader + area.index = 0 + area.count = 1 + hull.opaqueAreas.append(area) + locatorSet = trinity.EveSOFDataHullLocatorSet() + locatorSet.name = "damage" + locator = trinity.EveSOFDataTransform() + locator.position = 0, 0, 0 + locatorSet.locators.append(locator) + hull.locatorSets.append(locatorSet) + data.hull.append(hull) + + hull = trinity.EveSOFDataHull() + hull.name = 'static_hull2' + hull.geometryResFilePath = 'res:/mygeo2.cmf' + hull.boundingSphere = (0.0, 0.0, 0.0, 60.0) + area = trinity.EveSOFDataHullArea() + area.name = 'area' + area.shader = shader.shader + area.index = 0 + area.count = 1 + hull.opaqueAreas.append(area) + data.hull.append(hull) + + hull = trinity.EveSOFDataHull() + hull.name = 'anim_hull' + hull.isSkinned = True + hull.geometryResFilePath = 'res:/mygeo.cmf' + hull.boundingSphere = (0.0, 0.0, 0.0, 70.0) + area = trinity.EveSOFDataHullArea() + area.name = 'area' + area.shader = shader.shader + area.index = 0 + area.count = 1 + hull.opaqueAreas.append(area) + data.hull.append(hull) + + faction = trinity.EveSOFDataFaction() + faction.name = 'testfaction' + data.faction.append(faction) + race = trinity.EveSOFDataRace() + race.name = 'testrace' + data.race.append(race) + sof = trinity.EveSOF() + sof.dataMgr.SetData(data) + return sof + +_time = 1 + +def _UpdateTransfroms(ship): + global _time + scene = trinity.EveSpaceScene() + scene.objects.append(ship) + scene.UpdateScene(_time) + _time += 1 + + +class TestModular(unittest.TestCase): + def test_createEmptyShip(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + del modifier + self.assertTrue(ship) + self.assertTrue(len(ship.effectChildren) == 1) + self.assertTrue(isinstance(ship.effectChildren[0], trinity.EveChildPartData)) + + def test_addStaticHull(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + # Static meshes are added as instanced + instancedMeshes = blue.FindInterface(ship, 'EveChildInstancedMeshes')[0] + self.assertTrue(instancedMeshes.GetMeshCount() == 1) + + def test_addAnimatedHull(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('anim_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + self.assertTrue(len(blue.FindInterface(ship, 'EveChildInstancedMeshes')) == 0) + self.assertTrue(len(blue.FindInterface(ship, 'EveChildMesh')) == 1) + + def test_addTwoStaticHulls(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + modifier.AddHull('static_hull', 'testfaction', 'testrace', (1, 2, 3), (0, 0, 0, 1), (1,1,1)) + del modifier + + # Static meshes are added as instanced + instancedMeshes = blue.FindInterface(ship, 'EveChildInstancedMeshes')[0] + self.assertEqual(instancedMeshes.GetMeshCount(), 1) + info = instancedMeshes.GetMeshInfo(0) + instances = info[6] + self.assertEqual(instances, 2) + + def test_addTwoDifferentStaticHulls(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + modifier.AddHull('static_hull2', 'testfaction', 'testrace', (1, 2, 3), (0, 0, 0, 1), (1,1,1)) + del modifier + + # Static meshes are added as instanced + instancedMeshes = blue.FindInterface(ship, 'EveChildInstancedMeshes')[0] + self.assertEqual(instancedMeshes.GetMeshCount(), 2) + self.assertEqual(instancedMeshes.GetMeshInfo(0)[6], 1) + self.assertEqual(instancedMeshes.GetMeshInfo(1)[6], 1) + + def test_removeAnimatedHull(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + part = modifier.AddHull('anim_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + self.assertTrue(len(blue.FindInterface(ship, 'EveChildInstancedMeshes')) == 0) + self.assertTrue(len(blue.FindInterface(ship, 'EveChildMesh')) == 1) + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.Remove(part) + del modifier + + def test_removeStaticHull(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + part = modifier.AddHull('static_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + # Static meshes are added as instanced + instancedMeshes = blue.FindInterface(ship, 'EveChildInstancedMeshes')[0] + self.assertTrue(instancedMeshes.GetMeshCount() == 1) + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.Remove(part) + del modifier + + instancedMeshes = blue.FindInterface(ship, 'EveChildInstancedMeshes')[0] + self.assertTrue(instancedMeshes.GetMeshCount() == 0) + + def test_removeAnimatedHull(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "testrace") + part = modifier.AddHull('anim_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + self.assertEqual(len(blue.FindInterface(ship, 'EveChildMesh')), 1) + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.Remove(part) + del modifier + + self.assertEqual(len(blue.FindInterface(ship, 'EveChildMesh')), 0) + + def test_setBoundingSphere(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (0, 0, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + self.assertEqual(ship.boundingSphereRadius, 50) + + def test_boundingSphereAccountsForTransforms(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + self.assertEqual(ship.boundingSphereRadius, 50.0) + self.assertEqual(ship.boundingSphereCenter, (30, 0, 0)) + + def test_boundingSphereEncapsulatesAllChilren(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + modifier.AddHull('static_hull', 'testfaction', 'testrace', (-30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + self.assertEqual(ship.boundingSphereRadius, 30.0 + 50.0) + self.assertEqual(ship.boundingSphereCenter, (0, 0, 0)) + + def test_boundingSphereUpdatesOnRemoval(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + part = modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + modifier.AddHull('static_hull', 'testfaction', 'testrace', (-30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.Remove(part) + del modifier + + self.assertEqual(ship.boundingSphereRadius, 50.0) + self.assertEqual(ship.boundingSphereCenter, (-30, 0, 0)) + + def test_addsLocators(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + self.assertEqual(len(ship.locatorSets), 1) + self.assertEqual(ship.locatorSets[0].name, 'damage') + self.assertEqual(len(ship.locatorSets[0].locators), 1) + + def test_locatorsInheritTransform(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + self.assertEqual(ship.locatorSets[0].locators[0][0], (30, 0, 0)) + + def test_addsLocatorsMerged(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + modifier.AddHull('static_hull', 'testfaction', 'testrace', (-30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + self.assertEqual(len(ship.locatorSets), 1) + self.assertEqual(ship.locatorSets[0].name, 'damage') + self.assertEqual(len(ship.locatorSets[0].locators), 2) + + def test_addsLocatorsRemoved(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "restrace") + part = modifier.AddHull('static_hull', 'testfaction', 'testrace', (30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + modifier.AddHull('static_hull', 'testfaction', 'testrace', (-30, 0, 0), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.Remove(part) + del modifier + + self.assertEqual(len(ship.locatorSets), 1) + self.assertEqual(ship.locatorSets[0].name, 'damage') + self.assertEqual(len(ship.locatorSets[0].locators), 1) + + def test_moveAnimatedHull(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "testrace") + part = modifier.AddHull('anim_hull', 'testfaction', 'testrace', (10, 20, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + _UpdateTransfroms(ship) + mesh = blue.FindInterface(ship, 'EveChildMesh')[0] + self.assertEqual(mesh.worldTransform, ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (10, 20, 0, 1))) + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.SetTransform(part, (1, 2, 3), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + _UpdateTransfroms(ship) + + self.assertEqual(mesh.worldTransform, ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (1, 2, 3, 1))) + + def test_moveLocators(self): + sof = _CreateSof() + ship, modifier = trinity.CreateModularObject(sof, "testfaction", "testrace") + part = modifier.AddHull('static_hull', 'testfaction', 'testrace', (10, 20, 0), (0, 0, 0, 1), (1,1,1)) + del modifier + + self.assertEqual(ship.locatorSets[0].locators[0][0], (10, 20, 0)) + + modifier = trinity.ModifyModularObject(ship, sof) + modifier.SetTransform(part, (1, 2, 3), (0, 0, 0, 1), (1, 1, 1)) + del modifier + + self.assertEqual(ship.locatorSets[0].locators[0][0], (1, 2, 3)) + + +# TODO: audio emitters \ No newline at end of file diff --git a/trinity/tests/Python/trinity.py b/trinity/tests/Python/trinity.py new file mode 100644 index 000000000..d499a6968 --- /dev/null +++ b/trinity/tests/Python/trinity.py @@ -0,0 +1,18 @@ +# Copyright © 2026 CCP ehf. + +def _import_trinity(): + import blue + import os + + triPlatform = os.getenv("TRINITYPLATFORM", "stub") + flavor = os.getenv("TRINITYFLAVOR", "release") + if flavor == "release": + name = "_trinity_%s" % triPlatform + else: + name = "_trinity_%s_%s" % (triPlatform, flavor) + mod = __import__(name) + for memberName in dir(mod): + globals()[memberName] = getattr(mod, memberName) + globals()['platform'] = triPlatform + +_import_trinity() diff --git a/trinityal/CMakeLists.txt b/trinityal/CMakeLists.txt index df722a787..0ec5cb9e9 100644 --- a/trinityal/CMakeLists.txt +++ b/trinityal/CMakeLists.txt @@ -511,8 +511,6 @@ else() message(FATAL_ERROR "Unsupported trinity platform, only stub would be available!") endif() -option(BUILD_TESTING "Build and run tests. Enabled by default." ON) -message(STATUS " BUILD_TESTING VALUE IS ${BUILD_TESTING}") if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/trinityal/tests/CMakeLists.txt b/trinityal/tests/CMakeLists.txt index 105ff0a69..59889afc0 100644 --- a/trinityal/tests/CMakeLists.txt +++ b/trinityal/tests/CMakeLists.txt @@ -131,9 +131,9 @@ function(set_shared_trinityaltest_properties target) target_precompile_headers(${target} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/StdAfx.h) if(BUILD_FOR_PYTHON_2) - target_link_libraries(${target} PRIVATE GTest::GTest GTest::Main) + target_link_libraries(${target} PRIVATE GTest::GTest) else() - target_link_libraries(${target} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main) + target_link_libraries(${target} PRIVATE GTest::gtest) endif() if(APPLE) @@ -150,16 +150,7 @@ function(set_shared_trinityaltest_properties target) target_include_directories(${target} PRIVATE ${GENERATED_SOURCES_DIR}) - # override the output directory to ensure that all TrinityALTest outputs end up in the same folder. - # this is OK because we're differentiating them by OUTPUT_NAME - set(output_directory "${CMAKE_BINARY_DIR}/carbon/autobuild/TrinityALTest/${CCP_PLATFORM}/${CCP_ARCHITECTURE}/${CCP_TOOLSET}/$<$:>") - message(STATUS "Overriding ${target} output directory to ${output_directory}") - set_target_properties(${target} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY ${output_directory} - PDB_OUTPUT_DIRECTORY ${output_directory} - FOLDER Trinity/Tests - ) + set_target_properties(${target} PROPERTIES FOLDER Tests) endfunction() @@ -170,10 +161,6 @@ if(WIN32) target_compile_definitions(TrinityALTest_dx11 PRIVATE TRINITY_PLATFORM=TRINITY_DIRECTX11) set_shared_trinityaltest_properties(TrinityALTest_dx11) - #GTest - set_target_properties(TrinityALTest_dx11 PROPERTIES FOLDER "Tests") - #gtest_discover_tests(TrinityALTest_dx11 myListOfTests) - # Copy DLL dependencies add_custom_command( TARGET TrinityALTest_dx11 POST_BUILD @@ -207,6 +194,8 @@ if(WIN32) MAIN_DEPENDENCY ${shaderSource} ) endforeach() + + gtest_discover_tests(TrinityALTest_dx11 TEST_PREFIX TrinityAL_dx11.) endif() if (BUILD_DX12) @@ -258,7 +247,7 @@ if(WIN32) #GTest set_target_properties(TrinityALTest_dx12 PROPERTIES FOLDER "Tests") - #gtest_discover_tests(TrinityALTest_dx12 myListOfTests) + gtest_discover_tests(TrinityALTest_dx12 TEST_PREFIX TrinityALTest_dx12.) endif() diff --git a/trinityal/tests/Raytracing.cpp b/trinityal/tests/Raytracing.cpp index 5ac5c879f..5b8c94fb3 100644 --- a/trinityal/tests/Raytracing.cpp +++ b/trinityal/tests/Raytracing.cpp @@ -93,6 +93,11 @@ TEST_F( Raytracing, BLASIsInvalidBeforeCreation ) TEST_F( Raytracing, BLASIsValidAfterCreation ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + Tr2BufferAL vb, ib; // UNSURE ABOUT CPUUSAGE AND HOW TO NAVIGATE THAT ONE, need to have leave it like this for now because of possible metal bug w. buffers #if TRINITY_PLATFORM == TRINITY_METAL @@ -112,6 +117,11 @@ TEST_F( Raytracing, BLASIsValidAfterCreation ) TEST_F( Raytracing, BLASIsValidAfterUpdate ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + Tr2BufferAL vb, ib; // UNSURE ABOUT CPUUSAGE AND HOW TO NAVIGATE THAT ONE, need to have leave it like this for now because of possible metal bug w. buffers #if TRINITY_PLATFORM == TRINITY_METAL @@ -140,6 +150,11 @@ TEST_F( Raytracing, TLASIsInvalidBeforeCreation ) TEST_F( Raytracing, TLASIsValidAfterCreation ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + Tr2BufferAL vb, ib; // UNSURE ABOUT CPUUSAGE AND HOW TO NAVIGATE THAT ONE, need to have leave it like this for now because of possible metal bug w. buffers #if TRINITY_PLATFORM == TRINITY_METAL @@ -170,6 +185,11 @@ TEST_F( Raytracing, TLASIsValidAfterCreation ) TEST_F( Raytracing, CanCreateStateObject ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + uint8_t rayGenCode[] = { #include INCLUDE_SHADER_CODE( RayGen.rs ) }; @@ -200,6 +220,11 @@ TEST_F( Raytracing, CanCreateStateObject ) TEST_F( Raytracing, CanCreateShaderTable ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + uint8_t rayGenCode[] = { #include INCLUDE_SHADER_CODE( RayGen.rs ) }; @@ -238,6 +263,11 @@ TEST_F( Raytracing, CanCreateShaderTable ) TEST_F( Raytracing, ShaderTableCreationFailsWithInvalidShaderName ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + uint8_t rayGenCode[] = { #include INCLUDE_SHADER_CODE( RayGen.rs ) }; @@ -440,6 +470,11 @@ void Transpose( float viewMatrix[4][4] ) TEST_F( Raytracing, TraceRays ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + uint8_t rayGenCode[] = { #include INCLUDE_SHADER_CODE( RayGen.rs ) }; @@ -566,6 +601,11 @@ TEST_F( Raytracing, TraceRays ) TEST_F( Raytracing, CanUpdateBlas ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + uint8_t rayGenCode[] = { #include INCLUDE_SHADER_CODE( RayGen.rs ) }; @@ -709,6 +749,11 @@ TEST_F( Raytracing, CanUpdateBlas ) TEST_F( Raytracing, CanUseLocalConstants ) { + if( !renderContext->GetCaps().SupportsRaytracing() ) + { + GTEST_SKIP() << "Raytracing not supported on this device"; + } + uint8_t rayGenCode[] = { #include INCLUDE_SHADER_CODE( RayGen.rs ) }; diff --git a/vcpkg.json b/vcpkg.json index 46ac26106..a65ab7720 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -76,6 +76,11 @@ { "name": "carbon-mesh", "version>=": "1.0.1" + }, + { + "name": "carbon-exefile", + "version>=": "4.1.1", + "host": true } ], "features": {