diff --git a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp index ea9efc3df2..3094e1cea6 100644 --- a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp +++ b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include @@ -372,6 +374,106 @@ namespace } } + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.obj")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh_with_revision) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + mSettings.mEnableRecastMeshFileNameRevision = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.1.2.obj")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_recast_mesh_to_absent_file_should_not_fail_tile_generation) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + EXPECT_FALSE(std::filesystem::exists(dir)); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.bin")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh_with_revision) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + mSettings.mEnableNavMeshFileNameRevision = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.1.1.bin")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_navmesh_to_absent_file_should_not_fail_tile_generation) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + EXPECT_FALSE(std::filesystem::exists(dir)); + } + struct DetourNavigatorSpatialJobQueueTest : Test { const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) }; diff --git a/apps/components_tests/files/hash.cpp b/apps/components_tests/files/hash.cpp index 793965112b..f94cb1969d 100644 --- a/apps/components_tests/files/hash.cpp +++ b/apps/components_tests/files/hash.cpp @@ -30,7 +30,7 @@ namespace TEST(FilesGetHash, shouldClearErrors) { - const auto fileName = temporaryFilePath("fileName"); + const auto fileName = outputFilePath("fileName"); std::string content; std::fill_n(std::back_inserter(content), 1, 'a'); std::istringstream stream(content); @@ -41,7 +41,7 @@ namespace TEST_P(FilesGetHash, shouldReturnHashForStringStream) { - const auto fileName = temporaryFilePath("fileName"); + const auto fileName = outputFilePath("fileName"); std::string content; std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); std::istringstream stream(content); diff --git a/apps/components_tests/main.cpp b/apps/components_tests/main.cpp index dcfb2e9ba9..c1b41d184a 100644 --- a/apps/components_tests/main.cpp +++ b/apps/components_tests/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -24,5 +25,9 @@ int main(int argc, char** argv) Settings::StaticValues::init(); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + + const int result = RUN_ALL_TESTS(); + if (result == 0) + std::filesystem::remove_all(TestingOpenMW::outputDir()); + return result; } diff --git a/apps/components_tests/shader/shadermanager.cpp b/apps/components_tests/shader/shadermanager.cpp index 5b11d31a44..b80839d0ec 100644 --- a/apps/components_tests/shader/shadermanager.cpp +++ b/apps/components_tests/shader/shadermanager.cpp @@ -16,7 +16,7 @@ namespace ShaderManager mManager; ShaderManager::DefineMap mDefines; - ShaderManagerTest() { mManager.setShaderPath("tests_output"); } + ShaderManagerTest() { mManager.setShaderPath(TestingOpenMW::outputDir()); } template void withShaderFile(const std::string& content, F&& f) diff --git a/apps/opencs_tests/main.cpp b/apps/opencs_tests/main.cpp index fd7d4900c8..fed1cd2bb1 100644 --- a/apps/opencs_tests/main.cpp +++ b/apps/opencs_tests/main.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -7,5 +8,9 @@ int main(int argc, char* argv[]) Log::sMinDebugLevel = Debug::getDebugLevel(); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + + const int result = RUN_ALL_TESTS(); + if (result == 0) + std::filesystem::remove_all(TestingOpenMW::outputDir()); + return result; } diff --git a/apps/openmw_tests/main.cpp b/apps/openmw_tests/main.cpp index 6b7298596a..485298c863 100644 --- a/apps/openmw_tests/main.cpp +++ b/apps/openmw_tests/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -24,5 +25,9 @@ int main(int argc, char* argv[]) Settings::StaticValues::init(); testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + + const int result = RUN_ALL_TESTS(); + if (result == 0) + std::filesystem::remove_all(TestingOpenMW::outputDir()); + return result; } diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 71c8bbc2d3..5f82c85cf9 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -453,9 +453,9 @@ namespace DetourNavigator Misc::setCurrentThreadIdlePriority(); while (!mShouldStop) { - try + if (JobIt job = getNextJob(); job != mJobs.end()) { - if (JobIt job = getNextJob(); job != mJobs.end()) + try { const JobStatus status = processJob(*job); Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status @@ -480,12 +480,20 @@ namespace DetourNavigator } } } - else - cleanupLastUpdates(); + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to process navmesh job " << job->mId + << " for worldspace=" << job->mWorldspace << " agent=" << job->mAgentBounds + << " changedTile=(" << job->mChangedTile << ")" + << " changeType=" << job->mChangeType + << " by thread=" << std::this_thread::get_id() << ": " << e.what(); + unlockTile(job->mId, job->mAgentBounds, job->mChangedTile); + removeJob(job); + } } - catch (const std::exception& e) + else { - Log(Debug::Error) << "AsyncNavMeshUpdater::process exception: " << e.what(); + cleanupLastUpdates(); } } Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id(); @@ -493,7 +501,8 @@ namespace DetourNavigator JobStatus AsyncNavMeshUpdater::processJob(Job& job) { - Log(Debug::Debug) << "Processing job " << job.mId << " for agent=(" << job.mAgentBounds << ")" + Log(Debug::Debug) << "Processing job " << job.mId << " for worldspace=" << job.mWorldspace + << " agent=" << job.mAgentBounds << "" << " changedTile=(" << job.mChangedTile << ")" << " changeType=" << job.mChangeType << " by thread=" << std::this_thread::get_id(); @@ -543,7 +552,14 @@ namespace DetourNavigator return JobStatus::Done; } - writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + try + { + writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to write debug recast mesh: " << e.what(); + } NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentBounds, job.mChangedTile, *recastMesh); @@ -666,12 +682,19 @@ namespace DetourNavigator mPresentTiles.insert(std::make_tuple(job.mAgentBounds, job.mChangedTile)); } - writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); + try + { + writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to write debug navmesh: " << e.what(); + } return isSuccess(status) ? JobStatus::Done : JobStatus::Fail; } - JobIt AsyncNavMeshUpdater::getNextJob() + JobIt AsyncNavMeshUpdater::getNextJob() noexcept { std::unique_lock lock(mMutex); @@ -746,7 +769,7 @@ namespace DetourNavigator return mJobs.size(); } - void AsyncNavMeshUpdater::cleanupLastUpdates() + void AsyncNavMeshUpdater::cleanupLastUpdates() noexcept { const auto now = std::chrono::steady_clock::now(); diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index 7877ff8082..95919ed770 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -244,7 +244,7 @@ namespace DetourNavigator inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job, const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh); - JobIt getNextJob(); + inline JobIt getNextJob() noexcept; void postThreadJob(JobIt job, std::deque& queue); @@ -254,7 +254,7 @@ namespace DetourNavigator inline std::size_t getTotalJobs() const; - void cleanupLastUpdates(); + inline void cleanupLastUpdates() noexcept; inline void waitUntilJobsDoneForNotPresentTiles(Loading::Listener* listener); diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 11751e631c..fe63d27a1e 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -523,7 +523,7 @@ namespace DetourNavigator std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds, const RecastSettings& settings) { - RecastContext context(worldspace, tilePosition, agentBounds, settings.mMaxLogLevel); + RecastContext context(worldspace, tilePosition, agentBounds, recastMesh.getVersion(), settings.mMaxLogLevel); const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentBounds.mHalfExtents.z(), settings); diff --git a/components/detournavigator/recastcontext.cpp b/components/detournavigator/recastcontext.cpp index 89bb268b16..732517e423 100644 --- a/components/detournavigator/recastcontext.cpp +++ b/components/detournavigator/recastcontext.cpp @@ -23,20 +23,20 @@ namespace DetourNavigator return Debug::Debug; } - std::string formatPrefix( - ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds) + std::string formatPrefix(ESM::RefId worldspace, const TilePosition& tilePosition, + const AgentBounds& agentBounds, const Version& version) { std::ostringstream stream; stream << "Worldspace: " << worldspace << "; tile position: " << tilePosition.x() << ", " - << tilePosition.y() << "; agent bounds: " << agentBounds << "; "; + << tilePosition.y() << "; agent bounds: " << agentBounds << "; version: " << version << "; "; return stream.str(); } } RecastContext::RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, - const AgentBounds& agentBounds, Debug::Level maxLogLevel) + const AgentBounds& agentBounds, const Version& version, Debug::Level maxLogLevel) : mMaxLogLevel(maxLogLevel) - , mPrefix(formatPrefix(worldspace, tilePosition, agentBounds)) + , mPrefix(formatPrefix(worldspace, tilePosition, agentBounds, version)) { } diff --git a/components/detournavigator/recastcontext.hpp b/components/detournavigator/recastcontext.hpp index 7c9d50951b..b36c4b9842 100644 --- a/components/detournavigator/recastcontext.hpp +++ b/components/detournavigator/recastcontext.hpp @@ -13,12 +13,13 @@ namespace DetourNavigator { struct AgentBounds; + struct Version; class RecastContext final : public rcContext { public: explicit RecastContext(ESM::RefId worldspace, const TilePosition& tilePosition, const AgentBounds& agentBounds, - Debug::Level maxLogLevel); + const Version& version, Debug::Level maxLogLevel); const std::string& getPrefix() const { return mPrefix; } diff --git a/components/testing/util.hpp b/components/testing/util.hpp index 941f495458..53331d6d37 100644 --- a/components/testing/util.hpp +++ b/components/testing/util.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_COMPONENTS_TESTING_UTIL_H #define OPENMW_COMPONENTS_TESTING_UTIL_H +#include #include #include #include @@ -14,26 +15,37 @@ namespace TestingOpenMW { - inline std::filesystem::path outputFilePath(const std::string name) + inline std::filesystem::path outputDir() { - std::filesystem::path dir("tests_output"); - std::filesystem::create_directory(dir); + static const std::string run + = std::to_string(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); + std::filesystem::path dir = std::filesystem::temp_directory_path() / "openmw" / "tests" / run; + std::filesystem::create_directories(dir); + return dir; + } + + inline std::filesystem::path outputFilePath(std::string_view name) + { + std::filesystem::path dir = outputDir(); return dir / Misc::StringUtils::stringToU8String(name); } + inline std::filesystem::path outputDirPath(const std::filesystem::path& subpath) + { + std::filesystem::path path = outputDir(); + path /= subpath; + std::filesystem::create_directories(path); + return path; + } + inline std::filesystem::path outputFilePathWithSubDir(const std::filesystem::path& subpath) { - std::filesystem::path path("tests_output"); + std::filesystem::path path = outputDir(); path /= subpath; std::filesystem::create_directories(path.parent_path()); return path; } - inline std::filesystem::path temporaryFilePath(const std::string name) - { - return std::filesystem::temp_directory_path() / name; - } - class VFSTestFile : public VFS::File { public: