diff --git a/src/DataExchange/TKXSBase/GTests/FILES.cmake b/src/DataExchange/TKXSBase/GTests/FILES.cmake index f9484a455b9..4efeb3610f8 100644 --- a/src/DataExchange/TKXSBase/GTests/FILES.cmake +++ b/src/DataExchange/TKXSBase/GTests/FILES.cmake @@ -2,4 +2,6 @@ set(OCCT_TKXSBase_GTests_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}") set(OCCT_TKXSBase_GTests_FILES + Interface_InterfaceModel_Test.cxx + XSControl_Controller_Test.cxx ) diff --git a/src/DataExchange/TKXSBase/GTests/Interface_InterfaceModel_Test.cxx b/src/DataExchange/TKXSBase/GTests/Interface_InterfaceModel_Test.cxx new file mode 100644 index 00000000000..9c4ce2518b4 --- /dev/null +++ b/src/DataExchange/TKXSBase/GTests/Interface_InterfaceModel_Test.cxx @@ -0,0 +1,97 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ +class TestModel : public Interface_InterfaceModel +{ +public: + void ClearLabels() override {} + + void ClearHeader() override {} + + void GetFromAnother(const occ::handle&) override {} + + occ::handle NewEmptyModel() const override { return new TestModel; } + + void DumpHeader(Standard_OStream&, const int) const override {} + + void PrintLabel(const occ::handle&, Standard_OStream&) const override {} + + occ::handle StringLabel( + const occ::handle&) const override + { + return new TCollection_HAsciiString(""); + } + + DEFINE_STANDARD_RTTI_INLINE(TestModel, Interface_InterfaceModel) +}; +} // namespace + +// SetTemplate, HasTemplate and Template work on one process-wide map, so concurrent registrations +// must all survive. +TEST(Interface_InterfaceModelTest, ConcurrentTemplateRegistrationKeepsEveryTemplate) +{ + constexpr int aNbThreads = 8; + constexpr int aNbPerThread = 250; + + std::vector aNames; + for (int anIdx = 0; anIdx < aNbThreads * aNbPerThread; ++anIdx) + { + aNames.push_back(TCollection_AsciiString("imtest-") + anIdx); + } + const occ::handle aModel = new TestModel; + + std::atomic aStart(false); + std::vector aThreads; + for (int aThread = 0; aThread < aNbThreads; ++aThread) + { + aThreads.emplace_back([&, aThread]() { + while (!aStart.load()) + { + } + for (int anIdx = aThread * aNbPerThread; anIdx < (aThread + 1) * aNbPerThread; ++anIdx) + { + Interface_InterfaceModel::SetTemplate(aNames[anIdx].ToCString(), aModel); + Interface_InterfaceModel::Template(aNames[anIdx].ToCString()); + } + }); + } + aStart.store(true); + for (std::thread& aThread : aThreads) + { + aThread.join(); + } + + int aMissing = 0; + for (const TCollection_AsciiString& aName : aNames) + { + if (!Interface_InterfaceModel::HasTemplate(aName.ToCString())) + { + ++aMissing; + } + } + EXPECT_EQ(aMissing, 0); + EXPECT_GE(Interface_InterfaceModel::ListTemplates()->Length(), aNbThreads * aNbPerThread); +} diff --git a/src/DataExchange/TKXSBase/GTests/XSControl_Controller_Test.cxx b/src/DataExchange/TKXSBase/GTests/XSControl_Controller_Test.cxx new file mode 100644 index 00000000000..4aad5efd117 --- /dev/null +++ b/src/DataExchange/TKXSBase/GTests/XSControl_Controller_Test.cxx @@ -0,0 +1,86 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ +class TestController : public XSControl_Controller +{ +public: + TestController(const char* const theLongName, const char* const theShortName) + : XSControl_Controller(theLongName, theShortName) + { + } + + occ::handle NewModel() const override { return nullptr; } + + DEFINE_STANDARD_RTTI_INLINE(TestController, XSControl_Controller) +}; +} // namespace + +// Record and Recorded work on one process-wide map, so concurrent recordings must all survive. +TEST(XSControl_ControllerTest, ConcurrentRecordKeepsEveryController) +{ + constexpr int aNbThreads = 8; + constexpr int aNbPerThread = 250; + + // The controller constructor initialises shared parameters, so build them up front. + std::vector> aControllers; + std::vector aNames; + for (int anIdx = 0; anIdx < aNbThreads * aNbPerThread; ++anIdx) + { + const TCollection_AsciiString aName = TCollection_AsciiString("xsctest-") + anIdx; + aNames.push_back(aName); + aControllers.push_back(new TestController(aName.ToCString(), aName.ToCString())); + } + + std::atomic aStart(false); + std::vector aThreads; + for (int aThread = 0; aThread < aNbThreads; ++aThread) + { + aThreads.emplace_back([&, aThread]() { + while (!aStart.load()) + { + } + for (int anIdx = aThread * aNbPerThread; anIdx < (aThread + 1) * aNbPerThread; ++anIdx) + { + aControllers[anIdx]->Record(aNames[anIdx].ToCString()); + XSControl_Controller::Recorded(aNames[anIdx].ToCString()); + } + }); + } + aStart.store(true); + for (std::thread& aThread : aThreads) + { + aThread.join(); + } + + int aMissing = 0; + for (size_t anIdx = 0; anIdx < aNames.size(); ++anIdx) + { + if (XSControl_Controller::Recorded(aNames[anIdx].ToCString()) != aControllers[anIdx]) + { + ++aMissing; + } + } + EXPECT_EQ(aMissing, 0); +} diff --git a/src/DataExchange/TKXSBase/Interface/Interface_InterfaceModel.cxx b/src/DataExchange/TKXSBase/Interface/Interface_InterfaceModel.cxx index 029c03e5eae..4ba3df913a6 100644 --- a/src/DataExchange/TKXSBase/Interface/Interface_InterfaceModel.cxx +++ b/src/DataExchange/TKXSBase/Interface/Interface_InterfaceModel.cxx @@ -33,6 +33,8 @@ #include #include +#include + IMPLEMENT_STANDARD_RTTIEXT(Interface_InterfaceModel, Standard_Transient) // An Interface Model is a closed set of interface Entities: each one @@ -43,6 +45,14 @@ IMPLEMENT_STANDARD_RTTIEXT(Interface_InterfaceModel, Standard_Transient) // STATICS : the TEMPLATES static NCollection_DataMap> atemp; +//! Guards atemp, the process-wide registry of template models. Recursive because Template() calls +//! HasTemplate(). +static std::recursive_mutex& atempMutex() +{ + static std::recursive_mutex THE_MUTEX; + return THE_MUTEX; +} + static const occ::handle& typerep() { return STANDARD_TYPE(Interface_ReportEntity); @@ -1100,6 +1110,7 @@ int Interface_InterfaceModel::NextNumberForLabel(const char* const label, bool Interface_InterfaceModel::HasTemplate(const char* const name) { + std::lock_guard aLock(atempMutex()); return atemp.IsBound(name); } @@ -1107,6 +1118,7 @@ bool Interface_InterfaceModel::HasTemplate(const char* const name) occ::handle Interface_InterfaceModel::Template(const char* const name) { + std::lock_guard aLock(atempMutex()); occ::handle model, newmod; if (!HasTemplate(name)) { @@ -1123,6 +1135,7 @@ occ::handle Interface_InterfaceModel::Template(const c bool Interface_InterfaceModel::SetTemplate(const char* const name, const occ::handle& model) { + std::lock_guard aLock(atempMutex()); return atemp.Bind(name, model); } @@ -1131,6 +1144,8 @@ bool Interface_InterfaceModel::SetTemplate(const char* const occ::handle>> Interface_InterfaceModel:: ListTemplates() { + std::lock_guard aLock(atempMutex()); + occ::handle>> list = new NCollection_HSequence>(); if (atemp.IsEmpty()) diff --git a/src/DataExchange/TKXSBase/XSControl/XSControl_Controller.cxx b/src/DataExchange/TKXSBase/XSControl/XSControl_Controller.cxx index fa18006e1bc..02b226a20ab 100644 --- a/src/DataExchange/TKXSBase/XSControl/XSControl_Controller.cxx +++ b/src/DataExchange/TKXSBase/XSControl/XSControl_Controller.cxx @@ -51,6 +51,8 @@ #include #include +#include + IMPLEMENT_STANDARD_RTTIEXT(XSControl_Controller, Standard_Transient) // ParamEditor @@ -58,6 +60,13 @@ IMPLEMENT_STANDARD_RTTIEXT(XSControl_Controller, Standard_Transient) static NCollection_DataMap> listad; +//! Guards listad, the process-wide registry of controllers by name. +static std::recursive_mutex& listadMutex() +{ + static std::recursive_mutex THE_MUTEX; + return THE_MUTEX; +} + //================================================================================================= XSControl_Controller::XSControl_Controller(const char* const theLongName, @@ -106,6 +115,7 @@ void XSControl_Controller::SetNames(const char* const theLongName, const char* c void XSControl_Controller::Record(const char* const theName) const { + std::lock_guard aLock(listadMutex()); if (listad.IsBound(theName)) { occ::handle thisadapt(this); @@ -126,6 +136,8 @@ void XSControl_Controller::Record(const char* const theName) const occ::handle XSControl_Controller::Recorded(const char* const theName) { + std::lock_guard aLock(listadMutex()); + occ::handle recorded; return (listad.Find(theName, recorded) ? occ::down_cast(recorded) : occ::handle());