From 7eac75d2a8407f3768561025bcc05d646458572d Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Wed, 19 Aug 2026 13:11:14 +0200 Subject: [PATCH 01/11] UCAAS-1486: add remote capability gating to SnaccROSEBase and TS gluecode. Gate outbound invokes when negotiate snapshot is applied and mode is enabled, add ROSE_REJECT_REMOTENOTCAPABLE, module-capability helpers, and registry-aligned lookup APIs. Bump esnacc to 7.0.15. Co-authored-by: Cursor --- .../TSASN1Base.remoteCapability.test.ts | 128 +++++++++++++++ .../back-ends/ts-gen/gluecode/TSASN1Base.ts | 116 +++++++++++++ .../back-ends/ts-gen/gluecode/TSASN1Client.ts | 4 + .../back-ends/ts-gen/gluecode/TSASN1Server.ts | 4 + .../ts-gen/gluecode/TSModuleCapabilities.ts | 94 +++++++++++ .../back-ends/ts-gen/gluecode/TSROSEBase.ts | 20 +++ cpp-lib/include/SnaccModuleCapabilities.h | 34 ++++ cpp-lib/include/SnaccROSEBase.h | 21 +++ cpp-lib/include/SnaccROSEInterfaces.h | 2 + cpp-lib/include/SnaccRoseOperationLookup.h | 9 + cpp-lib/src/SnaccModuleCapabilities.cpp | 57 +++++++ cpp-lib/src/SnaccROSEBase.cpp | 71 ++++++++ cpp-lib/src/SnaccRoseOperationLookup.cpp | 9 + cpp-lib/tests/CMakeLists.txt | 1 + cpp-lib/tests/remote_capability_tests.cpp | 155 ++++++++++++++++++ version.h | 6 +- 16 files changed, 728 insertions(+), 3 deletions(-) create mode 100644 compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts create mode 100644 compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.ts create mode 100644 cpp-lib/include/SnaccModuleCapabilities.h create mode 100644 cpp-lib/src/SnaccModuleCapabilities.cpp create mode 100644 cpp-lib/tests/remote_capability_tests.cpp diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts new file mode 100644 index 0000000..3a7efcc --- /dev/null +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts @@ -0,0 +1,128 @@ +// Run: npx tsx compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { + ASN1ClassInstanceType, + EASN1TransportEncoding, + TSASN1Base, +} from "./TSASN1Base.js"; +import { + CustomInvokeProblemEnum, + RemoteCapabilityMode, + ROSE_REJECT_REMOTENOTCAPABLE, +} from "./TSROSEBase.js"; +import { buildRemoteModuleCapabilities } from "./TSModuleCapabilities.js"; +import type { IASN1InvokeData } from "./TSROSEBase.js"; +import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "./SNACCROSE.js"; + +class TestTransport extends TSASN1Base { + public sendInvokeCount = 0; + + public constructor() { + super(EASN1TransportEncoding.JSON, ASN1ClassInstanceType.TSASN1NodeClient); + } + + public async sendInvoke(data: IASN1InvokeData): Promise { + const localReject = this.tryRejectRemoteNotCapable(data.invoke); + if (localReject) + return localReject; + ++this.sendInvokeCount; + return undefined; + } + + public sendEventSync(_data: IASN1InvokeData): boolean { + return true; + } + + public getSessionID(): string | undefined { + return undefined; + } +} + +const noopHandler = { + getNameForOperationID: () => undefined, + getIDForOperationName: () => undefined, + onInvoke: async () => undefined, +}; + +function createInvoke(operationID: number, operationName: string, invokeID = 1): ROSEInvoke { + return { + invokeID, + operationID, + operationName, + } as ROSEInvoke; +} + +test("lookUpName and lookUpModuleName resolve registered handlers", () => { + const transport = new TestTransport(); + transport.registerModuleVersion("TestModule", "1.0.0"); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + + assert.equal(transport.lookUpName(100), "asnInvoke"); + assert.equal(transport.lookUpID("asnInvoke"), 100); + assert.equal(transport.lookUpModuleName(100), "TestModule"); +}); + +test("enabled without snapshot does not gate sendInvoke", async () => { + const transport = new TestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); + + await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + payLoad: new Uint8Array(), + } as IASN1InvokeData); + + assert.equal(transport.sendInvokeCount, 1); +}); + +test("enabled with unsupported op id returns remoteNotCapable reject", async () => { + const transport = new TestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ + { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, + ])); + transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); + + const result = await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + payLoad: new Uint8Array(), + } as IASN1InvokeData); + + assert.ok(result); + assert.equal((result as ROSEReject).reject.invokeProblem, CustomInvokeProblemEnum.remoteNotCapable); + assert.equal(CustomInvokeProblemEnum.remoteNotCapable, ROSE_REJECT_REMOTENOTCAPABLE); + assert.equal(transport.sendInvokeCount, 0); +}); + +test("isSupportedOperation reflects applied snapshot", () => { + const transport = new TestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ + { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [100] }, + ])); + + assert.equal(transport.hasRemoteModuleCapabilities(), true); + assert.equal(transport.isSupportedOperation(100), true); + assert.equal(transport.isSupportedOperation(200), false); +}); + +test("clearRemoteModuleCapabilities stops gating", async () => { + const transport = new TestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ + { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, + ])); + transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); + transport.clearRemoteModuleCapabilities(); + + await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + payLoad: new Uint8Array(), + } as IASN1InvokeData); + + assert.equal(transport.sendInvokeCount, 1); +}); diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts index 33335e4..ecd40d9 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts @@ -29,6 +29,7 @@ import { IROSELogger, ISendInvokeContext, ReceiveInvokeContext, + RemoteCapabilityMode, ASN1ByteArray, ROSEBase, } from "./TSROSEBase.js"; @@ -301,6 +302,10 @@ export abstract class TSASN1Base implements IASN1Transport { private handlersByName = new Map(); // Holds all loaded modules with operations registered on this stub private loadedModulesByName = new Map(); + // Peer negotiate snapshot applied on this stub (client/server outbound gating) + private remoteModuleCapabilitiesByName = new Map(); + private remoteModuleCapabilitiesSet = false; + private remoteCapabilityMode = RemoteCapabilityMode.Disabled; // The Logger Callback which must be set with the SetLogger Method protected logger?: IROSELogger; // Logs the raw transport (inbound before decoding, outbound after encoding) @@ -500,6 +505,117 @@ export abstract class TSASN1Base implements IASN1Transport { return this.loadedModulesByName; } + /** + * Resolves operation name from operation id via the registered handlers. + */ + public lookUpName(operationID: number): string | undefined { + return this.handlersByID.get(operationID)?.operationName; + } + + /** + * Resolves operation id from operation name via the registered handlers. + */ + public lookUpID(operationName: string): number | undefined { + return this.handlersByName.get(operationName)?.operationID; + } + + /** + * Resolves ASN.1 module name owning the given operation id on this stub. + */ + public lookUpModuleName(operationID: number): string | undefined { + return this.handlersByID.get(operationID)?.moduleName; + } + + /** + * Configures whether outbound invokes are gated on a negotiate snapshot. Default Disabled. + */ + public setRemoteCapabilityMode(mode: RemoteCapabilityMode): void { + this.remoteCapabilityMode = mode; + } + + /** + * Returns the current remote capability gating mode for outbound invokes. + */ + public getRemoteCapabilityMode(): RemoteCapabilityMode { + return this.remoteCapabilityMode; + } + + /** + * Stores the peer module snapshot from asnNegotiateInterface (or equivalent). + */ + public applyRemoteModuleCapabilities(remote: ReadonlyMap): void { + this.remoteModuleCapabilitiesByName = new Map( + [...remote.entries()].map(([moduleName, moduleInfo]) => [ + moduleName, + { + moduleName: moduleInfo.moduleName, + version: moduleInfo.version, + invokes: new Map(moduleInfo.invokes), + events: new Map(moduleInfo.events), + }, + ]), + ); + this.remoteModuleCapabilitiesSet = true; + } + + /** + * Clears the applied remote capability snapshot (disconnect / legacy fallback). + */ + public clearRemoteModuleCapabilities(): void { + this.remoteModuleCapabilitiesByName.clear(); + this.remoteModuleCapabilitiesSet = false; + } + + /** + * True after applyRemoteModuleCapabilities() was called (even when the map is empty). + */ + public hasRemoteModuleCapabilities(): boolean { + return this.remoteModuleCapabilitiesSet; + } + + /** + * Returns true when the negotiate snapshot offers this invoke OPID. + * Debug assert when hasRemoteModuleCapabilities() is false. + */ + public isSupportedOperation(operationID: number): boolean { + console.assert( + this.remoteModuleCapabilitiesSet, + "isSupportedOperation requires applyRemoteModuleCapabilities first", + ); + return this.internalIsRemoteOperationSupported(operationID); + } + + /** + * Local reject for outbound invokes blocked by remote capability gating. + * Events (invokeID 99999) are never gated here. + */ + protected tryRejectRemoteNotCapable(invoke: ROSEInvoke): ROSEReject | undefined { + if (invoke.invokeID === 99999) + return undefined; + if (this.remoteCapabilityMode !== RemoteCapabilityMode.Enabled || !this.remoteModuleCapabilitiesSet) + return undefined; + if (this.internalIsRemoteOperationSupported(invoke.operationID)) + return undefined; + return createInvokeReject( + invoke, + CustomInvokeProblemEnum.remoteNotCapable, + `Operation ${invoke.operationName} (${invoke.operationID}) is not offered by the remote peer`, + ); + } + + /** + * Returns true when the applied remote snapshot lists the invoke OPID for its module. + */ + private internalIsRemoteOperationSupported(operationID: number): boolean { + const moduleName = this.lookUpModuleName(operationID); + if (!moduleName) + return false; + const module = this.remoteModuleCapabilitiesByName.get(moduleName); + if (!module) + return false; + return module.invokes.has(operationID); + } + /** * Retrieves version information for a loaded asn1 module (a module that has registere ROSE invoke handlers) * diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts index 83ee34a..1d675d7 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts @@ -271,6 +271,10 @@ export abstract class TSASN1Client extends TSASN1Base implements IASN1Transport * If no timeout was specified we resolve in undefined to cleanup the promise object */ public async sendInvoke(data: IASN1InvokeData): Promise { + const localReject = this.tryRejectRemoteNotCapable(data.invoke); + if (localReject) + return localReject; + return new Promise((resolve): void => { let resolveUndefined = true; diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts index c1eed7f..fa235a1 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts @@ -135,6 +135,10 @@ export class TSASN1Server extends TSASN1Base implements IASN1Transport { * If no timeout was specified we resolve in undefined to cleanup the promise object */ public async sendInvoke(data: IASN1InvokeData): Promise { + const localReject = this.tryRejectRemoteNotCapable(data.invoke); + if (localReject) + return localReject; + const clientConnectionID = data.invokeContext.clientConnectionID || data.invoke.sessionID; if (!clientConnectionID) { return createInvokeReject( diff --git a/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.ts b/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.ts new file mode 100644 index 0000000..7b31f6d --- /dev/null +++ b/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.ts @@ -0,0 +1,94 @@ +// Run: npx tsx compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.ts +import type { ILoadedModuleInfo, IOpVersionInfo } from "./TSROSEBase.js"; + +/** + * One module entry extracted from asnNegotiateInterface (or equivalent). + */ +export interface IRemoteModuleDetailInput { + moduleName: string; + version: string; + invokeOpIds?: readonly number[]; + eventOpIds?: readonly number[]; +} + +/** + * ASN.1 module detail shape used by buildRemoteModuleCapabilitiesFromAsn(). + */ +export interface IAsnModuleDetailLike { + u8sName: string; + u8sASN1ModuleVersion: string; + iOperations?: readonly number[]; + iEvents?: readonly number[]; +} + +function applyOpIds(opIds: readonly number[] | undefined, target: Map): void { + if (!opIds) + return; + for (const opId of opIds) { + if (opId === 0) + continue; + target.set(opId, { addedUnix: 0, deprecatedUnix: 0 }); + } +} + +/** + * Merges one module detail into the remote capability map (creates the module entry when missing). + */ +export function applyModuleDetailToRemoteCapabilities( + moduleName: string, + version: string, + invokeOpIds: readonly number[] | undefined, + eventOpIds: readonly number[] | undefined, + inOutRemote: Map, +): void { + let module = inOutRemote.get(moduleName); + if (!module) { + module = { + moduleName, + version, + invokes: new Map(), + events: new Map(), + }; + inOutRemote.set(moduleName, module); + } else { + module.version = version; + } + + applyOpIds(invokeOpIds, module.invokes); + applyOpIds(eventOpIds, module.events); +} + +/** + * Builds a remote capability map from module detail inputs. + */ +export function buildRemoteModuleCapabilities( + details: readonly IRemoteModuleDetailInput[], +): Map { + const remote = new Map(); + for (const detail of details) { + applyModuleDetailToRemoteCapabilities( + detail.moduleName, + detail.version, + detail.invokeOpIds, + detail.eventOpIds, + remote, + ); + } + return remote; +} + +/** + * Builds a remote capability map from asnNegotiateInterface module-details payloads. + */ +export function buildRemoteModuleCapabilitiesFromAsn( + moduleDetails: readonly IAsnModuleDetailLike[], +): Map { + return buildRemoteModuleCapabilities( + moduleDetails.map((detail) => ({ + moduleName: detail.u8sName, + version: detail.u8sASN1ModuleVersion, + invokeOpIds: detail.iOperations, + eventOpIds: detail.iEvents, + })), + ); +} diff --git a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts index b8d3142..402d2f8 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts @@ -65,6 +65,17 @@ export enum CustomInvokeProblemEnum { messageTooBig = 501, // The stub received an empty ROSEreject without details. emptyRejectMessage = 502, + // Local stub rejected before send: peer negotiate snapshot does not offer this invoke OPID + remoteNotCapable = 0xE00, +} + +/** Client-local SendInvoke result: peer negotiate snapshot does not offer this invoke OPID. */ +export const ROSE_REJECT_REMOTENOTCAPABLE = 0x00000E00; + +/** Controls outbound invoke gating against a negotiate snapshot on TSASN1Base. */ +export enum RemoteCapabilityMode { + Disabled = 0, + Enabled = 1, } /** @@ -471,6 +482,15 @@ export interface IASN1Transport { registerModuleVersion(moduleName: string, version: string): void; unregisterModuleVersion(moduleName: string): void; getLoadedModules(): ReadonlyMap; + lookUpName(operationID: number): string | undefined; + lookUpID(operationName: string): number | undefined; + lookUpModuleName(operationID: number): string | undefined; + setRemoteCapabilityMode(mode: RemoteCapabilityMode): void; + getRemoteCapabilityMode(): RemoteCapabilityMode; + applyRemoteModuleCapabilities(remote: ReadonlyMap): void; + clearRemoteModuleCapabilities(): void; + hasRemoteModuleCapabilities(): boolean; + isSupportedOperation(operationID: number): boolean; getInvokeContextParams( context: Partial | undefined, operationID: number, diff --git a/cpp-lib/include/SnaccModuleCapabilities.h b/cpp-lib/include/SnaccModuleCapabilities.h new file mode 100644 index 0000000..f727bda --- /dev/null +++ b/cpp-lib/include/SnaccModuleCapabilities.h @@ -0,0 +1,34 @@ +#ifndef _SnaccModuleCapabilities_h_ +#define _SnaccModuleCapabilities_h_ + +/*! Helpers to build SnaccLoadedModuleMap snapshots from negotiate payloads. */ + +#include "SnaccRoseOperationLookup.h" + +#include + +/*! One module entry extracted from asnNegotiateInterface (or equivalent). */ +struct SnaccRemoteModuleDetailInput +{ + const char* m_szModuleName = nullptr; + const char* m_szVersion = nullptr; + const int* m_pInvokeOpIds = nullptr; + size_t m_stInvokeOpIdCount = 0; + const int* m_pEventOpIds = nullptr; + size_t m_stEventOpIdCount = 0; +}; + +/*! Merges one module detail into @p inOutRemote (creates the module entry when missing). */ +void SnaccApplyModuleDetailToRemoteCapabilities( + const char* szModuleName, + const char* szVersion, + const int* pInvokeOpIds, + size_t stInvokeOpIdCount, + const int* pEventOpIds, + size_t stEventOpIdCount, + SnaccLoadedModuleMap& inOutRemote); + +/*! Builds a remote capability map from an array of module detail inputs. Clears @p outRemote first. */ +void SnaccBuildRemoteModuleCapabilities(const SnaccRemoteModuleDetailInput* pDetails, size_t stDetailCount, SnaccLoadedModuleMap& outRemote); + +#endif // _SnaccModuleCapabilities_h_ diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index e098ea3..81fceb7 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -235,6 +235,20 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Resolves generated interface id (m_iid) from operation id via the lookup table. */ unsigned int LookUpInterfaceID(unsigned int uiOpID) const; + /*! Configures whether outbound invokes are gated on a negotiate snapshot. Default Disabled. */ + void SetRemoteCapabilityMode(SnaccRemoteCapabilityMode mode); + SnaccRemoteCapabilityMode GetRemoteCapabilityMode() const; + + /*! Stores the peer module snapshot from asnNegotiateInterface (or equivalent). */ + void ApplyRemoteModuleCapabilities(const SnaccLoadedModuleMap& remote); + void ClearRemoteModuleCapabilities(); + + /*! True after ApplyRemoteModuleCapabilities() was called (even when the map is empty). */ + bool HasRemoteModuleCapabilities() const; + + /*! True when the negotiate snapshot offers this invoke OPID. Debug ASSERT when !HasRemoteModuleCapabilities(). */ + bool IsSupportedOperation(unsigned int uiOpId) const; + /*! Writes JSON encoded log messages to the log file bOutbound = true in case the log entry is related to an outbound message bException = true in case this is an exception based error message @@ -442,6 +456,9 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback void HandleInboundUnknownEncodingDecodeFailure(const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData); void HandleInboundOuterDecodeFailure(unsigned long ulMessageSize, const char* szException, const char* szMethod, std::optional errorCode = std::nullopt); + /*! Returns true when the applied remote snapshot lists @p uiOpId as a supported invoke. */ + bool InternalIsRemoteOperationSupported(unsigned int uiOpId) const; + // The central process wide telemetry callback static inline SnaccTelemetryCallback* m_pTelemetryCallback{}; @@ -502,6 +519,10 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback // Transport Encoding to be used SNACC::TransportEncoding m_eTransportEncoding{SNACC::TransportEncoding::UNKNOWN}; + SnaccRemoteCapabilityMode m_remoteCapabilityMode{SnaccRemoteCapabilityMode::Disabled}; + SnaccLoadedModuleMap m_remoteModuleCapabilities; + bool m_bRemoteModuleCapabilitiesSet{false}; + // Detects the encoding from the transport data (used for inbound data) SNACC::TransportEncoding DetectEncoding(const char* lpBytes, unsigned long ulSize) const; diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index b76c92f..15e48ba 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -118,6 +118,8 @@ const long ROSE_REJECT_AUTHENTICATION_USER_TEMPORARY_LOCKED_OUT = 0x00000B00; const long ROSE_REJECT_AUTHENTICATION_USER_LOCKED_OUT = 0x00000C00; //! The invoke requires an argument but it was not specified const long ROSE_REJECT_ARGUMENT_MISSING = 0x00000D00; +//! Local stub rejected before send: peer negotiate snapshot does not offer this invoke OPID +const long ROSE_REJECT_REMOTENOTCAPABLE = 0x00000E00; //! ROSE Server side ROSEError answers: //! ROSEError Message received diff --git a/cpp-lib/include/SnaccRoseOperationLookup.h b/cpp-lib/include/SnaccRoseOperationLookup.h index 49218aa..0d11ec4 100644 --- a/cpp-lib/include/SnaccRoseOperationLookup.h +++ b/cpp-lib/include/SnaccRoseOperationLookup.h @@ -27,6 +27,13 @@ struct SnaccLoadedModuleInfo using SnaccLoadedModuleMap = std::unordered_map; +/*! Controls outbound invoke gating against a negotiate snapshot on SnaccROSEBase. */ +enum class SnaccRemoteCapabilityMode +{ + Disabled, + Enabled, +}; + /*! Immutable operation-id lookup table after Seal(). Fill at listener startup; share one instance across all connections on that listener. Lookup is read-only and needs no locking once sealed. */ @@ -66,6 +73,8 @@ class SnaccRoseOperationLookup const char* LookUpName(unsigned int uiOpID) const; unsigned int LookUpID(const char* szOpName) const; unsigned int LookUpInterfaceID(unsigned int uiOpID) const; + /*! Returns the ASN.1 module name owning @p uiOpID, or nullptr when unknown locally. */ + const char* LookUpModuleName(unsigned int uiOpID) const; private: bool m_bSealed = false; diff --git a/cpp-lib/src/SnaccModuleCapabilities.cpp b/cpp-lib/src/SnaccModuleCapabilities.cpp new file mode 100644 index 0000000..48303cd --- /dev/null +++ b/cpp-lib/src/SnaccModuleCapabilities.cpp @@ -0,0 +1,57 @@ +#include "../include/SnaccModuleCapabilities.h" + +namespace +{ +void ApplyOpIds(const int* pOpIds, const size_t stOpIdCount, std::unordered_map& inOutOps) +{ + if (!pOpIds) + return; + + for (size_t i = 0; i < stOpIdCount; ++i) + { + const unsigned int uiOpId = static_cast(pOpIds[i]); + if (uiOpId == 0) + continue; + inOutOps.emplace(uiOpId, SnaccOpVersionInfo{}); + } +} +} // namespace + +void SnaccApplyModuleDetailToRemoteCapabilities( + const char* szModuleName, + const char* szVersion, + const int* pInvokeOpIds, + const size_t stInvokeOpIdCount, + const int* pEventOpIds, + const size_t stEventOpIdCount, + SnaccLoadedModuleMap& inOutRemote) +{ + if (!szModuleName || !szVersion) + return; + + auto& module = inOutRemote[szModuleName]; + module.m_strModuleName = szModuleName; + module.m_strVersion = szVersion; + ApplyOpIds(pInvokeOpIds, stInvokeOpIdCount, module.m_invokes); + ApplyOpIds(pEventOpIds, stEventOpIdCount, module.m_events); +} + +void SnaccBuildRemoteModuleCapabilities(const SnaccRemoteModuleDetailInput* pDetails, const size_t stDetailCount, SnaccLoadedModuleMap& outRemote) +{ + outRemote.clear(); + if (!pDetails) + return; + + for (size_t i = 0; i < stDetailCount; ++i) + { + const SnaccRemoteModuleDetailInput& detail = pDetails[i]; + SnaccApplyModuleDetailToRemoteCapabilities( + detail.m_szModuleName, + detail.m_szVersion, + detail.m_pInvokeOpIds, + detail.m_stInvokeOpIdCount, + detail.m_pEventOpIds, + detail.m_stEventOpIdCount, + outRemote); + } +} diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 67eed51..1f09d53 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -655,6 +655,55 @@ unsigned int SnaccROSEBase::LookUpInterfaceID(unsigned int uiOpID) const return m_operationLookup.LookUpInterfaceID(uiOpID); } +void SnaccROSEBase::SetRemoteCapabilityMode(const SnaccRemoteCapabilityMode mode) +{ + m_remoteCapabilityMode = mode; +} + +SnaccRemoteCapabilityMode SnaccROSEBase::GetRemoteCapabilityMode() const +{ + return m_remoteCapabilityMode; +} + +void SnaccROSEBase::ApplyRemoteModuleCapabilities(const SnaccLoadedModuleMap& remote) +{ + m_remoteModuleCapabilities = remote; + m_bRemoteModuleCapabilitiesSet = true; +} + +void SnaccROSEBase::ClearRemoteModuleCapabilities() +{ + m_remoteModuleCapabilities.clear(); + m_bRemoteModuleCapabilitiesSet = false; +} + +bool SnaccROSEBase::HasRemoteModuleCapabilities() const +{ + return m_bRemoteModuleCapabilitiesSet; +} + +bool SnaccROSEBase::InternalIsRemoteOperationSupported(const unsigned int uiOpId) const +{ + const char* szModuleName = m_operationLookup.LookUpModuleName(uiOpId); + if (!szModuleName) + return false; + + const auto moduleIt = m_remoteModuleCapabilities.find(szModuleName); + if (moduleIt == m_remoteModuleCapabilities.end()) + return false; + + return moduleIt->second.m_invokes.find(uiOpId) != moduleIt->second.m_invokes.end(); +} + +bool SnaccROSEBase::IsSupportedOperation(const unsigned int uiOpId) const +{ +#ifdef _DEBUG + if (!m_bRemoteModuleCapabilitiesSet) + ASSERT(0); +#endif + return InternalIsRemoteOperationSupported(uiOpId); +} + SnaccROSEPendingOperation::SnaccROSEPendingOperation(long lInvokeID, unsigned int uiOperationID, const char* szOperationName) : m_lInvokeID(lInvokeID), m_uiOperationID(uiOperationID), @@ -1694,6 +1743,14 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResu return ROSE_TE_SHUTDOWN; } + if (m_remoteCapabilityMode == SnaccRemoteCapabilityMode::Enabled && m_bRemoteModuleCapabilitiesSet && !InternalIsRemoteOperationSupported(pInvoke->operationID)) + { + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); + telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::LOCAL_REJECT, ROSE_REJECT_REMOTENOTCAPABLE, std::nullopt, std::move(pCtx)); + OnInvokeProcessed(telemetry); + return ROSE_REJECT_REMOTENOTCAPABLE; + } + auto& pendingOP = AddPendingOperation(pInvoke->invokeID, pInvoke->operationID, szResolvedOperationName); size_t stRequestData = 0; @@ -1948,6 +2005,20 @@ long SnaccROSEBase::SendInvokeAsync(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* return ROSE_TE_SHUTDOWN; } + if (m_remoteCapabilityMode == SnaccRemoteCapabilityMode::Enabled && m_bRemoteModuleCapabilitiesSet && !InternalIsRemoteOperationSupported(pInvoke->operationID)) + { + SnaccInvokeAsyncCallback rejectCallback; + if (!bFireAndForget && pCtx->HasAsyncCompletion()) + rejectCallback = pCtx->AsyncCallback(); + + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); + telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::LOCAL_REJECT, ROSE_REJECT_REMOTENOTCAPABLE, std::nullopt, pCtx); + OnInvokeProcessed(telemetry); + if (rejectCallback) + rejectCallback(ROSE_REJECT_REMOTENOTCAPABLE, *pCtx); + return ROSE_REJECT_REMOTENOTCAPABLE; + } + auto& pendingOP = AddPendingOperation(pInvoke->invokeID, pInvoke->operationID, szResolvedOperationName); pendingOP.m_bAsyncInvoke = true; pendingOP.m_bFireAndForgetAsync = bFireAndForget; diff --git a/cpp-lib/src/SnaccRoseOperationLookup.cpp b/cpp-lib/src/SnaccRoseOperationLookup.cpp index 065fdd9..67db7e2 100644 --- a/cpp-lib/src/SnaccRoseOperationLookup.cpp +++ b/cpp-lib/src/SnaccRoseOperationLookup.cpp @@ -143,6 +143,15 @@ unsigned int SnaccRoseOperationLookup::LookUpInterfaceID(unsigned int uiOpID) co return 0; } +const char* SnaccRoseOperationLookup::LookUpModuleName(unsigned int uiOpID) const +{ + const auto it = m_mapIDToModuleKind.find(uiOpID); + if (it != m_mapIDToModuleKind.end()) + return it->second.first.c_str(); + + return nullptr; +} + SnaccRoseOperationLookupRegistrationHost::SnaccRoseOperationLookupRegistrationHost(SnaccRoseOperationLookup& operationLookup) : m_operationLookup(operationLookup) { diff --git a/cpp-lib/tests/CMakeLists.txt b/cpp-lib/tests/CMakeLists.txt index 9370846..620e7d4 100644 --- a/cpp-lib/tests/CMakeLists.txt +++ b/cpp-lib/tests/CMakeLists.txt @@ -58,6 +58,7 @@ add_executable(cpp-lib-sample-runtime-tests logical_failure_tests.cpp module_registry_tests.cpp public_api_tests.cpp + remote_capability_tests.cpp telemetry_tests.cpp transport_failure_tests.cpp lifecycle_tests.cpp diff --git a/cpp-lib/tests/remote_capability_tests.cpp b/cpp-lib/tests/remote_capability_tests.cpp new file mode 100644 index 0000000..3840fec --- /dev/null +++ b/cpp-lib/tests/remote_capability_tests.cpp @@ -0,0 +1,155 @@ +#include "test_support/sample_runtime_harness.h" + +#include + +#include + +namespace sample_runtime_tests +{ +namespace +{ +const char* kSettingsModuleName = "ENetUC_Settings_Manager"; + +SnaccLoadedModuleMap BuildRemoteSnapshotWithGetSettingsOnly() +{ + const int invokeOpIds[] = {4100}; + const SnaccRemoteModuleDetailInput detail{ + kSettingsModuleName, + "20240101.0.20240506", + invokeOpIds, + 1, + nullptr, + 0, + }; + SnaccLoadedModuleMap remote; + SnaccBuildRemoteModuleCapabilities(&detail, 1, remote); + return remote; +} + +SnaccLoadedModuleMap BuildRemoteSnapshotWithoutGetSettings() +{ + const int invokeOpIds[] = {4101}; + const SnaccRemoteModuleDetailInput detail{ + kSettingsModuleName, + "20240101.0.20240506", + invokeOpIds, + 1, + nullptr, + 0, + }; + SnaccLoadedModuleMap remote; + SnaccBuildRemoteModuleCapabilities(&detail, 1, remote); + return remote; +} +} // namespace + +class RemoteCapabilityRuntimeTest : public RuntimeTestBase +{ +protected: + void InitializeConnectedEndpoints() + { + InitializeEndpoints(TransportEncoding::JSON); + } + + long InvokeGetSettingsOnClient() + { + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + return m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error, 250); + } +}; + +TEST(RemoteCapabilityModuleHelperTest, BuildRemoteModuleCapabilitiesPopulatesInvokeAndEventOpIds) +{ + const int invokeOpIds[] = {4100, 4101}; + const int eventOpIds[] = {4150}; + const SnaccRemoteModuleDetailInput detail{ + kSettingsModuleName, + "20240101.0.20240506", + invokeOpIds, + 2, + eventOpIds, + 1, + }; + + SnaccLoadedModuleMap remote; + SnaccBuildRemoteModuleCapabilities(&detail, 1, remote); + + ASSERT_EQ(1u, remote.size()); + const auto& module = remote.at(kSettingsModuleName); + EXPECT_EQ(kSettingsModuleName, module.m_strModuleName); + EXPECT_EQ("20240101.0.20240506", module.m_strVersion); + EXPECT_NE(module.m_invokes.end(), module.m_invokes.find(4100u)); + EXPECT_NE(module.m_invokes.end(), module.m_invokes.find(4101u)); + EXPECT_NE(module.m_events.end(), module.m_events.find(4150u)); +} + +TEST_F(RemoteCapabilityRuntimeTest, EnabledWithoutSnapshotDoesNotGateInvoke) +{ + InitializeConnectedEndpoints(); + m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + + const long roseResult = InvokeGetSettingsOnClient(); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); +} + +TEST_F(RemoteCapabilityRuntimeTest, EnabledWithUnsupportedOpIdReturnsRemoteNotCapable) +{ + InitializeConnectedEndpoints(); + m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); + m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + + const long roseResult = InvokeGetSettingsOnClient(); + EXPECT_EQ(ROSE_REJECT_REMOTENOTCAPABLE, roseResult); + EXPECT_EQ(0u, m_server.InboundObservation().TransportSendCount()); +} + +TEST_F(RemoteCapabilityRuntimeTest, EnabledWithSupportedOpIdSendsInvoke) +{ + InitializeConnectedEndpoints(); + m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithGetSettingsOnly()); + m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + + const long roseResult = InvokeGetSettingsOnClient(); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); +} + +TEST_F(RemoteCapabilityRuntimeTest, DisabledWithSnapshotDoesNotGateInvoke) +{ + InitializeConnectedEndpoints(); + m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); + m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Disabled); + + const long roseResult = InvokeGetSettingsOnClient(); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); +} + +TEST_F(RemoteCapabilityRuntimeTest, ClearRemoteCapabilitiesStopsGating) +{ + InitializeConnectedEndpoints(); + m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); + m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + m_client.ClearRemoteModuleCapabilities(); + + const long roseResult = InvokeGetSettingsOnClient(); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); +} + +TEST_F(RemoteCapabilityRuntimeTest, IsSupportedOperationReflectsAppliedSnapshot) +{ + SnaccRoseOperationLookup lookup; + RuntimeEndpoint endpoint{L"RemoteCapabilityQuery", "remote-capability-query", lookup}; + ENetUC_Settings_ManagerROSE::RegisterOperations(lookup); + + endpoint.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithGetSettingsOnly()); + EXPECT_TRUE(endpoint.HasRemoteModuleCapabilities()); + EXPECT_TRUE(endpoint.IsSupportedOperation(4100u)); + EXPECT_FALSE(endpoint.IsSupportedOperation(4101u)); +} + +} // namespace sample_runtime_tests diff --git a/version.h b/version.h index 5f9c9b8..d7223d4 100644 --- a/version.h +++ b/version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "7.0.14" -#define VERSION_RC 7, 0, 14 -#define RELDATE "18.08.2026" +#define VERSION "7.0.15" +#define VERSION_RC 7, 0, 15 +#define RELDATE "19.08.2026" #endif // VERSION_H From 5277aae492aea57fe7c145ce6399be90f5e4bc50 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Wed, 19 Aug 2026 14:40:42 +0200 Subject: [PATCH 02/11] UCAAS-1486: add ensure_compiler scripts for CMake-based esnacc builds Pure CMD and bash helpers configure CMake when needed, run cmake --build --target compiler, and resolve the compiler path from CMakeCache. Wire ROSE/makesnaccrose and samples/prepare entry points. Co-authored-by: Cursor --- ROSE/makesnaccrose.bat | 30 ++----- docs/build.md | 13 +++ samples/prepare.bat | 6 ++ samples/prepare.js | 13 ++- samples/prepare.sh | 1 + scripts/ensure_compiler.bat | 174 ++++++++++++++++++++++++++++++++++++ scripts/ensure_compiler.sh | 146 ++++++++++++++++++++++++++++++ 7 files changed, 352 insertions(+), 31 deletions(-) create mode 100644 scripts/ensure_compiler.bat create mode 100644 scripts/ensure_compiler.sh diff --git a/ROSE/makesnaccrose.bat b/ROSE/makesnaccrose.bat index b1fa899..2ae0985 100644 --- a/ROSE/makesnaccrose.bat +++ b/ROSE/makesnaccrose.bat @@ -1,35 +1,19 @@ @echo off -cls +setlocal EnableExtensions -SET COMPILER= +set "COMPILER=" +call "%~dp0..\scripts\ensure_compiler.bat" +if errorlevel 1 goto end +set "COMPILER=%SNACC_COMPILER%" -echo Searching for esnacc executable in output directory -SET PATH=%PATH%;%CD%\..\output\bin\ -where esnaccd.exe 2>nul -IF %ERRORLEVEL% == 0 SET COMPILER=esnaccd.exe -IF NOT "%COMPILER%" == "" GOTO start -where esnacc.exe 2>nul -IF %ERRORLEVEL% == 0 SET COMPILER=esnacc.exe -IF NOT "%COMPILER%" == "" GOTO start - -echo Searching for esnacc executable in global buildtools -SET PATH=%PATH%;%CD%\..\..\..\buildtools\ -where esnacc7.exe 2>nul -IF %ERRORLEVEL% == 0 SET COMPILER=esnacc7.exe -IF NOT "%COMPILER%" == "" GOTO start - -echo Could not find esnaccd.exe or esnacc.exe, please build the compiler first -goto end - -:start echo %COMPILER% -ValidationLevel 0 -C -x -p -e -d -j SNACCROSE.asn1 -%COMPILER% -ValidationLevel 0 -C -x -p -e -d -j SNACCROSE.asn1 +"%COMPILER%" -ValidationLevel 0 -C -x -p -e -d -j SNACCROSE.asn1 if NOT %ERRORLEVEL% == 0 pause move SNACCROSE.cpp ..\cpp-lib\src\SNACCROSE.cpp >NUL move SNACCROSE.h ..\cpp-lib\include\SNACCROSE.h >NUL echo %COMPILER% -ValidationLevel 0 -JTE -j SNACCROSE.asn1 -%COMPILER% -ValidationLevel 0 -JTE -j SNACCROSE.asn1 +"%COMPILER%" -ValidationLevel 0 -JTE -j SNACCROSE.asn1 if NOT %ERRORLEVEL% == 0 pause move SNACCROSE.ts ..\compiler\back-ends\ts-gen\gluecode\SNACCROSE.ts >NUL move SNACCROSE_Converter.ts ..\compiler\back-ends\ts-gen\gluecode\SNACCROSE_Converter.ts >NUL diff --git a/docs/build.md b/docs/build.md index 83c1944..1d550f4 100644 --- a/docs/build.md +++ b/docs/build.md @@ -24,6 +24,19 @@ Outputs (defaults): | Compiler (`esnacc` / `esnaccd`) | `output/bin/` | | C++ library (`esnacc_cpp_lib`) | `output/libx64/` (x64) or `output/lib/` (x86) | +### Ensure compiler is up to date (no Node) + +Scripts under `scripts/` configure CMake if needed, run `cmake --build --target compiler` (CMake/MSBuild only rebuild when inputs changed), and resolve the path from `CMakeCache.txt` (`COMPILER_OUTPUT_PATH` / `COMPILER_OUTPUT_NAME`). + +| Platform | Entry point | +|----------|-------------| +| Windows (CMD) | `scripts\ensure_compiler.bat` — sets `SNACC_COMPILER` and prepends its directory to `PATH` | +| Linux / macOS | `scripts/ensure_compiler.sh` — use `--export` to emit shell `export` lines | + +Environment variables: `SNACCLIB7_ROOT`, `SNACC_CMAKE_BUILD_DIR`, `SNACC_CMAKE_GENERATOR`, `SNACC_CONFIGURATION`, `SNACC_SKIP_COMPILER_BUILD`, `SNACC_FORCE_COMPILER_BUILD`, `SNACC_COMPILER`. + +`samples/prepare.bat`, `samples/prepare.sh`, and `ROSE/makesnaccrose.bat` call these helpers before running Node or invoking the compiler directly. + ### CMake variables | Variable | Purpose | diff --git a/samples/prepare.bat b/samples/prepare.bat index 0861f20..3b95886 100644 --- a/samples/prepare.bat +++ b/samples/prepare.bat @@ -1,6 +1,12 @@ @echo off +call "%~dp0..\scripts\ensure_compiler.bat" +if errorlevel 1 ( + set EXIT_CODE=%ERRORLEVEL% + goto finish +) node "%~dp0prepare.js" %* set EXIT_CODE=%ERRORLEVEL% +:finish if defined SNACC_NO_PAUSE exit /b %EXIT_CODE% echo. if %EXIT_CODE% NEQ 0 ( diff --git a/samples/prepare.js b/samples/prepare.js index eb0871a..71b0c08 100644 --- a/samples/prepare.js +++ b/samples/prepare.js @@ -32,19 +32,16 @@ function fileIsExecutable(filePath) { } function resolveCompiler() { - if (process.env.SNACC_COMPILER) { - return path.resolve(process.env.SNACC_COMPILER); - } - if (process.env.CMAKE_COMPILER_TARGET) { - return path.resolve(process.env.CMAKE_COMPILER_TARGET); + for (const value of [process.env.SNACC_COMPILER, process.env.ESNACC_EXECUTABLE, process.env.CMAKE_COMPILER_TARGET]) { + if (value && fs.existsSync(value)) { + return path.resolve(value); + } } const searchDirs = [ path.join(SAMPLES_DIR, "..", "output", "bin"), BIN_DIR, - path.join(SAMPLES_DIR, "..", "..", "..", "buildtools"), ]; - for (const dir of searchDirs) { for (const name of COMPILER_CANDIDATES) { const candidate = path.join(dir, name); @@ -113,7 +110,7 @@ function runCompiler(compiler, outputDir, compilerArgs, asn1Files) { function generateStubs() { const compiler = resolveCompiler(); if (!compiler) { - console.error("error: esnacc compiler not found. Build esnacc first, then run prepare.bat (Windows) or ./prepare.sh (Linux)."); + console.error("error: esnacc compiler not found. Run scripts/ensure_compiler.bat or scripts/ensure_compiler.sh first, or use prepare.bat / prepare.sh."); return 1; } diff --git a/samples/prepare.sh b/samples/prepare.sh index e0dd4eb..a0c7b98 100644 --- a/samples/prepare.sh +++ b/samples/prepare.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +eval "$("${SCRIPT_DIR}/../scripts/ensure_compiler.sh" --export --quiet)" exec node "${SCRIPT_DIR}/prepare.js" "$@" diff --git a/scripts/ensure_compiler.bat b/scripts/ensure_compiler.bat new file mode 100644 index 0000000..3a8fda0 --- /dev/null +++ b/scripts/ensure_compiler.bat @@ -0,0 +1,174 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +rem Ensures the esnacc compiler is current and sets SNACC_COMPILER + PATH. +rem Pure CMD — no PowerShell or Node required. Linux/macOS: ensure_compiler.sh +rem +rem Environment: +rem SNACCLIB7_ROOT Override repository root (default: parent of scripts/) +rem SNACC_CMAKE_BUILD_DIR CMake build dir relative to repo root +rem SNACC_CMAKE_GENERATOR Optional CMake -G for first-time configure +rem SNACC_CONFIGURATION CMake build config (default: Release) +rem SNACC_SKIP_COMPILER_BUILD=1 Resolve only; never run cmake --build +rem SNACC_FORCE_COMPILER_BUILD=1 Bypass SNACC_COMPILER/CMAKE_COMPILER_TARGET and use CMake +rem SNACC_COMPILER If set and skip/force rules allow, use without building + +set "SCRIPT_DIR=%~dp0" +set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" +if defined SNACCLIB7_ROOT ( + for %%I in ("%SNACCLIB7_ROOT%") do set "REPO_ROOT=%%~fI" +) else ( + for %%I in ("%SCRIPT_DIR%\..") do set "REPO_ROOT=%%~fI" +) + +if not defined SNACC_CONFIGURATION set "SNACC_CONFIGURATION=Release" + +if "%SNACC_SKIP_COMPILER_BUILD%"=="1" goto :skip_build +if defined SNACC_COMPILER if not "%SNACC_FORCE_COMPILER_BUILD%"=="1" goto :use_existing_env +if defined CMAKE_COMPILER_TARGET if not "%SNACC_FORCE_COMPILER_BUILD%"=="1" goto :use_existing_env +goto :ensure_build + +:skip_build +call :ResolveExisting +if errorlevel 1 ( + echo SNACC_SKIP_COMPILER_BUILD is set but no esnacc compiler was found. 1>&2 + exit /b 1 +) +set "COMPILER_PATH=!RESOLVE_RESULT!" +goto :set_env + +:use_existing_env +call :ResolveExisting +if errorlevel 1 ( + echo SNACC_COMPILER/CMAKE_COMPILER_TARGET is set but the compiler was not found. 1>&2 + exit /b 1 +) +set "COMPILER_PATH=!RESOLVE_RESULT!" +goto :set_env + +:ensure_build +call :FindBuildDir +call :EnsureConfigured +if errorlevel 1 exit /b 1 +call :CompilerFromCache +set "COMPILER_PATH=!COMPILER_RESULT!" + +echo Building esnacc compiler (%SNACC_CONFIGURATION%) via CMake +cmake --build "!BUILD_DIR!" --config %SNACC_CONFIGURATION% --target compiler +if errorlevel 1 ( + echo cmake --build compiler failed 1>&2 + exit /b 1 +) +call :CompilerFromCache +set "COMPILER_PATH=!COMPILER_RESULT!" + +if not exist "!COMPILER_PATH!" ( + echo esnacc compiler not found at "!COMPILER_PATH!" 1>&2 + exit /b 1 +) + +:set_env +for %%I in ("!COMPILER_PATH!") do ( + endlocal + set "SNACC_COMPILER=%%~fI" + set "PATH=%%~dpI;%PATH%" +) +exit /b 0 + +rem --------------------------------------------------------------------------- +:FindBuildDir +if defined SNACC_CMAKE_BUILD_DIR ( + set "BUILD_DIR=!REPO_ROOT!\!SNACC_CMAKE_BUILD_DIR!" + exit /b 0 +) +for %%D in ( + build\x64_vc145 + build\win32_vc145 + build\release + build\debug + build_win\release + build_win\debug + build +) do ( + if exist "!REPO_ROOT!\%%D\CMakeCache.txt" ( + set "BUILD_DIR=!REPO_ROOT!\%%D" + exit /b 0 + ) +) +set "BUILD_DIR=!REPO_ROOT!\build" +exit /b 0 + +rem --------------------------------------------------------------------------- +:ReadCacheValue +set "CACHE_VALUE=" +set "CACHE_FILE=%~1" +set "CACHE_KEY=%~2" +if not exist "!CACHE_FILE!" exit /b 1 +for /f "usebackq tokens=1,* delims==" %%A in (`findstr /b /c:"!CACHE_KEY!:" "!CACHE_FILE!" 2^>nul`) do ( + set "CACHE_VALUE=%%B" +) +if not defined CACHE_VALUE exit /b 1 +if "!CACHE_VALUE:~0,13!"=="UNINITIALIZED=" set "CACHE_VALUE=!CACHE_VALUE:~13!" +exit /b 0 + +rem --------------------------------------------------------------------------- +:CompilerFromCache +set "CACHE_FILE=!BUILD_DIR!\CMakeCache.txt" +call :ReadCacheValue "!CACHE_FILE!" COMPILER_OUTPUT_PATH +if errorlevel 1 ( + set "OUTPUT_DIR=!REPO_ROOT!\output\bin" +) else ( + set "OUTPUT_DIR=!CACHE_VALUE!" +) +call :ReadCacheValue "!CACHE_FILE!" COMPILER_OUTPUT_NAME +if errorlevel 1 ( + set "OUTPUT_NAME=esnacc" +) else ( + set "OUTPUT_NAME=!CACHE_VALUE!" +) +set "COMPILER_RESULT=!OUTPUT_DIR!\!OUTPUT_NAME!.exe" +exit /b 0 + +rem --------------------------------------------------------------------------- +:EnsureConfigured +set "CACHE_FILE=!BUILD_DIR!\CMakeCache.txt" +if exist "!CACHE_FILE!" exit /b 0 + +echo Configuring esnacc CMake build in !BUILD_DIR! +if not exist "!BUILD_DIR!" mkdir "!BUILD_DIR!" +set "OUTPUT_DIR=!REPO_ROOT!\output\bin" +if defined SNACC_CMAKE_GENERATOR ( + cmake -G "!SNACC_CMAKE_GENERATOR!" -S "!REPO_ROOT!" -B "!BUILD_DIR!" -A x64 -DMSVC_STATIC_RUNTIME=ON -DBUILD_TESTING=OFF -DCOMPILER_OUTPUT_PATH="!OUTPUT_DIR!" -DCOMPILER_OUTPUT_NAME=esnacc +) else ( + cmake -S "!REPO_ROOT!" -B "!BUILD_DIR!" -A x64 -DMSVC_STATIC_RUNTIME=ON -DBUILD_TESTING=OFF -DCOMPILER_OUTPUT_PATH="!OUTPUT_DIR!" -DCOMPILER_OUTPUT_NAME=esnacc +) +if errorlevel 1 ( + echo cmake configure failed 1>&2 + exit /b 1 +) +exit /b 0 + +rem --------------------------------------------------------------------------- +:ResolveExisting +set "RESOLVE_RESULT=" +for %%C in ("%SNACC_COMPILER%" "%ESNACC_EXECUTABLE%" "%CMAKE_COMPILER_TARGET%") do ( + if not "%%~C"=="" if exist "%%~C" ( + for %%I in ("%%~C") do set "RESOLVE_RESULT=%%~fI" + exit /b 0 + ) +) +for %%D in ("!REPO_ROOT!\output\bin" "!REPO_ROOT!\samples\bin") do ( + for %%N in (esnacc.exe esnaccd.exe esnacc7.exe esnacc7d.exe) do ( + if exist "%%~D\%%N" ( + for %%I in ("%%~D\%%N") do set "RESOLVE_RESULT=%%~fI" + exit /b 0 + ) + ) +) +for %%N in (esnacc.exe esnaccd.exe esnacc7.exe esnacc7d.exe) do ( + for /f "delims=" %%P in ('where %%N 2^>nul') do ( + for %%I in ("%%P") do set "RESOLVE_RESULT=%%~fI" + exit /b 0 + ) +) +exit /b 1 diff --git a/scripts/ensure_compiler.sh b/scripts/ensure_compiler.sh new file mode 100644 index 0000000..f4608b3 --- /dev/null +++ b/scripts/ensure_compiler.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Ensures the esnacc compiler is current and prints/exports its CMake output path. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="${SNACCLIB7_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" +CONFIGURATION="${SNACC_CONFIGURATION:-Release}" +QUIET=0 +WRITE_PATH=0 +EXPORT_ENV=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --quiet) QUIET=1; shift ;; + --write-path) WRITE_PATH=1; shift ;; + --export) EXPORT_ENV=1; shift ;; + *) echo "Unknown argument: $1" >&2; exit 2 ;; + esac +done + +log() { + if [[ "$QUIET" -eq 0 ]]; then + echo "$1" + fi +} + +read_cache_value() { + local cache_file="$1" + local key="$2" + local line value + line="$(grep -m1 "^${key}:" "$cache_file" || true)" + if [[ -z "$line" ]]; then + return 1 + fi + value="${line#*=}" + if [[ "$value" == UNINITIALIZED=* ]]; then + echo "${value#UNINITIALIZED=}" + else + echo "$value" + fi +} + +find_cmake_build_dir() { + local candidate + if [[ -n "${SNACC_CMAKE_BUILD_DIR:-}" ]]; then + echo "$REPO_ROOT/$SNACC_CMAKE_BUILD_DIR" + return 0 + fi + for candidate in build/x64_vc145 build/win32_vc145 build/release build/debug build; do + if [[ -f "$REPO_ROOT/$candidate/CMakeCache.txt" ]]; then + echo "$REPO_ROOT/$candidate" + return 0 + fi + done + echo "$REPO_ROOT/build" +} + +default_output_dir() { + echo "$REPO_ROOT/output/bin" +} + +compiler_from_cache() { + local cache_file="$1" + local output_dir output_name + output_dir="$(read_cache_value "$cache_file" COMPILER_OUTPUT_PATH || default_output_dir)" + output_name="$(read_cache_value "$cache_file" COMPILER_OUTPUT_NAME || echo esnacc)" + echo "$output_dir/$output_name" +} + +resolve_existing_compiler() { + local candidate name dir + for candidate in "${SNACC_COMPILER:-}" "${ESNACC_EXECUTABLE:-}" "${CMAKE_COMPILER_TARGET:-}"; do + if [[ -n "$candidate" && -f "$candidate" ]]; then + echo "$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + return 0 + fi + done + for dir in "$(default_output_dir)" "$REPO_ROOT/samples/bin"; do + for name in esnacc esnaccd esnacc7 esnacc7d; do + if [[ -x "$dir/$name" ]]; then + echo "$dir/$name" + return 0 + fi + done + done + for name in esnacc esnaccd esnacc7 esnacc7d; do + if command -v "$name" >/dev/null 2>&1; then + command -v "$name" + return 0 + fi + done + return 1 +} + +ensure_cmake_configured() { + local build_dir="$1" + local cache_file="$build_dir/CMakeCache.txt" + if [[ -f "$cache_file" ]]; then + echo "$cache_file" + return 0 + fi + + log "Configuring esnacc CMake build in $build_dir" + mkdir -p "$build_dir" + local output_dir + output_dir="$(default_output_dir)" + local -a cmake_args=( + "-S" "$REPO_ROOT" + "-B" "$build_dir" + "-DMSVC_STATIC_RUNTIME=ON" + "-DBUILD_TESTING=OFF" + "-DCOMPILER_OUTPUT_PATH=$output_dir" + "-DCOMPILER_OUTPUT_NAME=esnacc" + ) + if [[ -n "${SNACC_CMAKE_GENERATOR:-}" ]]; then + cmake_args=("-G" "$SNACC_CMAKE_GENERATOR" "${cmake_args[@]}") + fi + cmake "${cmake_args[@]}" + echo "$cache_file" +} + +if [[ "${SNACC_SKIP_COMPILER_BUILD:-}" == "1" ]]; then + compiler="$(resolve_existing_compiler)" +elif [[ ( -n "${SNACC_COMPILER:-}" || -n "${CMAKE_COMPILER_TARGET:-}" ) && "${SNACC_FORCE_COMPILER_BUILD:-}" != "1" ]]; then + compiler="$(resolve_existing_compiler)" +else + build_dir="$(find_cmake_build_dir)" + cache_file="$(ensure_cmake_configured "$build_dir")" + log "Building esnacc compiler ($CONFIGURATION) via CMake" + cmake --build "$build_dir" --config "$CONFIGURATION" --target compiler + compiler="$(compiler_from_cache "$cache_file")" + if [[ ! -f "$compiler" ]]; then + echo "esnacc compiler not found at $compiler" >&2 + exit 1 + fi +fi + +export SNACC_COMPILER="$compiler" +if [[ "$EXPORT_ENV" -eq 1 ]]; then + printf 'export SNACC_COMPILER=%q\n' "$compiler" + printf 'export PATH=%q:$PATH\n' "$(dirname "$compiler")" +elif [[ "$WRITE_PATH" -eq 1 ]]; then + echo "$compiler" +else + log "Using esnacc compiler: $compiler" +fi From 33433849d604dbd81367d52bd0956f3da8756194 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Wed, 19 Aug 2026 15:11:15 +0200 Subject: [PATCH 03/11] UCAAS-1486: complete registry and remote-capability test coverage Expose LookUpModuleName on SnaccROSEBase, fix C++ remote-capability harness assertions, extend TS gluecode unit tests, and add run_gluecode_tests scripts. Co-authored-by: Cursor --- .../gluecode/TSASN1Base.registry.test.ts | 13 +++++- .../TSASN1Base.remoteCapability.test.ts | 36 +++++++++++++++- .../gluecode/TSModuleCapabilities.test.ts | 43 +++++++++++++++++++ cpp-lib/include/SnaccROSEBase.h | 3 ++ cpp-lib/src/SnaccROSEBase.cpp | 5 +++ cpp-lib/tests/module_registry_tests.cpp | 2 + cpp-lib/tests/remote_capability_tests.cpp | 10 ++--- docs/build.md | 2 + scripts/run_gluecode_tests.bat | 39 +++++++++++++++++ scripts/run_gluecode_tests.sh | 38 ++++++++++++++++ 10 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts create mode 100644 scripts/run_gluecode_tests.bat create mode 100644 scripts/run_gluecode_tests.sh diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts index 1102537..bc99b73 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts @@ -3,9 +3,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { ASN1ClassInstanceType, - EASN1TransportEncoding, TSASN1Base, } from "./TSASN1Base.js"; +import { EASN1TransportEncoding } from "./TSInvokeContext.js"; import type { IASN1InvokeData } from "./TSROSEBase.js"; import type { ROSEError, ROSEReject, ROSEResult } from "./SNACCROSE.js"; @@ -67,3 +67,14 @@ test("separate stub instances keep separate registries", () => { assert.ok(transportA.getLoadedModules().has("ModuleA")); assert.ok(!transportA.getLoadedModules().has("ModuleB")); }); + +test("lookUpName lookUpID and lookUpModuleName resolve registered operations", () => { + const transport = new TestTransport(); + transport.registerModuleVersion("TestModule", "1.0.0"); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + + assert.equal(transport.lookUpName(100), "asnInvoke"); + assert.equal(transport.lookUpID("asnInvoke"), 100); + assert.equal(transport.lookUpModuleName(100), "TestModule"); + assert.equal(transport.lookUpModuleName(999), undefined); +}); diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts index 3a7efcc..9e265a5 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts @@ -3,9 +3,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { ASN1ClassInstanceType, - EASN1TransportEncoding, TSASN1Base, } from "./TSASN1Base.js"; +import { EASN1TransportEncoding } from "./TSInvokeContext.js"; import { CustomInvokeProblemEnum, RemoteCapabilityMode, @@ -126,3 +126,37 @@ test("clearRemoteModuleCapabilities stops gating", async () => { assert.equal(transport.sendInvokeCount, 1); }); + +test("disabled with snapshot does not gate sendInvoke", async () => { + const transport = new TestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ + { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, + ])); + transport.setRemoteCapabilityMode(RemoteCapabilityMode.Disabled); + + await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + payLoad: new Uint8Array(), + } as IASN1InvokeData); + + assert.equal(transport.sendInvokeCount, 1); +}); + +test("enabled with supported op id sends invoke", async () => { + const transport = new TestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ + { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [100] }, + ])); + transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); + + await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + payLoad: new Uint8Array(), + } as IASN1InvokeData); + + assert.equal(transport.sendInvokeCount, 1); +}); diff --git a/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts b/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts new file mode 100644 index 0000000..96de4d9 --- /dev/null +++ b/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts @@ -0,0 +1,43 @@ +// Run: npx tsx compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { + buildRemoteModuleCapabilities, + buildRemoteModuleCapabilitiesFromAsn, +} from "./TSModuleCapabilities.js"; + +test("buildRemoteModuleCapabilities populates invoke and event op ids", () => { + const remote = buildRemoteModuleCapabilities([ + { + moduleName: "ENetUC_Settings_Manager", + version: "20240101.0.20240506", + invokeOpIds: [4100, 4101], + eventOpIds: [4150], + }, + ]); + + assert.equal(remote.size, 1); + const module = remote.get("ENetUC_Settings_Manager"); + assert.ok(module); + assert.equal(module.moduleName, "ENetUC_Settings_Manager"); + assert.equal(module.version, "20240101.0.20240506"); + assert.ok(module.invokes.has(4100)); + assert.ok(module.invokes.has(4101)); + assert.ok(module.events.has(4150)); +}); + +test("buildRemoteModuleCapabilitiesFromAsn maps ASN module detail fields", () => { + const remote = buildRemoteModuleCapabilitiesFromAsn([ + { + u8sName: "ENetUC_Settings_Manager", + u8sASN1ModuleVersion: "20240101.0.20240506", + iOperations: [4100], + iEvents: [4150], + }, + ]); + + const module = remote.get("ENetUC_Settings_Manager"); + assert.ok(module); + assert.ok(module.invokes.has(4100)); + assert.ok(module.events.has(4150)); +}); diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 81fceb7..2c83925 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -235,6 +235,9 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Resolves generated interface id (m_iid) from operation id via the lookup table. */ unsigned int LookUpInterfaceID(unsigned int uiOpID) const; + /*! Resolves ASN.1 module name owning the given operation id via the lookup table. */ + const char* LookUpModuleName(unsigned int uiOpID) const; + /*! Configures whether outbound invokes are gated on a negotiate snapshot. Default Disabled. */ void SetRemoteCapabilityMode(SnaccRemoteCapabilityMode mode); SnaccRemoteCapabilityMode GetRemoteCapabilityMode() const; diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 1f09d53..19e40c3 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -655,6 +655,11 @@ unsigned int SnaccROSEBase::LookUpInterfaceID(unsigned int uiOpID) const return m_operationLookup.LookUpInterfaceID(uiOpID); } +const char* SnaccROSEBase::LookUpModuleName(unsigned int uiOpID) const +{ + return m_operationLookup.LookUpModuleName(uiOpID); +} + void SnaccROSEBase::SetRemoteCapabilityMode(const SnaccRemoteCapabilityMode mode) { m_remoteCapabilityMode = mode; diff --git a/cpp-lib/tests/module_registry_tests.cpp b/cpp-lib/tests/module_registry_tests.cpp index 3f32806..1f2ed17 100644 --- a/cpp-lib/tests/module_registry_tests.cpp +++ b/cpp-lib/tests/module_registry_tests.cpp @@ -76,6 +76,8 @@ TEST(ModuleRegistryTest, RegisteredMetadataMatchesLoadedModuleSnapshot) EXPECT_EQ(4100u, endpoint.LookUpID("asnGetSettings")); EXPECT_STREQ("asnGetSettings", endpoint.LookUpName(4100u)); EXPECT_EQ(ENetUC_Settings_ManagerROSE::m_iid, endpoint.LookUpInterfaceID(4100u)); + EXPECT_STREQ(kSettingsModuleName, endpoint.LookUpModuleName(4100u)); + EXPECT_EQ(nullptr, endpoint.LookUpModuleName(9999u)); } TEST(ModuleRegistryTest, RuntimeFixtureKeepsClientAndServerRegistriesSeparate) diff --git a/cpp-lib/tests/remote_capability_tests.cpp b/cpp-lib/tests/remote_capability_tests.cpp index 3840fec..fc7d64e 100644 --- a/cpp-lib/tests/remote_capability_tests.cpp +++ b/cpp-lib/tests/remote_capability_tests.cpp @@ -92,7 +92,7 @@ TEST_F(RemoteCapabilityRuntimeTest, EnabledWithoutSnapshotDoesNotGateInvoke) const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); - EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); + EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } TEST_F(RemoteCapabilityRuntimeTest, EnabledWithUnsupportedOpIdReturnsRemoteNotCapable) @@ -103,7 +103,7 @@ TEST_F(RemoteCapabilityRuntimeTest, EnabledWithUnsupportedOpIdReturnsRemoteNotCa const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_REJECT_REMOTENOTCAPABLE, roseResult); - EXPECT_EQ(0u, m_server.InboundObservation().TransportSendCount()); + EXPECT_EQ(0u, ServerInboundObservation().TransportSendCount()); } TEST_F(RemoteCapabilityRuntimeTest, EnabledWithSupportedOpIdSendsInvoke) @@ -114,7 +114,7 @@ TEST_F(RemoteCapabilityRuntimeTest, EnabledWithSupportedOpIdSendsInvoke) const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); - EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); + EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } TEST_F(RemoteCapabilityRuntimeTest, DisabledWithSnapshotDoesNotGateInvoke) @@ -125,7 +125,7 @@ TEST_F(RemoteCapabilityRuntimeTest, DisabledWithSnapshotDoesNotGateInvoke) const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); - EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); + EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } TEST_F(RemoteCapabilityRuntimeTest, ClearRemoteCapabilitiesStopsGating) @@ -137,7 +137,7 @@ TEST_F(RemoteCapabilityRuntimeTest, ClearRemoteCapabilitiesStopsGating) const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); - EXPECT_GE(m_client.Transport().TransportSendCount(), 1u); + EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } TEST_F(RemoteCapabilityRuntimeTest, IsSupportedOperationReflectsAppliedSnapshot) diff --git a/docs/build.md b/docs/build.md index 1d550f4..e2ee9cf 100644 --- a/docs/build.md +++ b/docs/build.md @@ -37,6 +37,8 @@ Environment variables: `SNACCLIB7_ROOT`, `SNACC_CMAKE_BUILD_DIR`, `SNACC_CMAKE_G `samples/prepare.bat`, `samples/prepare.sh`, and `ROSE/makesnaccrose.bat` call these helpers before running Node or invoking the compiler directly. +Gluecode unit tests (registry + remote capability): run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. + ### CMake variables | Variable | Purpose | diff --git a/scripts/run_gluecode_tests.bat b/scripts/run_gluecode_tests.bat new file mode 100644 index 0000000..fd9e970 --- /dev/null +++ b/scripts/run_gluecode_tests.bat @@ -0,0 +1,39 @@ +@echo off +setlocal EnableExtensions + +set "SCRIPT_DIR=%~dp0" +set "REPO_ROOT=%SCRIPT_DIR%.." +set "GLUE_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\gluecode" +set "STUB_DIR=%REPO_ROOT%\samples\ts-microservice\node-client\src\stub" +set "NODE_MODULES=%REPO_ROOT%\samples\ts-microservice\node-client\node_modules" + +if not exist "%NODE_MODULES%\@estos\asn1ts" ( + echo error: run samples\prepare.bat first to install node-client dependencies. 1>&2 + exit /b 1 +) + +set "NODE_PATH=%NODE_MODULES%" +set "EXIT_CODE=0" + +for %%F in (ENetUC_Common.ts ENetUC_Common_Converter.ts) do ( + if not exist "%GLUE_DIR%\%%F" ( + copy /Y "%STUB_DIR%\%%F" "%GLUE_DIR%\%%F" >nul + set "COPIED_FIXTURE=1" + ) +) + +for %%T in ( + TSASN1Base.registry.test.ts + TSASN1Base.remoteCapability.test.ts + TSModuleCapabilities.test.ts +) do ( + echo Running %%T ... + npx --yes tsx "%GLUE_DIR%\%%T" + if errorlevel 1 set "EXIT_CODE=1" +) + +if defined COPIED_FIXTURE ( + del /Q "%GLUE_DIR%\ENetUC_Common.ts" "%GLUE_DIR%\ENetUC_Common_Converter.ts" 2>nul +) + +exit /b %EXIT_CODE% diff --git a/scripts/run_gluecode_tests.sh b/scripts/run_gluecode_tests.sh new file mode 100644 index 0000000..0ff0d4e --- /dev/null +++ b/scripts/run_gluecode_tests.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +GLUE_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/gluecode" +STUB_DIR="$REPO_ROOT/samples/ts-microservice/node-client/src/stub" +NODE_MODULES="$REPO_ROOT/samples/ts-microservice/node-client/node_modules" + +if [[ ! -d "$NODE_MODULES/@estos/asn1ts" ]]; then + echo "error: run samples/prepare.sh first to install node-client dependencies." >&2 + exit 1 +fi + +export NODE_PATH="$NODE_MODULES" +COPIED_FIXTURE=0 +cleanup() { + if [[ "$COPIED_FIXTURE" -eq 1 ]]; then + rm -f "$GLUE_DIR/ENetUC_Common.ts" "$GLUE_DIR/ENetUC_Common_Converter.ts" + fi +} +trap cleanup EXIT + +for fixture in ENetUC_Common.ts ENetUC_Common_Converter.ts; do + if [[ ! -f "$GLUE_DIR/$fixture" ]]; then + cp "$STUB_DIR/$fixture" "$GLUE_DIR/$fixture" + COPIED_FIXTURE=1 + fi +done + +for test_file in \ + TSASN1Base.registry.test.ts \ + TSASN1Base.remoteCapability.test.ts \ + TSModuleCapabilities.test.ts +do + echo "Running $test_file ..." + npx --yes tsx "$GLUE_DIR/$test_file" +done From 86180033cccd9f16fb9404d409b29712cee20d03 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 12:58:25 +0200 Subject: [PATCH 04/11] UCAAS-1486: add explicit and TryGet accessors for AsnInt Add GetInt/GetLong/GetInt64 and GetUInt/GetULong/GetUInt64 with range and sign checks that throw INTEGER_ERROR. GetInt and GetUInt return plain int and unsigned int for cast-free use (e.g. printf). Add matching TryGet* overloads for non-throwing reads; refactor existing conversion paths to share decode logic. Co-authored-by: Cursor --- cpp-lib/include/asn-incl.h | 41 +++++++++ cpp-lib/src/asn-int.cpp | 173 ++++++++++++++++++++++++++++++------- 2 files changed, 184 insertions(+), 30 deletions(-) diff --git a/cpp-lib/include/asn-incl.h b/cpp-lib/include/asn-incl.h index 2c72a09..8cd39fa 100644 --- a/cpp-lib/include/asn-incl.h +++ b/cpp-lib/include/asn-incl.h @@ -863,10 +863,51 @@ class SNACCDLL_API AsnInt : public AsnType, protected PERGeneral return "AsnInt"; } + /*! Legacy implicit conversion to 32-bit signed int; prefer GetInt() in new code. */ operator AsnIntType() const; + /*! Returns the INTEGER as a 32-bit signed @c int. Throws if the encoded value does not fit. */ + int GetInt() const; + + /*! Returns the INTEGER as @c long (width is platform-dependent). Throws if the encoded value does not fit in @c long long before narrowing. */ + long GetLong() const; + + /*! Returns the INTEGER as 64-bit signed. Throws if the encoded value does not fit. Same as GetLongLong(). */ + long long GetInt64() const + { + return GetLongLong(); + } + + /*! Returns the INTEGER as 64-bit signed. Prefer GetInt64() in new code. */ long long GetLongLong() const; + /*! Returns the INTEGER as a 32-bit @c unsigned int. Throws if negative or out of range. */ + unsigned int GetUInt() const; + + /*! Returns the INTEGER as @c unsigned long. Throws if negative or out of range. */ + unsigned long GetULong() const; + + /*! Returns the INTEGER as 64-bit unsigned. Throws if negative. Values above @c LLONG_MAX cannot be represented via the signed decode path. */ + unsigned long long GetUInt64() const; + + /*! Non-throwing @c GetInt(); returns @c false when the value does not fit in @c int. */ + bool TryGetInt(int& out) const; + + /*! Non-throwing @c GetLong(); returns @c false when the value does not fit @c long. */ + bool TryGetLong(long& out) const; + + /*! Non-throwing @c GetInt64(); returns @c false when the encoded value does not fit in 64-bit signed. */ + bool TryGetInt64(long long& out) const; + + /*! Non-throwing @c GetUInt(); returns @c false when negative or out of @c unsigned int range. */ + bool TryGetUInt(unsigned int& out) const; + + /*! Non-throwing @c GetULong(); returns @c false when negative or out of @c unsigned long range. */ + bool TryGetULong(unsigned long& out) const; + + /*! Non-throwing @c GetUInt64(); returns @c false when negative or not representable via the signed decode path. */ + bool TryGetUInt64(unsigned long long& out) const; + bool operator==(AsnIntType o) const; bool operator!=(AsnIntType o) const { diff --git a/cpp-lib/src/asn-int.cpp b/cpp-lib/src/asn-int.cpp index 631a070..2ddcd80 100644 --- a/cpp-lib/src/asn-int.cpp +++ b/cpp-lib/src/asn-int.cpp @@ -319,8 +319,39 @@ #include "../include/asn-incl.h" +#include +#include #include +namespace +{ + void SnaccThrowIfNegativeInt64(long long llValue) + { + if (llValue < 0) + throw EXCEPT("integer is negative", INTEGER_ERROR); + } + + void SnaccThrowIfInt64Exceeds(long long llValue, long long llMaxInclusive, const char* szTargetType) + { + if (llValue > llMaxInclusive) + { + char szMessage[128]; + snprintf(szMessage, sizeof(szMessage), "integer is too big for conversion to %s", szTargetType); + throw EXCEPT(szMessage, INTEGER_ERROR); + } + } + + bool SnaccInt64FitsSignedRange(long long llValue, long long llMinInclusive, long long llMaxInclusive) + { + return llValue >= llMinInclusive && llValue <= llMaxInclusive; + } + + bool SnaccInt64FitsUnsignedRange(long long llValue, long long llMaxInclusive) + { + return llValue >= 0 && llValue <= llMaxInclusive; + } +} // namespace + _BEGIN_SNACC_NAMESPACE #if META @@ -657,62 +688,144 @@ AsnInt& AsnInt::operator=(const AsnInt& that) // AsnInt::operator AsnIntType() const { - FUNC("AsnInt::operator AsnIntType"); + return GetInt(); +} + +int AsnInt::GetInt() const +{ + int iResult = 0; + if (!TryGetInt(iResult)) + throw EXCEPT("integer is too big for conversion to int", INTEGER_ERROR); + return iResult; +} - AsnIntType iResult = 0; +bool AsnInt::TryGetInt(int& out) const +{ + if (m_len > sizeof(int)) + return false; - if (m_len > sizeof(AsnIntType)) - throw EXCEPT("integer is too big for conversion to AsnIntType", INTEGER_ERROR); + int iResult = 0; - // If big int is negative initialize result to -1 - // - if ((m_bytes[0] >> 7 == 1)) + if (m_len > 0 && (m_bytes[0] >> 7 == 1)) iResult = -1; if (m_len > 0) { - /* - * write from buffer into AsnIntType - */ for (unsigned int i = 0; i < m_len; i++) iResult = (iResult << 8) | (AsnUIntType)(m_bytes[i]); } - else - { - iResult = 0; - } - return iResult; + out = iResult; + return true; } -long long AsnInt::GetLongLong() const +long AsnInt::GetLong() const { - FUNC("AsnInt::operator long long"); + long lResult = 0; + if (!TryGetLong(lResult)) + throw EXCEPT("integer is too big for conversion to long", INTEGER_ERROR); + return lResult; +} - long long iResult = 0; +bool AsnInt::TryGetLong(long& out) const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + return false; + if (!SnaccInt64FitsSignedRange(llValue, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()))) + return false; + out = static_cast(llValue); + return true; +} + +unsigned int AsnInt::GetUInt() const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + throw EXCEPT("integer is too big for conversion to unsigned int", INTEGER_ERROR); + SnaccThrowIfNegativeInt64(llValue); + SnaccThrowIfInt64Exceeds(llValue, static_cast(std::numeric_limits::max()), "unsigned int"); + return static_cast(llValue); +} + +bool AsnInt::TryGetUInt(unsigned int& out) const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + return false; + if (!SnaccInt64FitsUnsignedRange(llValue, static_cast(std::numeric_limits::max()))) + return false; + out = static_cast(llValue); + return true; +} + +unsigned long AsnInt::GetULong() const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + throw EXCEPT("integer is too big for conversion to unsigned long", INTEGER_ERROR); + SnaccThrowIfNegativeInt64(llValue); + SnaccThrowIfInt64Exceeds(llValue, static_cast(std::numeric_limits::max()), "unsigned long"); + return static_cast(llValue); +} + +bool AsnInt::TryGetULong(unsigned long& out) const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + return false; + if (!SnaccInt64FitsUnsignedRange(llValue, static_cast(std::numeric_limits::max()))) + return false; + out = static_cast(llValue); + return true; +} + +unsigned long long AsnInt::GetUInt64() const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + throw EXCEPT("integer is too big for conversion to unsigned long long", INTEGER_ERROR); + SnaccThrowIfNegativeInt64(llValue); + return static_cast(llValue); +} +bool AsnInt::TryGetUInt64(unsigned long long& out) const +{ + long long llValue = 0; + if (!TryGetInt64(llValue)) + return false; + if (llValue < 0) + return false; + out = static_cast(llValue); + return true; +} + +long long AsnInt::GetLongLong() const +{ + long long llResult = 0; + if (!TryGetInt64(llResult)) + throw EXCEPT("integer is too big for conversion to long long", INTEGER_ERROR); + return llResult; +} + +bool AsnInt::TryGetInt64(long long& out) const +{ if (m_len > sizeof(long long)) - throw EXCEPT("integer is too big for conversion to AsnIntType", INTEGER_ERROR); + return false; - // If big int is negative initialize result to -1 - // - if ((m_bytes[0] >> 7 == 1)) + long long iResult = 0; + + if (m_len > 0 && (m_bytes[0] >> 7 == 1)) iResult = -1; if (m_len > 0) { - /* - * write from buffer into AsnIntType - */ for (unsigned int i = 0; i < m_len; i++) iResult = (iResult << 8) | (AsnUIntType)(m_bytes[i]); } - else - { - iResult = 0; - } - return iResult; + out = iResult; + return true; } // Set AsnInt from a buffer. Buffer is assumed to be a proper // ASN.1 integer. From ba9a0175a0a1b66e6996702f6fc1cfb587827ca2 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 13:13:12 +0200 Subject: [PATCH 05/11] UCAAS-1486: improve debug asserts with messages and ASSERT_FAILED Extend snacc-assert.h with optional ASSERT messages, printf-style ASSERT_FAILED, and C-compatible inline helpers. Use descriptive asserts in SnaccROSEBase for remote-capability rejects, encoding detection, and registration preconditions; add matching snaccAssert helpers in TS gluecode. Co-authored-by: Cursor --- .../back-ends/ts-gen/gluecode/TSASN1Base.ts | 7 ++- .../back-ends/ts-gen/gluecode/TSROSEBase.ts | 13 ++++ cpp-lib/include/snacc-assert.h | 61 ++++++++++++++++--- cpp-lib/src/SnaccROSEBase.cpp | 55 ++++++----------- 4 files changed, 89 insertions(+), 47 deletions(-) diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts index ecd40d9..af16378 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts @@ -30,6 +30,8 @@ import { ISendInvokeContext, ReceiveInvokeContext, RemoteCapabilityMode, + snaccAssert, + snaccAssertFail, ASN1ByteArray, ROSEBase, } from "./TSROSEBase.js"; @@ -578,7 +580,7 @@ export abstract class TSASN1Base implements IASN1Transport { * Debug assert when hasRemoteModuleCapabilities() is false. */ public isSupportedOperation(operationID: number): boolean { - console.assert( + snaccAssert( this.remoteModuleCapabilitiesSet, "isSupportedOperation requires applyRemoteModuleCapabilities first", ); @@ -596,6 +598,9 @@ export abstract class TSASN1Base implements IASN1Transport { return undefined; if (this.internalIsRemoteOperationSupported(invoke.operationID)) return undefined; + snaccAssertFail( + `Outbound invoke blocked: operation not offered by remote (${invoke.operationName}, ${invoke.operationID})`, + ); return createInvokeReject( invoke, CustomInvokeProblemEnum.remoteNotCapable, diff --git a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts index 402d2f8..0951be2 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts @@ -72,6 +72,19 @@ export enum CustomInvokeProblemEnum { /** Client-local SendInvoke result: peer negotiate snapshot does not offer this invoke OPID. */ export const ROSE_REJECT_REMOTENOTCAPABLE = 0x00000E00; +/** + * Debug-only assert with a human-readable message (console.assert in Node/browser). + * Use snaccAssert(check, msg) for preconditions or snaccAssertFail(msg) when already in an error path. + */ +export function snaccAssert(condition: boolean, message: string): void { + console.assert(condition, message); +} + +/** Debug-only assert for a known error path (always fails when asserts are enabled). */ +export function snaccAssertFail(message: string): void { + console.assert(false, message); +} + /** Controls outbound invoke gating against a negotiate snapshot on TSASN1Base. */ export enum RemoteCapabilityMode { Disabled = 0, diff --git a/cpp-lib/include/snacc-assert.h b/cpp-lib/include/snacc-assert.h index 66f827a..835733f 100644 --- a/cpp-lib/include/snacc-assert.h +++ b/cpp-lib/include/snacc-assert.h @@ -1,8 +1,16 @@ #pragma once +#include + #if defined(NDEBUG) -#define ASSERT(expr) ((void)0) + +#define ASSERT(...) ((void)0) +#define ASSERT_FAILED(...) ((void)0) + #else + +#include + #if defined(_MSC_VER) #define DEBUG_BREAK() __debugbreak() #elif defined(__GNUC__) || defined(__clang__) @@ -12,13 +20,46 @@ #define DEBUG_BREAK() ((void)0) #endif -#define ASSERT(expr) \ - do \ - { \ - if (!(expr)) \ - { \ - fprintf(stderr, "ASSERT failed: %s (%s:%d)\n", #expr, __FILE__, __LINE__); \ - DEBUG_BREAK(); \ - } \ - } while (0) +#if defined(__cplusplus) +#define SNACC_ASSERT_INLINE inline +#else +#if defined(_MSC_VER) +#define SNACC_ASSERT_INLINE static __inline +#else +#define SNACC_ASSERT_INLINE static inline +#endif +#endif + +SNACC_ASSERT_INLINE void SnaccAssertImpl(int bCondition, const char* szMessage, const char* szExpr, const char* szFile, int iLine) +{ + if (bCondition) + return; + if (szExpr && szExpr[0] != '\0') + fprintf(stderr, "ASSERT failed: %s\n condition: %s\n at %s:%d\n", szMessage, szExpr, szFile, iLine); + else + fprintf(stderr, "ASSERT failed: %s\n at %s:%d\n", szMessage, szFile, iLine); + DEBUG_BREAK(); +} + +SNACC_ASSERT_INLINE void SnaccAssertFailV(const char* szFormat, ...) +{ + char szMessage[512]; + va_list args; + va_start(args, szFormat); + (void)vsnprintf(szMessage, sizeof(szMessage), szFormat, args); + va_end(args); + szMessage[sizeof(szMessage) - 1] = '\0'; + SnaccAssertImpl(0, szMessage, "", __FILE__, __LINE__); +} + +#define SNACC_ASSERT_GET_MACRO(_1, _2, NAME, ...) NAME +#define SNACC_ASSERT_1(condition) SnaccAssertImpl(!!(condition), #condition, #condition, __FILE__, __LINE__) +#define SNACC_ASSERT_2(condition, message) SnaccAssertImpl(!!(condition), (message), #condition, __FILE__, __LINE__) + +/* Debug assert: ASSERT(condition) uses #condition as message; ASSERT(condition, message) prints message. */ +#define ASSERT(...) SNACC_ASSERT_GET_MACRO(__VA_ARGS__, SNACC_ASSERT_2, SNACC_ASSERT_1)(__VA_ARGS__) + +/* Debug assert for a known error path; supports printf-style formatting. */ +#define ASSERT_FAILED(...) SnaccAssertFailV(__VA_ARGS__) + #endif diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 19e40c3..38ecf7a 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -16,13 +16,13 @@ using namespace SNACC; namespace { -int ResolveInvokeTimeoutMs(const SnaccInvokeContext& ctx, long lMaxInvokeWait) -{ - const int iTimeout = ctx.InvokeTimeout(); - if (iTimeout == -1) - return static_cast(lMaxInvokeWait); - return iTimeout; -} + int ResolveInvokeTimeoutMs(const SnaccInvokeContext& ctx, long lMaxInvokeWait) + { + const int iTimeout = ctx.InvokeTimeout(); + if (iTimeout == -1) + return static_cast(lMaxInvokeWait); + return iTimeout; + } } // namespace namespace @@ -540,7 +540,7 @@ const char* strstr_limited(const char* haystack, const char* needle, size_t limi } catch (...) { - ASSERT(0); + ASSERT_FAILED("Unhandled exception caught"); } return NULL; @@ -606,14 +606,7 @@ void SnaccROSEComponent::RegisterModuleVersion(const char* szModuleName, const c pStub->OperationLookupForRegistration().RegisterModuleVersion(szModuleName, szVersion); } -void SnaccROSEComponent::RegisterOperation( - unsigned int uiOpID, - const char* szOpName, - unsigned int uiInterfaceID, - const char* szModuleName, - bool bIsEvent, - unsigned long long ullAddedUnix, - unsigned long long ullDeprecatedUnix) +void SnaccROSEComponent::RegisterOperation(unsigned int uiOpID, const char* szOpName, unsigned int uiInterfaceID, const char* szModuleName, bool bIsEvent, unsigned long long ullAddedUnix, unsigned long long ullDeprecatedUnix) { if (auto* pHost = dynamic_cast(m_pSB)) pHost->OperationLookup().RegisterOperation(uiOpID, szOpName, uiInterfaceID, szModuleName, bIsEvent, ullAddedUnix, ullDeprecatedUnix); @@ -623,10 +616,7 @@ void SnaccROSEComponent::RegisterOperation( SnaccRoseOperationLookup& SnaccROSEBase::OperationLookupForRegistration() { -#ifdef _DEBUG - if (m_operationLookup.IsSealed()) - ASSERT(0); -#endif + ASSERT(!m_operationLookup.IsSealed(), "m_operationLookup is already sealed"); return const_cast(m_operationLookup); } @@ -702,10 +692,7 @@ bool SnaccROSEBase::InternalIsRemoteOperationSupported(const unsigned int uiOpId bool SnaccROSEBase::IsSupportedOperation(const unsigned int uiOpId) const { -#ifdef _DEBUG - if (!m_bRemoteModuleCapabilitiesSet) - ASSERT(0); -#endif + ASSERT(m_bRemoteModuleCapabilitiesSet, "isSupportedOperation requires ApplyRemoteModuleCapabilities first"); return InternalIsRemoteOperationSupported(uiOpId); } @@ -843,12 +830,10 @@ void SnaccROSEBase::CompleteAllPendingOperations() std::lock_guard guard(m_InternalProtectMutex); for (const auto& entry : m_PendingOperations) - { if (entry.second->m_bAsyncInvoke) asyncInvokeIds.push_back(entry.second->m_lInvokeID); else entry.second->CompleteOperation(ROSE_TE_SHUTDOWN); - } } for (const long invokeID : asyncInvokeIds) @@ -1121,7 +1106,7 @@ std::string SnaccROSEBase::GetEncoded(const SNACC::TransportEncoding encoding, c } break; default: - ASSERT(false); + ASSERT_FAILED("Invalid encoding provided %u", (unsigned int)encoding); throw std::runtime_error("invalid encoding"); break; } @@ -1133,7 +1118,7 @@ SNACC::TransportEncoding SnaccROSEBase::DetectEncoding(const char* lpBytes, unsi if (!ulSize) return SNACC::TransportEncoding::UNKNOWN; - unsigned char byFirst = (unsigned char)*lpBytes; + unsigned char byFirst = (unsigned char)lpBytes[0]; if (byFirst == 0xA1 || byFirst == 0xA2 || byFirst == 0xA3 || byFirst == 0xA4) return SNACC::TransportEncoding::BER; else if (byFirst == 'J') @@ -1142,7 +1127,7 @@ SNACC::TransportEncoding SnaccROSEBase::DetectEncoding(const char* lpBytes, unsi return SNACC::TransportEncoding::JSON_NO_HEADING; else { - ASSERT(false); + ASSERT_FAILED("Could not detect encoding from data %02x", byFirst); return SNACC::TransportEncoding::UNKNOWN; } } @@ -1617,7 +1602,7 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName // You need to specify the transport encoding when the connection is setup // The connection is no longer defaulting to BER. This needs to be set explicit. // Inbound connections adopt the encoding as soon as the first payload is received - ASSERT(0); + ASSERT_FAILED("Invalid m_eTransportEncoding %u", (unsigned int)m_eTransportEncoding); throw std::runtime_error("invalid m_eTransportEncoding"); } @@ -1750,6 +1735,7 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResu if (m_remoteCapabilityMode == SnaccRemoteCapabilityMode::Enabled && m_bRemoteModuleCapabilitiesSet && !InternalIsRemoteOperationSupported(pInvoke->operationID)) { + ASSERT_FAILED("Outbound invoke blocked: operation %s (%u) is not offered by the remote peer", szResolvedOperationName ? szResolvedOperationName : "?", pInvoke->operationID.GetUInt()); auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::LOCAL_REJECT, ROSE_REJECT_REMOTENOTCAPABLE, std::nullopt, std::move(pCtx)); OnInvokeProcessed(telemetry); @@ -1850,9 +1836,7 @@ void SnaccROSEBase::FinishAsyncInvoke(long invokeID, long lRoseResult, std::uniq long lFinalRoseResult = lRoseResult; std::shared_ptr pCtx = pending->m_pAsyncContext; if (pending->m_pAnswerMessage && pCtx) - { lFinalRoseResult = HandleInvokeResult(lFinalRoseResult, *pending->m_pAnswerMessage, pCtx->AsyncResultBuffer(), pCtx->AsyncErrorBuffer(), *pCtx); - } pending->EnsureOutboundTelemetry(); @@ -1990,9 +1974,7 @@ long SnaccROSEBase::SendInvokeAsync(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* ASSERT(!bFireAndForget || !pCtx->HasAsyncCompletion()); if (!bFireAndForget && !pCtx->HasAsyncCompletion()) - { pCtx->SetAsyncCompletion([](long, SnaccInvokeContext&) {}, pResult, pError); - } auto& ctx = *pCtx; @@ -2012,6 +1994,7 @@ long SnaccROSEBase::SendInvokeAsync(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* if (m_remoteCapabilityMode == SnaccRemoteCapabilityMode::Enabled && m_bRemoteModuleCapabilitiesSet && !InternalIsRemoteOperationSupported(pInvoke->operationID)) { + ASSERT_FAILED("Outbound invoke blocked: operation %s (%u) is not offered by the remote peer", szResolvedOperationName ? szResolvedOperationName : "?", pInvoke->operationID.GetUInt()); SnaccInvokeAsyncCallback rejectCallback; if (!bFireAndForget && pCtx->HasAsyncCompletion()) rejectCallback = pCtx->AsyncCallback(); @@ -2561,7 +2544,7 @@ bool SnaccROSEBase::PrintJSONToLog(const bool bOutbound, const bool bException, { // No length was handed over // When trying to get the length from a zero terminated string it looks like we ran into memory we are not allowed to read - ASSERT(false); + ASSERT(0); } } @@ -2604,7 +2587,7 @@ bool SnaccROSEBase::PrintJSONToLog(const bool bOutbound, const bool bException, } catch (...) { - ASSERT(false); + ASSERT(0); } return false; } From ad3c7f87a0aab06553b96d27fbe9ce937a49e9b6 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 13:39:50 +0200 Subject: [PATCH 06/11] UCAAS-1486: refresh RELDATE for esnacc 7.0.15 Co-authored-by: Cursor --- version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.h b/version.h index d7223d4..17c5f86 100644 --- a/version.h +++ b/version.h @@ -3,6 +3,6 @@ #define VERSION "7.0.15" #define VERSION_RC 7, 0, 15 -#define RELDATE "19.08.2026" +#define RELDATE "20.08.2026" #endif // VERSION_H From c54e4c35e7612475c9a05f59fdf580f007ea865d Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 17:32:50 +0200 Subject: [PATCH 07/11] UCAAS-1486: simplify ROSE registration codegen and ordered module maps Route generated RegisterOperations through SnaccROSEComponent static helpers, keep module metadata in generated .cpp only, and restore inline m_iid for switch dispatch. Use ordered loaded-module maps with debug-only operation names and TS opName metadata. Co-authored-by: Cursor --- compiler/back-ends/c++-gen/gen-code.c | 63 ++++++++++++------- compiler/back-ends/c++-gen/gen-vals.c | 3 +- .../gluecode/TSASN1Base.registry.test.ts | 2 + .../back-ends/ts-gen/gluecode/TSASN1Base.ts | 5 +- .../back-ends/ts-gen/gluecode/TSROSEBase.ts | 2 + cpp-lib/include/SnaccROSEInterfaces.h | 13 ++-- cpp-lib/include/SnaccRoseOperationLookup.h | 10 ++- cpp-lib/src/SnaccModuleCapabilities.cpp | 2 +- cpp-lib/src/SnaccROSEBase.cpp | 18 +++--- cpp-lib/src/SnaccRoseOperationLookup.cpp | 4 ++ cpp-lib/tests/module_registry_tests.cpp | 6 ++ 11 files changed, 80 insertions(+), 48 deletions(-) diff --git a/compiler/back-ends/c++-gen/gen-code.c b/compiler/back-ends/c++-gen/gen-code.c index f08c326..c020c24 100644 --- a/compiler/back-ends/c++-gen/gen-code.c +++ b/compiler/back-ends/c++-gen/gen-code.c @@ -4584,48 +4584,63 @@ void PrintROSECode(FILE* src, FILE* hdr, FILE* hdrInterface, ModuleList* mods, M // Constructor fprintf(hdr, "\t%s(SnaccROSESender* pBase);\n", m->ROSEClassName); - fprintf(src, "%s::%s(SnaccROSESender* pBase) : SnaccROSEComponent(pBase)\n", m->ROSEClassName, m->ROSEClassName); - fprintf(src, "{\n"); - fprintf(src, "}\n\n"); - // Function for triggering the registration of all Operations (name/id) - fprintf(hdr, "\t// Registers all known operations on a listener lookup table at startup (UCAAS-1485)\n"); - fprintf(hdr, "\tstatic void RegisterOperations(SnaccRoseOperationLookup& lookup);\n"); + FOR_EACH_LIST_ELMT(vd, m->valueDefs) + { + if (IsDeprecatedNoOutputOperation(m, vd->definedName)) + continue; + if (vd->value->basicValue->choiceId != BASICVALUE_INTEGER) + continue; + if (vd->value->type->basicType->choiceId != BASICTYPE_MACROTYPE) + continue; + if (vd->value->type->basicType->a.macroType->choiceId != MACROTYPE_ROSOPERATION) + continue; + if (!iFirstIIDFound) + { + iFirstIIDFound = 1; + fprintf(hdr, "\tstatic const int m_iid = %d;\n", vd->value->basicValue->a.integer); + } + break; + } - fprintf(src, "void %s::RegisterOperations(SnaccRoseOperationLookup& lookup)\n", m->ROSEClassName); - fprintf(src, "{\n"); + fprintf(src, "namespace\n{\n"); + fprintf(src, "constexpr const char kModuleName[] = \"%s\";\n", m->moduleName); if (gMajorInterfaceVersion >= 0) { long long lPatchVersion = GetModulePatchVersion(m->moduleName); char* szNumericDate = ConvertUnixTimeToNumericDate(lPatchVersion); if (szNumericDate) { - fprintf(src, "\tlookup.RegisterModuleVersion(\"%s\", \"%i.0.%s\");\n", m->moduleName, gMajorInterfaceVersion, szNumericDate); + fprintf(src, "constexpr const char kModuleVersion[] = \"%i.0.%s\";\n", gMajorInterfaceVersion, szNumericDate); free(szNumericDate); } + else + fprintf(src, "constexpr const char kModuleVersion[] = \"\";\n"); } + else + fprintf(src, "constexpr const char kModuleVersion[] = \"\";\n"); + fprintf(src, "} // namespace\n\n"); + + fprintf(src, "%s::%s(SnaccROSESender* pBase) : SnaccROSEComponent(pBase)\n", m->ROSEClassName, m->ROSEClassName); + fprintf(src, "{\n"); + fprintf(src, "}\n\n"); + + // Function for triggering the registration of all Operations (name/id) + fprintf(hdr, "\t// Registers all known operations on a listener lookup table at startup (UCAAS-1485)\n"); + fprintf(hdr, "\tstatic void RegisterOperations(SnaccRoseOperationLookup& lookup);\n"); + + fprintf(src, "void %s::RegisterOperations(SnaccRoseOperationLookup& lookup)\n", m->ROSEClassName); + fprintf(src, "{\n"); + if (gMajorInterfaceVersion >= 0) + fprintf(src, "\tRegisterModuleVersion(lookup, kModuleName, kModuleVersion);\n"); FOR_EACH_LIST_ELMT(vd, m->valueDefs) { if (IsDeprecatedNoOutputOperation(m, vd->definedName)) continue; - if (PrintROSEOperationRegistrationLookup(src, r, m, vd) && !iFirstIIDFound) - { - iFirstIIDFound = 1; - fprintf(hdr, "\tstatic const int m_iid = %d;\n", vd->value->basicValue->a.integer); - } + PrintROSEOperationRegistration(src, r, m, vd); } fprintf(src, "}\n\n"); - if (iFirstIIDFound) - { - fprintf(hdr, "protected:\n"); - fprintf(hdr, "\tvoid RegisterOperation(unsigned int uiOpID, const char* szOpName, bool bIsEvent = false, unsigned long long ullAddedUnix = 0, unsigned long long ullDeprecatedUnix = 0)\n"); - fprintf(hdr, "\t{\n"); - fprintf(hdr, "\t\tSnaccROSEComponent::RegisterOperation(uiOpID, szOpName, m_iid, \"%s\", bIsEvent, ullAddedUnix, ullDeprecatedUnix);\n", m->moduleName); - fprintf(hdr, "\t}\n\n"); - fprintf(hdr, "public:\n"); - } - fflush(src); fflush(hdr); diff --git a/compiler/back-ends/c++-gen/gen-vals.c b/compiler/back-ends/c++-gen/gen-vals.c index 799bc38..b9fc561 100644 --- a/compiler/back-ends/c++-gen/gen-vals.c +++ b/compiler/back-ends/c++-gen/gen-vals.c @@ -109,8 +109,7 @@ int PrintROSEOperationRegistration(FILE* src, CxxRules* r, Module* mod, ValueDef /* * put instantiation in src file */ - fprintf(src, "\tRegisterOperation("); - fprintf(src, "%d, \"", v->value->basicValue->a.integer); + fprintf(src, "\tRegisterOperation(lookup, m_iid, kModuleName, %d, \"", v->value->basicValue->a.integer); PrintCxxValueDefsName(src, r, v); fprintf(src, "\""); if (bIsEvent) diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts index bc99b73..65b17ce 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts @@ -46,7 +46,9 @@ test("registerOperation metadata appears in getLoadedModules", () => { assert.equal(module.version, "20240101.0.20240506"); assert.equal(module.invokes.get(100)?.addedUnix, 1714968000); assert.equal(module.invokes.get(100)?.deprecatedUnix, 0); + assert.equal(module.invokes.get(100)?.opName, "asnInvoke"); assert.equal(module.events.get(200)?.deprecatedUnix, 1715054400); + assert.equal(module.events.get(200)?.opName, "asnEvent"); transport.unregisterModule("TestModule"); assert.equal(transport.getLoadedModules().size, 0); diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts index af16378..a2769c3 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts @@ -402,7 +402,7 @@ export abstract class TSASN1Base implements IASN1Transport { ); this.handlersByID.set(operationID, handler); this.handlersByName.set(operationName, handler); - this.trackRegisteredOperation(operationID, moduleName, addedUnix, deprecatedUnix, isEvent); + this.trackRegisteredOperation(operationID, operationName, moduleName, addedUnix, deprecatedUnix, isEvent); } else { // trying to re-register a handler for an already registered operationID, this should not happen and indicates a problem in the calling code debugger; @@ -414,6 +414,7 @@ export abstract class TSASN1Base implements IASN1Transport { */ private trackRegisteredOperation( operationID: number, + operationName: string, moduleName: string, addedUnix: number, deprecatedUnix: number, @@ -430,7 +431,7 @@ export abstract class TSASN1Base implements IASN1Transport { this.loadedModulesByName.set(moduleName, module); } - const info: IOpVersionInfo = { addedUnix, deprecatedUnix }; + const info: IOpVersionInfo = { addedUnix, deprecatedUnix, opName: operationName }; if (isEvent) module.events.set(operationID, info); else diff --git a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts index 0951be2..2c8faf4 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts @@ -439,6 +439,8 @@ export enum ELogSeverity { export interface IOpVersionInfo { addedUnix: number; deprecatedUnix: number; + /** Set during local registerOperation; absent on remote negotiate snapshots. */ + opName?: string; } /** diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index 15e48ba..aef4ced 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -6,6 +6,8 @@ #include #include +class SnaccRoseOperationLookup; + namespace SNACC { class AsnType; @@ -376,8 +378,7 @@ class SnaccScopedInvokeMessage for all generated ROSE Protocol Handlers (V2) that need OnInvoke */ -/*! SnaccROSEComponent is the base class for all generated ROSE Protocol Handlers (V2) - */ +/*! SnaccROSEComponent is the base class for all generated ROSE Protocol Handlers (V2). */ class SnaccROSEComponent { public: @@ -387,11 +388,11 @@ class SnaccROSEComponent } protected: - /*! Registers module version metadata on the lookup table referenced by @c m_pSB. */ - void RegisterModuleVersion(const char* szModuleName, const char* szVersion); + /*! Registers module version metadata on a startup lookup table (static RegisterOperations). */ + static void RegisterModuleVersion(SnaccRoseOperationLookup& lookup, const char* szModuleName, const char* szModuleVersion); - /*! Registers one ROSE operation on the lookup table referenced by @c m_pSB (UCAAS-1485). */ - void RegisterOperation(unsigned int uiOpID, const char* szOpName, unsigned int uiInterfaceID, const char* szModuleName, bool bIsEvent = false, unsigned long long ullAddedUnix = 0, unsigned long long ullDeprecatedUnix = 0); + /*! Registers one operation on a startup lookup table (static RegisterOperations). */ + static void RegisterOperation(SnaccRoseOperationLookup& lookup, unsigned int uiInterfaceId, const char* szModuleName, unsigned int uiOpID, const char* szOpName, bool bIsEvent = false, unsigned long long ullAddedUnix = 0, unsigned long long ullDeprecatedUnix = 0); SnaccROSESender* m_pSB; }; diff --git a/cpp-lib/include/SnaccRoseOperationLookup.h b/cpp-lib/include/SnaccRoseOperationLookup.h index 0d11ec4..8ce062d 100644 --- a/cpp-lib/include/SnaccRoseOperationLookup.h +++ b/cpp-lib/include/SnaccRoseOperationLookup.h @@ -14,6 +14,10 @@ struct SnaccOpVersionInfo { unsigned long long m_ullAddedUnix = 0; unsigned long long m_ullDeprecatedUnix = 0; +#ifdef _DEBUG + /*! Local registration only; empty for remote negotiate snapshots. */ + std::string m_strOpName; +#endif }; /*! Loaded module snapshot for negotiate / introspection on one lookup table. */ @@ -21,11 +25,11 @@ struct SnaccLoadedModuleInfo { std::string m_strModuleName; std::string m_strVersion; - std::unordered_map m_invokes; - std::unordered_map m_events; + std::map m_invokes; + std::map m_events; }; -using SnaccLoadedModuleMap = std::unordered_map; +using SnaccLoadedModuleMap = std::map; /*! Controls outbound invoke gating against a negotiate snapshot on SnaccROSEBase. */ enum class SnaccRemoteCapabilityMode diff --git a/cpp-lib/src/SnaccModuleCapabilities.cpp b/cpp-lib/src/SnaccModuleCapabilities.cpp index 48303cd..3ef8c86 100644 --- a/cpp-lib/src/SnaccModuleCapabilities.cpp +++ b/cpp-lib/src/SnaccModuleCapabilities.cpp @@ -2,7 +2,7 @@ namespace { -void ApplyOpIds(const int* pOpIds, const size_t stOpIdCount, std::unordered_map& inOutOps) +void ApplyOpIds(const int* pOpIds, const size_t stOpIdCount, std::map& inOutOps) { if (!pOpIds) return; diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 38ecf7a..822743f 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -598,20 +598,18 @@ SnaccTelemetryData::Stage GetOutboundUnhandledStageFromResult(const long lRoseRe } // check if at least one Operation has been registerd -void SnaccROSEComponent::RegisterModuleVersion(const char* szModuleName, const char* szVersion) +void SnaccROSEComponent::RegisterModuleVersion(SnaccRoseOperationLookup& lookup, const char* szModuleName, const char* szModuleVersion) { - if (auto* pHost = dynamic_cast(m_pSB)) - pHost->OperationLookup().RegisterModuleVersion(szModuleName, szVersion); - else if (auto* pStub = dynamic_cast(m_pSB)) - pStub->OperationLookupForRegistration().RegisterModuleVersion(szModuleName, szVersion); + if (szModuleName && szModuleVersion && *szModuleVersion) + lookup.RegisterModuleVersion(szModuleName, szModuleVersion); } -void SnaccROSEComponent::RegisterOperation(unsigned int uiOpID, const char* szOpName, unsigned int uiInterfaceID, const char* szModuleName, bool bIsEvent, unsigned long long ullAddedUnix, unsigned long long ullDeprecatedUnix) +void SnaccROSEComponent::RegisterOperation(SnaccRoseOperationLookup& lookup, unsigned int uiInterfaceId, const char* szModuleName, unsigned int uiOpID, const char* szOpName, bool bIsEvent, unsigned long long ullAddedUnix, unsigned long long ullDeprecatedUnix) { - if (auto* pHost = dynamic_cast(m_pSB)) - pHost->OperationLookup().RegisterOperation(uiOpID, szOpName, uiInterfaceID, szModuleName, bIsEvent, ullAddedUnix, ullDeprecatedUnix); - else if (auto* pStub = dynamic_cast(m_pSB)) - pStub->OperationLookupForRegistration().RegisterOperation(uiOpID, szOpName, uiInterfaceID, szModuleName, bIsEvent, ullAddedUnix, ullDeprecatedUnix); + if (!szModuleName || !szOpName) + return; + + lookup.RegisterOperation(uiOpID, szOpName, uiInterfaceId, szModuleName, bIsEvent, ullAddedUnix, ullDeprecatedUnix); } SnaccRoseOperationLookup& SnaccROSEBase::OperationLookupForRegistration() diff --git a/cpp-lib/src/SnaccRoseOperationLookup.cpp b/cpp-lib/src/SnaccRoseOperationLookup.cpp index 67db7e2..92fa5ed 100644 --- a/cpp-lib/src/SnaccRoseOperationLookup.cpp +++ b/cpp-lib/src/SnaccRoseOperationLookup.cpp @@ -67,6 +67,10 @@ void SnaccRoseOperationLookup::RegisterOperation( SnaccOpVersionInfo info; info.m_ullAddedUnix = ullAddedUnix; info.m_ullDeprecatedUnix = ullDeprecatedUnix; +#ifdef _DEBUG + if (szOpName) + info.m_strOpName = szOpName; +#endif if (bIsEvent) module.m_events[uiOpID] = info; else diff --git a/cpp-lib/tests/module_registry_tests.cpp b/cpp-lib/tests/module_registry_tests.cpp index 1f2ed17..8cc1654 100644 --- a/cpp-lib/tests/module_registry_tests.cpp +++ b/cpp-lib/tests/module_registry_tests.cpp @@ -69,6 +69,12 @@ TEST(ModuleRegistryTest, RegisteredMetadataMatchesLoadedModuleSnapshot) ExpectOpInfo(module.m_invokes.at(4100u), true, false); ExpectOpInfo(module.m_invokes.at(4101u), false, false); ExpectOpInfo(module.m_invokes.at(4102u), false, true); +#ifdef _DEBUG + EXPECT_STREQ(endpoint.LookUpName(4100u), module.m_invokes.at(4100u).m_strOpName.c_str()); + EXPECT_STREQ(endpoint.LookUpName(4101u), module.m_invokes.at(4101u).m_strOpName.c_str()); + EXPECT_STREQ(endpoint.LookUpName(4102u), module.m_invokes.at(4102u).m_strOpName.c_str()); + EXPECT_STREQ(endpoint.LookUpName(4150u), module.m_events.at(4150u).m_strOpName.c_str()); +#endif ASSERT_NE(module.m_events.end(), module.m_events.find(4150u)); ExpectOpInfo(module.m_events.at(4150u), false, false); From 7a9908beb3e77e2491a1488232fbe6bc4eb2e24e Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 17:36:20 +0200 Subject: [PATCH 08/11] UCAAS-1486: move TypeScript gluecode tests to ts-gen/tests Keep gluecode limited to compiler-emitted runtime files; run scripts and build docs point at the dedicated test folder. Co-authored-by: Cursor --- .../TSASN1Base.registry.test.ts | 10 +++++----- .../TSASN1Base.remoteCapability.test.ts | 14 +++++++------- .../TSModuleCapabilities.test.ts | 4 ++-- docs/build.md | 2 +- scripts/run_gluecode_tests.bat | 3 ++- scripts/run_gluecode_tests.sh | 3 ++- 6 files changed, 19 insertions(+), 17 deletions(-) rename compiler/back-ends/ts-gen/{gluecode => tests}/TSASN1Base.registry.test.ts (89%) rename compiler/back-ends/ts-gen/{gluecode => tests}/TSASN1Base.remoteCapability.test.ts (93%) rename compiler/back-ends/ts-gen/{gluecode => tests}/TSModuleCapabilities.test.ts (90%) diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts similarity index 89% rename from compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts rename to compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts index 65b17ce..51e05bb 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts @@ -1,13 +1,13 @@ -// Run: npx tsx compiler/back-ends/ts-gen/gluecode/TSASN1Base.registry.test.ts +// Run: npx tsx compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts import assert from "node:assert/strict"; import test from "node:test"; import { ASN1ClassInstanceType, TSASN1Base, -} from "./TSASN1Base.js"; -import { EASN1TransportEncoding } from "./TSInvokeContext.js"; -import type { IASN1InvokeData } from "./TSROSEBase.js"; -import type { ROSEError, ROSEReject, ROSEResult } from "./SNACCROSE.js"; +} from "../gluecode/TSASN1Base.js"; +import { EASN1TransportEncoding } from "../gluecode/TSInvokeContext.js"; +import type { IASN1InvokeData } from "../gluecode/TSROSEBase.js"; +import type { ROSEError, ROSEReject, ROSEResult } from "../gluecode/SNACCROSE.js"; class TestTransport extends TSASN1Base { public constructor() { diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts similarity index 93% rename from compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts rename to compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts index 9e265a5..d298820 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts @@ -1,19 +1,19 @@ -// Run: npx tsx compiler/back-ends/ts-gen/gluecode/TSASN1Base.remoteCapability.test.ts +// Run: npx tsx compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts import assert from "node:assert/strict"; import test from "node:test"; import { ASN1ClassInstanceType, TSASN1Base, -} from "./TSASN1Base.js"; -import { EASN1TransportEncoding } from "./TSInvokeContext.js"; +} from "../gluecode/TSASN1Base.js"; +import { EASN1TransportEncoding } from "../gluecode/TSInvokeContext.js"; import { CustomInvokeProblemEnum, RemoteCapabilityMode, ROSE_REJECT_REMOTENOTCAPABLE, -} from "./TSROSEBase.js"; -import { buildRemoteModuleCapabilities } from "./TSModuleCapabilities.js"; -import type { IASN1InvokeData } from "./TSROSEBase.js"; -import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "./SNACCROSE.js"; +} from "../gluecode/TSROSEBase.js"; +import { buildRemoteModuleCapabilities } from "../gluecode/TSModuleCapabilities.js"; +import type { IASN1InvokeData } from "../gluecode/TSROSEBase.js"; +import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "../gluecode/SNACCROSE.js"; class TestTransport extends TSASN1Base { public sendInvokeCount = 0; diff --git a/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts b/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts similarity index 90% rename from compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts rename to compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts index 96de4d9..3cbf506 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts @@ -1,10 +1,10 @@ -// Run: npx tsx compiler/back-ends/ts-gen/gluecode/TSModuleCapabilities.test.ts +// Run: npx tsx compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts import assert from "node:assert/strict"; import test from "node:test"; import { buildRemoteModuleCapabilities, buildRemoteModuleCapabilitiesFromAsn, -} from "./TSModuleCapabilities.js"; +} from "../gluecode/TSModuleCapabilities.js"; test("buildRemoteModuleCapabilities populates invoke and event op ids", () => { const remote = buildRemoteModuleCapabilities([ diff --git a/docs/build.md b/docs/build.md index e2ee9cf..dc0b22e 100644 --- a/docs/build.md +++ b/docs/build.md @@ -37,7 +37,7 @@ Environment variables: `SNACCLIB7_ROOT`, `SNACC_CMAKE_BUILD_DIR`, `SNACC_CMAKE_G `samples/prepare.bat`, `samples/prepare.sh`, and `ROSE/makesnaccrose.bat` call these helpers before running Node or invoking the compiler directly. -Gluecode unit tests (registry + remote capability): run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. +TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + remote capability). Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. ### CMake variables diff --git a/scripts/run_gluecode_tests.bat b/scripts/run_gluecode_tests.bat index fd9e970..7e73266 100644 --- a/scripts/run_gluecode_tests.bat +++ b/scripts/run_gluecode_tests.bat @@ -4,6 +4,7 @@ setlocal EnableExtensions set "SCRIPT_DIR=%~dp0" set "REPO_ROOT=%SCRIPT_DIR%.." set "GLUE_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\gluecode" +set "TEST_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\tests" set "STUB_DIR=%REPO_ROOT%\samples\ts-microservice\node-client\src\stub" set "NODE_MODULES=%REPO_ROOT%\samples\ts-microservice\node-client\node_modules" @@ -28,7 +29,7 @@ for %%T in ( TSModuleCapabilities.test.ts ) do ( echo Running %%T ... - npx --yes tsx "%GLUE_DIR%\%%T" + npx --yes tsx "%TEST_DIR%\%%T" if errorlevel 1 set "EXIT_CODE=1" ) diff --git a/scripts/run_gluecode_tests.sh b/scripts/run_gluecode_tests.sh index 0ff0d4e..06113ec 100644 --- a/scripts/run_gluecode_tests.sh +++ b/scripts/run_gluecode_tests.sh @@ -4,6 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" GLUE_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/gluecode" +TEST_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/tests" STUB_DIR="$REPO_ROOT/samples/ts-microservice/node-client/src/stub" NODE_MODULES="$REPO_ROOT/samples/ts-microservice/node-client/node_modules" @@ -34,5 +35,5 @@ for test_file in \ TSModuleCapabilities.test.ts do echo "Running $test_file ..." - npx --yes tsx "$GLUE_DIR/$test_file" + npx --yes tsx "$TEST_DIR/$test_file" done From 7b98d4234882cf1f0593d6161cd3dfc6163b360c Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 17:39:27 +0200 Subject: [PATCH 09/11] UCAAS-1486: keep generated ASN stubs out of source gluecode Test runners assemble tests/workdir/gluecode from gluecode plus sample ENetUC_Common stubs instead of copying fixtures into the compiler gluecode tree. Co-authored-by: Cursor --- .gitignore | 1 + .../ts-gen/tests/TSASN1Base.registry.test.ts | 8 ++++---- .../tests/TSASN1Base.remoteCapability.test.ts | 12 ++++++------ .../ts-gen/tests/TSModuleCapabilities.test.ts | 2 +- docs/build.md | 2 +- scripts/run_gluecode_tests.bat | 16 +++++++--------- scripts/run_gluecode_tests.sh | 16 ++++++---------- 7 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 24a2296..d4ad9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ *.tsbuildinfo **/node_modules/ **/.pnpm-store/ +compiler/back-ends/ts-gen/tests/workdir/ CMakeCache.txt CMakeFiles/ Makefile diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts index 51e05bb..47574ab 100644 --- a/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts @@ -4,10 +4,10 @@ import test from "node:test"; import { ASN1ClassInstanceType, TSASN1Base, -} from "../gluecode/TSASN1Base.js"; -import { EASN1TransportEncoding } from "../gluecode/TSInvokeContext.js"; -import type { IASN1InvokeData } from "../gluecode/TSROSEBase.js"; -import type { ROSEError, ROSEReject, ROSEResult } from "../gluecode/SNACCROSE.js"; +} from "./workdir/gluecode/TSASN1Base.js"; +import { EASN1TransportEncoding } from "./workdir/gluecode/TSInvokeContext.js"; +import type { IASN1InvokeData } from "./workdir/gluecode/TSROSEBase.js"; +import type { ROSEError, ROSEReject, ROSEResult } from "./workdir/gluecode/SNACCROSE.js"; class TestTransport extends TSASN1Base { public constructor() { diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts index d298820..3478c60 100644 --- a/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts @@ -4,16 +4,16 @@ import test from "node:test"; import { ASN1ClassInstanceType, TSASN1Base, -} from "../gluecode/TSASN1Base.js"; -import { EASN1TransportEncoding } from "../gluecode/TSInvokeContext.js"; +} from "./workdir/gluecode/TSASN1Base.js"; +import { EASN1TransportEncoding } from "./workdir/gluecode/TSInvokeContext.js"; import { CustomInvokeProblemEnum, RemoteCapabilityMode, ROSE_REJECT_REMOTENOTCAPABLE, -} from "../gluecode/TSROSEBase.js"; -import { buildRemoteModuleCapabilities } from "../gluecode/TSModuleCapabilities.js"; -import type { IASN1InvokeData } from "../gluecode/TSROSEBase.js"; -import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "../gluecode/SNACCROSE.js"; +} from "./workdir/gluecode/TSROSEBase.js"; +import { buildRemoteModuleCapabilities } from "./workdir/gluecode/TSModuleCapabilities.js"; +import type { IASN1InvokeData } from "./workdir/gluecode/TSROSEBase.js"; +import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "./workdir/gluecode/SNACCROSE.js"; class TestTransport extends TSASN1Base { public sendInvokeCount = 0; diff --git a/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts b/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts index 3cbf506..7a96d28 100644 --- a/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts @@ -4,7 +4,7 @@ import test from "node:test"; import { buildRemoteModuleCapabilities, buildRemoteModuleCapabilitiesFromAsn, -} from "../gluecode/TSModuleCapabilities.js"; +} from "./workdir/gluecode/TSModuleCapabilities.js"; test("buildRemoteModuleCapabilities populates invoke and event op ids", () => { const remote = buildRemoteModuleCapabilities([ diff --git a/docs/build.md b/docs/build.md index dc0b22e..972dc8e 100644 --- a/docs/build.md +++ b/docs/build.md @@ -37,7 +37,7 @@ Environment variables: `SNACCLIB7_ROOT`, `SNACC_CMAKE_BUILD_DIR`, `SNACC_CMAKE_G `samples/prepare.bat`, `samples/prepare.sh`, and `ROSE/makesnaccrose.bat` call these helpers before running Node or invoking the compiler directly. -TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + remote capability). Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. +TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + remote capability). The run scripts assemble a ephemeral `tests/workdir/gluecode/` copy (gluecode plus sample `ENetUC_Common` stubs) so the source `gluecode/` tree stays compiler-only. Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. ### CMake variables diff --git a/scripts/run_gluecode_tests.bat b/scripts/run_gluecode_tests.bat index 7e73266..3051e08 100644 --- a/scripts/run_gluecode_tests.bat +++ b/scripts/run_gluecode_tests.bat @@ -5,6 +5,7 @@ set "SCRIPT_DIR=%~dp0" set "REPO_ROOT=%SCRIPT_DIR%.." set "GLUE_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\gluecode" set "TEST_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\tests" +set "WORKDIR=%TEST_DIR%\workdir\gluecode" set "STUB_DIR=%REPO_ROOT%\samples\ts-microservice\node-client\src\stub" set "NODE_MODULES=%REPO_ROOT%\samples\ts-microservice\node-client\node_modules" @@ -16,12 +17,11 @@ if not exist "%NODE_MODULES%\@estos\asn1ts" ( set "NODE_PATH=%NODE_MODULES%" set "EXIT_CODE=0" -for %%F in (ENetUC_Common.ts ENetUC_Common_Converter.ts) do ( - if not exist "%GLUE_DIR%\%%F" ( - copy /Y "%STUB_DIR%\%%F" "%GLUE_DIR%\%%F" >nul - set "COPIED_FIXTURE=1" - ) -) +if exist "%TEST_DIR%\workdir" rmdir /s /q "%TEST_DIR%\workdir" +mkdir "%WORKDIR%" +xcopy /E /I /Y /Q "%GLUE_DIR%\*" "%WORKDIR%\" >nul +copy /Y "%STUB_DIR%\ENetUC_Common.ts" "%WORKDIR%\" >nul +copy /Y "%STUB_DIR%\ENetUC_Common_Converter.ts" "%WORKDIR%\" >nul for %%T in ( TSASN1Base.registry.test.ts @@ -33,8 +33,6 @@ for %%T in ( if errorlevel 1 set "EXIT_CODE=1" ) -if defined COPIED_FIXTURE ( - del /Q "%GLUE_DIR%\ENetUC_Common.ts" "%GLUE_DIR%\ENetUC_Common_Converter.ts" 2>nul -) +if exist "%TEST_DIR%\workdir" rmdir /s /q "%TEST_DIR%\workdir" exit /b %EXIT_CODE% diff --git a/scripts/run_gluecode_tests.sh b/scripts/run_gluecode_tests.sh index 06113ec..70e0674 100644 --- a/scripts/run_gluecode_tests.sh +++ b/scripts/run_gluecode_tests.sh @@ -5,6 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" GLUE_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/gluecode" TEST_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/tests" +WORKDIR="$TEST_DIR/workdir/gluecode" STUB_DIR="$REPO_ROOT/samples/ts-microservice/node-client/src/stub" NODE_MODULES="$REPO_ROOT/samples/ts-microservice/node-client/node_modules" @@ -14,20 +15,15 @@ if [[ ! -d "$NODE_MODULES/@estos/asn1ts" ]]; then fi export NODE_PATH="$NODE_MODULES" -COPIED_FIXTURE=0 cleanup() { - if [[ "$COPIED_FIXTURE" -eq 1 ]]; then - rm -f "$GLUE_DIR/ENetUC_Common.ts" "$GLUE_DIR/ENetUC_Common_Converter.ts" - fi + rm -rf "$TEST_DIR/workdir" } trap cleanup EXIT -for fixture in ENetUC_Common.ts ENetUC_Common_Converter.ts; do - if [[ ! -f "$GLUE_DIR/$fixture" ]]; then - cp "$STUB_DIR/$fixture" "$GLUE_DIR/$fixture" - COPIED_FIXTURE=1 - fi -done +rm -rf "$TEST_DIR/workdir" +mkdir -p "$WORKDIR" +cp "$GLUE_DIR"/* "$WORKDIR/" +cp "$STUB_DIR/ENetUC_Common.ts" "$STUB_DIR/ENetUC_Common_Converter.ts" "$WORKDIR/" for test_file in \ TSASN1Base.registry.test.ts \ From 7f60b0a431b782a8c81cf42b88e9eb27bf64de88 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 17:41:36 +0200 Subject: [PATCH 10/11] UCAAS-1486: flatten ts-gen test workdir layout Copy gluecode and sample stubs directly into tests/workdir instead of nesting another gluecode folder. Co-authored-by: Cursor --- .../ts-gen/tests/TSASN1Base.registry.test.ts | 8 ++++---- .../ts-gen/tests/TSASN1Base.remoteCapability.test.ts | 12 ++++++------ .../ts-gen/tests/TSModuleCapabilities.test.ts | 2 +- docs/build.md | 2 +- scripts/run_gluecode_tests.bat | 2 +- scripts/run_gluecode_tests.sh | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts index 47574ab..7b4724c 100644 --- a/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts @@ -4,10 +4,10 @@ import test from "node:test"; import { ASN1ClassInstanceType, TSASN1Base, -} from "./workdir/gluecode/TSASN1Base.js"; -import { EASN1TransportEncoding } from "./workdir/gluecode/TSInvokeContext.js"; -import type { IASN1InvokeData } from "./workdir/gluecode/TSROSEBase.js"; -import type { ROSEError, ROSEReject, ROSEResult } from "./workdir/gluecode/SNACCROSE.js"; +} from "./workdir/TSASN1Base.js"; +import { EASN1TransportEncoding } from "./workdir/TSInvokeContext.js"; +import type { IASN1InvokeData } from "./workdir/TSROSEBase.js"; +import type { ROSEError, ROSEReject, ROSEResult } from "./workdir/SNACCROSE.js"; class TestTransport extends TSASN1Base { public constructor() { diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts index 3478c60..62e68ee 100644 --- a/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts @@ -4,16 +4,16 @@ import test from "node:test"; import { ASN1ClassInstanceType, TSASN1Base, -} from "./workdir/gluecode/TSASN1Base.js"; -import { EASN1TransportEncoding } from "./workdir/gluecode/TSInvokeContext.js"; +} from "./workdir/TSASN1Base.js"; +import { EASN1TransportEncoding } from "./workdir/TSInvokeContext.js"; import { CustomInvokeProblemEnum, RemoteCapabilityMode, ROSE_REJECT_REMOTENOTCAPABLE, -} from "./workdir/gluecode/TSROSEBase.js"; -import { buildRemoteModuleCapabilities } from "./workdir/gluecode/TSModuleCapabilities.js"; -import type { IASN1InvokeData } from "./workdir/gluecode/TSROSEBase.js"; -import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "./workdir/gluecode/SNACCROSE.js"; +} from "./workdir/TSROSEBase.js"; +import { buildRemoteModuleCapabilities } from "./workdir/TSModuleCapabilities.js"; +import type { IASN1InvokeData } from "./workdir/TSROSEBase.js"; +import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "./workdir/SNACCROSE.js"; class TestTransport extends TSASN1Base { public sendInvokeCount = 0; diff --git a/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts b/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts index 7a96d28..f50894c 100644 --- a/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSModuleCapabilities.test.ts @@ -4,7 +4,7 @@ import test from "node:test"; import { buildRemoteModuleCapabilities, buildRemoteModuleCapabilitiesFromAsn, -} from "./workdir/gluecode/TSModuleCapabilities.js"; +} from "./workdir/TSModuleCapabilities.js"; test("buildRemoteModuleCapabilities populates invoke and event op ids", () => { const remote = buildRemoteModuleCapabilities([ diff --git a/docs/build.md b/docs/build.md index 972dc8e..ebaa800 100644 --- a/docs/build.md +++ b/docs/build.md @@ -37,7 +37,7 @@ Environment variables: `SNACCLIB7_ROOT`, `SNACC_CMAKE_BUILD_DIR`, `SNACC_CMAKE_G `samples/prepare.bat`, `samples/prepare.sh`, and `ROSE/makesnaccrose.bat` call these helpers before running Node or invoking the compiler directly. -TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + remote capability). The run scripts assemble a ephemeral `tests/workdir/gluecode/` copy (gluecode plus sample `ENetUC_Common` stubs) so the source `gluecode/` tree stays compiler-only. Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. +TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + remote capability). The run scripts assemble an ephemeral `tests/workdir/` copy (gluecode plus sample `ENetUC_Common` stubs) so the source `gluecode/` tree stays compiler-only. Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. ### CMake variables diff --git a/scripts/run_gluecode_tests.bat b/scripts/run_gluecode_tests.bat index 3051e08..eee9e14 100644 --- a/scripts/run_gluecode_tests.bat +++ b/scripts/run_gluecode_tests.bat @@ -5,7 +5,7 @@ set "SCRIPT_DIR=%~dp0" set "REPO_ROOT=%SCRIPT_DIR%.." set "GLUE_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\gluecode" set "TEST_DIR=%REPO_ROOT%\compiler\back-ends\ts-gen\tests" -set "WORKDIR=%TEST_DIR%\workdir\gluecode" +set "WORKDIR=%TEST_DIR%\workdir" set "STUB_DIR=%REPO_ROOT%\samples\ts-microservice\node-client\src\stub" set "NODE_MODULES=%REPO_ROOT%\samples\ts-microservice\node-client\node_modules" diff --git a/scripts/run_gluecode_tests.sh b/scripts/run_gluecode_tests.sh index 70e0674..e7a5402 100644 --- a/scripts/run_gluecode_tests.sh +++ b/scripts/run_gluecode_tests.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" GLUE_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/gluecode" TEST_DIR="$REPO_ROOT/compiler/back-ends/ts-gen/tests" -WORKDIR="$TEST_DIR/workdir/gluecode" +WORKDIR="$TEST_DIR/workdir" STUB_DIR="$REPO_ROOT/samples/ts-microservice/node-client/src/stub" NODE_MODULES="$REPO_ROOT/samples/ts-microservice/node-client/node_modules" From 01f92d398f722b036fd61589df8f736d0c326dca Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Thu, 20 Aug 2026 17:58:13 +0200 Subject: [PATCH 11/11] UCAAS-1486: use OPID defines in RegisterOperations codegen Emit OPID_* macros from generated ROSE headers instead of duplicating numeric operation IDs in RegisterOperation calls. Co-authored-by: Cursor --- compiler/back-ends/c++-gen/gen-vals.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/back-ends/c++-gen/gen-vals.c b/compiler/back-ends/c++-gen/gen-vals.c index b9fc561..247ced1 100644 --- a/compiler/back-ends/c++-gen/gen-vals.c +++ b/compiler/back-ends/c++-gen/gen-vals.c @@ -109,7 +109,9 @@ int PrintROSEOperationRegistration(FILE* src, CxxRules* r, Module* mod, ValueDef /* * put instantiation in src file */ - fprintf(src, "\tRegisterOperation(lookup, m_iid, kModuleName, %d, \"", v->value->basicValue->a.integer); + fprintf(src, "\tRegisterOperation(lookup, m_iid, kModuleName, OPID_"); + PrintCxxValueDefsName(src, r, v); + fprintf(src, ", \""); PrintCxxValueDefsName(src, r, v); fprintf(src, "\""); if (bIsEvent) @@ -157,7 +159,9 @@ int PrintROSEOperationRegistrationLookup(FILE* src, CxxRules* r, Module* mod, Va } fprintf(src, "\tlookup.RegisterOperation("); - fprintf(src, "%d, \"", v->value->basicValue->a.integer); + fprintf(src, "OPID_"); + PrintCxxValueDefsName(src, r, v); + fprintf(src, ", \""); PrintCxxValueDefsName(src, r, v); fprintf(src, "\", m_iid, \"%s\"", mod->moduleName); if (bIsEvent)