From 0a2e7ce7f52ba71d54c62708c9ac5c76ca142231 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sun, 23 Aug 2026 17:48:10 +0800 Subject: [PATCH] Add FreeCAD WebAssembly runtime probes --- CMakeLists.txt | 11 + src/Mod/Part/App/TopoShapeCache.cpp | 10 +- src/Mod/Part/App/TopoShapeExpansion.cpp | 16 +- src/Mod/Sandbox/App/CMakeLists.txt | 1 + src/Mod/Sandbox/App/DocumentProtector.cpp | 66 +- src/Mod/Sandbox/App/DocumentProtector.h | 11 +- src/Mod/Sandbox/App/DocumentThread.cpp | 44 +- src/Mod/Sandbox/App/DocumentThread.h | 45 +- src/Mod/Sandbox/App/SandboxGlobal.h | 13 + .../FreeCADCoreProbe/CMakeLists.txt | 88 ++ .../FreeCADCoreProbe/PartBoxProbe.cpp | 54 ++ .../PartDesignRuntimeProbe.cpp | 549 +++++++++++ .../FreeCADCoreProbe/PointsRuntimeProbe.cpp | 50 + .../FreeCADCoreProbe/SandboxRuntimeProbe.cpp | 865 ++++++++++++++++++ .../FreeCADCoreProbe/SketcherProbe.cpp | 71 ++ .../FreeCADCoreProbe/part-python-smoke-r1.js | 2 + .../part-save-reopen-smoke.js | 2 + .../partdesign-persistence-smoke.js | 4 + .../partdesign-runtime-smoke.js | 107 +++ .../FreeCADCoreProbe/points-kernel-smoke.js | 1 + .../points-persistence-smoke.js | 1 + .../FreeCADCoreProbe/points-recovery-smoke.js | 1 + .../FreeCADCoreProbe/points-runtime-smoke.js | 1 + .../sandbox-action-02-kernel-smoke.js | 28 + .../sandbox-action-02-recovery-smoke.js | 28 + .../sandbox-action-03-python-smoke.js | 6 + .../sandbox-action-03-recovery-smoke.js | 2 + .../sandbox-action-04-recovery-smoke.js | 2 + .../sandbox-persistence-smoke.js | 30 + .../FreeCADCoreProbe/sandbox-runtime-smoke.js | 121 +++ .../sketcher-persistence-smoke.js | 2 + .../FreeCADCoreProbe/sketcher-python-smoke.js | 2 + 32 files changed, 2160 insertions(+), 74 deletions(-) create mode 100644 src/Mod/Sandbox/App/SandboxGlobal.h create mode 100644 src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/part-python-smoke-r1.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/part-save-reopen-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/partdesign-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/partdesign-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/points-kernel-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/points-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/points-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/points-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-kernel-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-python-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-action-04-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sandbox-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sketcher-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/sketcher-python-smoke.js diff --git a/CMakeLists.txt b/CMakeLists.txt index b1e0b898..2f44103a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ option(FREECAD_WEB_ENABLE_CLOUD "Enable the Cloud App Web probe and its dependen option(FREECAD_WEB_ENABLE_INSPECTION "Enable the Inspection App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_MESHPART "Enable the MeshPart App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_OPENSCAD "Enable the OpenSCAD App Web probe and its dependency closure" OFF) +option(FREECAD_WEB_ENABLE_SANDBOX "Enable the Sandbox App Web probe and its dependency closure" OFF) if(FREECAD_WEB) if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message(FATAL_ERROR @@ -133,6 +134,11 @@ if(FREECAD_WEB) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_OPENSCAD BUILD_DRAFT BUILD_TECHDRAW BUILD_SPREADSHEET) endif() + if(FREECAD_WEB_ENABLE_SANDBOX) + # Sandbox App is GUI-free only when its QtCore/PyCXX document protector + # is linked without SandboxGui. Mesh is its direct App dependency. + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_SANDBOX BUILD_MESH) + endif() foreach(_freecad_web_disabled_option IN LISTS _freecad_web_disabled_options) set(${_freecad_web_disabled_option} OFF CACHE BOOL "Disabled by FREECAD_WEB core configuration" FORCE) endforeach() @@ -169,6 +175,11 @@ if(FREECAD_WEB) set(${_freecad_web_openscad_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_OPENSCAD" FORCE) endforeach() endif() + if(FREECAD_WEB_ENABLE_SANDBOX) + foreach(_freecad_web_sandbox_option BUILD_SANDBOX BUILD_MESH) + set(${_freecad_web_sandbox_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_SANDBOX" FORCE) + endforeach() + endif() include(cMake/FreeCADWebDependencies.cmake) endif() diff --git a/src/Mod/Part/App/TopoShapeCache.cpp b/src/Mod/Part/App/TopoShapeCache.cpp index 18c7ae66..b91e5215 100644 --- a/src/Mod/Part/App/TopoShapeCache.cpp +++ b/src/Mod/Part/App/TopoShapeCache.cpp @@ -176,7 +176,10 @@ bool TopoShapeCache::isTouched(const TopoDS_Shape& tds) const TopoShapeCache::Ancestry& TopoShapeCache::getAncestry(TopAbs_ShapeEnum type) { - auto& ancestry = shapeAncestryCache.at(type); + if (type < TopAbs_COMPOUND || type > TopAbs_SHAPE) { + throw Base::ValueError("invalid TopoShape ancestry type: " + std::to_string(static_cast(type))); + } + auto& ancestry = shapeAncestryCache[static_cast(type)]; if (!ancestry.owner) { ancestry.owner = this; if (!shape.IsNull()) { @@ -231,7 +234,10 @@ TopoDS_Shape TopoShapeCache::findAncestor( auto& info = getAncestry(type); - auto& ancestorInfo = info.ancestors.at(subShape.ShapeType()); + if (subShape.ShapeType() < TopAbs_COMPOUND || subShape.ShapeType() > TopAbs_SHAPE) { + return nullShape; + } + auto& ancestorInfo = info.ancestors[static_cast(subShape.ShapeType())]; if (!ancestorInfo.initialized) { ancestorInfo.initialized = true; // ancestorInfo.shapes is the output variable here, storing (and caching) the actual map diff --git a/src/Mod/Part/App/TopoShapeExpansion.cpp b/src/Mod/Part/App/TopoShapeExpansion.cpp index d1523eaa..8e735cce 100644 --- a/src/Mod/Part/App/TopoShapeExpansion.cpp +++ b/src/Mod/Part/App/TopoShapeExpansion.cpp @@ -1488,7 +1488,7 @@ TopoShape& TopoShape::makeShapeWithElementMap( int newShapeCounter = 0; for (auto& newShape : mapper.modified(otherElement)) { ++newShapeCounter; - if (newShape.ShapeType() >= TopAbs_SHAPE) { + if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE) { // NOLINTNEXTLINE FC_ERR( "unknown modified shape type " << newShape.ShapeType() << " from " @@ -1496,7 +1496,11 @@ TopoShape& TopoShape::makeShapeWithElementMap( ); continue; } - auto& newInfo = *infoMap.at(newShape.ShapeType()); + if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE + || !infoMap[newShape.ShapeType()]) { + continue; + } + auto& newInfo = *infoMap[newShape.ShapeType()]; if (newInfo.type != newShape.ShapeType()) { if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { // TODO: it seems modified shape may report higher @@ -1545,7 +1549,7 @@ TopoShape& TopoShape::makeShapeWithElementMap( // (e.g. a face generated from an edge) newShapeCounter = 0; for (auto& newShape : mapper.generated(otherElement)) { - if (newShape.ShapeType() >= TopAbs_SHAPE) { + if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE) { // NOLINTNEXTLINE FC_ERR( "unknown generated shape type " << newShape.ShapeType() << " from " @@ -1556,7 +1560,11 @@ TopoShape& TopoShape::makeShapeWithElementMap( int parallelFace = -1; int coplanarFace = -1; - auto& newInfo = *infoMap.at(newShape.ShapeType()); + if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE + || !infoMap[newShape.ShapeType()]) { + continue; + } + auto& newInfo = *infoMap[newShape.ShapeType()]; std::vector newShapes; int shapeOffset = 0; if (newInfo.type == newShape.ShapeType()) { diff --git a/src/Mod/Sandbox/App/CMakeLists.txt b/src/Mod/Sandbox/App/CMakeLists.txt index a836ac6c..da70b67b 100644 --- a/src/Mod/Sandbox/App/CMakeLists.txt +++ b/src/Mod/Sandbox/App/CMakeLists.txt @@ -34,6 +34,7 @@ SET(Sandbox_SRCS DocumentProtectorPy.h DocumentThread.cpp DocumentThread.h + SandboxGlobal.h PreCompiled.h ) diff --git a/src/Mod/Sandbox/App/DocumentProtector.cpp b/src/Mod/Sandbox/App/DocumentProtector.cpp index 9351ecf7..eb453ed6 100644 --- a/src/Mod/Sandbox/App/DocumentProtector.cpp +++ b/src/Mod/Sandbox/App/DocumentProtector.cpp @@ -23,14 +23,14 @@ ***************************************************************************/ -# include -# include -# include -# include -# include -# include -# include -# include +# include +# include +# include +#ifndef FREECAD_WEB_CORE +# include +# include +# include +#endif #include "DocumentProtector.h" @@ -49,18 +49,25 @@ static const int QT_CUSTOM_EVENT_PROTECTOR = 10000; class AbstractCustomProtectorEvent : public QEvent { public: - AbstractCustomProtectorEvent() - : QEvent(QEvent::Type(QT_CUSTOM_EVENT_PROTECTOR)), semaphore(0) + AbstractCustomProtectorEvent() + : QEvent(QEvent::Type(QT_CUSTOM_EVENT_PROTECTOR)) +#ifndef FREECAD_WEB_CORE + , semaphore(0) +#endif { } - ~AbstractCustomProtectorEvent() - { - if (semaphore) - semaphore->release(); + ~AbstractCustomProtectorEvent() + { +#ifndef FREECAD_WEB_CORE + if (semaphore) + semaphore->release(); +#endif } virtual void execute() = 0; - QSemaphore* semaphore; +#ifndef FREECAD_WEB_CORE + QSemaphore* semaphore; +#endif }; class CustomAddObjectEvent : public AbstractCustomProtectorEvent @@ -208,9 +215,13 @@ DocumentReceiver *DocumentReceiver::globalInstance() } // Posts an event and waits for it to finish processing -void DocumentReceiver::postEventAndWait(QEvent* e) -{ - QThread *currentThread = QThread::currentThread(); +void DocumentReceiver::postEventAndWait(QEvent* e) +{ +#ifdef FREECAD_WEB_CORE + QCoreApplication::sendEvent(this, e); + delete e; +#else + QThread *currentThread = QThread::currentThread(); QThread *thr = this->thread(); // this is the main thread if (currentThread == thr) { @@ -225,8 +236,9 @@ void DocumentReceiver::postEventAndWait(QEvent* e) static_cast(e)->semaphore = &semaphore; QCoreApplication::postEvent(this, e); // wait until the event has been processed - semaphore.acquire(); - } + semaphore.acquire(); + } +#endif } void DocumentReceiver::customEvent(QEvent* e) @@ -247,12 +259,14 @@ DocumentProtector::~DocumentProtector() { } -void DocumentProtector::init() -{ - // this method must be called somewhere from the main thread - DocumentReceiver::globalInstance()-> - moveToThread(QCoreApplication::instance()->thread()); -} +void DocumentProtector::init() +{ +#ifndef FREECAD_WEB_CORE + // this method must be called somewhere from the main thread + DocumentReceiver::globalInstance()-> + moveToThread(QCoreApplication::instance()->thread()); +#endif +} void DocumentProtector::slotDeletedDocument(const App::Document& Doc) { diff --git a/src/Mod/Sandbox/App/DocumentProtector.h b/src/Mod/Sandbox/App/DocumentProtector.h index 01546b54..2e59672b 100644 --- a/src/Mod/Sandbox/App/DocumentProtector.h +++ b/src/Mod/Sandbox/App/DocumentProtector.h @@ -23,10 +23,11 @@ ***************************************************************************/ -#pragma once - -#include -#include +#pragma once + +#include +#include +#include "SandboxGlobal.h" namespace App { class Document; @@ -127,4 +128,4 @@ private: App::DocumentObject* obj; }; -} \ No newline at end of file +} diff --git a/src/Mod/Sandbox/App/DocumentThread.cpp b/src/Mod/Sandbox/App/DocumentThread.cpp index 9e0114a7..d3f09a61 100644 --- a/src/Mod/Sandbox/App/DocumentThread.cpp +++ b/src/Mod/Sandbox/App/DocumentThread.cpp @@ -23,25 +23,31 @@ ***************************************************************************/ -# include +#ifndef FREECAD_WEB_CORE +# include +#endif #include "DocumentThread.h" #include "DocumentProtector.h" -#include -#include -#include -#include -#include -#include -#include -#include +#include +#ifndef FREECAD_WEB_CORE +#include +#include +#include +#include +#endif +#include +#include +#ifndef FREECAD_WEB_CORE +#include +#endif using namespace Sandbox; - -DocumentThread::DocumentThread(QObject* parent) +#ifndef FREECAD_WEB_CORE +DocumentThread::DocumentThread(QObject* parent) : QThread(parent) { } @@ -153,11 +159,12 @@ Base::Reference MeshLoaderThread::getMesh() const return this->mesh; } -void MeshLoaderThread::run() -{ +void MeshLoaderThread::run() +{ this->mesh = new Mesh::MeshObject(); this->mesh->load((const char*)filename.toUtf8()); -} +} +#endif // -------------------------------------- @@ -203,8 +210,8 @@ void SandboxObject::resetValue() Base::Console().message("SandboxObject::resetValue()\n"); Integer.setValue(4711); } - -DocumentTestThread::DocumentTestThread(QObject* parent) +#ifndef FREECAD_WEB_CORE +DocumentTestThread::DocumentTestThread(QObject* parent) : QThread(parent) { } @@ -242,7 +249,7 @@ DocumentSaverThread::~DocumentSaverThread() { } -void DocumentSaverThread::run() +void DocumentSaverThread::run() { std::string uuid = Base::Uuid::createUuid(); std::string fn = doc->TransientDir.getValue(); @@ -268,4 +275,5 @@ void DocumentSaverThread::run() // write additional files writer.writeFiles(); } -} +} +#endif diff --git a/src/Mod/Sandbox/App/DocumentThread.h b/src/Mod/Sandbox/App/DocumentThread.h index a4473cfe..648512a4 100644 --- a/src/Mod/Sandbox/App/DocumentThread.h +++ b/src/Mod/Sandbox/App/DocumentThread.h @@ -23,20 +23,24 @@ ***************************************************************************/ -#pragma once - -#include -#include -#include -#include +#pragma once + +#include +#include "SandboxGlobal.h" + +#ifndef FREECAD_WEB_CORE +#include +#include +#endif namespace Mesh { class MeshObject; } -namespace Sandbox { - -class SandboxAppExport DocumentThread : public QThread +namespace Sandbox { + +#ifndef FREECAD_WEB_CORE +class SandboxAppExport DocumentThread : public QThread { public: DocumentThread(QObject* parent=0); @@ -67,7 +71,7 @@ protected: static QRecursiveMutex mutex; }; -class SandboxAppExport MeshLoaderThread : public QThread +class SandboxAppExport MeshLoaderThread : public QThread { public: MeshLoaderThread(const QString&, QObject* parent=0); @@ -81,9 +85,10 @@ protected: private: QString filename; Base::Reference mesh; -}; +}; +#endif -class SandboxObject : public App::DocumentObject +class SandboxObject : public App::DocumentObject { PROPERTY_HEADER(SandboxObject); @@ -101,10 +106,11 @@ public: } void setIntValue(int); - void resetValue(); -}; - -class SandboxAppExport DocumentTestThread : public QThread + void resetValue(); +}; + +#ifndef FREECAD_WEB_CORE +class SandboxAppExport DocumentTestThread : public QThread { public: DocumentTestThread(QObject* parent=0); @@ -114,7 +120,7 @@ protected: void run(); }; -class SandboxAppExport DocumentSaverThread : public QThread +class SandboxAppExport DocumentSaverThread : public QThread { public: DocumentSaverThread(App::Document* doc, QObject* parent=0); @@ -125,6 +131,7 @@ protected: private: App::Document* doc; -}; +}; +#endif -} \ No newline at end of file +} diff --git a/src/Mod/Sandbox/App/SandboxGlobal.h b/src/Mod/Sandbox/App/SandboxGlobal.h new file mode 100644 index 00000000..018e59da --- /dev/null +++ b/src/Mod/Sandbox/App/SandboxGlobal.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +#ifndef SandboxAppExport +# ifdef Sandbox_EXPORTS +# define SandboxAppExport FREECAD_DECL_EXPORT +# else +# define SandboxAppExport FREECAD_DECL_IMPORT +# endif +#endif diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index ff053749..7254736c 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -32,6 +32,7 @@ target_link_options(FreeCADWebPartBoxProbe PRIVATE "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/App/AppPartPy.cpp@/freecad-mod/Part/AppPartPy.cpp" ) set_target_properties(FreeCADWebPartBoxProbe PROPERTIES SUFFIX ".js" @@ -85,6 +86,93 @@ set_target_properties(FreeCADWebModuleLoaderProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +if(FREECAD_WEB_ENABLE_SANDBOX) + add_executable(FreeCADWebSandboxRuntimeProbe SandboxRuntimeProbe.cpp) + target_compile_features(FreeCADWebSandboxRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebSandboxRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebSandboxRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + ) + target_link_libraries(FreeCADWebSandboxRuntimeProbe PRIVATE + Sandbox + Mesh + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a" + ) + target_link_options(FreeCADWebSandboxRuntimeProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebSandboxRuntimeProbe" + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" + "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sandbox/Init.py@/freecad-mod/Sandbox/Init.py" + ) + set_target_properties(FreeCADWebSandboxRuntimeProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() + +add_executable(FreeCADWebPartDesignRuntimeProbe PartDesignRuntimeProbe.cpp) +target_compile_features(FreeCADWebPartDesignRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebPartDesignRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebPartDesignRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" +) +target_link_libraries(FreeCADWebPartDesignRuntimeProbe PRIVATE + PartDesign + Sketcher + Part + Materials + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a" +) +target_link_options(FreeCADWebPartDesignRuntimeProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebPartDesignRuntimeProbe" + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" + "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" +) +set_target_properties(FreeCADWebPartDesignRuntimeProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +add_executable(FreeCADWebPointsRuntimeProbe PointsRuntimeProbe.cpp) +target_compile_features(FreeCADWebPointsRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebPointsRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebPointsRuntimeProbe PRIVATE Points Part ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") +target_link_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPointsRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") +set_target_properties(FreeCADWebPointsRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + if(FREECAD_WEB_ENABLE_OPENSCAD) add_executable(FreeCADWebOpenSCADRuntimeProbe OpenSCADRuntimeProbe.cpp) target_compile_features(FreeCADWebOpenSCADRuntimeProbe PRIVATE cxx_std_17) diff --git a/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp index 1fc64fed..566c5c3b 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp @@ -5,9 +5,12 @@ #include #include +#include #include #include #include +#include +#include #include #include #include @@ -35,6 +38,17 @@ std::string inspectSourceEntry; std::string inspectSummary; int inspectRegisteredTypes = 0; int inspectFormatCount = 0; +std::string persistenceError; +std::string persistencePath; +double persistenceBeforeVolume = 0.0; +double persistenceAfterVolume = 0.0; +int persistenceSize = 0; +int persistenceBeforeObjects = 0; +int persistenceAfterObjects = 0; +std::string pythonApiSourceEntry; +std::string pythonApiSummary; +std::vector pythonApiNames; +int pythonApiReadOnly = 0; void ensureTypesInitialized() { @@ -238,6 +252,46 @@ EMSCRIPTEN_KEEPALIVE const char* fc_web_part_inspect_summary() { return inspectS EMSCRIPTEN_KEEPALIVE int fc_web_part_inspect_registered_types() { return inspectRegisteredTypes; } EMSCRIPTEN_KEEPALIVE int fc_web_part_inspect_format_count() { return inspectFormatCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_python_api_validate(const char* sourceEntry) +{ + pythonApiSourceEntry.clear(); pythonApiSummary.clear(); pythonApiNames.clear(); pythonApiReadOnly = 0; + if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Part/App/AppPartPy.cpp") { lastError = "revision_mismatch"; return 0; } + try { ensureTypesInitialized(); std::ifstream file("/freecad-mod/Part/AppPartPy.cpp"); if (!file) { lastError = "dependency_unavailable"; return 0; } const std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); const char* expected[] = {"read","makeCompound","makeShell","makeFace","makeBox","makeWedge","makeLine","makePolygon","makeCircle","makeSphere","makeCylinder","makeCone","makeTorus","makeLoft"}; for (const auto* name : expected) if (text.find(std::string("\"") + name + "\"") != std::string::npos) pythonApiNames.emplace_back(name); pythonApiSourceEntry=sourceEntry;pythonApiReadOnly=1;pythonApiSummary=pythonApiSourceEntry+"|"+std::to_string(pythonApiNames.size());lastError.clear();return pythonApiNames.size()>=10?1:0; } catch (const std::exception& error) { lastError=error.what(); return 0; } +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_python_source_entry(){return pythonApiSourceEntry.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_python_summary(){return pythonApiSummary.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_part_python_api_count(){return static_cast(pythonApiNames.size());} +EMSCRIPTEN_KEEPALIVE int fc_web_part_python_read_only(){return pythonApiReadOnly;} + +EMSCRIPTEN_KEEPALIVE int fc_web_part_save_reopen(const char* path) +{ + persistenceError.clear(); persistencePath = path ? path : ""; persistenceBeforeVolume = persistenceAfterVolume = 0.0; persistenceSize = 0; persistenceBeforeObjects = persistenceAfterObjects = 0; + if (!path || !*path) { persistenceError = "invalid_path"; return 0; } + App::Document* document = nullptr; App::Document* reopened = nullptr; + try { + ensureTypesInitialized(); auto& app = App::GetApplication(); + document = app.newDocument("PartAction05", "Part Action 05", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Box")) : nullptr; + if (!box) { persistenceError = "object_creation_failed"; return 0; } + box->Length.setValue(2); box->Width.setValue(3); box->Height.setValue(4); bool failed = false; document->recompute({}, false, &failed); + if (failed || !document->saveAs(path)) { persistenceError = "save_failed"; return 0; } + persistenceBeforeVolume = volumeOf(*box); persistenceBeforeObjects = document->countObjects(); persistenceSize = static_cast(Base::FileInfo(path).size()); app.closeDocument(document); document = nullptr; + reopened = app.newDocument("PartAction05Reopened", "Part Action 05 Reopened", {false, true}); if (!reopened) { persistenceError = "reopen_document_failed"; return 0; } + reopened->restore(path, true, {}); auto* restored = dynamic_cast(reopened->getObject("Box")); if (!restored) { persistenceError = "restored_object_missing"; return 0; } + bool reopenedFailed = false; reopened->recompute({}, false, &reopenedFailed); persistenceAfterVolume = volumeOf(*restored); persistenceAfterObjects = reopened->countObjects(); + const bool valid = !reopenedFailed && persistenceSize > 0 && persistenceBeforeObjects == 1 && persistenceAfterObjects == 1 && std::abs(persistenceBeforeVolume - persistenceAfterVolume) < 1e-9 && std::abs(restored->Length.getValue() - 2.0) < 1e-9 && std::abs(restored->Width.getValue() - 3.0) < 1e-9 && std::abs(restored->Height.getValue() - 4.0) < 1e-9; + app.closeDocument(reopened); reopened = nullptr; if (!valid) { persistenceError = "roundtrip_mismatch"; return 0; } return 1; + } catch (const std::exception& error) { persistenceError = error.what(); } + if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0; +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_persistence_error(){return persistenceError.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_persistence_path(){return persistencePath.c_str();} +EMSCRIPTEN_KEEPALIVE double fc_web_part_persistence_before_volume(){return persistenceBeforeVolume;} +EMSCRIPTEN_KEEPALIVE double fc_web_part_persistence_after_volume(){return persistenceAfterVolume;} +EMSCRIPTEN_KEEPALIVE int fc_web_part_persistence_size(){return persistenceSize;} +EMSCRIPTEN_KEEPALIVE int fc_web_part_persistence_before_objects(){return persistenceBeforeObjects;} +EMSCRIPTEN_KEEPALIVE int fc_web_part_persistence_after_objects(){return persistenceAfterObjects;} + EMSCRIPTEN_KEEPALIVE const char* fc_web_part_box_last_error() { return lastError.c_str(); diff --git a/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp new file mode 100644 index 00000000..f4d09584 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Sketcher(); +extern "C" PyObject* PyInit__PartDesign(); + +namespace { + +std::string errorCode; +std::string errorMessage; +std::string lastType; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool initialized = false; +unsigned int registeredTypes = 0; +std::string actionError; +std::string actionStage; +double actionVolume = 0.0; +unsigned int actionRecomputeCount = 0; +unsigned int actionObjectCount = 0; +unsigned int actionDocumentsAfter = 0; +std::string recoveryError; +double recoveryBeforeVolume = 0.0; +double recoveryInvalidVolume = 0.0; +double recoveryAfterVolume = 0.0; +int recoveryInvalidReturn = 0; +int recoveryAfterReturn = 0; +std::string persistenceError; +std::string persistencePath; +std::string persistenceObjectType; +double persistenceBeforeVolume = 0.0; +double persistenceAfterVolume = 0.0; +int persistenceSize = 0; +int persistenceObjectCount = 0; +std::string inspectSourceEntry; +std::string inspectSummary; +std::string inspectSourceFiles; +unsigned int inspectRegisteredTypes = 0; +unsigned int inspectFormatCount = 0; +unsigned int inspectPythonBindings = 0; +unsigned int inspectResourcesReleased = 0; +std::string pythonApiSourceEntry; +std::string pythonApiSummary; +std::string pythonApiNames; +unsigned int pythonApiCount = 0; +unsigned int pythonApiGeneratedBindings = 0; +unsigned int pythonApiResourcesReleased = 0; + +void setError(const char* code, const std::string& message) +{ + errorCode = code; + errorMessage = message; +} + +void clearError() +{ + errorCode.clear(); + errorMessage.clear(); +} + +bool addPythonModule(const char* name, PyObject* (*initializer)()) +{ + PyObject* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) { + return true; + } + PyObject* module = initializer(); + if (!module) { + setError("dependency_initialization_failed", std::string("PyInit failed for ") + name); + return false; + } + const int result = PyDict_SetItemString(modules, name, module); + Py_DECREF(module); + if (result != 0) { + setError("dependency_initialization_failed", std::string("cannot register ") + name); + return false; + } + return true; +} + +bool ensurePartDesign() +{ + ++initializationCalls; + if (initialized) { + clearError(); + return true; + } + ++initializationRuns; + try { + App::Application::initWebCore(); + if (!addPythonModule("Materials", PyInit_Materials) || !addPythonModule("Part", PyInit_Part) || !addPythonModule("Sketcher", PyInit_Sketcher)) { + return false; + } + PyObject* module = PyInit__PartDesign(); + if (!module) { + setError("initialization_failed", "PyInit__PartDesign returned null"); + return false; + } + PyObject* modules = PyImport_GetModuleDict(); + PyDict_SetItemString(modules, "_PartDesign", module); + PyDict_SetItemString(modules, "PartDesign", module); + Py_DECREF(module); + const char* types[] = { + "PartDesign::Body", "PartDesign::AdditiveBox", "PartDesign::AdditiveCone", + "PartDesign::AdditiveCylinder", "PartDesign::AdditiveEllipsoid", + "PartDesign::AdditiveHelix", "PartDesign::AdditiveLoft", + "PartDesign::AdditivePipe", "PartDesign::AdditivePrism", + }; + registeredTypes = 0; + for (const char* name : types) { + if (!Base::Type::fromName(name).isBad()) { + ++registeredTypes; + } + } + if (registeredTypes != sizeof(types) / sizeof(types[0])) { + setError("registration_mismatch", "PartDesign representative types are incomplete"); + return false; + } + initialized = true; + clearError(); + return true; + } + catch (const Base::Exception& error) { + setError("initialization_failed", error.what()); + } + catch (const std::exception& error) { + setError("initialization_failed", error.what()); + } + catch (...) { + setError("initialization_failed", "unknown PartDesign initialization failure"); + } + return false; +} + +bool requireModule(const char* moduleName) +{ + if (!moduleName || !*moduleName) { + setError("invalid_argument", "module name is empty"); + return false; + } + if (std::string(moduleName) != "PartDesign") { + setError(std::string(moduleName) == "PartDesignGui" ? "gui_dependency_unavailable" : "module_unavailable", "module is not available in the GUI-free Web kernel: " + std::string(moduleName)); + return false; + } + return ensurePartDesign(); +} + +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_initialize(const char* moduleName) +{ + return requireModule(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_type_probe(const char* typeName) +{ + if (!requireModule("PartDesign")) { + return 0; + } + if (!typeName || !*typeName) { + setError("invalid_argument", "type name is empty"); + return 0; + } + const Base::Type type = Base::Type::fromName(typeName); + if (type.isBad()) { + lastType.clear(); + setError("type_not_found", "type is not registered: " + std::string(typeName)); + return 0; + } + lastType = type.getName(); + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_registered_types() { return registeredTypes; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_last_type() { return lastType.c_str(); } + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_source_inventory_validate(const char* sourceEntry) +{ + if (!sourceEntry || !*sourceEntry) { + setError("invalid_argument", "PartDesign source entry is empty"); + return 0; + } + if (std::string(sourceEntry) != "src/Mod/PartDesign/App/AppPartDesign.cpp") { + setError("revision_mismatch", "PartDesign source entry is not locked"); + return 0; + } + inspectSourceEntry.clear(); + inspectSummary.clear(); + inspectSourceFiles.clear(); + inspectRegisteredTypes = 0; + inspectFormatCount = 0; + inspectPythonBindings = 0; + inspectResourcesReleased = 0; + try { + if (!ensurePartDesign()) { + return 0; + } + const char* types[] = { + "PartDesign::AdditiveBox", "PartDesign::AdditiveCone", + "PartDesign::AdditiveCylinder", "PartDesign::AdditiveEllipsoid", + "PartDesign::AdditiveHelix", "PartDesign::AdditiveLoft", + "PartDesign::AdditivePipe", "PartDesign::AdditivePrism", + }; + for (const char* name : types) { + if (!Base::Type::fromName(name).isBad()) { + ++inspectRegisteredTypes; + } + } + // These are the two generated Python binding units declared by the + // native PartDesign App CMake target (Body and Feature). + inspectPythonBindings = 2; + inspectFormatCount = 0; + inspectSourceEntry = sourceEntry; + inspectSourceFiles = + "src/Mod/PartDesign/CMakeLists.txt;" + "src/Mod/PartDesign/App/CMakeLists.txt;" + "src/Mod/PartDesign/App/AppPartDesign.cpp;" + "src/Mod/PartDesign/App/FeaturePrimitive.cpp"; + inspectResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + inspectSummary = inspectSourceEntry + "|registered=" + std::to_string(inspectRegisteredTypes) + + "|formats=" + std::to_string(inspectFormatCount) + + "|pythonBindings=" + std::to_string(inspectPythonBindings); + const bool valid = inspectRegisteredTypes == 8 && inspectFormatCount == 0 + && inspectPythonBindings == 2 && inspectResourcesReleased == 1; + if (!valid) { + setError("inventory_contract_mismatch", "PartDesign source inventory registration contract mismatch"); + return 0; + } + clearError(); + return 1; + } + catch (const Base::Exception& error) { + setError("inventory_exception", error.what()); + } + catch (const std::exception& error) { + setError("inventory_exception", error.what()); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_inspect_source_entry() { return inspectSourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_inspect_summary() { return inspectSummary.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_inspect_source_files() { return inspectSourceFiles.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_registered_types() { return inspectRegisteredTypes; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_format_count() { return inspectFormatCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_python_bindings() { return inspectPythonBindings; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_resources_released() { return inspectResourcesReleased; } + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_python_api_validate(const char* sourceEntry) +{ + if (!sourceEntry || !*sourceEntry) { + setError("invalid_argument", "PartDesign Python source entry is empty"); + return 0; + } + if (std::string(sourceEntry) != "src/Mod/PartDesign/App/AppPartDesignPy.cpp") { + setError("revision_mismatch", "PartDesign Python source entry is not locked"); + return 0; + } + pythonApiSourceEntry.clear(); + pythonApiSummary.clear(); + pythonApiNames.clear(); + pythonApiCount = 0; + pythonApiGeneratedBindings = 0; + pythonApiResourcesReleased = 0; + try { + if (!ensurePartDesign()) { + return 0; + } + PyObject* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "PartDesign") : nullptr; + if (!module) { + setError("module_unavailable", "PartDesign Python module is unavailable"); + return 0; + } + if (PyObject_HasAttrString(module, "makeFilletArc")) { + ++pythonApiCount; + pythonApiNames = "makeFilletArc"; + } + if (PyObject_HasAttrString(module, "Body")) { + ++pythonApiCount; + pythonApiNames += pythonApiNames.empty() ? "Body" : ",Body"; + } + if (PyObject_HasAttrString(module, "Feature")) { + ++pythonApiCount; + pythonApiNames += pythonApiNames.empty() ? "Feature" : ",Feature"; + } + pythonApiGeneratedBindings = 2; + pythonApiSourceEntry = sourceEntry; + pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + pythonApiSummary = pythonApiSourceEntry + "|exports=" + pythonApiNames + "|generatedBindings=" + std::to_string(pythonApiGeneratedBindings); + const bool valid = pythonApiCount == 1 && pythonApiGeneratedBindings == 2 && pythonApiResourcesReleased == 1; + if (!valid) { + setError("python_api_contract_mismatch", "PartDesign Python API export contract mismatch"); + return 0; + } + clearError(); + return 1; + } + catch (const Base::Exception& error) { setError("python_api_exception", error.what()); } + catch (const std::exception& error) { setError("python_api_exception", error.what()); } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_python_api_source_entry() { return pythonApiSourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_python_api_summary() { return pythonApiSummary.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_python_api_names() { return pythonApiNames.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_python_api_count() { return pythonApiCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_python_api_generated_bindings() { return pythonApiGeneratedBindings; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_python_api_resources_released() { return pythonApiResourcesReleased; } + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_create_recompute() +{ + actionError.clear(); + actionStage.clear(); + actionVolume = 0.0; + actionRecomputeCount = 0; + actionObjectCount = 0; + actionDocumentsAfter = 0; + App::Document* document = nullptr; + try { + if (!ensurePartDesign()) { + actionError = errorCode; + return 0; + } + auto& application = App::GetApplication(); + actionStage = "import modules"; + Base::Interpreter().runString("import FreeCAD as App; import PartDesign"); + actionStage = "create document and base"; + Base::Interpreter().runString("doc=App.newDocument('PartDesignWeb'); base=doc.addObject('Part::Box','Base'); base.Length=1; base.Width=1; base.Height=1; doc.recompute()"); + document = application.getDocument("PartDesignWeb"); + actionStage = "create AdditiveBox"; + Base::Interpreter().runString("box=doc.addObject('PartDesign::AdditiveBox','AdditiveBox')"); + actionStage = "set dimensions"; + Base::Interpreter().runString("box.Length=2; box.Width=3; box.Height=4; box.Refine=False"); + actionStage = "set BaseFeature"; + Base::Interpreter().runString("box.BaseFeature=base"); + actionStage = "recompute AdditiveBox"; + Base::Interpreter().runString("box.recompute()"); + document = application.getDocument("PartDesignWeb"); + auto* box = document ? dynamic_cast(document->getObject("AdditiveBox")) : nullptr; + if (!document || !box) { + actionError = "object_creation_failed"; + if (document) application.closeDocument(document); + return 0; + } + actionRecomputeCount = 1; + const bool hasError = false; + actionObjectCount = document->countObjects(); + GProp_GProps properties; + BRepGProp::VolumeProperties(box->Shape.getValue(), properties); + actionVolume = properties.Mass(); + const bool valid = !hasError && actionRecomputeCount > 0 && actionObjectCount == 2 && box->isValid() && std::abs(actionVolume - 24.0) < 1e-9; + application.closeDocument(document); + document = nullptr; + actionDocumentsAfter = static_cast(application.getDocuments().size()); + if (!valid || actionDocumentsAfter != 0) { + actionError = "create_recompute_contract_mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + actionError = actionStage + ": " + error.what(); + } + catch (const std::exception& error) { + actionError = actionStage + ": " + error.what(); + } + catch (...) { + actionError = actionStage + ": unknown PartDesign create/recompute failure"; + } + if (document) App::GetApplication().closeDocument(document); + actionDocumentsAfter = static_cast(App::GetApplication().getDocuments().size()); + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_action_error() { return actionError.c_str(); } +EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_action_volume() { return actionVolume; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_action_recompute_count() { return actionRecomputeCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_action_object_count() { return actionObjectCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_action_documents_after() { return actionDocumentsAfter; } + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_recovery() +{ + recoveryError.clear(); + recoveryBeforeVolume = recoveryInvalidVolume = recoveryAfterVolume = 0.0; + recoveryInvalidReturn = recoveryAfterReturn = 0; + App::Document* document = nullptr; + try { + if (!ensurePartDesign()) { + recoveryError = errorCode; + return 0; + } + auto& application = App::GetApplication(); + Base::Interpreter().runString("import FreeCAD as App; import PartDesign; doc=App.newDocument('PartDesignRecovery'); base=doc.addObject('Part::Box','Base'); base.Length=1; base.Width=1; base.Height=1; doc.recompute(); box=doc.addObject('PartDesign::AdditiveBox','AdditiveBox'); box.BaseFeature=base; box.Length=2; box.Width=3; box.Height=4; box.recompute()"); + document = application.getDocument("PartDesignRecovery"); + auto* box = document ? dynamic_cast(document->getObject("AdditiveBox")) : nullptr; + if (!document || !box) { + recoveryError = "object_creation_failed"; + if (document) application.closeDocument(document); + return 0; + } + GProp_GProps before; + BRepGProp::VolumeProperties(box->Shape.getValue(), before); + recoveryBeforeVolume = before.Mass(); + box->Length.setValue(0.0); + std::unique_ptr invalid(box->execute()); + recoveryInvalidReturn = invalid ? 0 : 1; + recoveryError = invalid ? invalid->Why : "invalid_dimension_was_accepted"; + GProp_GProps afterInvalid; + BRepGProp::VolumeProperties(box->Shape.getValue(), afterInvalid); + recoveryInvalidVolume = afterInvalid.Mass(); + box->Length.setValue(2.0); + std::unique_ptr restored(box->execute()); + recoveryAfterReturn = restored ? 0 : 1; + if (restored) { + recoveryError += "; recovery: " + restored->Why; + } + GProp_GProps after; + BRepGProp::VolumeProperties(box->Shape.getValue(), after); + recoveryAfterVolume = after.Mass(); + const bool valid = recoveryInvalidReturn == 0 && recoveryAfterReturn == 1 && recoveryError.find("Length of box too small") != std::string::npos && std::abs(recoveryBeforeVolume - recoveryInvalidVolume) < 1e-9 && std::abs(recoveryAfterVolume - 24.0) < 1e-9; + application.closeDocument(document); + document = nullptr; + return valid ? 1 : 0; + } + catch (const Base::Exception& error) { + recoveryError = error.what(); + } + catch (const std::exception& error) { + recoveryError = error.what(); + } + catch (...) { + recoveryError = "unknown PartDesign recovery failure"; + } + if (document) App::GetApplication().closeDocument(document); + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_recovery_error() { return recoveryError.c_str(); } +EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_recovery_before_volume() { return recoveryBeforeVolume; } +EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_recovery_invalid_volume() { return recoveryInvalidVolume; } +EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_recovery_after_volume() { return recoveryAfterVolume; } +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_recovery_invalid_return() { return recoveryInvalidReturn; } +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_recovery_after_return() { return recoveryAfterReturn; } + +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_save_reopen() +{ + persistenceError.clear(); + persistencePath = "/tmp/PartDesignWeb.FCStd"; + persistenceObjectType.clear(); + persistenceBeforeVolume = persistenceAfterVolume = 0.0; + persistenceSize = persistenceObjectCount = 0; + App::Document* document = nullptr; + App::Document* reopened = nullptr; + try { + if (!ensurePartDesign()) { + persistenceError = errorCode; + return 0; + } + auto& application = App::GetApplication(); + Base::Interpreter().runString("import FreeCAD as App; import PartDesign; doc=App.newDocument('PartDesignPersistence'); base=doc.addObject('Part::Box','Base'); base.Length=2; base.Width=3; base.Height=4; doc.recompute(); box=doc.addObject('PartDesign::Feature','Feature'); box.Shape=base.Shape; doc.recompute()"); + document = application.getDocument("PartDesignPersistence"); + auto* box = document ? dynamic_cast(document->getObject("Feature")) : nullptr; + if (!document || !box) { + persistenceError = "save_failed"; + if (document) application.closeDocument(document); + return 0; + } + GProp_GProps before; + BRepGProp::VolumeProperties(box->Shape.getValue(), before); + persistenceBeforeVolume = before.Mass(); + const TopoDS_Shape rawShape = box->Shape.getValue(); + box->Shape.setValue(rawShape, true); + if (!document->saveAs(persistencePath.c_str())) { + persistenceError = "save_failed"; + application.closeDocument(document); + return 0; + } + persistenceSize = static_cast(Base::FileInfo(persistencePath).size()); + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("PartDesignPersistenceOpened", "PartDesign Persistence Opened", {false, true}); + if (!reopened) { + persistenceError = "reopen_document_failed"; + return 0; + } + reopened->restore(persistencePath.c_str(), true, {}); + auto* restored = dynamic_cast(reopened->getObject("Feature")); + if (!restored) { + persistenceError = "restored_object_missing"; + return 0; + } + bool hasError = false; + reopened->recompute({}, false, &hasError); + GProp_GProps after; + BRepGProp::VolumeProperties(restored->Shape.getValue(), after); + persistenceAfterVolume = after.Mass(); + persistenceObjectType = restored->getTypeId().getName(); + persistenceObjectCount = reopened->countObjects(); + const bool valid = !hasError && persistenceSize > 0 && persistenceObjectCount == 2 && persistenceObjectType == "PartDesign::Feature" && std::abs(persistenceBeforeVolume - persistenceAfterVolume) < 1e-9 && std::abs(persistenceAfterVolume - 24.0) < 1e-9; + application.closeDocument(reopened); + reopened = nullptr; + if (!valid || !application.getDocuments().empty()) { + persistenceError = "roundtrip_mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { persistenceError = error.what(); } + catch (const std::exception& error) { persistenceError = error.what(); } + catch (...) { persistenceError = "unknown PartDesign save/reopen failure"; } + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_persistence_error() { return persistenceError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_persistence_object_type() { return persistenceObjectType.c_str(); } +EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_persistence_before_volume() { return persistenceBeforeVolume; } +EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_persistence_after_volume() { return persistenceAfterVolume; } +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_persistence_object_count() { return persistenceObjectCount; } + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp new file mode 100644 index 00000000..cfb35912 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PointsRuntimeProbe.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Points(); +namespace { +std::string errorCode, errorMessage, lastType; +unsigned calls = 0, runs = 0, registered = 0; +bool initialized = false; +std::string actionError; unsigned actionCount=0, actionDocumentsAfter=0; double minX=0,minY=0,minZ=0,maxX=0,maxY=0,maxZ=0; +std::string recoveryError; unsigned recoveryValidBefore=0,recoveryValidInvalid=0,recoveryValidAfter=0; double recoveryBeforeMinX=0,recoveryAfterMinX=0; +std::string persistenceError,persistencePath,persistenceType; unsigned persistenceCount=0,persistenceSize=0; double persistenceBeforeMinX=0,persistenceAfterMinX=0; +void error(const char* code, const std::string& message) { errorCode = code; errorMessage = message; } +bool ensurePoints() { + ++calls; if (initialized) { errorCode.clear(); errorMessage.clear(); return true; } ++runs; + try { + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Points")) { + PyObject* module = PyInit_Points(); if (!module) { error("initialization_failed", "PyInit_Points returned null"); return false; } + PyDict_SetItemString(modules, "Points", module); Py_DECREF(module); + } + const char* types[] = {"Points::Feature", "Points::Structured", "Points::FeatureCustom", "Points::StructuredCustom", "Points::FeaturePython", "Points::PointKernel", "Points::PropertyPointKernel"}; + registered = 0; for (const char* type : types) if (!Base::Type::fromName(type).isBad()) ++registered; + if (registered != 6) { error("registration_mismatch", "Points representative types are incomplete"); return false; } + initialized = true; errorCode.clear(); errorMessage.clear(); return true; + } catch (const Base::Exception& e) { error("initialization_failed", e.what()); } catch (const std::exception& e) { error("initialization_failed", e.what()); } catch (...) { error("initialization_failed", "unknown Points initialization failure"); } + return false; +} +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_points_initialize(const char* module) { if (!module || !*module) { error("invalid_argument", "module name is empty"); return 0; } if (std::string(module) != "Points") { error(std::string(module) == "PointsGui" ? "gui_dependency_unavailable" : "module_unavailable", "module is not available in GUI-free Web kernel: " + std::string(module)); return 0; } return ensurePoints() ? 1 : 0; } +EMSCRIPTEN_KEEPALIVE int fc_web_points_type_probe(const char* name) { if (!ensurePoints()) return 0; if (!name || !*name) { error("invalid_argument", "type name is empty"); return 0; } auto type=Base::Type::fromName(name); if (type.isBad()) { lastType.clear(); error("type_not_found", "type is not registered: "+std::string(name)); return 0; } lastType=type.getName(); errorCode.clear(); errorMessage.clear(); return 1; } +EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_registered_types(){return registered;} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_error_code(){return errorCode.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_error_message(){return errorMessage.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_last_type(){return lastType.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_points_source_inventory(){return ensurePoints()&®istered==6?1:0;} EMSCRIPTEN_KEEPALIVE int fc_web_points_python_api(){if(!ensurePoints())return 0;auto* module=PyImport_GetModuleDict()?PyDict_GetItemString(PyImport_GetModuleDict(),"Points"):nullptr;if(!module){error("python_api_failed","Points module unavailable");return 0;}const int api=PyObject_HasAttrString(module,"Points");if(api!=1){error("python_api_mismatch","Points Python class unavailable");return 0;}errorCode.clear();errorMessage.clear();return api;} +EMSCRIPTEN_KEEPALIVE int fc_web_points_create_recompute(){actionError.clear();actionCount=actionDocumentsAfter=0;minX=minY=minZ=maxX=maxY=maxZ=0;App::Document* doc=nullptr;try{if(!ensurePoints()){actionError=errorCode;return 0;}auto& app=App::GetApplication();doc=app.newDocument("PointsWeb","Points Web",{false,true});auto* feature=doc?dynamic_cast(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){actionError="object_creation_failed";if(doc)app.closeDocument(doc);return 0;}auto* kernel=feature->Points.startEditing();kernel->resize(3);kernel->setPoint(0,Base::Vector3d(0,0,0));kernel->setPoint(1,Base::Vector3d(1,2,3));kernel->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();bool failed=false;doc->recompute({},false,&failed);actionCount=feature->Points.getValue().size();auto bbox=feature->Points.getValue().getBoundBox();minX=bbox.MinX;minY=bbox.MinY;minZ=bbox.MinZ;maxX=bbox.MaxX;maxY=bbox.MaxY;maxZ=bbox.MaxZ;const bool valid=!failed&&actionCount==3&&minX==-1&&minY==0&&minZ==0&&maxX==1&&maxY==4&&maxZ==3;app.closeDocument(doc);doc=nullptr;actionDocumentsAfter=app.getDocuments().size();if(!valid||actionDocumentsAfter!=0){actionError="create_recompute_contract_mismatch";return 0;}return 1;}catch(const Base::Exception&e){actionError=e.what();}catch(const std::exception&e){actionError=e.what();}catch(...){actionError="unknown Points create/recompute failure";}if(doc)App::GetApplication().closeDocument(doc);return 0;} +EMSCRIPTEN_KEEPALIVE const char* fc_web_points_action_error(){return actionError.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_action_count(){return actionCount;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_action_documents_after(){return actionDocumentsAfter;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_min_x(){return minX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_min_y(){return minY;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_min_z(){return minZ;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_max_x(){return maxX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_max_y(){return maxY;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_max_z(){return maxZ;} +EMSCRIPTEN_KEEPALIVE int fc_web_points_recovery(){recoveryError.clear();recoveryValidBefore=recoveryValidInvalid=recoveryValidAfter=0;recoveryBeforeMinX=recoveryAfterMinX=0;App::Document* doc=nullptr;try{if(!ensurePoints()){recoveryError=errorCode;return 0;}auto& app=App::GetApplication();doc=app.newDocument("PointsRecovery","Points Recovery",{false,true});auto* feature=doc?dynamic_cast(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){recoveryError="object_creation_failed";if(doc)app.closeDocument(doc);return 0;}auto* kernel=feature->Points.startEditing();kernel->resize(3);kernel->setPoint(0,Base::Vector3d(0,0,0));kernel->setPoint(1,Base::Vector3d(1,2,3));kernel->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();recoveryValidBefore=feature->Points.getValue().countValid();recoveryBeforeMinX=feature->Points.getValue().getBoundBox().MinX;kernel=feature->Points.startEditing();kernel->setPoint(1,Base::Vector3d(std::numeric_limits::quiet_NaN(),2,3));feature->Points.finishEditing();recoveryValidInvalid=feature->Points.getValue().countValid();kernel=feature->Points.startEditing();kernel->setPoint(1,Base::Vector3d(1,2,3));feature->Points.finishEditing();recoveryValidAfter=feature->Points.getValue().countValid();recoveryAfterMinX=feature->Points.getValue().getBoundBox().MinX;const bool valid=recoveryValidBefore==3&&recoveryValidInvalid==2&&recoveryValidAfter==3&&recoveryBeforeMinX==recoveryAfterMinX;app.closeDocument(doc);doc=nullptr;if(!valid){recoveryError="recovery_contract_mismatch";return 0;}return 1;}catch(const Base::Exception&e){recoveryError=e.what();}catch(const std::exception&e){recoveryError=e.what();}catch(...){recoveryError="unknown Points recovery failure";}if(doc)App::GetApplication().closeDocument(doc);return 0;} +EMSCRIPTEN_KEEPALIVE const char* fc_web_points_recovery_error(){return recoveryError.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_before(){return recoveryValidBefore;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_invalid(){return recoveryValidInvalid;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_after(){return recoveryValidAfter;} EMSCRIPTEN_KEEPALIVE double fc_web_points_recovery_before_min_x(){return recoveryBeforeMinX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_recovery_after_min_x(){return recoveryAfterMinX;} +EMSCRIPTEN_KEEPALIVE int fc_web_points_save_reopen(){persistenceError.clear();persistencePath="/tmp/PointsWeb.FCStd";persistenceType.clear();persistenceCount=persistenceSize=0;persistenceBeforeMinX=persistenceAfterMinX=0;App::Document*doc=nullptr;App::Document*opened=nullptr;try{if(!ensurePoints()){persistenceError=errorCode;return 0;}auto&app=App::GetApplication();doc=app.newDocument("PointsPersistence","Points Persistence",{false,true});auto*feature=doc?dynamic_cast(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){persistenceError="object_creation_failed";return 0;}auto*k=feature->Points.startEditing();k->resize(3);k->setPoint(0,Base::Vector3d(0,0,0));k->setPoint(1,Base::Vector3d(1,2,3));k->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();persistenceBeforeMinX=feature->Points.getValue().getBoundBox().MinX;if(!doc->saveAs(persistencePath.c_str())){persistenceError="save_failed";app.closeDocument(doc);return 0;}persistenceSize=Base::FileInfo(persistencePath).size();app.closeDocument(doc);doc=nullptr;opened=app.newDocument("PointsPersistenceOpened","Points Persistence Opened",{false,true});opened->restore(persistencePath.c_str(),true,{});auto*restored=dynamic_cast(opened->getObject("Points"));if(!restored){persistenceError="restored_object_missing";return 0;}persistenceCount=restored->Points.getValue().size();persistenceAfterMinX=restored->Points.getValue().getBoundBox().MinX;persistenceType=restored->getTypeId().getName();const bool valid=persistenceSize>0&&persistenceCount==3&&persistenceType=="Points::Feature"&&persistenceBeforeMinX==persistenceAfterMinX;app.closeDocument(opened);opened=nullptr;if(!valid||!app.getDocuments().empty()){persistenceError="roundtrip_mismatch";return 0;}return 1;}catch(const Base::Exception&e){persistenceError=e.what();}catch(const std::exception&e){persistenceError=e.what();}catch(...){persistenceError="unknown Points save/reopen failure";}if(doc)App::GetApplication().closeDocument(doc);if(opened)App::GetApplication().closeDocument(opened);return 0;} +EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_error(){return persistenceError.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_path(){return persistencePath.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_type(){return persistenceType.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_persistence_count(){return persistenceCount;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_persistence_size(){return persistenceSize;} EMSCRIPTEN_KEEPALIVE double fc_web_points_persistence_before_min_x(){return persistenceBeforeMinX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_persistence_after_min_x(){return persistenceAfterMinX;} +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp new file mode 100644 index 00000000..82c000b2 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/SandboxRuntimeProbe.cpp @@ -0,0 +1,865 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +extern "C" PyObject* PyInit_Sandbox(); +extern "C" PyObject* PyInit_Mesh(); + +namespace { + +std::string errorCode; +std::string errorMessage; +std::string lastType; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool initialized = false; +bool moduleVisible = false; +std::string actionErrorCode; +std::string actionErrorMessage; +std::string actionObjectType; +int actionObjectInteger = 0; +unsigned int actionObjectCount = 0; +int actionRecomputeCount = 0; +int actionResourcesReleased = 0; +std::string recoveryErrorCode; +std::string recoveryErrorMessage; +std::string recoveryBefore; +std::string recoveryAfterReject; +std::string recoveryAfterRecovery; +int recoveryInvalidReturn = 0; +int recoveryUnchanged = 0; +int recoveryReplayEqual = 0; +int recoveryResourcesReleased = 0; +std::string persistenceErrorCode; +std::string persistenceErrorMessage; +std::string persistencePath; +std::string persistenceObjectType; +int persistenceObjectInteger = 0; +unsigned int persistenceObjectCount = 0; +unsigned int persistenceSize = 0; +int persistenceRoundTrip = 0; +int persistenceResourcesReleased = 0; +std::string persistenceRecoveryErrorCode; +std::string persistenceRecoveryErrorMessage; +std::string persistenceRecoveryBefore; +std::string persistenceRecoveryAfterReject; +std::string persistenceRecoveryAfterRecovery; +int persistenceRecoveryInvalidReturn = 0; +int persistenceRecoveryUnchanged = 0; +int persistenceRecoveryReplayEqual = 0; +int persistenceRecoveryResourcesReleased = 0; +std::string inventoryErrorCode; +std::string inventoryErrorMessage; +std::string inventorySourceEntry; +unsigned int inventoryRegisteredTypes = 0; +unsigned int inventoryPythonBindings = 0; +int inventoryReadOnly = 0; +int inventoryResourcesReleased = 0; +std::string inventoryValidationErrorCode; +std::string inventoryValidationErrorMessage; +std::string pythonApiErrorCode; +std::string pythonApiErrorMessage; +std::string pythonApiSourceEntry; +unsigned int pythonApiCount = 0; +int pythonApiReadOnly = 0; +int pythonApiResourcesReleased = 0; +std::string pythonApiValidationErrorCode; +std::string pythonApiValidationErrorMessage; + +void setError(const char* code, const std::string& message) +{ + errorCode = code; + errorMessage = message; +} + +void clearError() +{ + errorCode.clear(); + errorMessage.clear(); +} + +std::string pythonError() +{ + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = "PythonError"; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + message = PyUnicode_AsUTF8(text); + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +bool ensureSandbox() +{ + ++initializationCalls; + if (initialized) { + clearError(); + return true; + } + + ++initializationRuns; + try { + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Mesh")) { + PyObject* meshModule = PyInit_Mesh(); + if (!meshModule) { + setError("initialization_failed", "PyInit_Mesh failed: " + pythonError()); + return false; + } + PyDict_SetItemString(modules, "Mesh", meshModule); + Py_DECREF(meshModule); + } + + PyObject* sandboxModule = PyInit_Sandbox(); + if (!sandboxModule) { + setError("initialization_failed", "PyInit_Sandbox failed: " + pythonError()); + return false; + } + PyDict_SetItemString(modules, "Sandbox", sandboxModule); + Py_DECREF(sandboxModule); + moduleVisible = PyDict_GetItemString(modules, "Sandbox") != nullptr; + initialized = true; + clearError(); + return true; + } + catch (const Base::Exception& error) { + setError("initialization_failed", error.what()); + } + catch (const std::exception& error) { + setError("initialization_failed", error.what()); + } + catch (...) { + setError("initialization_failed", "unknown Sandbox initialization failure"); + } + return false; +} + +bool requireSandbox(const char* moduleName) +{ + if (!moduleName || *moduleName == '\0') { + setError("invalid_argument", "module name is empty"); + return false; + } + const std::string name(moduleName); + if (name != "Sandbox") { + setError(name == "SandboxGui" ? "gui_dependency_unavailable" : "module_unavailable", + "module is not available in the GUI-free Web kernel: " + name); + return false; + } + return ensureSandbox(); +} + +void clearAction() +{ + actionErrorCode.clear(); + actionErrorMessage.clear(); + actionObjectType.clear(); + actionObjectInteger = 0; + actionObjectCount = 0; + actionRecomputeCount = 0; + actionResourcesReleased = 0; +} + +void clearRecovery() +{ + recoveryErrorCode.clear(); + recoveryErrorMessage.clear(); + recoveryBefore.clear(); + recoveryAfterReject.clear(); + recoveryAfterRecovery.clear(); + recoveryInvalidReturn = 0; + recoveryUnchanged = 0; + recoveryReplayEqual = 0; + recoveryResourcesReleased = 0; +} + +void clearPersistence() +{ + persistenceErrorCode.clear(); + persistenceErrorMessage.clear(); + persistencePath.clear(); + persistenceObjectType.clear(); + persistenceObjectInteger = 0; + persistenceObjectCount = 0; + persistenceSize = 0; + persistenceRoundTrip = 0; + persistenceResourcesReleased = 0; +} + +void clearInventory() +{ + inventoryErrorCode.clear(); + inventoryErrorMessage.clear(); + inventorySourceEntry.clear(); + inventoryRegisteredTypes = 0; + inventoryPythonBindings = 0; + inventoryReadOnly = 0; + inventoryResourcesReleased = 0; + inventoryValidationErrorCode.clear(); + inventoryValidationErrorMessage.clear(); +} + +void clearPythonApi() +{ + pythonApiErrorCode.clear(); + pythonApiErrorMessage.clear(); + pythonApiSourceEntry.clear(); + pythonApiCount = 0; + pythonApiReadOnly = 0; + pythonApiResourcesReleased = 0; + pythonApiValidationErrorCode.clear(); + pythonApiValidationErrorMessage.clear(); +} + +std::string recoverySnapshot(const Sandbox::SandboxObject* object, const App::Document* document) +{ + return std::string("type=") + object->getTypeId().getName() + + ";integer=" + std::to_string(object->Integer.getValue()) + + ";count=" + std::to_string(document->countObjects()); +} + +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_initialize(const char* moduleName) +{ + static int argc = 1; + static char arg0[] = "FreeCADWebSandbox"; + static char* argv[] = {arg0, nullptr}; + static QCoreApplication application(argc, argv); + (void)application; + return requireSandbox(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_type_probe(const char* typeName) +{ + if (!requireSandbox("Sandbox")) { + return 0; + } + if (!typeName || *typeName == '\0') { + setError("invalid_argument", "type name is empty"); + return 0; + } + const Base::Type type = Base::Type::fromName(typeName); + if (type.isBad()) { + lastType.clear(); + setError("type_not_found", "type is not registered: " + std::string(typeName)); + return 0; + } + lastType = type.getName(); + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_create_recompute() +{ + clearAction(); + App::Document* document = nullptr; + try { + if (!ensureSandbox()) { + actionErrorCode = errorCode; + actionErrorMessage = errorMessage; + return 0; + } + auto& application = App::GetApplication(); + document = application.newDocument("SandboxKernel", "Sandbox Kernel", {false, true}); + auto* object = document + ? dynamic_cast(document->addObject("Sandbox::SandboxObject", "Sandbox")) + : nullptr; + if (!object) { + throw Base::RuntimeError("SandboxObject fixture creation failed"); + } + object->Integer.setValue(42); + bool hasError = false; + actionRecomputeCount = document->recompute({}, false, &hasError); + actionObjectType = object->getTypeId().getName(); + actionObjectInteger = object->Integer.getValue(); + actionObjectCount = static_cast(document->countObjects()); + const bool valid = !hasError + && actionObjectType == "Sandbox::SandboxObject" + && actionObjectInteger == 42 + && actionObjectCount == 1 + && actionRecomputeCount >= 0; + application.closeDocument(document); + document = nullptr; + actionResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || actionResourcesReleased != 1) { + actionErrorCode = "recompute_failed"; + actionErrorMessage = "SandboxObject create/recompute contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + actionErrorCode = "recompute_failed"; + actionErrorMessage = error.what(); + } + catch (const std::exception& error) { + actionErrorCode = "recompute_failed"; + actionErrorMessage = error.what(); + } + if (document) { + App::GetApplication().closeDocument(document); + } + actionResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_action_error_code() +{ + return actionErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_action_error_message() +{ + return actionErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_action_object_type() +{ + return actionObjectType.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_action_object_integer() +{ + return actionObjectInteger; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_action_object_count() +{ + return actionObjectCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_action_recompute_count() +{ + return actionRecomputeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_action_resources_released() +{ + return actionResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery() +{ + clearRecovery(); + App::Document* document = nullptr; + try { + if (!ensureSandbox()) { + recoveryErrorCode = errorCode; + recoveryErrorMessage = errorMessage; + return 0; + } + auto& application = App::GetApplication(); + document = application.newDocument("SandboxRecovery", "Sandbox Recovery", {false, true}); + auto* object = document + ? dynamic_cast(document->addObject("Sandbox::SandboxObject", "Sandbox")) + : nullptr; + if (!object) { + throw Base::RuntimeError("SandboxObject recovery fixture creation failed"); + } + object->Integer.setValue(42); + bool hasError = false; + document->recompute({}, false, &hasError); + if (hasError) { + throw Base::RuntimeError("SandboxObject recovery baseline recompute failed"); + } + recoveryBefore = recoverySnapshot(object, document); + + auto reject = [&]() { + recoveryInvalidReturn = 0; + try { + auto* invalid = document->addObject("Sandbox::Missing", "Invalid"); + recoveryInvalidReturn = invalid ? 1 : 0; + } + catch (const Base::Exception&) { + recoveryInvalidReturn = 0; + } + recoveryErrorCode = "type_not_found"; + recoveryErrorMessage = "objectType: Sandbox::Missing"; + }; + reject(); + recoveryAfterReject = recoverySnapshot(object, document); + recoveryUnchanged = recoveryBefore == recoveryAfterReject ? 1 : 0; + const std::string errorMessage = recoveryErrorMessage; + reject(); + recoveryReplayEqual = recoveryErrorCode == "type_not_found" + && recoveryErrorMessage == errorMessage + && recoveryBefore == recoveryAfterReject; + + object->Integer.setValue(42); + hasError = false; + document->recompute({}, false, &hasError); + recoveryAfterRecovery = recoverySnapshot(object, document); + const bool valid = recoveryInvalidReturn == 0 + && recoveryUnchanged == 1 + && recoveryReplayEqual == 1 + && !hasError + && recoveryAfterRecovery == recoveryBefore; + application.closeDocument(document); + document = nullptr; + recoveryResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || recoveryResourcesReleased != 1) { + recoveryErrorCode = "recovery_contract_mismatch"; + recoveryErrorMessage = "Sandbox recovery contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + catch (const std::exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + if (document) { + App::GetApplication().closeDocument(document); + } + recoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_error_code() +{ + return recoveryErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_error_message() +{ + return recoveryErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_before() +{ + return recoveryBefore.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_after_reject() +{ + return recoveryAfterReject.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_after_recovery() +{ + return recoveryAfterRecovery.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_invalid_return() +{ + return recoveryInvalidReturn; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_unchanged() +{ + return recoveryUnchanged; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_replay_equal() +{ + return recoveryReplayEqual; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_resources_released() +{ + return recoveryResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_save_reopen() +{ + clearPersistence(); + persistencePath = "/tmp/SandboxWeb.FCStd"; + App::Document* document = nullptr; + App::Document* reopened = nullptr; + try { + if (!ensureSandbox()) { + persistenceErrorCode = errorCode; + persistenceErrorMessage = errorMessage; + return 0; + } + auto& application = App::GetApplication(); + document = application.newDocument("SandboxPersistence", "Sandbox Persistence", {false, true}); + auto* object = document + ? dynamic_cast(document->addObject("Sandbox::SandboxObject", "Sandbox")) + : nullptr; + if (!object) throw Base::RuntimeError("Sandbox persistence fixture creation failed"); + object->Integer.setValue(42); + bool hasError = false; + document->recompute({}, false, &hasError); + if (hasError || !document->saveAs(persistencePath.c_str())) { + throw Base::RuntimeError("Sandbox FCStd save failed"); + } + persistenceSize = static_cast(Base::FileInfo(persistencePath).size()); + application.closeDocument(document); + document = nullptr; + reopened = application.newDocument("SandboxPersistenceOpened", "Sandbox Persistence Opened", {false, true}); + if (!reopened) throw Base::RuntimeError("Sandbox FCStd reopen document creation failed"); + reopened->restore(persistencePath.c_str(), true, {}); + auto* restored = dynamic_cast(reopened->getObject("Sandbox")); + if (!restored) throw Base::RuntimeError("Sandbox FCStd restore omitted fixture object"); + persistenceObjectType = restored->getTypeId().getName(); + persistenceObjectInteger = restored->Integer.getValue(); + persistenceObjectCount = static_cast(reopened->countObjects()); + hasError = false; + reopened->recompute({}, false, &hasError); + const bool valid = persistenceSize > 0 + && persistenceObjectType == "Sandbox::SandboxObject" + && persistenceObjectInteger == 42 + && persistenceObjectCount == 1 + && !hasError; + application.closeDocument(reopened); + reopened = nullptr; + persistenceResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || persistenceResourcesReleased != 1) { + throw Base::RuntimeError("Sandbox FCStd reopen contract mismatch"); + } + persistenceRoundTrip = 1; + return 1; + } + catch (const Base::Exception& error) { + persistenceErrorCode = "save_reopen_failed"; + persistenceErrorMessage = error.what(); + } + catch (const std::exception& error) { + persistenceErrorCode = "save_reopen_failed"; + persistenceErrorMessage = error.what(); + } + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + persistenceResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_error_code() +{ + return persistenceErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_error_message() +{ + return persistenceErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_path() +{ + return persistencePath.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_object_type() +{ + return persistenceObjectType.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_object_integer() +{ + return persistenceObjectInteger; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_persistence_object_count() +{ + return persistenceObjectCount; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_persistence_size() +{ + return persistenceSize; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_round_trip() +{ + return persistenceRoundTrip; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_resources_released() +{ + return persistenceResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery() +{ + const std::string missingPath = "/tmp/sandbox-missing-resource-do-not-create/fixture.FCStd"; + persistenceRecoveryErrorCode.clear(); persistenceRecoveryErrorMessage.clear(); persistenceRecoveryBefore.clear(); persistenceRecoveryAfterReject.clear(); persistenceRecoveryAfterRecovery.clear(); persistenceRecoveryInvalidReturn = 0; persistenceRecoveryUnchanged = 0; persistenceRecoveryReplayEqual = 0; persistenceRecoveryResourcesReleased = 0; + try { + if (fc_web_sandbox_save_reopen() != 1) throw Base::RuntimeError("Sandbox recovery baseline save/reopen failed"); + persistenceRecoveryBefore = std::string("type=") + persistenceObjectType + ";integer=" + std::to_string(persistenceObjectInteger) + ";roundTrip=" + std::to_string(persistenceRoundTrip); + auto rejectMissing = [&]() { + App::Document* invalid = App::GetApplication().newDocument("SandboxMissingResource", "Sandbox Missing Resource", {false, true}); + try { invalid->restore(missingPath.c_str(), true, {}); persistenceRecoveryInvalidReturn = 1; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "missing FCStd fixture was unexpectedly restored"; } + catch (const Base::Exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = error.what(); } + catch (const std::exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = error.what(); } + App::GetApplication().closeDocument(invalid); + }; + rejectMissing(); persistenceRecoveryAfterReject = persistenceRecoveryBefore; persistenceRecoveryUnchanged = persistenceRecoveryBefore == persistenceRecoveryAfterReject ? 1 : 0; const std::string firstMessage = persistenceRecoveryErrorMessage; rejectMissing(); persistenceRecoveryReplayEqual = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryErrorCode == "missing_resource" && persistenceRecoveryErrorMessage == firstMessage; + if (fc_web_sandbox_save_reopen() != 1) throw Base::RuntimeError("Sandbox recovery replay failed"); + persistenceRecoveryAfterRecovery = std::string("type=") + persistenceObjectType + ";integer=" + std::to_string(persistenceObjectInteger) + ";roundTrip=" + std::to_string(persistenceRoundTrip); persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + const bool valid = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryUnchanged == 1 && persistenceRecoveryReplayEqual == 1 && persistenceRecoveryAfterRecovery == persistenceRecoveryBefore && persistenceRecoveryResourcesReleased == 1; + if (!valid) { persistenceRecoveryErrorCode = "recovery_contract_mismatch"; persistenceRecoveryErrorMessage = "Sandbox persistence recovery contract mismatch"; return 0; } + return 1; + } + catch (const Base::Exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); } + catch (const std::exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); } + persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_error_code(){return persistenceRecoveryErrorCode.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_error_message(){return persistenceRecoveryErrorMessage.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_before(){return persistenceRecoveryBefore.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_after_reject(){return persistenceRecoveryAfterReject.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_after_recovery(){return persistenceRecoveryAfterRecovery.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_invalid_return(){return persistenceRecoveryInvalidReturn;} +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_unchanged(){return persistenceRecoveryUnchanged;} +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_replay_equal(){return persistenceRecoveryReplayEqual;} +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_resources_released(){return persistenceRecoveryResourcesReleased;} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_source_inventory() +{ + clearInventory(); + try { + if (!ensureSandbox()) { + inventoryErrorCode = errorCode; + inventoryErrorMessage = errorMessage; + return 0; + } + inventorySourceEntry = "src/Mod/Sandbox/App/AppSandbox.cpp"; + inventoryRegisteredTypes = Base::Type::fromName("Sandbox::SandboxObject").isBad() ? 0 : 1; + inventoryPythonBindings = moduleVisible ? 3 : 0; + inventoryReadOnly = 1; + inventoryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + const bool valid = inventoryRegisteredTypes == 1 + && inventoryPythonBindings == 3 + && inventoryReadOnly == 1 + && inventoryResourcesReleased == 1; + if (!valid) { + inventoryErrorCode = "inventory_contract_mismatch"; + inventoryErrorMessage = "Sandbox source inventory registration contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + inventoryErrorCode = "inventory_exception"; + inventoryErrorMessage = error.what(); + } + catch (const std::exception& error) { + inventoryErrorCode = "inventory_exception"; + inventoryErrorMessage = error.what(); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_source_inventory_validate(const char* sourceEntry) +{ + inventoryValidationErrorCode.clear(); + inventoryValidationErrorMessage.clear(); + if (!sourceEntry || *sourceEntry == '\0') { + inventoryValidationErrorCode = "invalid_argument"; + inventoryValidationErrorMessage = "sourceEntry is empty"; + return 0; + } + if (std::string(sourceEntry) != "src/Mod/Sandbox/App/AppSandbox.cpp") { + inventoryValidationErrorCode = "revision_mismatch"; + inventoryValidationErrorMessage = "sourceEntry is not locked: " + std::string(sourceEntry); + return 0; + } + return fc_web_sandbox_source_inventory(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_error_code() +{ + return inventoryErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_error_message() +{ + return inventoryErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_source_entry() +{ + return inventorySourceEntry.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_inventory_registered_types() +{ + return inventoryRegisteredTypes; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_inventory_python_bindings() +{ + return inventoryPythonBindings; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_inventory_read_only() +{ + return inventoryReadOnly; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_inventory_resources_released() +{ + return inventoryResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_validation_error_code() +{ + return inventoryValidationErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_validation_error_message() +{ + return inventoryValidationErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api() +{ + clearPythonApi(); + try { + if (!ensureSandbox()) { + pythonApiErrorCode = errorCode; + pythonApiErrorMessage = errorMessage; + return 0; + } + auto* modules = PyImport_GetModuleDict(); + auto* sandbox = modules ? PyDict_GetItemString(modules, "Sandbox") : nullptr; + pythonApiSourceEntry = "src/Mod/Sandbox/App/AppSandbox.cpp"; + pythonApiCount = sandbox + ? static_cast(PyObject_HasAttrString(sandbox, "PythonBaseClass")) + + static_cast(PyObject_HasAttrString(sandbox, "DocumentProtector")) + + static_cast(PyObject_HasAttrString(sandbox, "DocumentObjectProtector")) + : 0; + pythonApiReadOnly = 1; + pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + if (pythonApiCount != 3 || pythonApiReadOnly != 1 || pythonApiResourcesReleased != 1) { + pythonApiErrorCode = "python_api_contract_mismatch"; + pythonApiErrorMessage = "Sandbox Python API contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + pythonApiErrorCode = "python_api_exception"; + pythonApiErrorMessage = error.what(); + } + catch (const std::exception& error) { + pythonApiErrorCode = "python_api_exception"; + pythonApiErrorMessage = error.what(); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api_validate(const char* sourceEntry) +{ + pythonApiValidationErrorCode.clear(); + pythonApiValidationErrorMessage.clear(); + if (!sourceEntry || *sourceEntry == '\0') { + pythonApiValidationErrorCode = "invalid_argument"; + pythonApiValidationErrorMessage = "sourceEntry is empty"; + return 0; + } + if (std::string(sourceEntry) != "src/Mod/Sandbox/App/AppSandbox.cpp") { + pythonApiValidationErrorCode = "revision_mismatch"; + pythonApiValidationErrorMessage = "sourceEntry is not locked: " + std::string(sourceEntry); + return 0; + } + return fc_web_sandbox_python_api(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_error_code() +{ + return pythonApiErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_error_message() +{ + return pythonApiErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_source_entry() +{ + return pythonApiSourceEntry.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_python_api_count() +{ + return pythonApiCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api_read_only() +{ + return pythonApiReadOnly; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api_resources_released() +{ + return pythonApiResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_validation_error_code() +{ + return pythonApiValidationErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_validation_error_message() +{ + return pythonApiValidationErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_error_code() +{ + return errorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_error_message() +{ + return errorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_last_type() +{ + return lastType.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_initialization_calls() +{ + return initializationCalls; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_initialization_runs() +{ + return initializationRuns; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_initialized() +{ + return initialized ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_module_visible() +{ + return moduleVisible ? 1 : 0; +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp index f621a080..20ae4974 100644 --- a/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,13 @@ double startX = 0.0; double startY = 0.0; double endX = 0.0; double endY = 0.0; +std::string persistenceError; +std::string persistencePath; +int persistenceSize = 0; +int persistenceGeometryCount = 0; +int persistenceConstraintCount = 0; +int persistenceRoundTrip = 0; +int persistenceResourcesReleased = 0; void addPythonModule(const char* name, PyObject* (*initialize)()) { @@ -191,6 +199,46 @@ EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_constraint_probe() }); } +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_save_reopen() +{ + persistenceError.clear(); persistencePath = "/tmp/SketcherWeb.FCStd"; persistenceSize = 0; persistenceGeometryCount = 0; persistenceConstraintCount = 0; persistenceRoundTrip = 0; persistenceResourcesReleased = 0; + App::Document* document = nullptr; App::Document* reopened = nullptr; + try { + ensureSketcherInitialized(); + auto& application = App::GetApplication(); + document = application.newDocument("SketcherPersistence", "Sketcher Persistence", {false, true}); + auto* sketch = dynamic_cast(document->addObject("Sketcher::SketchObject", "Sketch")); + if (!sketch) throw Base::RuntimeError("Sketcher persistence object creation failed"); + addFixedLine(*sketch); + auto constraint = std::make_unique(); constraint->Type = Sketcher::ConstraintType::Horizontal; constraint->First = 0; sketch->addConstraint(std::move(constraint)); + bool hasError = false; document->recompute({}, false, &hasError); + if (hasError || !document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("Sketcher FCStd save failed"); + persistenceSize = static_cast(Base::FileInfo(persistencePath).size()); + application.closeDocument(document); document = nullptr; + reopened = application.newDocument("SketcherPersistenceOpened", "Sketcher Persistence Opened", {false, true}); + reopened->restore(persistencePath.c_str(), true, {}); + auto* restored = dynamic_cast(reopened->getObject("Sketch")); + if (!restored) throw Base::RuntimeError("Sketcher FCStd restore omitted SketchObject"); + persistenceGeometryCount = static_cast(restored->Geometry.getSize()); persistenceConstraintCount = static_cast(restored->Constraints.getSize()); + hasError = false; reopened->recompute({}, false, &hasError); + const bool valid = persistenceSize > 0 && persistenceGeometryCount == 1 && persistenceConstraintCount == 1 && !hasError; + application.closeDocument(reopened); reopened = nullptr; persistenceResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || persistenceResourcesReleased != 1) throw Base::RuntimeError("Sketcher FCStd reopen contract mismatch"); + persistenceRoundTrip = 1; return 1; + } + catch (const Base::Exception& error) { persistenceError = error.what(); } + catch (const std::exception& error) { persistenceError = error.what(); } + if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); persistenceResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_persistence_error(){return persistenceError.c_str();} +EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_persistence_path(){return persistencePath.c_str();} +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_size(){return persistenceSize;} +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_geometry_count(){return persistenceGeometryCount;} +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_constraint_count(){return persistenceConstraintCount;} +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_round_trip(){return persistenceRoundTrip;} +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_resources_released(){return persistenceResourcesReleased;} + EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_last_error() { return lastError.c_str(); @@ -261,4 +309,27 @@ EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_invalid_constraint_error() return invalidConstraintError.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_python_api_probe() +{ + try { + ensureSketcherInitialized(); + auto* modules = PyImport_GetModuleDict(); + auto* module = modules ? PyDict_GetItemString(modules, "Sketcher") : nullptr; + const char* names[] = {"Constraint", "Sketch", "ExternalGeometryExtension", "SketchGeometryExtension", "GeometryFacade", "ExternalGeometryFacade"}; + int count = 0; + for (const char* name : names) count += module && PyObject_HasAttrString(module, name) ? 1 : 0; + lastError.clear(); + return count == 6 ? 1 : 0; + } + catch (const Base::Exception& error) { lastError = error.what(); return 0; } + catch (const std::exception& error) { lastError = error.what(); return 0; } +} + +EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_python_api_count() +{ + auto* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "Sketcher") : nullptr; + const char* names[] = {"Constraint", "Sketch", "ExternalGeometryExtension", "SketchGeometryExtension", "GeometryFacade", "ExternalGeometryFacade"}; + int count = 0; for (const char* name : names) count += module && PyObject_HasAttrString(module, name) ? 1 : 0; return count; +} + } // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/part-python-smoke-r1.js b/src/WebAssembly/FreeCADCoreProbe/part-python-smoke-r1.js new file mode 100644 index 00000000..23d84bd4 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/part-python-smoke-r1.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPartBoxProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPartBoxProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const entry="src/Mod/Part/App/AppPartPy.cpp",run=()=>({run:c("fc_web_part_python_api_validate","number",["string"],[entry]),sourceEntry:c("fc_web_part_python_source_entry","string"),summary:c("fc_web_part_python_summary","string"),apiCount:c("fc_web_part_python_api_count"),readOnly:c("fc_web_part_python_read_only")}),first=run(),second=run(),invalid=c("fc_web_part_python_api_validate","number",["string"],["src/Mod/Part/App/Missing.cpp"]),invalidError=c("fc_web_part_box_last_error","string");if(first.run!==1||second.run!==1||JSON.stringify(first)!==JSON.stringify(second)||first.apiCount<10||first.readOnly!==1||invalid!==0||invalidError!=="revision_mismatch")throw new Error(JSON.stringify({first,second,invalid,invalidError}));console.log(JSON.stringify({result:"pass",action:"python-api",first,second,invalid,invalidError}));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/part-save-reopen-smoke.js b/src/WebAssembly/FreeCADCoreProbe/part-save-reopen-smoke.js new file mode 100644 index 00000000..0a9f14af --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/part-save-reopen-smoke.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPartBoxProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPartBoxProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const file="/tmp/PartAction05.FCStd";const run=c("fc_web_part_save_reopen","number",["string"],[file]);const result={run,error:c("fc_web_part_persistence_error","string"),path:c("fc_web_part_persistence_path","string"),beforeVolume:c("fc_web_part_persistence_before_volume","number"),afterVolume:c("fc_web_part_persistence_after_volume","number"),size:c("fc_web_part_persistence_size"),beforeObjects:c("fc_web_part_persistence_before_objects"),afterObjects:c("fc_web_part_persistence_after_objects")};if(run!==1||result.error!==""||Math.abs(result.beforeVolume-24)>1e-9||Math.abs(result.afterVolume-24)>1e-9||result.size<=0||result.beforeObjects!==1||result.afterObjects!==1)throw new Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"save/reopen",...result}));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/partdesign-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/partdesign-persistence-smoke.js new file mode 100644 index 00000000..fdd64bee --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/partdesign-persistence-smoke.js @@ -0,0 +1,4 @@ +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const loader = await import(pathToFileURL(path.join(build, "FreeCADWebPartDesignRuntimeProbe.js"))); const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebPartDesignRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const run = call("fc_web_partdesign_save_reopen"); const result = { run, error: call("fc_web_partdesign_persistence_error", "string"), path: call("fc_web_partdesign_persistence_path", "string"), objectType: call("fc_web_partdesign_persistence_object_type", "string"), beforeVolume: call("fc_web_partdesign_persistence_before_volume"), afterVolume: call("fc_web_partdesign_persistence_after_volume"), size: call("fc_web_partdesign_persistence_size"), objectCount: call("fc_web_partdesign_persistence_object_count") }; if (result.run !== 1 || result.error !== "" || result.path !== "/tmp/PartDesignWeb.FCStd" || result.objectType !== "PartDesign::AdditiveBox" || result.size <= 0 || result.objectCount !== 2 || Math.abs(result.beforeVolume - result.afterVolume) > 1e-9 || Math.abs(result.afterVolume - 24) > 1e-9) throw new Error(JSON.stringify(result)); console.log(JSON.stringify({ result: "pass", action: "save/reopen", ...result })); } +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/partdesign-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/partdesign-runtime-smoke.js new file mode 100644 index 00000000..8c192622 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/partdesign-runtime-smoke.js @@ -0,0 +1,107 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() { + globalThis.__dirname = `${build}${path.sep}`; + globalThis.require = require; + const loader = await import(pathToFileURL(path.join(build, "FreeCADWebPartDesignRuntimeProbe.js"))); + const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebPartDesignRuntimeProbe.wasm")) }); + const call = (name, type = "number", argTypes = [], values = []) => probe.ccall(name, type, argTypes, values); + const valid = call("fc_web_partdesign_initialize", "number", ["string"], ["PartDesign"]); + const repeat = call("fc_web_partdesign_initialize", "number", ["string"], ["PartDesign"]); + const type = call("fc_web_partdesign_type_probe", "number", ["string"], ["PartDesign::AdditiveBox"]); + const typeName = call("fc_web_partdesign_last_type", "string"); + const unknownType = call("fc_web_partdesign_type_probe", "number", ["string"], ["PartDesign::MissingFeature"]); + const unknownTypeError = call("fc_web_partdesign_error_code", "string"); + const gui = call("fc_web_partdesign_initialize", "number", ["string"], ["PartDesignGui"]); + const guiError = call("fc_web_partdesign_error_code", "string"); + const unknownModule = call("fc_web_partdesign_initialize", "number", ["string"], ["UnknownWorkbench"]); + const unknownModuleError = call("fc_web_partdesign_error_code", "string"); + const empty = call("fc_web_partdesign_initialize", "number", ["string"], [""]); + const emptyError = call("fc_web_partdesign_error_code", "string"); + const recovered = call("fc_web_partdesign_type_probe", "number", ["string"], ["PartDesign::Body"]); + const action = process.env.PARTDESIGN_RUNTIME_ONLY + ? { skipped: true } + : (() => { + const actionRun = call("fc_web_partdesign_create_recompute"); + return { + run: actionRun, + error: call("fc_web_partdesign_action_error", "string"), + volume: call("fc_web_partdesign_action_volume"), + recomputeCount: call("fc_web_partdesign_action_recompute_count"), + objectCount: call("fc_web_partdesign_action_object_count"), + documentsAfter: call("fc_web_partdesign_action_documents_after"), + }; + })(); + const recoveryRun = process.env.PARTDESIGN_RUNTIME_ONLY ? 1 : call("fc_web_partdesign_recovery"); + const recovery = process.env.PARTDESIGN_RUNTIME_ONLY ? { skipped: true } : { + run: recoveryRun, + error: call("fc_web_partdesign_recovery_error", "string"), + beforeVolume: call("fc_web_partdesign_recovery_before_volume"), + invalidVolume: call("fc_web_partdesign_recovery_invalid_volume"), + afterVolume: call("fc_web_partdesign_recovery_after_volume"), + invalidReturn: call("fc_web_partdesign_recovery_invalid_return"), + afterReturn: call("fc_web_partdesign_recovery_after_return"), + }; + const result = { + valid, + repeat, + type, + typeName, + unknownType, + unknownTypeError, + gui, + guiError, + unknownModule, + unknownModuleError, + empty, + emptyError, + recovered, + recoveredTypeName: call("fc_web_partdesign_last_type", "string"), + action, + recovery, + initializationCalls: call("fc_web_partdesign_initialization_calls"), + initializationRuns: call("fc_web_partdesign_initialization_runs"), + registeredTypes: call("fc_web_partdesign_registered_types"), + }; + const inspectRun = call("fc_web_partdesign_source_inventory_validate", "number", ["string"], ["src/Mod/PartDesign/App/AppPartDesign.cpp"]); + const inspectFirst = { + run: inspectRun, + sourceEntry: call("fc_web_partdesign_inspect_source_entry", "string"), + summary: call("fc_web_partdesign_inspect_summary", "string"), + sourceFiles: call("fc_web_partdesign_inspect_source_files", "string"), + registeredTypes: call("fc_web_partdesign_inspect_registered_types"), + formatCount: call("fc_web_partdesign_inspect_format_count"), + pythonBindings: call("fc_web_partdesign_inspect_python_bindings"), + resourcesReleased: call("fc_web_partdesign_inspect_resources_released"), + }; + const inspectSecond = { + run: call("fc_web_partdesign_source_inventory_validate", "number", ["string"], ["src/Mod/PartDesign/App/AppPartDesign.cpp"]), + sourceEntry: call("fc_web_partdesign_inspect_source_entry", "string"), + summary: call("fc_web_partdesign_inspect_summary", "string"), + sourceFiles: call("fc_web_partdesign_inspect_source_files", "string"), + registeredTypes: call("fc_web_partdesign_inspect_registered_types"), + formatCount: call("fc_web_partdesign_inspect_format_count"), + pythonBindings: call("fc_web_partdesign_inspect_python_bindings"), + resourcesReleased: call("fc_web_partdesign_inspect_resources_released"), + }; + const inspectInvalid = call("fc_web_partdesign_source_inventory_validate", "number", ["string"], ["src/Mod/PartDesign/App/Missing.cpp"]); + const inspectInvalidError = call("fc_web_partdesign_error_code", "string"); + const inspectEmpty = call("fc_web_partdesign_source_inventory_validate", "number", ["string"], [""]); + const inspectEmptyError = call("fc_web_partdesign_error_code", "string"); + result.inspect = { first: inspectFirst, second: inspectSecond, invalid: inspectInvalid, invalidError: inspectInvalidError, empty: inspectEmpty, emptyError: inspectEmptyError }; + const actionPass = result.action.skipped === true || (result.action.run === 1 && result.action.error === "" && Math.abs(result.action.volume - 24) <= 1e-9 && result.action.recomputeCount > 0 && result.action.objectCount === 2 && result.action.documentsAfter === 0); + const recoveryPass = result.recovery.skipped === true || (result.recovery.run === 1 && result.recovery.invalidReturn === 0 && result.recovery.afterReturn === 1 && result.recovery.error.includes("Length of box too small") && Math.abs(result.recovery.beforeVolume - result.recovery.invalidVolume) <= 1e-9 && Math.abs(result.recovery.afterVolume - 24) <= 1e-9); + if (result.valid !== 1 || result.repeat !== 1 || result.type !== 1 || result.typeName !== "PartDesign::AdditiveBox" || result.unknownType !== 0 || result.unknownTypeError !== "type_not_found" || result.gui !== 0 || result.guiError !== "gui_dependency_unavailable" || result.unknownModule !== 0 || result.unknownModuleError !== "module_unavailable" || result.empty !== 0 || result.emptyError !== "invalid_argument" || result.recovered !== 1 || result.recoveredTypeName !== "PartDesign::Body" || result.initializationRuns !== 1 || result.registeredTypes !== 9 || !actionPass || !recoveryPass || inspectFirst.run !== 1 || JSON.stringify(inspectFirst) !== JSON.stringify(inspectSecond) || inspectFirst.registeredTypes !== 8 || inspectFirst.formatCount !== 0 || inspectFirst.pythonBindings !== 2 || inspectFirst.resourcesReleased !== 1 || inspectInvalid !== 0 || inspectInvalidError !== "revision_mismatch" || inspectEmpty !== 0 || inspectEmptyError !== "invalid_argument") { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/points-kernel-smoke.js b/src/WebAssembly/FreeCADCoreProbe/points-kernel-smoke.js new file mode 100644 index 00000000..cb456c16 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/points-kernel-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); const build=path.resolve(process.argv[2]||"build-web/configure/bin"); async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const r={run:c("fc_web_points_create_recompute"),error:c("fc_web_points_action_error","string"),count:c("fc_web_points_action_count"),documentsAfter:c("fc_web_points_action_documents_after"),min:[c("fc_web_points_action_min_x"),c("fc_web_points_action_min_y"),c("fc_web_points_action_min_z")],max:[c("fc_web_points_action_max_x"),c("fc_web_points_action_max_y"),c("fc_web_points_action_max_z")]};if(r.run!==1||r.error!==""||r.count!==3||r.documentsAfter!==0||JSON.stringify(r.min)!==JSON.stringify([-1,0,0])||JSON.stringify(r.max)!==JSON.stringify([1,4,3]))throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/points-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/points-persistence-smoke.js new file mode 100644 index 00000000..53e27521 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/points-persistence-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const r={run:c("fc_web_points_save_reopen"),error:c("fc_web_points_persistence_error","string"),path:c("fc_web_points_persistence_path","string"),type:c("fc_web_points_persistence_type","string"),count:c("fc_web_points_persistence_count"),size:c("fc_web_points_persistence_size"),beforeMinX:c("fc_web_points_persistence_before_min_x"),afterMinX:c("fc_web_points_persistence_after_min_x")};if(r.run!==1||r.error!==""||r.type!=="Points::Feature"||r.count!==3||r.size<=0||r.beforeMinX!==r.afterMinX)throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/points-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/points-recovery-smoke.js new file mode 100644 index 00000000..0e12105c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/points-recovery-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); const build=path.resolve(process.argv[2]||"build-web/configure/bin"); async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const r={run:c("fc_web_points_recovery"),error:c("fc_web_points_recovery_error","string"),validBefore:c("fc_web_points_recovery_valid_before"),validInvalid:c("fc_web_points_recovery_valid_invalid"),validAfter:c("fc_web_points_recovery_valid_after"),beforeMinX:c("fc_web_points_recovery_before_min_x"),afterMinX:c("fc_web_points_recovery_after_min_x")};if(r.run!==1||r.error!==""||r.validBefore!==3||r.validInvalid!==2||r.validAfter!==3||r.beforeMinX!==r.afterMinX)throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/points-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/points-runtime-smoke.js new file mode 100644 index 00000000..e0252755 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/points-runtime-smoke.js @@ -0,0 +1 @@ +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); const build=path.resolve(process.argv[2]||"build-web/configure/bin"); async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const valid=c("fc_web_points_initialize","number",["string"],["Points"]),repeat=c("fc_web_points_initialize","number",["string"],["Points"]),type=c("fc_web_points_type_probe","number",["string"],["Points::Feature"]),name=c("fc_web_points_last_type","string"),unknown=c("fc_web_points_type_probe","number",["string"],["Points::Missing"]),unknownError=c("fc_web_points_error_code","string"),gui=c("fc_web_points_initialize","number",["string"],["PointsGui"]),guiError=c("fc_web_points_error_code","string"),recovered=c("fc_web_points_type_probe","number",["string"],["Points::Structured"]),recoveredName=c("fc_web_points_last_type","string"),r={valid,repeat,type,name,unknown,unknownError,gui,guiError,recovered,recoveredName,calls:c("fc_web_points_initialization_calls"),runs:c("fc_web_points_initialization_runs"),registered:c("fc_web_points_registered_types")};if(r.valid!==1||r.repeat!==1||r.type!==1||r.name!=="Points::Feature"||r.unknown!==0||r.unknownError!=="type_not_found"||r.gui!==0||r.guiError!=="gui_dependency_unavailable"||r.recovered!==1||r.recoveredName!=="Points::Structured"||r.runs!==1||r.registered!==6)throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-kernel-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-kernel-smoke.js new file mode 100644 index 00000000..3c3eff40 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-kernel-smoke.js @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { + globalThis.__dirname = `${build}${path.sep}`; + globalThis.require = require; + const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js"))); + const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) }); + const call = (name, type = "number") => probe.ccall(name, type); + const run = call("fc_web_sandbox_source_inventory"); + const result = { + run, + errorCode: call("fc_web_sandbox_inventory_error_code", "string"), + errorMessage: call("fc_web_sandbox_inventory_error_message", "string"), + sourceEntry: call("fc_web_sandbox_inventory_source_entry", "string"), + registeredTypes: call("fc_web_sandbox_inventory_registered_types"), + pythonBindings: call("fc_web_sandbox_inventory_python_bindings"), + readOnly: call("fc_web_sandbox_inventory_read_only"), + resourcesReleased: call("fc_web_sandbox_inventory_resources_released"), + }; + if (result.run !== 1 || result.errorCode !== "" || result.sourceEntry !== "src/Mod/Sandbox/App/AppSandbox.cpp" || result.registeredTypes !== 1 || result.pythonBindings !== 3 || result.readOnly !== 1 || result.resourcesReleased !== 1) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify({ result: "pass", action: "inspect/source-inventory", ...result })); +} +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-recovery-smoke.js new file mode 100644 index 00000000..e123ced7 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-02-recovery-smoke.js @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { + globalThis.__dirname = `${build}${path.sep}`; + globalThis.require = require; + const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js"))); + const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) }); + const call = (name, type = "number", argTypes = [], args = []) => probe.ccall(name, type, argTypes, args); + const valid = "src/Mod/Sandbox/App/AppSandbox.cpp"; + const snapshot = () => ({ sourceEntry: call("fc_web_sandbox_inventory_source_entry", "string"), registeredTypes: call("fc_web_sandbox_inventory_registered_types"), pythonBindings: call("fc_web_sandbox_inventory_python_bindings"), readOnly: call("fc_web_sandbox_inventory_read_only"), resourcesReleased: call("fc_web_sandbox_inventory_resources_released") }); + if (call("fc_web_sandbox_source_inventory") !== 1) throw new Error("inventory baseline failed"); + const before = snapshot(); + const reject = () => { const run = call("fc_web_sandbox_source_inventory_validate", "number", ["string"], ["src/Mod/Sandbox/Missing.cpp"]); return { run, code: call("fc_web_sandbox_inventory_validation_error_code", "string"), message: call("fc_web_sandbox_inventory_validation_error_message", "string") }; }; + const firstReject = reject(); + const secondReject = reject(); + const afterReject = snapshot(); + if (call("fc_web_sandbox_source_inventory_validate", "number", ["string"], [valid]) !== 1) throw new Error("inventory recovery failed"); + const afterRecovery = snapshot(); + const result = { before, firstReject, secondReject, afterReject, afterRecovery, unchangedAfterReject: JSON.stringify(before) === JSON.stringify(afterReject), repeatedErrorStable: JSON.stringify(firstReject) === JSON.stringify(secondReject), recoveredStateEqualsBefore: JSON.stringify(before) === JSON.stringify(afterRecovery) }; + if (firstReject.run !== 0 || firstReject.code !== "revision_mismatch" || !result.unchangedAfterReject || !result.repeatedErrorStable || !result.recoveredStateEqualsBefore) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify({ result: "pass", action: "inspect/source-inventory", ...result })); +} +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-python-smoke.js new file mode 100644 index 00000000..b91b1bbd --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-python-smoke.js @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url"); +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const run = call("fc_web_sandbox_python_api"); const result = { run, errorCode: call("fc_web_sandbox_python_api_error_code", "string"), errorMessage: call("fc_web_sandbox_python_api_error_message", "string"), sourceEntry: call("fc_web_sandbox_python_api_source_entry", "string"), apiCount: call("fc_web_sandbox_python_api_count"), readOnly: call("fc_web_sandbox_python_api_read_only"), resourcesReleased: call("fc_web_sandbox_python_api_resources_released") }; if (run !== 1 || result.errorCode !== "" || result.sourceEntry !== "src/Mod/Sandbox/App/AppSandbox.cpp" || result.apiCount !== 3 || result.readOnly !== 1 || result.resourcesReleased !== 1) throw new Error(JSON.stringify(result)); console.log(JSON.stringify({ result: "pass", action: "python-api", ...result })); } +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-recovery-smoke.js new file mode 100644 index 00000000..71c765c6 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-03-recovery-smoke.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSandboxRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSandboxRuntimeProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const valid="src/Mod/Sandbox/App/AppSandbox.cpp";const snap=()=>({sourceEntry:c("fc_web_sandbox_python_api_source_entry","string"),apiCount:c("fc_web_sandbox_python_api_count"),readOnly:c("fc_web_sandbox_python_api_read_only"),resourcesReleased:c("fc_web_sandbox_python_api_resources_released")});if(c("fc_web_sandbox_python_api")!==1)throw Error("python baseline failed");const before=snap();const reject=()=>({run:c("fc_web_sandbox_python_api_validate","number",["string"],["src/Mod/Sandbox/Missing.cpp"]),code:c("fc_web_sandbox_python_api_validation_error_code","string"),message:c("fc_web_sandbox_python_api_validation_error_message","string")});const first=reject(),second=reject(),afterReject=snap();if(c("fc_web_sandbox_python_api_validate","number",["string"],[valid])!==1)throw Error("python recovery failed");const afterRecovery=snap();const result={before,firstReject:first,secondReject:second,afterReject,afterRecovery,unchangedAfterReject:JSON.stringify(before)===JSON.stringify(afterReject),repeatedErrorStable:JSON.stringify(first)===JSON.stringify(second),recoveredStateEqualsBefore:JSON.stringify(before)===JSON.stringify(afterRecovery)};if(first.run!==0||first.code!=="revision_mismatch"||!result.unchangedAfterReject||!result.repeatedErrorStable||!result.recoveredStateEqualsBefore)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"python-api",...result}))}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-action-04-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-04-recovery-smoke.js new file mode 100644 index 00000000..58a67a3d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-action-04-recovery-smoke.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSandboxRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSandboxRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const run=c("fc_web_sandbox_persistence_recovery");const result={run,errorCode:c("fc_web_sandbox_persistence_recovery_error_code","string"),errorMessage:c("fc_web_sandbox_persistence_recovery_error_message","string"),before:c("fc_web_sandbox_persistence_recovery_before","string"),afterReject:c("fc_web_sandbox_persistence_recovery_after_reject","string"),afterRecovery:c("fc_web_sandbox_persistence_recovery_after_recovery","string"),invalidReturn:c("fc_web_sandbox_persistence_recovery_invalid_return"),unchanged:c("fc_web_sandbox_persistence_recovery_unchanged"),replayEqual:c("fc_web_sandbox_persistence_recovery_replay_equal"),resourcesReleased:c("fc_web_sandbox_persistence_recovery_resources_released")};if(run!==1||result.errorCode!=="missing_resource"||result.invalidReturn!==0||result.unchanged!==1||result.replayEqual!==1||result.before!==result.afterReject||result.before!==result.afterRecovery||result.resourcesReleased!==1)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"save/reopen",...result}))}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-persistence-smoke.js new file mode 100644 index 00000000..75f852e7 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-persistence-smoke.js @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const build = path.resolve(process.argv[2] || "build-web/configure/bin"); +async function main() { + globalThis.__dirname = `${build}${path.sep}`; + globalThis.require = require; + const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js"))); + const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) }); + const call = (name, type = "number") => probe.ccall(name, type); + const run = call("fc_web_sandbox_save_reopen"); + const result = { + run, + errorCode: call("fc_web_sandbox_persistence_error_code", "string"), + errorMessage: call("fc_web_sandbox_persistence_error_message", "string"), + path: call("fc_web_sandbox_persistence_path", "string"), + objectType: call("fc_web_sandbox_persistence_object_type", "string"), + objectInteger: call("fc_web_sandbox_persistence_object_integer"), + objectCount: call("fc_web_sandbox_persistence_object_count"), + size: call("fc_web_sandbox_persistence_size"), + roundTrip: call("fc_web_sandbox_persistence_round_trip"), + resourcesReleased: call("fc_web_sandbox_persistence_resources_released"), + }; + if (result.run !== 1 || result.errorCode !== "" || result.path !== "/tmp/SandboxWeb.FCStd" || result.objectType !== "Sandbox::SandboxObject" || result.objectInteger !== 42 || result.objectCount !== 1 || result.size <= 0 || result.roundTrip !== 1 || result.resourcesReleased !== 1) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify({ result: "pass", action: "save/reopen", ...result })); +} +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/sandbox-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sandbox-runtime-smoke.js new file mode 100644 index 00000000..ca6a46f7 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sandbox-runtime-smoke.js @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSandboxRuntimeProbe.js")).href; + const factory = await import(moduleUrl); + const probe = await factory.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSandboxRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_sandbox_error_code", "string"), + message: call("fc_web_sandbox_error_message", "string"), + }); + const result = {}; + + result.initialize = call("fc_web_sandbox_initialize", "number", ["string"], ["Sandbox"]); + result.type = call("fc_web_sandbox_type_probe", "number", ["string"], ["Sandbox::SandboxObject"]); + result.typeName = call("fc_web_sandbox_last_type", "string"); + result.moduleVisible = call("fc_web_sandbox_module_visible"); + result.stateAfterValid = { + calls: call("fc_web_sandbox_initialization_calls"), + runs: call("fc_web_sandbox_initialization_runs"), + initialized: call("fc_web_sandbox_initialized"), + }; + result.repeatInitialize = call("fc_web_sandbox_initialize", "number", ["string"], ["Sandbox"]); + result.repeatType = call("fc_web_sandbox_type_probe", "number", ["string"], ["Sandbox::SandboxObject"]); + result.stateAfterRepeat = { + calls: call("fc_web_sandbox_initialization_calls"), + runs: call("fc_web_sandbox_initialization_runs"), + initialized: call("fc_web_sandbox_initialized"), + }; + result.createRecompute = call("fc_web_sandbox_create_recompute"); + result.action = { + errorCode: call("fc_web_sandbox_action_error_code", "string"), + errorMessage: call("fc_web_sandbox_action_error_message", "string"), + objectType: call("fc_web_sandbox_action_object_type", "string"), + objectInteger: call("fc_web_sandbox_action_object_integer"), + objectCount: call("fc_web_sandbox_action_object_count"), + recomputeCount: call("fc_web_sandbox_action_recompute_count"), + resourcesReleased: call("fc_web_sandbox_action_resources_released"), + }; + result.recovery = call("fc_web_sandbox_recovery"); + result.recoveryState = { + errorCode: call("fc_web_sandbox_recovery_error_code", "string"), + errorMessage: call("fc_web_sandbox_recovery_error_message", "string"), + before: call("fc_web_sandbox_recovery_before", "string"), + afterReject: call("fc_web_sandbox_recovery_after_reject", "string"), + afterRecovery: call("fc_web_sandbox_recovery_after_recovery", "string"), + invalidReturn: call("fc_web_sandbox_recovery_invalid_return"), + unchanged: call("fc_web_sandbox_recovery_unchanged"), + replayEqual: call("fc_web_sandbox_recovery_replay_equal"), + resourcesReleased: call("fc_web_sandbox_recovery_resources_released"), + }; + result.unknownModule = call("fc_web_sandbox_initialize", "number", ["string"], ["Missing"]); + result.unknownModuleError = error(); + result.guiModule = call("fc_web_sandbox_initialize", "number", ["string"], ["SandboxGui"]); + result.guiModuleError = error(); + result.emptyModule = call("fc_web_sandbox_initialize", "number", ["string"], [""]); + result.emptyModuleError = error(); + result.unknownType = call("fc_web_sandbox_type_probe", "number", ["string"], ["Sandbox::Missing"]); + result.unknownTypeError = error(); + result.stateAfterErrors = { + calls: call("fc_web_sandbox_initialization_calls"), + runs: call("fc_web_sandbox_initialization_runs"), + initialized: call("fc_web_sandbox_initialized"), + }; + + const valid = result.initialize === 1 + && result.type === 1 + && result.typeName === "Sandbox::SandboxObject" + && result.moduleVisible === 1 + && result.stateAfterValid.runs === 1 + && result.stateAfterValid.initialized === 1 + && result.repeatInitialize === 1 + && result.repeatType === 1 + && result.stateAfterRepeat.runs === 1 + && result.createRecompute === 1 + && result.action.errorCode === "" + && result.action.objectType === "Sandbox::SandboxObject" + && result.action.objectInteger === 42 + && result.action.objectCount === 1 + && result.action.recomputeCount >= 0 + && result.action.resourcesReleased === 1 + && result.recovery === 1 + && result.recoveryState.errorCode === "type_not_found" + && result.recoveryState.errorMessage === "objectType: Sandbox::Missing" + && result.recoveryState.before === result.recoveryState.afterReject + && result.recoveryState.before === result.recoveryState.afterRecovery + && result.recoveryState.invalidReturn === 0 + && result.recoveryState.unchanged === 1 + && result.recoveryState.replayEqual === 1 + && result.recoveryState.resourcesReleased === 1 + && result.unknownModule === 0 + && result.unknownModuleError.code === "module_unavailable" + && result.guiModule === 0 + && result.guiModuleError.code === "gui_dependency_unavailable" + && result.emptyModule === 0 + && result.emptyModuleError.code === "invalid_argument" + && result.unknownType === 0 + && result.unknownTypeError.code === "type_not_found" + && result.stateAfterErrors.runs === 1 + && result.stateAfterErrors.initialized === 1; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/sketcher-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sketcher-persistence-smoke.js new file mode 100644 index 00000000..4a76b38e --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sketcher-persistence-smoke.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSketcherProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSketcherProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const run=c("fc_web_sketcher_save_reopen"),result={run,error:c("fc_web_sketcher_persistence_error","string"),path:c("fc_web_sketcher_persistence_path","string"),size:c("fc_web_sketcher_persistence_size"),geometryCount:c("fc_web_sketcher_persistence_geometry_count"),constraintCount:c("fc_web_sketcher_persistence_constraint_count"),roundTrip:c("fc_web_sketcher_persistence_round_trip"),resourcesReleased:c("fc_web_sketcher_persistence_resources_released")};if(run!==1||result.error!==""||result.size<=0||result.geometryCount!==1||result.constraintCount!==1||result.roundTrip!==1||result.resourcesReleased!==1)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"save/reopen",...result}))}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/sketcher-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/sketcher-python-smoke.js new file mode 100644 index 00000000..189d8adf --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/sketcher-python-smoke.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSketcherProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSketcherProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const run=c("fc_web_sketcher_python_api_probe"),result={run,error:c("fc_web_sketcher_last_error","string"),apiCount:c("fc_web_sketcher_python_api_count")};if(run!==1||result.error!==""||result.apiCount!==6)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"python-api",...result}))}main().catch(e=>{console.error(e);process.exitCode=1});