diff --git a/include/geode/basic/detail/geode_input_impl.hpp b/include/geode/basic/detail/geode_input_impl.hpp index cc6b9436a..5a6d9b862 100644 --- a/include/geode/basic/detail/geode_input_impl.hpp +++ b/include/geode/basic/detail/geode_input_impl.hpp @@ -64,9 +64,10 @@ namespace geode::detail const Timer timer; auto input = geode_object_input_reader< Factory >( filename ); OpenGeodeBasicException::check_exception( - std::filesystem::exists( input->filename() ), nullptr, - OpenGeodeException::TYPE::data, "[", type, "] File \"", filename, - "\" does not exist" ); + std::filesystem::exists( + std::filesystem::u8path( to_string( input->filename() ) ) ), + nullptr, OpenGeodeException::TYPE::data, "[", type, "] File \"", + filename, "\" does not exist" ); auto object = input->read( std::forward< Args >( args )... ); Logger::info( type, " loaded from ", filename, " in ", timer.duration() ); diff --git a/include/geode/basic/detail/geode_output_impl.hpp b/include/geode/basic/detail/geode_output_impl.hpp index 47bbbbf02..77a086917 100644 --- a/include/geode/basic/detail/geode_output_impl.hpp +++ b/include/geode/basic/detail/geode_output_impl.hpp @@ -63,7 +63,7 @@ namespace geode::detail const Timer timer; auto output = geode_object_output_writer< Factory >( filename ); const auto directories = filepath_without_filename( - std::filesystem::path{ expand_predefined_folders( filename ) } ); + std::filesystem::u8path( expand_predefined_folders( filename ) ) ); if( !directories.empty() ) { std::filesystem::create_directories( directories ); diff --git a/include/geode/basic/logger.hpp b/include/geode/basic/logger.hpp index 11dd77e5a..5b5da140a 100644 --- a/include/geode/basic/logger.hpp +++ b/include/geode/basic/logger.hpp @@ -142,6 +142,38 @@ namespace geode private: IMPLEMENTATION_MEMBER( impl_ ); }; + + namespace detail + { + /*! + * Set the Logger level for the lifetime of this object and restore + * the previous level on destruction, even if an exception is thrown. + * The Logger level is global: scopes running concurrently in several + * threads may restore each other's level. + */ + class ScopedLoggerLevel + { + public: + explicit ScopedLoggerLevel( Logger::LEVEL level ) + : previous_level_{ Logger::level() } + { + Logger::set_level( level ); + } + + ~ScopedLoggerLevel() + { + Logger::set_level( previous_level_ ); + } + + ScopedLoggerLevel( const ScopedLoggerLevel & ) = delete; + ScopedLoggerLevel &operator=( const ScopedLoggerLevel & ) = delete; + ScopedLoggerLevel( ScopedLoggerLevel && ) = delete; + ScopedLoggerLevel &operator=( ScopedLoggerLevel && ) = delete; + + private: + Logger::LEVEL previous_level_; + }; + } // namespace detail } // namespace geode #include diff --git a/include/geode/model/mixin/builder/vertex_identifier_builder.hpp b/include/geode/model/mixin/builder/vertex_identifier_builder.hpp index 9c3f6ff89..d88540af8 100644 --- a/include/geode/model/mixin/builder/vertex_identifier_builder.hpp +++ b/include/geode/model/mixin/builder/vertex_identifier_builder.hpp @@ -117,6 +117,13 @@ namespace geode */ std::vector< index_t > delete_isolated_vertices(); + /*! + * Remove component vertices referencing an unregistered component or + * an invalid vertex, and restore missing links from component vertices + * to their unique vertex. Used after loading a model. + */ + void repair_component_vertices(); + private: VertexIdentifier& vertex_identifier_; }; diff --git a/include/geode/model/mixin/core/detail/components_storage.hpp b/include/geode/model/mixin/core/detail/components_storage.hpp index 3a39aebf5..338468c14 100644 --- a/include/geode/model/mixin/core/detail/components_storage.hpp +++ b/include/geode/model/mixin/core/detail/components_storage.hpp @@ -35,10 +35,12 @@ #include #include +#include #include #include +#include #include #include @@ -46,6 +48,45 @@ namespace geode::detail { + /*! + * Return the given mesh implementation if it is registered, otherwise + * the default implementation of the given mesh type (with a warning). + */ + [[nodiscard]] inline MeshImpl loadable_mesh_impl( const MeshImpl& impl, + const MeshType& fallback_type, + std::string_view file ) + { + if( MeshFactory::has_creator( impl ) ) + { + return impl; + } + const auto& fallback = MeshFactory::default_impl( fallback_type ); + Logger::warning( "[ComponentsStorage] Unknown mesh implementation ", + impl.get(), " for file ", file, ", loading it as ", + fallback.get() ); + return fallback; + } + + /*! + * Create an empty mesh for a component whose mesh file is missing, using + * the component mesh implementation if it is registered, otherwise the + * default implementation of the given mesh type. + */ + template < typename Component > + [[nodiscard]] std::unique_ptr< typename Component::Mesh > + empty_mesh_of_missing_file( + const Component& component, const MeshType& fallback_type ) + { + Logger::warning( "[ComponentsStorage] Missing mesh file of ", + component.component_id().string(), ", an empty mesh is used" ); + if( MeshFactory::has_creator( component.mesh_type() ) ) + { + return Component::Mesh::create( component.mesh_type() ); + } + return Component::Mesh::create( + MeshFactory::default_impl( fallback_type ) ); + } + template < typename Component > class ComponentsStorage { @@ -98,7 +139,13 @@ namespace geode::detail void save_components( std::string_view filename ) const { - std::ofstream file{ to_string( filename ), std::ofstream::binary }; + std::ofstream file{ std::filesystem::u8path( + to_string( filename ) ), + std::ofstream::binary }; + OpenGeodeModelException::check_exception( file.is_open(), nullptr, + OpenGeodeException::TYPE::internal, + "[ComponentsStorage::save_components] Cannot open file: ", + filename ); TContext context{}; BitseryExtensions::register_serialize_pcontext( std::get< 0 >( context ) ); @@ -106,7 +153,7 @@ namespace geode::detail archive.object( *this ); archive.adapter().flush(); OpenGeodeModelException::check_exception( - std::get< 1 >( context ).isValid(), nullptr, + file.good() && std::get< 1 >( context ).isValid(), nullptr, OpenGeodeException::TYPE::internal, "[ComponentsStorage::save_components] Error while writing " "file: ", @@ -118,13 +165,26 @@ namespace geode::detail components_.erase( components_.find( component_id ) ); } - void load_components( std::string_view filename ) + /*! + * Load the components stored in the given file. + * @param[in] required If false, a missing file is expected (e.g. for + * files written before this component type existed) and silently + * ignored. Otherwise a warning is emitted. + */ + void load_components( std::string_view filename, bool required = true ) { - if( !std::filesystem::exists( to_string( filename ) ) ) + const auto path = std::filesystem::u8path( to_string( filename ) ); + if( !std::filesystem::exists( path ) ) { + if( required ) + { + Logger::warning( "[ComponentsStorage::load_components] " + "Missing file: ", + filename, ", no component loaded from it" ); + } return; } - std::ifstream file{ to_string( filename ), std::ifstream::binary }; + std::ifstream file{ path, std::ifstream::binary }; TContext context{}; BitseryExtensions::register_deserialize_pcontext( std::get< 0 >( context ) ); @@ -139,21 +199,23 @@ namespace geode::detail "[ComponentsStorage::load_components] Error while reading " "file: ", filename ); + repair_loaded_components( filename ); } [[nodiscard]] absl::flat_hash_map< std::string, std::string > file_mapping( std::string_view directory ) const { absl::flat_hash_map< std::string, std::string > mapping; - for( const auto& file : - std::filesystem::directory_iterator( to_string( directory ) ) ) + for( const auto& file : std::filesystem::directory_iterator( + std::filesystem::u8path( to_string( directory ) ) ) ) { auto path = file.path(); - auto filename = path.replace_extension( "" ).string(); + auto filename = path.replace_extension( "" ).u8string(); if( filename.size() > 36 ) { auto uuid = filename.substr( filename.size() - 36 ); - mapping.emplace( std::move( uuid ), file.path().string() ); + mapping.emplace( + std::move( uuid ), file.path().u8string() ); } } return mapping; @@ -179,6 +241,52 @@ namespace geode::detail } } } ); } + void repair_loaded_components( std::string_view filename ) + { + bool has_wrong_component{ false }; + for( const auto& [component_id, component] : components_ ) + { + if( !component || component->id() != component_id ) + { + has_wrong_component = true; + break; + } + } + if( !has_wrong_component ) + { + return; + } + ComponentsStore repaired; + repaired.reserve( components_.size() ); + for( auto& [component_id, component] : components_ ) + { + if( !component ) + { + Logger::warning( "[ComponentsStorage::load_components] " + "Removing null component ", + component_id.string(), " from file: ", filename ); + continue; + } + auto id = component->id(); + if( id != component_id ) + { + Logger::warning( "[ComponentsStorage::load_components] " + "Component stored with id ", + component_id.string(), " has id ", id.string(), + " in file: ", filename ); + } + const auto [_, inserted] = + repaired.emplace( id, std::move( component ) ); + if( !inserted ) + { + Logger::warning( "[ComponentsStorage::load_components] " + "Removing duplicated component ", + id.string(), " from file: ", filename ); + } + } + components_ = std::move( repaired ); + } + [[nodiscard]] Component& find_component( const uuid& component_id ) const { diff --git a/include/geode/model/mixin/core/detail/relationships_impl.hpp b/include/geode/model/mixin/core/detail/relationships_impl.hpp index 9bb9b4ce7..e9320bc23 100644 --- a/include/geode/model/mixin/core/detail/relationships_impl.hpp +++ b/include/geode/model/mixin/core/detail/relationships_impl.hpp @@ -106,7 +106,13 @@ namespace geode [[nodiscard]] const Graph& graph() const; - void delete_isolated_vertices(); + /*! + * Rebuild the uuid to graph vertex map from the ids stored on the + * graph vertices, after loading a file. + * Vertices without a valid or with a duplicated id are removed with + * their relations, as well as isolated vertices. + */ + void repair_after_load(); private: friend class bitsery::Access; @@ -123,7 +129,7 @@ namespace geode archive.object( impl.uuid2index_ ); archive.ext( impl.ids_, bitsery::ext::StdSmartPtr{} ); - impl.delete_isolated_vertices(); + impl.repair_after_load(); }, []( Archive& archive, RelationshipsImpl& impl ) { archive.ext( @@ -131,7 +137,7 @@ namespace geode archive.object( impl.uuid2index_ ); archive.ext( impl.ids_, bitsery::ext::StdSmartPtr{} ); - impl.delete_isolated_vertices(); + impl.repair_after_load(); } } } ); } diff --git a/include/geode/model/mixin/core/detail/uuid_to_index.hpp b/include/geode/model/mixin/core/detail/uuid_to_index.hpp index 7433cfdd4..1271b6f9e 100644 --- a/include/geode/model/mixin/core/detail/uuid_to_index.hpp +++ b/include/geode/model/mixin/core/detail/uuid_to_index.hpp @@ -74,6 +74,16 @@ namespace geode uuid2index_[id] = index; } + void clear() + { + uuid2index_.clear(); + } + + void reserve( index_t capacity ) + { + uuid2index_.reserve( capacity ); + } + void update( absl::Span< const index_t > old2new ) { for( auto& it : uuid2index_ ) diff --git a/include/geode/model/mixin/core/vertex_identifier.hpp b/include/geode/model/mixin/core/vertex_identifier.hpp index 4a34a330f..226b9d5f5 100644 --- a/include/geode/model/mixin/core/vertex_identifier.hpp +++ b/include/geode/model/mixin/core/vertex_identifier.hpp @@ -214,6 +214,13 @@ namespace geode */ std::vector< index_t > delete_isolated_vertices( BuilderKey /*key*/ ); + /*! + * Remove component vertices referencing an unregistered component or + * an invalid vertex, and restore missing links from component vertices + * to their unique vertex. Used after loading a model. + */ + void repair_component_vertices( BuilderKey /*key*/ ); + protected: VertexIdentifier( VertexIdentifier&& other ) noexcept; VertexIdentifier& operator=( VertexIdentifier&& other ) noexcept; diff --git a/include/geode/model/representation/builder/detail/filter.hpp b/include/geode/model/representation/builder/detail/filter.hpp index 46c0b021e..19efd3127 100644 --- a/include/geode/model/representation/builder/detail/filter.hpp +++ b/include/geode/model/representation/builder/detail/filter.hpp @@ -23,7 +23,9 @@ #pragma once +#include #include +#include #include #include @@ -64,6 +66,27 @@ namespace geode } }; + template < typename Mixin, typename Model > + [[nodiscard]] bool has_mixin_component( + const Model& model, const uuid& component_id ) + { + return model.Mixin::has_component( component_id ); + } + + template < typename > + struct ComponentsExistenceChecker; + + template < typename... Components > + struct ComponentsExistenceChecker< std::tuple< Components... > > + { + template < typename Model > + bool apply( const Model& model, const uuid& component_id ) const + { + return ( + has_mixin_component< Components >( model, component_id ) + || ... ); + } + }; } // namespace internal namespace detail @@ -74,6 +97,9 @@ namespace geode typename Model::Builder builder{ model }; const internal::ComponentsTypesChecker< typename Model::Components > checker; + const internal::ComponentsExistenceChecker< + typename Model::Components > + existence_checker; std::vector< uuid > components_to_remove; for( const auto vertex : Range{ model.nb_components_with_relations() } ) @@ -84,6 +110,13 @@ namespace geode { components_to_remove.push_back( component_id.id ); } + else if( !existence_checker.apply( model, component_id.id ) ) + { + Logger::warning( "[filter_unsupported_components] " + "Removing relations of missing ", + component_id.string() ); + components_to_remove.push_back( component_id.id ); + } } for( const auto& component : components_to_remove ) { diff --git a/include/geode/model/representation/io/geode/geode_brep_input.hpp b/include/geode/model/representation/io/geode/geode_brep_input.hpp index 322a82f13..1ba41c160 100644 --- a/include/geode/model/representation/io/geode/geode_brep_input.hpp +++ b/include/geode/model/representation/io/geode/geode_brep_input.hpp @@ -70,8 +70,9 @@ namespace geode void load_brep_files( Model& brep, std::string_view directory ) { BRepBuilder builder{ brep }; - const auto level = Logger::level(); - Logger::set_level( Logger::LEVEL::warning ); + const detail::ScopedLoggerLevel scoped_level{ + Logger::LEVEL::warning + }; async::parallel_invoke( [&builder, &directory] { builder.load_identifier( directory ); @@ -112,9 +113,9 @@ namespace geode [&builder, &directory] { builder.load_unique_vertices( directory ); } ); - Logger::set_level( level ); detail::register_all_components( brep ); detail::filter_unsupported_components( brep ); + builder.repair_component_vertices(); } } // namespace detail } // namespace geode diff --git a/include/geode/model/representation/io/geode/geode_section_input.hpp b/include/geode/model/representation/io/geode/geode_section_input.hpp index 394ad7e0a..85d12dd9f 100644 --- a/include/geode/model/representation/io/geode/geode_section_input.hpp +++ b/include/geode/model/representation/io/geode/geode_section_input.hpp @@ -70,8 +70,9 @@ namespace geode void load_section_files( Model& section, std::string_view directory ) { SectionBuilder builder{ section }; - const auto level = Logger::level(); - Logger::set_level( Logger::LEVEL::warning ); + const detail::ScopedLoggerLevel scoped_level{ + Logger::LEVEL::warning + }; async::parallel_invoke( [&builder, &directory] { builder.load_identifier( directory ); @@ -103,9 +104,9 @@ namespace geode [&builder, &directory] { builder.load_unique_vertices( directory ); } ); - Logger::set_level( level ); detail::register_all_components( section ); detail::filter_unsupported_components( section ); + builder.repair_component_vertices(); } } // namespace detail } // namespace geode diff --git a/src/geode/model/mixin/builder/vertex_identifier_builder.cpp b/src/geode/model/mixin/builder/vertex_identifier_builder.cpp index 0dbd81afe..5d53799f7 100644 --- a/src/geode/model/mixin/builder/vertex_identifier_builder.cpp +++ b/src/geode/model/mixin/builder/vertex_identifier_builder.cpp @@ -77,4 +77,10 @@ namespace geode return vertex_identifier_.delete_isolated_vertices( VertexIdentifier::BuilderKey{} ); } + + void VertexIdentifierBuilder::repair_component_vertices() + { + vertex_identifier_.repair_component_vertices( + VertexIdentifier::BuilderKey{} ); + } } // namespace geode diff --git a/src/geode/model/mixin/core/block_collections.cpp b/src/geode/model/mixin/core/block_collections.cpp index a5957d59b..635054ca6 100644 --- a/src/geode/model/mixin/core/block_collections.cpp +++ b/src/geode/model/mixin/core/block_collections.cpp @@ -100,7 +100,7 @@ namespace geode std::string_view directory, BlockCollectionsBuilderKey /*key*/ ) { impl_->load_components( - absl::StrCat( directory, "/block_collections" ) ); + absl::StrCat( directory, "/block_collections" ), false ); } template < index_t dimension > diff --git a/src/geode/model/mixin/core/blocks.cpp b/src/geode/model/mixin/core/blocks.cpp index 822b57ab5..5c2ae3d44 100644 --- a/src/geode/model/mixin/core/blocks.cpp +++ b/src/geode/model/mixin/core/blocks.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -33,17 +34,46 @@ #include #include #include +#include #include #include #include #include #include +#include +#include #include #include #include #include +namespace +{ + template < geode::index_t dimension > + geode::MeshType solid_type_from_file( std::string_view file ) + { + const auto extension = + geode::to_string( geode::extension_from_filename( file ) ); + if( geode::TetrahedralSolidInputFactory< dimension >::has_creator( + extension ) ) + { + return geode::TetrahedralSolid< dimension >::type_name_static(); + } + if( geode::HybridSolidInputFactory< dimension >::has_creator( + extension ) ) + { + return geode::HybridSolid< dimension >::type_name_static(); + } + if( geode::RegularGridInputFactory< dimension >::has_creator( + extension ) ) + { + return geode::RegularGrid< dimension >::type_name_static(); + } + return geode::PolyhedralSolid< dimension >::type_name_static(); + } +} // namespace + namespace geode { template < index_t dimension > @@ -128,6 +158,12 @@ namespace geode { save_polyhedral_solid( *poly, file ); } + else if( const auto* grid = + dynamic_cast< const RegularGrid< dimension >* >( + &mesh ) ) + { + save_regular_grid( *grid, file ); + } else { throw OpenGeodeModelException{ nullptr, @@ -154,25 +190,41 @@ namespace geode for( auto& block : modifiable_blocks( builder_key ) ) { tasks[count++] = async::spawn( [&block, &mapping] { - const auto file = mapping.at( block.id().string() ); - if( MeshFactory::type( block.mesh_type() ) - == TetrahedralSolid< dimension >::type_name_static() ) + const auto file = mapping.find( block.id().string() ); + if( file == mapping.end() ) + { + block.set_mesh( + detail::empty_mesh_of_missing_file( block, + PolyhedralSolid< dimension >::type_name_static() ), + typename Block< dimension >::BlocksKey{} ); + return; + } + const auto impl = detail::loadable_mesh_impl( block.mesh_type(), + solid_type_from_file< dimension >( file->second ), + file->second ); + const auto& type = MeshFactory::type( impl ); + if( type == TetrahedralSolid< dimension >::type_name_static() ) { block.set_mesh( load_tetrahedral_solid< dimension >( - block.mesh_type(), file ), + impl, file->second ), + typename Block< dimension >::BlocksKey{} ); + } + else if( type == HybridSolid< dimension >::type_name_static() ) + { + block.set_mesh( + load_hybrid_solid< dimension >( impl, file->second ), typename Block< dimension >::BlocksKey{} ); } - else if( MeshFactory::type( block.mesh_type() ) - == HybridSolid< dimension >::type_name_static() ) + else if( type == RegularGrid< dimension >::type_name_static() ) { - block.set_mesh( load_hybrid_solid< dimension >( - block.mesh_type(), file ), + block.set_mesh( + load_regular_grid< dimension >( impl, file->second ), typename Block< dimension >::BlocksKey{} ); } else { block.set_mesh( load_polyhedral_solid< dimension >( - block.mesh_type(), file ), + impl, file->second ), typename Block< dimension >::BlocksKey{} ); } } ); diff --git a/src/geode/model/mixin/core/component.cpp b/src/geode/model/mixin/core/component.cpp index 6e3b31cf3..886b95f49 100644 --- a/src/geode/model/mixin/core/component.cpp +++ b/src/geode/model/mixin/core/component.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -66,7 +67,7 @@ namespace geode Growable< Archive, Impl >{ { []( Archive& local_archive, Impl& impl ) { local_archive.text1b( impl.backward_compatible_name_, - impl.backward_compatible_name_.max_size() ); + MAX_SERIALIZED_NAME_SIZE ); local_archive.object( impl.backward_compatible_id_ ); }, []( Archive& local_archive, Impl& impl ) { diff --git a/src/geode/model/mixin/core/corner_collections.cpp b/src/geode/model/mixin/core/corner_collections.cpp index 52b5ace47..77e8b3b04 100644 --- a/src/geode/model/mixin/core/corner_collections.cpp +++ b/src/geode/model/mixin/core/corner_collections.cpp @@ -101,7 +101,7 @@ namespace geode std::string_view directory, CornerCollectionsBuilderKey /*key*/ ) { impl_->load_components( - absl::StrCat( directory, "/corner_collections" ) ); + absl::StrCat( directory, "/corner_collections" ), false ); } template < index_t dimension > diff --git a/src/geode/model/mixin/core/corners.cpp b/src/geode/model/mixin/core/corners.cpp index 45d1a2285..8670cce6c 100644 --- a/src/geode/model/mixin/core/corners.cpp +++ b/src/geode/model/mixin/core/corners.cpp @@ -124,9 +124,20 @@ namespace geode for( auto& corner : modifiable_corners( key ) ) { tasks[count++] = async::spawn( [&corner, &mapping] { - const auto file = mapping.at( corner.id().string() ); + const auto& type = PointSet< dimension >::type_name_static(); + const auto file = mapping.find( corner.id().string() ); + if( file == mapping.end() ) + { + corner.set_mesh( + detail::empty_mesh_of_missing_file( corner, type ), + typename Corner< dimension >::CornersKey{} ); + return; + } corner.set_mesh( - load_point_set< dimension >( corner.mesh_type(), file ), + load_point_set< dimension >( + detail::loadable_mesh_impl( + corner.mesh_type(), type, file->second ), + file->second ), typename Corner< dimension >::CornersKey{} ); } ); } diff --git a/src/geode/model/mixin/core/detail/relationships_impl.cpp b/src/geode/model/mixin/core/detail/relationships_impl.cpp index 18e79fd12..1e626b9b6 100644 --- a/src/geode/model/mixin/core/detail/relationships_impl.cpp +++ b/src/geode/model/mixin/core/detail/relationships_impl.cpp @@ -25,7 +25,10 @@ #include +#include + #include +#include #include #include @@ -306,18 +309,79 @@ namespace geode return register_component( component_id ); } - void RelationshipsImpl::delete_isolated_vertices() + void RelationshipsImpl::repair_after_load() { - auto builder = GraphBuilder::create( *graph_ ); - const auto old2new = builder->delete_isolated_vertices(); - for( const auto v : Indices{ old2new } ) + OpenGeodeModelException::check_exception( graph_ != nullptr, + nullptr, OpenGeodeException::TYPE::data, + "[Relationships::load] Missing relationships graph" ); + if( !ids_ ) { - if( old2new[v] == NO_ID ) + initialize_attributes(); + } + const auto nb_vertices = graph_->nb_vertices(); + const ComponentType undefined_type{ "undefined" }; + std::vector< bool > vertices_to_delete( nb_vertices, false ); + absl::flat_hash_set< uuid > valid_ids; + valid_ids.reserve( nb_vertices ); + index_t nb_undefined{ 0 }; + index_t nb_duplicated{ 0 }; + index_t nb_wrong_mappings{ 0 }; + for( const auto vertex : Range{ nb_vertices } ) + { + const auto& component_id = ids_->value( vertex ); + if( component_id.type == undefined_type ) { - uuid2index_.erase( v ); + vertices_to_delete[vertex] = true; + nb_undefined++; + continue; + } + if( !valid_ids.emplace( component_id.id ).second ) + { + vertices_to_delete[vertex] = true; + nb_duplicated++; + continue; + } + if( uuid2index_.index( component_id.id ) != vertex ) + { + nb_wrong_mappings++; } } - uuid2index_.update( old2new ); + if( nb_wrong_mappings > 0 + || uuid2index_.nb_mappings() != valid_ids.size() ) + { + Logger::warning( "[Relationships::load] Inconsistent component " + "index, rebuilding it from component ids" ); + } + if( nb_undefined > 0 || nb_duplicated > 0 ) + { + Logger::warning( "[Relationships::load] Removing ", + nb_undefined, " component(s) with undefined id and ", + nb_duplicated, + " duplicated component(s) with their relations" ); + std::vector< bool > edges_to_delete( + graph_->nb_edges(), false ); + for( const auto vertex : Range{ nb_vertices } ) + { + if( !vertices_to_delete[vertex] ) + { + continue; + } + for( const auto& edge : + graph_->edges_around_vertex( vertex ) ) + { + edges_to_delete[edge.edge_id] = true; + } + } + GraphBuilder::create( *graph_ )->delete_edges( + edges_to_delete ); + } + GraphBuilder::create( *graph_ )->delete_isolated_vertices(); + uuid2index_.clear(); + uuid2index_.reserve( graph_->nb_vertices() ); + for( const auto vertex : Range{ graph_->nb_vertices() } ) + { + uuid2index_.set_new_mapping( ids_->value( vertex ).id, vertex ); + } } } // namespace detail } // namespace geode diff --git a/src/geode/model/mixin/core/line_collections.cpp b/src/geode/model/mixin/core/line_collections.cpp index ee220bd82..9bcd9b5d6 100644 --- a/src/geode/model/mixin/core/line_collections.cpp +++ b/src/geode/model/mixin/core/line_collections.cpp @@ -100,7 +100,7 @@ namespace geode std::string_view directory, LineCollectionsBuilderKey /*key*/ ) { impl_->load_components( - absl::StrCat( directory, "/line_collections" ) ); + absl::StrCat( directory, "/line_collections" ), false ); } template < index_t dimension > diff --git a/src/geode/model/mixin/core/lines.cpp b/src/geode/model/mixin/core/lines.cpp index 9e39d28aa..9b1c8d1ed 100644 --- a/src/geode/model/mixin/core/lines.cpp +++ b/src/geode/model/mixin/core/lines.cpp @@ -122,9 +122,19 @@ namespace geode for( auto& line : modifiable_lines( key ) ) { tasks[count++] = async::spawn( [&line, &mapping] { - const auto file = mapping.at( line.id().string() ); - line.set_mesh( - load_edged_curve< dimension >( line.mesh_type(), file ), + const auto& type = EdgedCurve< dimension >::type_name_static(); + const auto file = mapping.find( line.id().string() ); + if( file == mapping.end() ) + { + line.set_mesh( + detail::empty_mesh_of_missing_file( line, type ), + typename Line< dimension >::LinesKey{} ); + return; + } + line.set_mesh( load_edged_curve< dimension >( + detail::loadable_mesh_impl( + line.mesh_type(), type, file->second ), + file->second ), typename Line< dimension >::LinesKey{} ); } ); } diff --git a/src/geode/model/mixin/core/physical_properties.cpp b/src/geode/model/mixin/core/physical_properties.cpp index d8daa4e57..4a1b5b6b8 100644 --- a/src/geode/model/mixin/core/physical_properties.cpp +++ b/src/geode/model/mixin/core/physical_properties.cpp @@ -98,7 +98,11 @@ namespace geode { const auto filename = absl::StrCat( directory, "/physical_properties" ); - std::ofstream file{ filename, std::ofstream::binary }; + std::ofstream file{ std::filesystem::u8path( filename ), + std::ofstream::binary }; + OpenGeodeModelException::check_exception( file.is_open(), nullptr, + OpenGeodeException::TYPE::internal, + "[PhysicalProperties::save] Cannot open file: ", filename ); TContext context{}; BitseryExtensions::register_serialize_pcontext( std::get< 0 >( context ) ); @@ -106,7 +110,7 @@ namespace geode archive.object( *this ); archive.adapter().flush(); OpenGeodeModelException::check_exception( - std::get< 1 >( context ).isValid(), nullptr, + file.good() && std::get< 1 >( context ).isValid(), nullptr, OpenGeodeException::TYPE::internal, "[PhysicalProperties::save] Error while writing file: ", filename ); @@ -116,11 +120,13 @@ namespace geode { const auto filename = absl::StrCat( directory, "/physical_properties" ); - if( !std::filesystem::exists( filename ) ) + if( !std::filesystem::exists( + std::filesystem::u8path( filename ) ) ) { return; } - std::ifstream file{ filename, std::ifstream::binary }; + std::ifstream file{ std::filesystem::u8path( filename ), + std::ifstream::binary }; TContext context{}; BitseryExtensions::register_deserialize_pcontext( std::get< 0 >( context ) ); diff --git a/src/geode/model/mixin/core/relationships.cpp b/src/geode/model/mixin/core/relationships.cpp index 56b9803c1..912d2d05e 100644 --- a/src/geode/model/mixin/core/relationships.cpp +++ b/src/geode/model/mixin/core/relationships.cpp @@ -23,6 +23,7 @@ #include +#include #include #include @@ -180,7 +181,11 @@ namespace geode void save( std::string_view directory ) const { const auto filename = absl::StrCat( directory, "/relationships" ); - std::ofstream file{ filename, std::ofstream::binary }; + std::ofstream file{ std::filesystem::u8path( filename ), + std::ofstream::binary }; + OpenGeodeModelException::check_exception( file.is_open(), nullptr, + OpenGeodeException::TYPE::internal, + "[Relationships::save] Cannot open file: ", filename ); TContext context{}; BitseryExtensions::register_serialize_pcontext( std::get< 0 >( context ) ); @@ -188,7 +193,7 @@ namespace geode archive.object( *this ); archive.adapter().flush(); OpenGeodeModelException::check_exception( - std::get< 1 >( context ).isValid(), nullptr, + file.good() && std::get< 1 >( context ).isValid(), nullptr, OpenGeodeException::TYPE::internal, "[Relationships::save] Error while writing file: ", filename ); } @@ -196,7 +201,8 @@ namespace geode void load( std::string_view directory ) { const auto filename = absl::StrCat( directory, "/relationships" ); - std::ifstream file{ filename, std::ifstream::binary }; + std::ifstream file{ std::filesystem::u8path( filename ), + std::ifstream::binary }; TContext context{}; BitseryExtensions::register_deserialize_pcontext( std::get< 0 >( context ) ); @@ -229,7 +235,7 @@ namespace geode std::move( graph ) ); impl.initialize_attributes(); impl.initialize_relation_attribute(); - impl.delete_isolated_vertices(); + impl.repair_after_load(); }, []( Archive& archive, Impl& impl ) { impl.graph_ = std::make_unique< OpenGeodeGraph >( @@ -241,7 +247,7 @@ namespace geode bitsery::ext::StdSmartPtr{} ); archive.ext( impl.ids_, bitsery::ext::StdSmartPtr{} ); - impl.delete_isolated_vertices(); + impl.repair_after_load(); }, []( Archive& archive, Impl& impl ) { archive.ext( diff --git a/src/geode/model/mixin/core/surface_collections.cpp b/src/geode/model/mixin/core/surface_collections.cpp index ffbee5469..8009639b9 100644 --- a/src/geode/model/mixin/core/surface_collections.cpp +++ b/src/geode/model/mixin/core/surface_collections.cpp @@ -102,7 +102,7 @@ namespace geode std::string_view directory, SurfaceCollectionsBuilderKey /*key*/ ) { impl_->load_components( - absl::StrCat( directory, "/surface_collections" ) ); + absl::StrCat( directory, "/surface_collections" ), false ); } template < index_t dimension > diff --git a/src/geode/model/mixin/core/surfaces.cpp b/src/geode/model/mixin/core/surfaces.cpp index f369e805a..efe88f792 100644 --- a/src/geode/model/mixin/core/surfaces.cpp +++ b/src/geode/model/mixin/core/surfaces.cpp @@ -26,21 +26,80 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include +#include +#include #include #include #include #include +namespace +{ + template < geode::index_t dimension > + geode::MeshType surface_type_from_file( std::string_view file ) + { + if( geode::TriangulatedSurfaceInputFactory< dimension >::has_creator( + geode::to_string( geode::extension_from_filename( file ) ) ) ) + { + return geode::TriangulatedSurface< dimension >::type_name_static(); + } + if constexpr( dimension == 2 ) + { + if( geode::RegularGridInputFactory< 2 >::has_creator( + geode::to_string( + geode::extension_from_filename( file ) ) ) ) + { + return geode::RegularGrid< 2 >::type_name_static(); + } + } + return geode::PolygonalSurface< dimension >::type_name_static(); + } + + template < geode::index_t dimension > + bool save_grid_surface( + const geode::SurfaceMesh< dimension >& mesh, std::string_view file ) + { + if constexpr( dimension == 2 ) + { + if( const auto* grid = + dynamic_cast< const geode::RegularGrid< 2 >* >( &mesh ) ) + { + geode::save_regular_grid( *grid, file ); + return true; + } + } + return false; + } + + template < geode::index_t dimension > + std::unique_ptr< geode::SurfaceMesh< dimension > > load_grid_surface( + const geode::MeshType& type, + const geode::MeshImpl& impl, + std::string_view file ) + { + if constexpr( dimension == 2 ) + { + if( type == geode::RegularGrid< 2 >::type_name_static() ) + { + return geode::load_regular_grid< 2 >( impl, file ); + } + } + return nullptr; + } +} // namespace + namespace geode { template < index_t dimension > @@ -120,7 +179,7 @@ namespace geode { save_polygonal_surface( *polygonal, file ); } - else + else if( !save_grid_surface< dimension >( mesh, file ) ) { throw OpenGeodeModelException{ nullptr, OpenGeodeException::TYPE::data, @@ -146,18 +205,37 @@ namespace geode for( auto& surface : modifiable_surfaces( key ) ) { tasks[count++] = async::spawn( [&surface, &mapping] { - const auto file = mapping.at( surface.id().string() ); - if( MeshFactory::type( surface.mesh_type() ) + const auto file = mapping.find( surface.id().string() ); + if( file == mapping.end() ) + { + surface.set_mesh( + detail::empty_mesh_of_missing_file( surface, + PolygonalSurface< dimension >::type_name_static() ), + typename Surface< dimension >::SurfacesKey{} ); + return; + } + const auto impl = + detail::loadable_mesh_impl( surface.mesh_type(), + surface_type_from_file< dimension >( file->second ), + file->second ); + const auto& type = MeshFactory::type( impl ); + if( type == TriangulatedSurface< dimension >::type_name_static() ) { surface.set_mesh( load_triangulated_surface< dimension >( - surface.mesh_type(), file ), + impl, file->second ), + typename Surface< dimension >::SurfacesKey{} ); + } + else if( auto grid = load_grid_surface< dimension >( + type, impl, file->second ) ) + { + surface.set_mesh( std::move( grid ), typename Surface< dimension >::SurfacesKey{} ); } else { surface.set_mesh( load_polygonal_surface< dimension >( - surface.mesh_type(), file ), + impl, file->second ), typename Surface< dimension >::SurfacesKey{} ); } } ); diff --git a/src/geode/model/mixin/core/vertex_identifier.cpp b/src/geode/model/mixin/core/vertex_identifier.cpp index b10bc005c..807b83d0e 100644 --- a/src/geode/model/mixin/core/vertex_identifier.cpp +++ b/src/geode/model/mixin/core/vertex_identifier.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -460,10 +461,46 @@ namespace geode return old2new; } + void repair_component_vertices() + { + // Read-only parallel scan, nothing to repair in a valid model + std::vector< uint8_t > to_repair( nb_unique_vertices(), 0 ); + async::parallel_for( + async::irange( index_t{ 0 }, nb_unique_vertices() ), + [this, &to_repair]( index_t unique_vertex ) { + for( const auto& vertex : + component_vertices_->value( unique_vertex ) ) + { + if( !is_linked_to_unique_vertex( + vertex, unique_vertex ) ) + { + to_repair[unique_vertex] = 1; + return; + } + } + } ); + // Sequential repair: a component vertex may be listed in several + // unique vertices, the first one keeps it + ComponentVerticesRepair repair; + for( const auto unique_vertex : Range{ nb_unique_vertices() } ) + { + if( to_repair[unique_vertex] != 0 ) + { + repair_unique_vertex( unique_vertex, repair ); + nb_component_mesh_vertices_up_to_date_ = false; + } + } + repair.log(); + } + void save( std::string_view directory ) const { const auto filename = absl::StrCat( directory, "/vertices" ); - std::ofstream file{ filename, std::ofstream::binary }; + std::ofstream file{ std::filesystem::u8path( filename ), + std::ofstream::binary }; + OpenGeodeModelException::check_exception( file.is_open(), nullptr, + OpenGeodeException::TYPE::internal, + "[VertexIdentifier::save] Cannot open file: ", filename ); TContext context{}; BitseryExtensions::register_serialize_pcontext( std::get< 0 >( context ) ); @@ -471,7 +508,7 @@ namespace geode archive.object( *this ); archive.adapter().flush(); OpenGeodeModelException::check_exception( - std::get< 1 >( context ).isValid(), nullptr, + file.good() && std::get< 1 >( context ).isValid(), nullptr, OpenGeodeException::TYPE::internal, "[VertexIdentifier::save] Error while writing file: ", filename ); @@ -480,7 +517,8 @@ namespace geode void load( std::string_view directory ) { const auto filename = absl::StrCat( directory, "/vertices" ); - std::ifstream file{ filename, std::ifstream::binary }; + std::ifstream file{ std::filesystem::u8path( filename ), + std::ifstream::binary }; TContext context{}; BitseryExtensions::register_deserialize_pcontext( std::get< 0 >( context ) ); @@ -676,16 +714,31 @@ namespace geode const auto unique_vertices_ids = vertex_attribute_manager.attribute_ids_matching_name( UNIQUE_VERTICES_NAME ); - OpenGeodeModelException::check_exception( - unique_vertices_ids.has_value(), nullptr, - OpenGeodeException::TYPE::data, - "[VertexIdentifier::load_component] Unique vertices " - "attribute not found." ); - OpenGeodeModelException::check_exception( - unique_vertices_ids.value().size() == 1, nullptr, - OpenGeodeException::TYPE::data, - "[VertexIdentifier::load_component] Unique vertices " - "attribute is not unique." ); + if( !unique_vertices_ids.has_value() ) + { + Logger::warning( + "[VertexIdentifier::load_component] Unique vertices " + "attribute not found, it will be restored from the " + "unique vertices" ); + AttributeProperties attribute_properties; + attribute_properties.assignable = false; + attribute_properties.interpolable = false; + attribute_properties.transferable = false; + AttributeValues< index_t > unique_vertex_attribute_values; + unique_vertex_attribute_values.default_value = NO_ID; + unique_vertex_attribute_values.no_value = NO_ID; + vertex_attribute_manager + .template create_attribute< VariableAttribute, index_t >( + UNIQUE_VERTICES_NAME, unique_vertex_id_, + unique_vertex_attribute_values, attribute_properties ); + return; + } + if( unique_vertices_ids.value().size() != 1 ) + { + Logger::warning( + "[VertexIdentifier::load_component] Unique vertices " + "attribute is not unique, using the first one" ); + } const auto old_attribute = vertex_attribute_manager .template find_attribute< VariableAttribute, index_t >( @@ -704,11 +757,110 @@ namespace geode new_attribute->set_value( vertex, old_attribute->value( vertex ) ); } - vertex_attribute_manager.delete_attribute( - unique_vertices_ids.value().front() ); + for( const auto& old_id : unique_vertices_ids.value() ) + { + vertex_attribute_manager.delete_attribute( old_id ); + } } private: + struct ComponentVerticesRepair + { + void log() const + { + if( nb_unknown_components > 0 ) + { + Logger::warning( "[VertexIdentifier::load] Removed ", + nb_unknown_components, + " component vertices of unknown components" ); + } + if( nb_invalid_vertices > 0 ) + { + Logger::warning( "[VertexIdentifier::load] Removed ", + nb_invalid_vertices, + " component vertices with invalid vertex index" ); + } + if( nb_inconsistent_vertices > 0 ) + { + Logger::warning( "[VertexIdentifier::load] Removed ", + nb_inconsistent_vertices, + " component vertices linked to another unique " + "vertex" ); + } + if( nb_restored_vertices > 0 ) + { + Logger::warning( "[VertexIdentifier::load] Restored ", + nb_restored_vertices, + " component vertex links to their unique vertex" ); + } + } + + index_t nb_unknown_components{ 0 }; + index_t nb_invalid_vertices{ 0 }; + index_t nb_inconsistent_vertices{ 0 }; + index_t nb_restored_vertices{ 0 }; + }; + + [[nodiscard]] bool is_linked_to_unique_vertex( + const ComponentMeshVertex& vertex, index_t unique_vertex ) const + { + const auto it = + vertex2unique_vertex_.find( vertex.component_id.id ); + if( it == vertex2unique_vertex_.end() ) + { + return false; + } + const auto& attribute = *it->second.attribute; + return vertex.vertex < attribute.size() + && attribute.value( vertex.vertex ) == unique_vertex; + } + + void repair_unique_vertex( + index_t unique_vertex, ComponentVerticesRepair& repair ) + { + const auto& vertices = component_vertices_->value( unique_vertex ); + std::vector< bool > to_keep( vertices.size(), true ); + bool update{ false }; + for( const auto v : Indices{ vertices } ) + { + const auto& vertex = vertices[v]; + const auto it = + vertex2unique_vertex_.find( vertex.component_id.id ); + if( it == vertex2unique_vertex_.end() ) + { + to_keep[v] = false; + update = true; + repair.nb_unknown_components++; + continue; + } + auto& attribute = *it->second.attribute; + if( vertex.vertex >= attribute.size() ) + { + to_keep[v] = false; + update = true; + repair.nb_invalid_vertices++; + continue; + } + const auto value = attribute.value( vertex.vertex ); + if( value == NO_ID ) + { + attribute.set_value( vertex.vertex, unique_vertex ); + repair.nb_restored_vertices++; + } + else if( value != unique_vertex ) + { + to_keep[v] = false; + update = true; + repair.nb_inconsistent_vertices++; + } + } + if( update ) + { + component_vertices_->set_value( unique_vertex, + extract_vector_elements( to_keep, vertices ) ); + } + } + OpenGeodeVertexSet unique_vertices_; std::shared_ptr< VariableAttribute< std::vector< ComponentMeshVertex > > > @@ -845,6 +997,11 @@ namespace geode return impl_->delete_isolated_vertices(); } + void VertexIdentifier::repair_component_vertices( BuilderKey /*key*/ ) + { + impl_->repair_component_vertices(); + } + template void opengeode_model_api VertexIdentifier::load_mesh_component( const Corner2D&, BuilderKey /*key*/ ); template void opengeode_model_api VertexIdentifier::load_mesh_component( diff --git a/src/geode/model/representation/io/brep_input.cpp b/src/geode/model/representation/io/brep_input.cpp index 9da629b3b..16c0300c2 100644 --- a/src/geode/model/representation/io/brep_input.cpp +++ b/src/geode/model/representation/io/brep_input.cpp @@ -70,6 +70,13 @@ namespace geode OpenGeodeException::TYPE::data, "Cannot load BRep from file: ", filename, ": ", e.what() }; } + catch( const std::exception& e ) + { + Logger::error( e.what() ); + throw OpenGeodeModelException{ nullptr, + OpenGeodeException::TYPE::internal, + "Cannot load BRep from file: ", filename }; + } } AdditionalFiles brep_additional_files( std::string_view filename ) diff --git a/src/geode/model/representation/io/brep_time_series_input.cpp b/src/geode/model/representation/io/brep_time_series_input.cpp index 5238a07e0..3cc4a70ab 100644 --- a/src/geode/model/representation/io/brep_time_series_input.cpp +++ b/src/geode/model/representation/io/brep_time_series_input.cpp @@ -95,6 +95,13 @@ namespace geode "Cannot load BRep time series from file: ", filename, ": ", e.what() }; } + catch( const std::exception& e ) + { + Logger::error( e.what() ); + throw OpenGeodeModelException{ nullptr, + OpenGeodeException::TYPE::internal, + "Cannot load BRep time series from file: ", filename }; + } } AdditionalFiles brep_time_series_additional_files( diff --git a/src/geode/model/representation/io/geode/geode_brep_output.cpp b/src/geode/model/representation/io/geode/geode_brep_output.cpp index f281a60dd..699136166 100644 --- a/src/geode/model/representation/io/geode/geode_brep_output.cpp +++ b/src/geode/model/representation/io/geode/geode_brep_output.cpp @@ -49,8 +49,7 @@ namespace geode void OpenGeodeBRepOutput::save_brep_files( const BRep& brep, std::string_view directory ) const { - const auto level = Logger::level(); - Logger::set_level( Logger::LEVEL::warning ); + const detail::ScopedLoggerLevel scoped_level{ Logger::LEVEL::warning }; async::parallel_invoke( [&directory, &brep] { brep.save_identifier( directory ); @@ -91,7 +90,6 @@ namespace geode [&directory, &brep] { brep.save_block_collections( directory ); } ); - Logger::set_level( level ); } std::vector< std::string > OpenGeodeBRepOutput::write( diff --git a/src/geode/model/representation/io/geode/geode_section_output.cpp b/src/geode/model/representation/io/geode/geode_section_output.cpp index 62a87da42..bc8d90d19 100644 --- a/src/geode/model/representation/io/geode/geode_section_output.cpp +++ b/src/geode/model/representation/io/geode/geode_section_output.cpp @@ -39,8 +39,7 @@ namespace geode void OpenGeodeSectionOutput::save_section_files( const Section& section, std::string_view directory ) const { - const auto level = Logger::level(); - Logger::set_level( Logger::LEVEL::warning ); + const detail::ScopedLoggerLevel scoped_level{ Logger::LEVEL::warning }; async::parallel_invoke( [&directory, §ion] { section.save_identifier( directory ); @@ -62,7 +61,6 @@ namespace geode section.save_line_collections( directory ); section.save_surface_collections( directory ); } ); - Logger::set_level( level ); } void OpenGeodeSectionOutput::archive_section_files( diff --git a/src/geode/model/representation/io/section_input.cpp b/src/geode/model/representation/io/section_input.cpp index 81c7515e5..41591731a 100644 --- a/src/geode/model/representation/io/section_input.cpp +++ b/src/geode/model/representation/io/section_input.cpp @@ -69,6 +69,13 @@ namespace geode OpenGeodeException::TYPE::internal, "Cannot load Section from file: ", filename, ": ", e.what() }; } + catch( const std::exception& e ) + { + Logger::error( e.what() ); + throw OpenGeodeModelException{ nullptr, + OpenGeodeException::TYPE::internal, + "Cannot load Section from file: ", filename }; + } } AdditionalFiles section_additional_files( std::string_view filename ) diff --git a/tests/model/CMakeLists.txt b/tests/model/CMakeLists.txt index 196061a1b..b07483ff9 100644 --- a/tests/model/CMakeLists.txt +++ b/tests/model/CMakeLists.txt @@ -101,6 +101,14 @@ add_geode_test( ${PROJECT_NAME}::geometry ${PROJECT_NAME}::model ) +add_geode_test( + SOURCE "test-model-corrupted-files.cpp" + DEPENDENCIES + ${PROJECT_NAME}::basic + ${PROJECT_NAME}::geometry + ${PROJECT_NAME}::mesh + ${PROJECT_NAME}::model +) add_geode_test( SOURCE "test-model-mapping.cpp" DEPENDENCIES diff --git a/tests/model/test-model-corrupted-files.cpp b/tests/model/test-model-corrupted-files.cpp new file mode 100644 index 000000000..701ae9e05 --- /dev/null +++ b/tests/model/test-model-corrupted-files.cpp @@ -0,0 +1,885 @@ +/* + * Copyright (c) 2019 - 2026 Geode-solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + /* Records warnings to check that repairs are reported */ + class WarningRecorder : public geode::LoggerClient + { + public: + static void start() + { + static const auto registered = [] { + geode::LoggerManager::register_client( + std::make_unique< WarningRecorder >() ); + return true; + }(); + geode_unused( registered ); + std::lock_guard< std::mutex > lock{ mutex() }; + warnings().clear(); + } + + static bool has_warning( std::string_view text ) + { + std::lock_guard< std::mutex > lock{ mutex() }; + for( const auto& warning : warnings() ) + { + if( absl::StrContains( warning, text ) ) + { + return true; + } + } + return false; + } + + void trace( const std::string& /*message*/ ) override {} + void debug( const std::string& /*message*/ ) override {} + void info( const std::string& /*message*/ ) override {} + void warning( const std::string& message ) override + { + std::lock_guard< std::mutex > lock{ mutex() }; + warnings().push_back( message ); + } + void error( const std::string& /*message*/ ) override {} + void critical( const std::string& /*message*/ ) override {} + + private: + static std::mutex& mutex() + { + static std::mutex mutex; + return mutex; + } + + static std::vector< std::string >& warnings() + { + static std::vector< std::string > warnings; + return warnings; + } + }; + + void check_warning( std::string_view text ) + { + geode::OpenGeodeModelException::test( + WarningRecorder::has_warning( text ), + "[Test] Missing warning: ", text ); + } + + /* Files of a model archive: file name -> file content */ + using ArchiveFiles = absl::btree_map< std::string, std::string >; + + std::string read_file( const std::filesystem::path& path ) + { + std::ifstream file{ path, std::ifstream::binary }; + return { std::istreambuf_iterator< char >{ file }, + std::istreambuf_iterator< char >{} }; + } + + ArchiveFiles extract_archive( std::string_view filename ) + { + const geode::UnzipFile unzip{ filename, geode::uuid{}.string() }; + unzip.extract_all(); + ArchiveFiles files; + for( const auto& file : std::filesystem::directory_iterator( + std::filesystem::u8path( unzip.directory() ) ) ) + { + files.emplace( + file.path().filename().u8string(), read_file( file.path() ) ); + } + return files; + } + + void write_archive( const ArchiveFiles& files, std::string_view filename ) + { + geode::ZipFile zip{ filename, geode::uuid{}.string() }; + for( const auto& [name, content] : files ) + { + const auto path = absl::StrCat( zip.directory(), "/", name ); + { + std::ofstream file{ std::filesystem::u8path( path ), + std::ofstream::binary }; + file << content; + } + zip.archive_file( path ); + } + zip.finalize(); + } + + /* Serialized uuid: Growable version 2 followed by the 16 bytes */ + std::string serialized_uuid( const geode::uuid& id ) + { + std::string bytes{ static_cast< char >( 2 ) }; + const auto hex = id.string(); + for( std::size_t i = 0; i < hex.size(); i++ ) + { + if( hex[i] == '-' ) + { + continue; + } + bytes.push_back( static_cast< char >( + std::stoi( hex.substr( i, 2 ), {}, 16 ) ) ); + i++; + } + return bytes; + } + + std::string serialized_u32( std::uint32_t value ) + { + std::string bytes; + for( const auto i : geode::LRange{ 4 } ) + { + bytes.push_back( static_cast< char >( value >> ( 8 * i ) ) ); + } + return bytes; + } + + void replace_unique( std::string& content, + const std::string& from, + const std::string& to, + std::string_view what ) + { + const auto position = content.find( from ); + geode::OpenGeodeModelException::test( + position != std::string::npos, "[Test] ", what, " not found" ); + geode::OpenGeodeModelException::test( + content.find( from, position + 1 ) == std::string::npos, "[Test] ", + what, " is not unique" ); + content.replace( position, from.size(), to ); + } + + geode::BRep saved_brep( std::string_view filename ) + { + auto brep = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "box_brep.og_brep" ) ); + geode::save_brep( brep, filename ); + return brep; + } + + /* + * The relationships file stores a uuid -> graph vertex map duplicating + * the ids stored on the graph vertices. A corrupted map must not change + * the loaded relations. + */ + void test_inconsistent_relationships_map() + { + const auto brep = saved_brep( "relationships.og_brep" ); + const auto& id0 = brep.component_with_relation( 0 ).id; + geode::index_t other{ geode::NO_ID }; + for( const auto v : + geode::Range{ 1, brep.nb_components_with_relations() } ) + { + if( brep.nb_relations( brep.component_with_relation( v ).id ) + != brep.nb_relations( id0 ) ) + { + other = v; + break; + } + } + geode::OpenGeodeModelException::test( + other != geode::NO_ID, "[Test] No component to swap with" ); + const auto& id1 = brep.component_with_relation( other ).id; + auto files = extract_archive( "relationships.og_brep" ); + auto& content = files.at( "relationships" ); + replace_unique( content, + absl::StrCat( serialized_uuid( id0 ), serialized_u32( 0 ) ), + absl::StrCat( serialized_uuid( id0 ), serialized_u32( other ) ), + "First uuid mapping" ); + replace_unique( content, + absl::StrCat( serialized_uuid( id1 ), serialized_u32( other ) ), + absl::StrCat( serialized_uuid( id1 ), serialized_u32( 0 ) ), + "Second uuid mapping" ); + write_archive( files, "corrupted_relationships.og_brep" ); + + const auto loaded = + geode::load_brep( "corrupted_relationships.og_brep" ); + geode::OpenGeodeModelException::test( + loaded.nb_components_with_relations() + == brep.nb_components_with_relations(), + "[Test] Wrong number of components with relations" ); + for( const auto v : + geode::Range{ brep.nb_components_with_relations() } ) + { + const auto& id = brep.component_with_relation( v ).id; + geode::OpenGeodeModelException::test( + loaded.nb_relations( id ) == brep.nb_relations( id ), + "[Test] Wrong number of relations for ", id.string() ); + absl::flat_hash_set< geode::uuid > related; + for( const auto& component : brep.relations( id ) ) + { + related.insert( component.id ); + } + for( const auto& component : loaded.relations( id ) ) + { + geode::OpenGeodeModelException::test( + related.contains( component.id ), + "[Test] Wrong relation for ", id.string() ); + } + } + } + + bool has_mesh_component( const geode::BRep& brep, const geode::uuid& id ) + { + return brep.has_corner( id ) || brep.has_line( id ) + || brep.has_surface( id ) || brep.has_block( id ); + } + + std::string mesh_file_name( + const ArchiveFiles& files, std::string_view prefix ) + { + for( const auto& file : files ) + { + if( absl::StartsWith( file.first, prefix ) + && file.first.size() > prefix.size() + 36 ) + { + return file.first; + } + } + throw geode::OpenGeodeModelException{ nullptr, + geode::OpenGeodeException::TYPE::internal, "[Test] No ", prefix, + " mesh file" }; + } + + void check_same_unique_vertices( + const geode::BRep& brep, const geode::BRep& loaded ) + { + geode::OpenGeodeModelException::test( + loaded.nb_unique_vertices() == brep.nb_unique_vertices(), + "[Test] Wrong number of unique vertices" ); + for( const auto unique_vertex : + geode::Range{ brep.nb_unique_vertices() } ) + { + const auto& vertices = + brep.component_mesh_vertices( unique_vertex ); + geode::OpenGeodeModelException::test( + loaded.component_mesh_vertices( unique_vertex ).size() + == vertices.size(), + "[Test] Wrong number of component vertices for unique vertex ", + unique_vertex ); + for( const auto& vertex : vertices ) + { + geode::OpenGeodeModelException::test( + loaded.unique_vertex( vertex ) == unique_vertex, + "[Test] Wrong unique vertex for component vertex" ); + } + } + } + + /* + * Mesh components have a "unique vertices" attribute linking their + * vertices to the model unique vertices. When this attribute is missing, + * it is restored from the unique vertices. + */ + void test_missing_unique_vertices_attribute() + { + const auto brep = saved_brep( "unique_vertices.og_brep" ); + auto files = extract_archive( "unique_vertices.og_brep" ); + replace_unique( files.at( "vertices" ), + serialized_uuid( brep.unique_vertex_attribute_id() ), + serialized_uuid( geode::uuid{} ), "Unique vertex attribute id" ); + for( const auto prefix : { "Corner", "Line", "Surface", "Block" } ) + { + replace_unique( files.at( mesh_file_name( files, prefix ) ), + "unique vertices", "unique_vertices", + "Unique vertices attribute name" ); + } + write_archive( files, "corrupted_unique_vertices.og_brep" ); + + const auto loaded = + geode::load_brep( "corrupted_unique_vertices.og_brep" ); + check_same_unique_vertices( brep, loaded ); + } + + /* + * Unique vertices referencing a component that does not exist in the + * model are removed. + */ + void test_unknown_component_vertices() + { + saved_brep( "with_corner.og_brep" ); + auto without_corner = geode::load_brep( "with_corner.og_brep" ); + const auto& corner = *without_corner.corners().begin(); + const auto corner_id = corner.id(); + geode::BRepBuilder{ without_corner }.remove_corner( corner ); + geode::save_brep( without_corner, "without_corner.og_brep" ); + auto files = extract_archive( "without_corner.og_brep" ); + files.at( "vertices" ) = + extract_archive( "with_corner.og_brep" ).at( "vertices" ); + write_archive( files, "unknown_corner_vertices.og_brep" ); + + const auto loaded = + geode::load_brep( "unknown_corner_vertices.og_brep" ); + geode::OpenGeodeModelException::test( !loaded.has_corner( corner_id ), + "[Test] Removed corner should not be loaded" ); + for( const auto unique_vertex : + geode::Range{ loaded.nb_unique_vertices() } ) + { + for( const auto& vertex : + loaded.component_mesh_vertices( unique_vertex ) ) + { + geode::OpenGeodeModelException::test( + has_mesh_component( loaded, vertex.component_id.id ), + "[Test] Unique vertex ", unique_vertex, + " references an unknown component" ); + geode::OpenGeodeModelException::test( + loaded.unique_vertex( vertex ) == unique_vertex, + "[Test] Wrong unique vertex for component vertex" ); + } + } + } + + /* + * The components file of each component type is always saved. A missing + * file is reported. + */ + void test_missing_components_file() + { + saved_brep( "missing_components.og_brep" ); + auto files = extract_archive( "missing_components.og_brep" ); + files.erase( "lines" ); + write_archive( files, "missing_components_file.og_brep" ); + + WarningRecorder::start(); + const auto loaded = + geode::load_brep( "missing_components_file.og_brep" ); + check_warning( "Missing file" ); + geode::OpenGeodeModelException::test( + loaded.nb_lines() == 0, "[Test] No line should be loaded" ); + } + + /* + * Components are stored in a map: uuid -> component. A component stored + * under another uuid than its own is stored back under its own uuid. + */ + void test_wrong_component_key() + { + const auto brep = saved_brep( "component_key.og_brep" ); + const auto& corner_id = ( *brep.corners().begin() ).id(); + auto files = extract_archive( "component_key.og_brep" ); + auto& content = files.at( "corners" ); + const auto key = serialized_uuid( corner_id ); + const auto position = content.find( key ); + geode::OpenGeodeModelException::test( + position != std::string::npos, "[Test] Corner key not found" ); + content.replace( + position, key.size(), serialized_uuid( geode::uuid{} ) ); + geode::OpenGeodeModelException::test( + content.find( key ) != std::string::npos, + "[Test] Corner id not found" ); + write_archive( files, "wrong_component_key.og_brep" ); + + WarningRecorder::start(); + const auto loaded = geode::load_brep( "wrong_component_key.og_brep" ); + check_warning( "has id" ); + geode::OpenGeodeModelException::test( + loaded.nb_corners() == brep.nb_corners(), + "[Test] Wrong number of corners" ); + geode::OpenGeodeModelException::test( loaded.has_corner( corner_id ), + "[Test] Corner should be stored under its id" ); + geode::OpenGeodeModelException::test( + loaded.corner( corner_id ).mesh().nb_vertices() + == brep.corner( corner_id ).mesh().nb_vertices(), + "[Test] Corner mesh should be loaded" ); + } + + /* + * Relations referencing a component that does not exist in the model are + * removed. + */ + void test_unknown_component_relations() + { + saved_brep( "relations_with_corner.og_brep" ); + auto without_corner = + geode::load_brep( "relations_with_corner.og_brep" ); + const auto& corner = *without_corner.corners().begin(); + const auto corner_id = corner.id(); + geode::BRepBuilder{ without_corner }.remove_corner( corner ); + geode::save_brep( without_corner, "relations_without_corner.og_brep" ); + auto files = extract_archive( "relations_without_corner.og_brep" ); + files.at( "relationships" ) = + extract_archive( "relations_with_corner.og_brep" ) + .at( "relationships" ); + write_archive( files, "unknown_corner_relations.og_brep" ); + + WarningRecorder::start(); + const auto loaded = + geode::load_brep( "unknown_corner_relations.og_brep" ); + check_warning( "Removing relations of missing" ); + geode::OpenGeodeModelException::test( + loaded.nb_relations( corner_id ) == 0, + "[Test] Removed corner should not have relations" ); + for( const auto vertex : + geode::Range{ loaded.nb_components_with_relations() } ) + { + const auto& component = loaded.component_with_relation( vertex ); + geode::OpenGeodeModelException::test( component.id != corner_id, + "[Test] Removed corner should not be in relationships" ); + } + for( const auto& line : loaded.lines() ) + { + for( const auto& boundary : loaded.boundaries( line ) ) + { + geode::OpenGeodeModelException::test( + boundary.id() != corner_id, + "[Test] Removed corner should not be a boundary" ); + } + } + } + + void replace_all( std::string& content, + std::string_view from, + std::string_view to, + std::string_view what ) + { + geode::OpenGeodeModelException::test( + content.find( from ) != std::string::npos, "[Test] ", what, + " not found" ); + content = absl::StrReplaceAll( content, { { from, to } } ); + } + + /* + * A component whose mesh file is missing gets an empty mesh. + */ + void test_missing_mesh_file() + { + const auto brep = saved_brep( "missing_mesh.og_brep" ); + auto files = extract_archive( "missing_mesh.og_brep" ); + const auto surface_file = mesh_file_name( files, "Surface" ); + const auto line_file = mesh_file_name( files, "Line" ); + files.erase( surface_file ); + files.erase( line_file ); + write_archive( files, "missing_mesh_file.og_brep" ); + + WarningRecorder::start(); + const auto loaded = geode::load_brep( "missing_mesh_file.og_brep" ); + check_warning( "Missing mesh file" ); + geode::OpenGeodeModelException::test( + loaded.nb_surfaces() == brep.nb_surfaces(), + "[Test] Wrong number of surfaces" ); + geode::index_t nb_empty_surfaces{ 0 }; + for( const auto& surface : loaded.surfaces() ) + { + const auto& mesh = surface.mesh(); + geode::OpenGeodeModelException::test( + surface.mesh_type() == mesh.impl_name(), + "[Test] Wrong surface mesh type" ); + if( mesh.nb_vertices() == 0 ) + { + nb_empty_surfaces++; + continue; + } + geode::OpenGeodeModelException::test( + mesh.nb_polygons() + == brep.surface( surface.id() ).mesh().nb_polygons(), + "[Test] Wrong surface mesh" ); + } + geode::OpenGeodeModelException::test( + nb_empty_surfaces == 1, "[Test] One surface should be empty" ); + for( const auto unique_vertex : + geode::Range{ loaded.nb_unique_vertices() } ) + { + for( const auto& vertex : + loaded.component_mesh_vertices( unique_vertex ) ) + { + geode::OpenGeodeModelException::test( + loaded.unique_vertex( vertex ) == unique_vertex, + "[Test] Wrong unique vertex for component vertex" ); + } + } + } + + /* + * A component whose mesh implementation is not registered is loaded with + * the default implementation of the mesh type matching its file. + */ + void test_unknown_mesh_impl() + { + const auto brep = saved_brep( "unknown_impl.og_brep" ); + auto files = extract_archive( "unknown_impl.og_brep" ); + replace_all( files.at( "surfaces" ), "OpenGeodeTriangulatedSurface3D", + "UnknownImplTriangulatedSurface", "Surface mesh type" ); + write_archive( files, "unknown_mesh_impl.og_brep" ); + + WarningRecorder::start(); + const auto loaded = geode::load_brep( "unknown_mesh_impl.og_brep" ); + check_warning( "Unknown mesh implementation" ); + for( const auto& surface : loaded.surfaces() ) + { + geode::OpenGeodeModelException::test( + surface.mesh_type().get() == "OpenGeodeTriangulatedSurface3D", + "[Test] Wrong surface mesh type" ); + geode::OpenGeodeModelException::test( + surface.mesh().nb_polygons() + == brep.surface( surface.id() ).mesh().nb_polygons(), + "[Test] Wrong surface mesh" ); + } + } + + /* + * Surfaces and blocks may have a RegularGrid mesh. + */ + void test_regular_grid_meshes() + { + geode::Section section; + geode::SectionBuilder section_builder{ section }; + const auto& surface_id = section_builder.add_surface(); + auto surface_grid = geode::RegularGrid2D::create(); + geode::RegularGridBuilder2D::create( *surface_grid ) + ->initialize_grid( geode::Point2D{ { 0., 0. } }, { 3, 4 }, 1 ); + section_builder.update_surface_mesh( + section.surface( surface_id ), std::move( surface_grid ) ); + geode::save_section( section, "grid.og_sctn" ); + const auto loaded_section = geode::load_section( "grid.og_sctn" ); + const auto& loaded_surface = loaded_section.surface( surface_id ); + geode::OpenGeodeModelException::test( + geode::MeshFactory::type( loaded_surface.mesh_type() ) + == geode::RegularGrid2D::type_name_static(), + "[Test] Surface mesh should be a RegularGrid" ); + geode::OpenGeodeModelException::test( + loaded_surface.mesh().nb_polygons() == 12, + "[Test] Wrong number of grid surface cells" ); + + geode::BRep brep; + geode::BRepBuilder brep_builder{ brep }; + const auto& block_id = brep_builder.add_block(); + auto block_grid = geode::RegularGrid3D::create(); + geode::RegularGridBuilder3D::create( *block_grid ) + ->initialize_grid( + geode::Point3D{ { 0., 0., 0. } }, { 2, 3, 4 }, 1 ); + brep_builder.update_block_mesh( + brep.block( block_id ), std::move( block_grid ) ); + geode::save_brep( brep, "grid.og_brep" ); + const auto loaded_brep = geode::load_brep( "grid.og_brep" ); + const auto& loaded_block = loaded_brep.block( block_id ); + geode::OpenGeodeModelException::test( + geode::MeshFactory::type( loaded_block.mesh_type() ) + == geode::RegularGrid3D::type_name_static(), + "[Test] Block mesh should be a RegularGrid" ); + geode::OpenGeodeModelException::test( + loaded_block.mesh().nb_polyhedra() == 24, + "[Test] Wrong number of grid block cells" ); + } + + template < typename Saver > + void check_save_failure( std::string_view what, const Saver& saver ) + { + try + { + saver(); + } + catch( const geode::OpenGeodeException& exception ) + { + geode::Logger::info( "Saving ", what, + " in a missing directory throws as expected: ", + exception.what() ); + return; + } + throw geode::OpenGeodeModelException{ nullptr, + geode::OpenGeodeException::TYPE::internal, "[Test] Saving ", what, + " in a missing directory should throw" }; + } + + /* + * Saving model files in a directory that cannot be written throws. + */ + void test_save_in_missing_directory() + { + const auto brep = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "box_brep.og_brep" ) ); + const std::string directory{ "missing_directory/missing" }; + check_save_failure( "relationships", [&] { + brep.save_relationships( directory ); + } ); + check_save_failure( "unique vertices", [&] { + brep.save_unique_vertices( directory ); + } ); + check_save_failure( "physical properties", [&] { + brep.save_physical_properties( directory ); + } ); + check_save_failure( "model boundaries", [&] { + brep.save_model_boundaries( directory ); + } ); + } + + /* + * Model files are saved and loaded with UTF-8 paths. + */ + void test_utf8_path() + { + const auto brep = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "box_brep.og_brep" ) ); + const std::string filename{ "mod\xc3\xa8le_\xc3\xa9t\xc3\xa9.og_brep" }; + geode::save_brep( brep, filename ); + const auto loaded = geode::load_brep( filename ); + geode::OpenGeodeModelException::test( + loaded.nb_surfaces() == brep.nb_surfaces(), + "[Test] Wrong number of surfaces with UTF-8 path" ); + check_same_unique_vertices( brep, loaded ); + // Non-ASCII directories are not tested: mesh files, written in a + // temporary directory next to the archive, still use narrow paths. + } + + /* + * Names of old components are bounded by MAX_SERIALIZED_NAME_SIZE. + */ + void test_too_long_component_name() + { + auto files = extract_archive( + absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); + auto& content = files.at( "corners" ); + const std::string name{ "\x07unknown" }; + const auto position = content.find( name ); + geode::OpenGeodeModelException::test( + position != std::string::npos, "[Test] Component name not found" ); + // bitsery 4-byte size encoding: two high bytes, then a little endian + // 16-bit word + constexpr auto TOO_LONG = geode::MAX_SERIALIZED_NAME_SIZE + 1; + std::string too_long_name{ static_cast< char >( + 0xC0u | ( TOO_LONG >> 24 ) ), + static_cast< char >( TOO_LONG >> 16 ), + static_cast< char >( TOO_LONG ), + static_cast< char >( TOO_LONG >> 8 ) }; + too_long_name.append( TOO_LONG, 'a' ); + content.replace( position, name.size(), too_long_name ); + write_archive( files, "too_long_component_name.og_brep" ); + try + { + geode_unused( + geode::load_brep( "too_long_component_name.og_brep" ) ); + } + catch( const geode::OpenGeodeException& exception ) + { + geode::Logger::info( "Too long component name throws as expected: ", + exception.what() ); + return; + } + throw geode::OpenGeodeModelException{ nullptr, + geode::OpenGeodeException::TYPE::internal, + "[Test] Too long component name should not be loaded" }; + } + + /* + * The Logger level changed while loading a model is restored when the + * loading fails. + */ + void test_logger_level_restored() + { + saved_brep( "logger_level.og_brep" ); + auto files = extract_archive( "logger_level.og_brep" ); + auto& content = files.at( "relationships" ); + content.resize( content.size() / 2 ); + write_archive( files, "truncated_relationships.og_brep" ); + geode::Logger::set_level( geode::Logger::LEVEL::info ); + try + { + geode_unused( + geode::load_brep( "truncated_relationships.og_brep" ) ); + } + catch( const geode::OpenGeodeException& /*unused*/ ) + { + geode::OpenGeodeModelException::test( + geode::Logger::level() == geode::Logger::LEVEL::info, + "[Test] Logger level should be restored" ); + return; + } + throw geode::OpenGeodeModelException{ nullptr, + geode::OpenGeodeException::TYPE::internal, + "[Test] Truncated relationships should not be loaded" }; + } + + template < typename Base > + class ThrowingInput final : public Base + { + public: + explicit ThrowingInput( std::string_view filename ) : Base{ filename } + { + } + + typename Base::InputData read() override + { + throw std::runtime_error{ "[Test] Not an OpenGeodeException" }; + } + + geode::AdditionalFiles additional_files() const override + { + return {}; + } + + geode::index_t object_priority() const override + { + return 0; + } + + geode::Percentage is_loadable() const override + { + return geode::Percentage{ 1 }; + } + }; + + class ThrowingTimeSeriesInput final : public geode::BRepTimeSeriesInput + { + public: + explicit ThrowingTimeSeriesInput( std::string_view filename ) + : geode::BRepTimeSeriesInput{ filename } + { + } + + void read( geode::BRep& /*brep*/ ) override + { + throw std::runtime_error{ "[Test] Not an OpenGeodeException" }; + } + + geode::AdditionalFiles additional_files() const override + { + return {}; + } + + geode::Percentage is_loadable() const override + { + return geode::Percentage{ 1 }; + } + }; + + template < typename Function > + void check_opengeode_exception( + std::string_view what, const Function& function ) + { + try + { + function(); + } + catch( const geode::OpenGeodeException& exception ) + { + geode::Logger::info( + what, " throws as expected: ", exception.what() ); + return; + } + catch( const std::exception& exception ) + { + throw geode::OpenGeodeModelException{ nullptr, + geode::OpenGeodeException::TYPE::internal, "[Test] ", what, + " throws a std exception: ", exception.what() }; + } + throw geode::OpenGeodeModelException{ nullptr, + geode::OpenGeodeException::TYPE::internal, "[Test] ", what, + " should throw" }; + } + + /* + * Std exceptions raised while loading a model are turned into + * OpenGeodeException. + */ + void test_std_exceptions() + { + const std::string extension{ "throwing_input" }; + const auto filename = absl::StrCat( "std_exception.", extension ); + { + std::ofstream file{ filename }; + file << "content"; + } + geode::BRepInputFactory::register_creator< + ThrowingInput< geode::BRepInput > >( extension ); + geode::SectionInputFactory::register_creator< + ThrowingInput< geode::SectionInput > >( extension ); + geode::BRepTimeSeriesInputFactory::register_creator< + ThrowingTimeSeriesInput >( extension ); + check_opengeode_exception( "Loading BRep", [&filename] { + geode_unused( geode::load_brep( filename ) ); + } ); + check_opengeode_exception( "Loading Section", [&filename] { + geode_unused( geode::load_section( filename ) ); + } ); + check_opengeode_exception( "Loading BRep time series", [&filename] { + geode::BRep brep; + geode::load_brep_time_series( brep, filename ); + } ); + } +} // namespace + +void test() +{ + geode::OpenGeodeModelLibrary::initialize(); + test_inconsistent_relationships_map(); + test_missing_unique_vertices_attribute(); + test_unknown_component_vertices(); + test_missing_components_file(); + test_wrong_component_key(); + test_unknown_component_relations(); + test_missing_mesh_file(); + test_unknown_mesh_impl(); + test_regular_grid_meshes(); + test_save_in_missing_directory(); + test_utf8_path(); + test_too_long_component_name(); + test_logger_level_restored(); + test_std_exceptions(); +} + +OPENGEODE_TEST( "model-corrupted-files" )