diff --git a/MODULE.bazel b/MODULE.bazel index 44cb0f648..69bf48367 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -176,6 +176,7 @@ oci.pull( use_repo(oci, "debian-test-runtime", "debian-test-runtime_linux_amd64") bazel_dep(name = "score_baselibs", version = "0.2.10") +bazel_dep(name = "score_communication", version = "0.3.0") # Hedron's Compile Commands Extractor for Bazel # https://github.com/hedronvision/bazel-compile-commands-extractor diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 0df551317..83dd50858 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -119,40 +119,90 @@ "https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/source.json": "d353c410d47a8b65d09fa98e83d57ebec257a2c2b9c6e42d6fda1cb25e5464a5", "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/source.json": "a2d30458fd86cf022c2b6331e652526fa08e17573b2f5034a9dbcacdf9c2583c", + "https://bcr.bazel.build/modules/boost.any/1.83.0.bcr.4/MODULE.bazel": "b0dd78068b3d4b509e7d2cbafc498687ac347621e303bec26312f63baee1c97d", + "https://bcr.bazel.build/modules/boost.any/1.83.0.bcr.4/source.json": "0b0e71cc436ae601b4ecacf85c7246866c6d39178af0689906b9b997890f984d", + "https://bcr.bazel.build/modules/boost.array/1.83.0.bcr.4/MODULE.bazel": "419a97f36e8cc9cc75a783707051d51e6eaa88f49fda575811c78c76885f097a", + "https://bcr.bazel.build/modules/boost.array/1.83.0.bcr.4/source.json": "551152a3790532596761d6b8bd5f41589b65d4e617c503dce4b3c3ade1d28a71", "https://bcr.bazel.build/modules/boost.assert/1.83.0.bcr.4/MODULE.bazel": "0cc01302bf8f0098013098098f441b06068e6f4d05a4bb4f75a2133d11239e5b", "https://bcr.bazel.build/modules/boost.assert/1.83.0.bcr.4/source.json": "225cb551191ac7f5fb4eba0476a0d0397405cbb360dd9755bf29b28033415379", + "https://bcr.bazel.build/modules/boost.bind/1.83.0.bcr.4/MODULE.bazel": "12265d54e554bcba38ab4d5d9195a597874e202309fe0102ad24ca02d07948cd", + "https://bcr.bazel.build/modules/boost.bind/1.83.0.bcr.4/source.json": "23c74d95825ce383e74922b2ccd0013f0e122be9c0b5a4b80c5aa7c6e2c99496", + "https://bcr.bazel.build/modules/boost.concept_check/1.83.0.bcr.4/MODULE.bazel": "287e9cb97c2dcc11d3c7a08ac58a75a55d1beadff7795164546aebd8d242b515", + "https://bcr.bazel.build/modules/boost.concept_check/1.83.0.bcr.4/source.json": "6d91113521d7906d61e72896eff03b4979beb8eb11e79769fc847d6a89541dd2", "https://bcr.bazel.build/modules/boost.config/1.83.0.bcr.4/MODULE.bazel": "d6433498d31931ef3ec5e237f9a780bd8dbb110a73cccaac15f99df2ff0795a9", "https://bcr.bazel.build/modules/boost.config/1.83.0.bcr.4/source.json": "0a5b5bf8d2afcf0731fcd452fe69f20d45b0d08ba07e70698cc7ee36f0ce7e9c", "https://bcr.bazel.build/modules/boost.container/1.83.0.bcr.4/MODULE.bazel": "8c2711b962cabb11a91dfa24c8019141d7709e2a7d9ad888c7b7bd1f702565ae", "https://bcr.bazel.build/modules/boost.container/1.83.0.bcr.4/source.json": "246ae4564faf1c08995ac3f6a6886999e197821d0e3b3dd641911ae417657602", "https://bcr.bazel.build/modules/boost.container_hash/1.83.0.bcr.4/MODULE.bazel": "2f8246d0e34f19d3e09ba41022e85775566449f6a133640e65b4d9f1cbf110b8", "https://bcr.bazel.build/modules/boost.container_hash/1.83.0.bcr.4/source.json": "0410e34fef0a3dc16edc0f6a8f49783cba04a891183d76286ba974e4d9806ef0", + "https://bcr.bazel.build/modules/boost.conversion/1.83.0.bcr.4/MODULE.bazel": "b8497ee8fd93305ca26b051b28a7dbab2c0754a7b212a7766dc0b419b8d66eb8", + "https://bcr.bazel.build/modules/boost.conversion/1.83.0.bcr.4/source.json": "5c743efda68a0b7b77f2a87b7d66f55b569f3056959e252e472c992ad23f4668", "https://bcr.bazel.build/modules/boost.core/1.83.0.bcr.4/MODULE.bazel": "145932521c0509dbb268a8f062ba5432f0fe2620e6f82a580dab0eacda79a140", "https://bcr.bazel.build/modules/boost.core/1.83.0.bcr.4/source.json": "d709cff67716e366200ca77680b39e9c98141c7289ddbf207b1c710f4de7bf03", "https://bcr.bazel.build/modules/boost.describe/1.83.0.bcr.4/MODULE.bazel": "e705d8ed1d12aba965bb860b9130d05738854168b28573283a75dfa9d98ebc86", "https://bcr.bazel.build/modules/boost.describe/1.83.0.bcr.4/source.json": "748e4e74723c381d304f888849efd52caf5710f48ceb67b91d4e43fc800ccd81", + "https://bcr.bazel.build/modules/boost.detail/1.83.0.bcr.4/MODULE.bazel": "b0716cedd38e241e44c67cdb0616b16062f40e6d0aee62a675abafe58cf81a94", + "https://bcr.bazel.build/modules/boost.detail/1.83.0.bcr.4/source.json": "786d571fbd8482901b62c7c752ca0047c644989610d4cea733b230b93a6500cd", + "https://bcr.bazel.build/modules/boost.function/1.83.0.bcr.4/MODULE.bazel": "69e1415e6c1de48fd9a75cd0f21edddcb3b5a99aba345304548de8b48c5bc258", + "https://bcr.bazel.build/modules/boost.function/1.83.0.bcr.4/source.json": "a0cabb8ba9338689964c5e467186caabed4dd5426b579269be15d1c5c94a5522", + "https://bcr.bazel.build/modules/boost.function_types/1.83.0.bcr.4/MODULE.bazel": "55088b1ae6083c8c58051d006bce058dcd8d3587bbbbd088fc285211ce23f8c2", + "https://bcr.bazel.build/modules/boost.function_types/1.83.0.bcr.4/source.json": "8c58b4cb6d2b2ce126eafb9a060e7b9b6ad60e9f2d409aa2caca84db2369a8ae", + "https://bcr.bazel.build/modules/boost.functional/1.83.0.bcr.4/MODULE.bazel": "6551ccb5fb2c601ff3bd3ca66d0d10deebe79a4b080f98abb61a92fb7ae68c1a", + "https://bcr.bazel.build/modules/boost.functional/1.83.0.bcr.4/source.json": "ce96d3fd8ec61d2889b35734b9d116278b64f473361f8bdeb9a9f336e514356e", + "https://bcr.bazel.build/modules/boost.fusion/1.83.0.bcr.4/MODULE.bazel": "5c809e2a0b84df52732f1982206da585d5366a78bf030ca5eded541f1d7a61ad", + "https://bcr.bazel.build/modules/boost.fusion/1.83.0.bcr.4/source.json": "efce523d915fad1cd37824843ca6381b1e9866bb7ee88851511d85aa37005713", + "https://bcr.bazel.build/modules/boost.integer/1.83.0.bcr.4/MODULE.bazel": "173ce805a8ac6d3a61e2b8891b4bdf32188d36fbbf73c5a5f3ea0e210e246a1d", + "https://bcr.bazel.build/modules/boost.integer/1.83.0.bcr.4/source.json": "6aedf2334ff54a527699cf78a853916826a15252fccdad70b81bfaa1d49904bf", "https://bcr.bazel.build/modules/boost.interprocess/1.83.0.bcr.4/MODULE.bazel": "b1c4cf58b387c07c10cea0709ad808283c62aa8024bebc9ce7a59248c9980729", "https://bcr.bazel.build/modules/boost.interprocess/1.83.0.bcr.4/source.json": "4f9c40d790c736a619c03b5788b80575fadffbb712fb10944572b420313c5dee", "https://bcr.bazel.build/modules/boost.intrusive/1.83.0.bcr.4/MODULE.bazel": "8cfe7ec4ae0e4c33297df4edf5c22e6498cde0d7a1ecf98720392b829918eb61", "https://bcr.bazel.build/modules/boost.intrusive/1.83.0.bcr.4/source.json": "04475d40cac42ebe96888a0599b2b9b35d4092502710f421cb3dc9ad2c6b8099", + "https://bcr.bazel.build/modules/boost.io/1.83.0.bcr.4/MODULE.bazel": "804ae356aacddd2478af291fa29aa682ad813ede8b43a86fa3444b7663b0a855", + "https://bcr.bazel.build/modules/boost.io/1.83.0.bcr.4/source.json": "f07574feba2522bdbb312e99d2978dc1db8bd0a309a118cc6b02559ddca179cc", + "https://bcr.bazel.build/modules/boost.iterator/1.83.0.bcr.4/MODULE.bazel": "7ebfd731d3bbb7d99556652673344093f46799226da22a1d65ddc005fb8adb94", + "https://bcr.bazel.build/modules/boost.iterator/1.83.0.bcr.4/source.json": "298dfee2164bf529195b83641bae0c929ed42e5b2b9097b14c5627a97f40faa8", + "https://bcr.bazel.build/modules/boost.lexical_cast/1.83.0.bcr.4/MODULE.bazel": "1e497e85f499fc55316baa25938a4e02de0a3764fc0152450975fd4ba60b8bc5", + "https://bcr.bazel.build/modules/boost.lexical_cast/1.83.0.bcr.4/source.json": "2731c6c73680ca65a3a60deed4810dd9c7e7f1dd233c07829a313911f90b7fbb", "https://bcr.bazel.build/modules/boost.move/1.83.0.bcr.4/MODULE.bazel": "ddc12982f87e67597eeede240369cc73d4ba83ec764a752c4a471245834ee060", "https://bcr.bazel.build/modules/boost.move/1.83.0.bcr.4/source.json": "6f9971fd617c8d94e7c4a02c618052edec5ad0f30a1b18b4bf9e995443545330", "https://bcr.bazel.build/modules/boost.mp11/1.83.0.bcr.4/MODULE.bazel": "27fa864c5e1799e514bbe2f0b9439ad29380f5d436ae3b39c29109609d81316e", "https://bcr.bazel.build/modules/boost.mp11/1.83.0.bcr.4/source.json": "8c2924bab093f7d0e44cb2ac55dea985497b9d85d6a246c89377a49d27d452a6", + "https://bcr.bazel.build/modules/boost.mpl/1.83.0.bcr.4/MODULE.bazel": "5ffae433a4da96fe4a80e89b7ea2ed9ae638baa8733542e41c04b51654d8ebb0", + "https://bcr.bazel.build/modules/boost.mpl/1.83.0.bcr.4/source.json": "5486afd07c5aed527e6c090cae630e73fa5e95c57fbcbd3e89b6f76759d88acc", + "https://bcr.bazel.build/modules/boost.numeric_conversion/1.83.0.bcr.4/MODULE.bazel": "2f1e9d5483bc5601908aa8db11b4e6d3aa8ee34821740621763ce7388bfe1e44", + "https://bcr.bazel.build/modules/boost.numeric_conversion/1.83.0.bcr.4/source.json": "59be0456e0abf960844d67774f414a2e6165d666bf13d255b30408dde0306dff", + "https://bcr.bazel.build/modules/boost.optional/1.83.0.bcr.4/MODULE.bazel": "5ec81db11f58442b8794af3e8fd868698772f28ba831e2eb2767e8294c222428", + "https://bcr.bazel.build/modules/boost.optional/1.83.0.bcr.4/source.json": "5f52bc2e02908d07e4ad53cacec91520a745471715070ec659c1b1f81d05f5e3", "https://bcr.bazel.build/modules/boost.predef/1.83.0.bcr.4/MODULE.bazel": "4d842ef8cadb835dcfff0ee90a7ff0b6e8df4774c72e4e8443ca2339083cd58b", "https://bcr.bazel.build/modules/boost.predef/1.83.0.bcr.4/source.json": "4bc4c6af4bcc909b1d4abebba5583d03d5e09447c658fa63fe8fdd8a5b2906e5", "https://bcr.bazel.build/modules/boost.preprocessor/1.83.0.bcr.4/MODULE.bazel": "aee3f5151c6c5738a494c7a2b842d1f80db0e60601cc6c1ef2b74e0c7e548fa6", "https://bcr.bazel.build/modules/boost.preprocessor/1.83.0.bcr.4/source.json": "ce770e480d05b987d953d7e64f80cebdfb5b008a9b9da80355d9e86a3d09bbb7", + "https://bcr.bazel.build/modules/boost.program_options/1.83.0.bcr.4/MODULE.bazel": "2e74f997e3e27a604a9dab3d33393737396953eb213429707c65ac60fdec6990", + "https://bcr.bazel.build/modules/boost.program_options/1.83.0.bcr.4/source.json": "b5bb402252a25d926d66ec1bc3946a8264d786f5f82e90a9268abe82e8220c3a", + "https://bcr.bazel.build/modules/boost.range/1.83.0.bcr.4/MODULE.bazel": "6e3089857dd3bb9b31f9c3fcfda86a0d03b9004dd0b6ee5656daee8ade769b54", + "https://bcr.bazel.build/modules/boost.range/1.83.0.bcr.4/source.json": "cb7c9a5fb9d32d08450f75860e6de2f5e9ef3c3b717b48307bba6c73e68ac779", + "https://bcr.bazel.build/modules/boost.regex/1.83.0.bcr.4/MODULE.bazel": "aa6e90068802f8f695712708bb4bd80b4bfc1affb1079a342e506e3563cb892f", + "https://bcr.bazel.build/modules/boost.regex/1.83.0.bcr.4/source.json": "a3ebf3b988c2db3aafa6b91d7fd4cf6f7ac4544829479f7e656d84540abd501f", + "https://bcr.bazel.build/modules/boost.smart_ptr/1.83.0.bcr.4/MODULE.bazel": "5217115fdeb485e3be40802adf97f16c2cc432e050ea0246733b55aeb3ce0047", + "https://bcr.bazel.build/modules/boost.smart_ptr/1.83.0.bcr.4/source.json": "a953c6e0b526efde316af8f6b38170616c559729790efaa121e42230e014f53b", "https://bcr.bazel.build/modules/boost.static_assert/1.83.0.bcr.4/MODULE.bazel": "55a9d94e82ebdc01857bae5590e83a7afab56fc57e86f345288601992eb5cd8e", "https://bcr.bazel.build/modules/boost.static_assert/1.83.0.bcr.4/source.json": "d14305e5679f008a352f75e8eaa725ab9ff71ec1dd2553cda1b171e39fd20cfe", "https://bcr.bazel.build/modules/boost.throw_exception/1.83.0.bcr.4/MODULE.bazel": "6dc898672c5de822af4b4da0fc6d0ed233d9fa942356c4e1bbcd748916ba0c58", "https://bcr.bazel.build/modules/boost.throw_exception/1.83.0.bcr.4/source.json": "ec9d540d7ca6beaffdc7f0395cd5eea0acda924b3ab306966786b242df74b512", + "https://bcr.bazel.build/modules/boost.tokenizer/1.83.0.bcr.4/MODULE.bazel": "0b63f536a387b2ce44641f29be3057e0746554467a0e9081df08c83036854c4a", + "https://bcr.bazel.build/modules/boost.tokenizer/1.83.0.bcr.4/source.json": "ec29f1a3794b132da76d19fbb741c243c890d5a17950fb9e0b7f6bb618b31959", "https://bcr.bazel.build/modules/boost.tuple/1.83.0.bcr.4/MODULE.bazel": "3a21cf25cd86a03bdc78684f16d5b25e91f075ff95230b849545484a7d4d506b", "https://bcr.bazel.build/modules/boost.tuple/1.83.0.bcr.4/source.json": "e145722cc962e7f4385f554ce6e8f4373bab792691712788fa16befa9082b938", + "https://bcr.bazel.build/modules/boost.type_index/1.83.0.bcr.4/MODULE.bazel": "401befa0b9b5a4ee6f2628d7cb0d7664f6a039d9022b982f36a6672ffedeb14d", + "https://bcr.bazel.build/modules/boost.type_index/1.83.0.bcr.4/source.json": "d6b66135155a4094819cc1393d89e4eace6bdf985c5148d31bc96824471382ef", "https://bcr.bazel.build/modules/boost.type_traits/1.83.0.bcr.4/MODULE.bazel": "ecf03c91b87bc800d9c699718fab225973063fc1975082e155298e5a87e19573", "https://bcr.bazel.build/modules/boost.type_traits/1.83.0.bcr.4/source.json": "107e5bef1443ee75efc197198b45fb01d6460b475e20d1fa0d52af64600c8204", + "https://bcr.bazel.build/modules/boost.typeof/1.83.0.bcr.4/MODULE.bazel": "9d76ccc98ad052f184f15741672840519b9b55d793c649c267944099c9107e61", + "https://bcr.bazel.build/modules/boost.typeof/1.83.0.bcr.4/source.json": "b9ac0d3bfded3f175fd0b58514f2409782672407bee2242938df5829176f891e", "https://bcr.bazel.build/modules/boost.unordered/1.83.0.bcr.4/MODULE.bazel": "122f475aca3eb7af7b3e66c1bc6a0139c95ee3629c3625ec7af7ac646be7fad6", "https://bcr.bazel.build/modules/boost.unordered/1.83.0.bcr.4/source.json": "1afd0e208be668faeea97b63d18ef23232c522e300b454d4a2f164c369dcfb2e", + "https://bcr.bazel.build/modules/boost.utility/1.83.0.bcr.4/MODULE.bazel": "c88f9877366b57c1e05b9e4c498cbf42ab45fc4a44d23d634845793af02651c0", + "https://bcr.bazel.build/modules/boost.utility/1.83.0.bcr.4/source.json": "c291874612c0758f629e5852c2133ede86df3e7c53f9d589aa26bcb0578587e7", "https://bcr.bazel.build/modules/boost.winapi/1.83.0.bcr.4/MODULE.bazel": "3a36763aafd5fef4844b012bacdc104674cc3ee6a1e8b501a6d68e964a7df20e", "https://bcr.bazel.build/modules/boost.winapi/1.83.0.bcr.4/source.json": "d4be5cdf5cfdeae83f70211faa034bc41168c75fd98a560464131c8cfc6365fa", "https://bcr.bazel.build/modules/boost/1.83.0.bcr.4/MODULE.bazel": "0ce94f87f4514f6abd77d7ea9d221cc3432e48957c82d712829b36d53183dd68", @@ -641,23 +691,48 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_worker_api/0.0.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_worker_api/0.0.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/bazel_worker_java/0.0.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.any/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.array/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.assert/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.bind/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.concept_check/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.config/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.container/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.container_hash/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.conversion/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.core/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.describe/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.detail/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.function/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.function_types/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.functional/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.fusion/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.integer/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.interprocess/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.intrusive/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.io/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.iterator/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.lexical_cast/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.move/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.mp11/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.mpl/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.numeric_conversion/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.optional/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.predef/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.preprocessor/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.program_options/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.range/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.regex/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.smart_ptr/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.static_assert/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.throw_exception/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.tokenizer/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.tuple/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.type_index/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.type_traits/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.typeof/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.unordered/1.83.0.bcr.4/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.utility/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost.winapi/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boost/1.83.0.bcr.4/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "not found", @@ -941,6 +1016,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_swift/2.1.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.10/MODULE.bazel": "eef4a41f8f8b6d5cf55c1445b340a2e8643a8dc83ad9ff52d0d26fe9a77973ec", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.10/source.json": "543286fb4828ff58404a0ddd034cac3e7937657353997119a3976dbaf913f2cf", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_baselibs/0.2.9/MODULE.bazel": "2f95cc2e07c5ed9df69e6af35a98843235ce43a96d0aef8c55398ebe6813b58a", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.5.4/MODULE.bazel": "5f14457f9b708302db05b5dc3c07bd41ae19b91cabf98a0dbffdca622bd6ac91", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_cpp_toolchains/0.5.4/source.json": "2946be24c6b978aa788c934643aa7ec68aac4a926bea4eaa81d512fd48e9f4a8", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.0.3/MODULE.bazel": "14c96e378c08705a46abe0799d6236fe3095c342c34f83f8d1b3f6046ce00651", @@ -948,6 +1024,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/0.1.2/MODULE.bazel": "d1889bf36241521c5d5c401aaf1e98242a60f9cab9d223f8190e5d7087956e83", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/1.0.0/MODULE.bazel": "e5e386e6fc0a447a2f4a47e872e3747822e9d58994b7b55e248c78e5a3e382c0", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_bazel_platforms/1.0.0/source.json": "fb428e5f4cb6cbf0e00442e72c8d038746e0eaa7493dcec394ee99be727dcb3b", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_communication/0.3.0/MODULE.bazel": "7674d445cb51ea31e7ea69bc5c98f9dd6b81beeced915cb9c4e21b99debb61e0", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_communication/0.3.0/source.json": "d84f2add08dcbb2979078b3b7ffe8e10eb7c723bb8584ce74dd8476e98a75784", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cpp_policies/0.0.1/MODULE.bazel": "e9d8ecb12ebd8d1fb2af6cfc3ba136600953d951983ef1e710f2f589de69a8c6", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cpp_policies/0.0.1/source.json": "75f76a0d1a1208a94cbb43fc6455fc943ac262f354cfbb245da4220447d4c7f6", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_cr_checker/0.2.2/MODULE.bazel": "dc36d9c35543db918c3fb5b93a8e684431f56c7c784cf2a1b90f35802a373c98", diff --git a/score/launch_manager/src/execution_error.h b/score/launch_manager/src/execution_error.h index 2dcd913d8..63f2659db 100644 --- a/score/launch_manager/src/execution_error.h +++ b/score/launch_manager/src/execution_error.h @@ -39,7 +39,11 @@ enum class ExecErrc : score::result::ErrorCode kInTransitionToSameState = 11, ///< Transition to the requested Process Group state failed because transition to ///< requested state is already in progress kNoTimeStamp = 12, ///< DeterministicClient time stamp information is not available - kCycleOverrun = 13 ///< Deterministic activation cycle time exceeded + kCycleOverrun = 13, ///< Deterministic activation cycle time exceeded + kActivationInProgress = + 14, ///< A Run Target activation is already in progress; no single Run Target is currently active + kRequestQueueIsFull = 15, ///< The activation request queue is full; the request was discarded + kRunTargetDoesntExist = 16 ///< The requested Run Target name does not exist in the current configuration }; class ExecErrorDomain final : public score::result::ErrorDomain @@ -77,18 +81,24 @@ class ExecErrorDomain final : public score::result::ErrorDomain return "DeterministicClient time stamp information is not available"; case ExecErrc::kCycleOverrun: return "Deterministic activation cycle time exceeded"; + case ExecErrc::kActivationInProgress: + return "A Run Target activation is already in progress; no single Run Target is currently active"; + case ExecErrc::kRequestQueueIsFull: + return "The activation request queue is full; the request was discarded"; + case ExecErrc::kRunTargetDoesntExist: + return "The requested Run Target name does not exist in the current configuration"; default: return "Unknown error"; } } }; -constexpr ExecErrorDomain g_ExecErrorDomain{}; +/// @brief The single ExecErrorDomain instance every ExecErrc-based Error refers to. +inline constexpr ExecErrorDomain g_ExecErrorDomain{}; constexpr score::result::Error MakeError(ExecErrc code, const std::string_view user_message = "") noexcept { return score::result::Error{static_cast(code), g_ExecErrorDomain, user_message}; - ; } } // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/BUILD b/score/launch_manager/src/lm_control/BUILD new file mode 100644 index 000000000..e9d7151a2 --- /dev/null +++ b/score/launch_manager/src/lm_control/BUILD @@ -0,0 +1,76 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_library") +load("//tests/utils/bazel:runfiles.bzl", "runfiles_at") +load("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") + +cc_library( + name = "lm_control", + srcs = [ + "src/details/ilm_control.cpp", + ], + hdrs = [ + "src/details/lm_control_impl.hpp", + "src/details/lm_control_service.h", + "src/fixed_string.hpp", + "src/ilm_control.hpp", + "src/run_target_activation_source.hpp", + ], + include_prefix = "score/mw/lifecycle", + strip_include_prefix = "/score/launch_manager/src/lm_control/src", + visibility = [ + "//score:__subpackages__", + "//tests:__subpackages__", + ], + deps = [ + "//score/launch_manager:error", + "//score/launch_manager/src/daemon/src/common:log", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + "@score_communication//score/mw/com", + ], +) + +lm_cc_test( + name = "lm_control_impl_UT", + srcs = ["src/details/lm_control_impl_UT.cpp"], + deps = [ + ":lm_control", + "//score/launch_manager:error", + "@googletest//:gtest_main", + "@score_baselibs//score/result", + "@score_communication//score/mw/com", + ], +) + +# mw::com reads its manifest from the working-directory-relative default path +# ./etc/mw_com_config.json unless told otherwise via CLI args. +# CLI args are currently not supported with score_qnx_unit_tests, so +# making sure the file is available at the default path in the runfiles of the test. +runfiles_at( + name = "default_mw_com_config", + testonly = True, + src = "src/details/test_lm_mw_com_config.json", + path = "etc/mw_com_config.json", +) + +lm_cc_test( + name = "ilm_control_UT", + srcs = ["src/details/ilm_control_UT.cpp"], + data = [":default_mw_com_config"], + deps = [ + ":lm_control", + "@googletest//:gtest_main", + "@score_communication//score/mw/com", + ], +) diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp new file mode 100644 index 000000000..a17e777f3 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/ilm_control.cpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/lifecycle/ilm_control.hpp" +#include "score/mw/lifecycle/details/lm_control_impl.hpp" + +namespace score::mw::lifecycle +{ + +score::Result> ILmControl::Create(std::string_view instance_specifier) +{ + auto instance = std::make_unique(); + const auto init_result = instance->init(instance_specifier); + if (!init_result.has_value()) + { + return score::MakeUnexpected>(init_result.error()); + } + return instance; +} + +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp new file mode 100644 index 000000000..c76e05d18 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/ilm_control_UT.cpp @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +#include +#include + +#include "score/mw/com/runtime.h" +#include "score/mw/lifecycle/ilm_control.hpp" + +namespace score::mw::lifecycle +{ +namespace +{ + +// Present in the test manifest (lm_mw_com_config.json). +constexpr std::string_view kValidSpecifier = "StateManager/LaunchManager/Instance"; + +class ILmControlUT : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + + // The following two equivalence classes are tested here: + // * Return a valid instance + // * Return an error + // The different error cases are tested in the underlying impl class LmControlImpl unit tests + } +}; + +TEST_F(ILmControlUT, ReturnsValidInstance) +{ + RecordProperty("Description", "ILmControl::Create returns a valid instance for a valid instance specifier."); + + auto result = ILmControl::Create(std::string{kValidSpecifier}); + + ASSERT_TRUE(result.has_value()); + EXPECT_NE(result.value(), nullptr); +} + +TEST_F(ILmControlUT, ReturnsInitErrorForInvalidSpecifier) +{ + RecordProperty( + "Description", "ILmControl::Create returns kInvalidArguments error for an invalid instance specifier."); + + auto result = ILmControl::Create(""); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); +} + +} // namespace +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp new file mode 100644 index 000000000..37147e4fd --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl.hpp @@ -0,0 +1,397 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP +#define SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP + +#include +#include +#include +#include +#include +#include + +#include "score/mw/launch_manager/common/log.hpp" +#include "score/mw/lifecycle/details/lm_control_service.h" +#include "score/mw/lifecycle/ilm_control.hpp" + +#include + +namespace score::mw::lifecycle +{ + +/// @brief Translates a RunTargetActivationSource to a human-readable string for logging. +constexpr std::string_view toStringView(const RunTargetActivationSource source) noexcept +{ + switch (source) + { + case RunTargetActivationSource::kStateManagerRequest: + return "StateManagerRequest"; + case RunTargetActivationSource::kRecoveryAction: + return "RecoveryAction"; + } + return "Unknown"; +} + +/// @brief Layer of indirection for mw::com usage +/// @details This allows to inject a fake proxy in tests, while the production code uses the real mw::com proxy. +struct MwComProxyTraits +{ + using Proxy = LmControlProxy; + using HandleType = LmControlProxy::HandleType; + using FindServiceHandle = score::mw::com::FindServiceHandle; + + template + static score::Result StartFindService( + Handler&& handler, + score::mw::com::InstanceSpecifier specifier) + { + return Proxy::StartFindService(std::forward(handler), std::move(specifier)); + } + + static score::Result StopFindService(FindServiceHandle find_handle) + { + return Proxy::StopFindService(find_handle); + } + + static score::Result Create(HandleType handle) + { + return Proxy::Create(handle); + } +}; + +/// @brief mw::com proxy-based implementation of ILmControl. +/// +/// Connects to the Launch Manager via mw::com. Construction yields an inert, unconnected +/// object; init() then starts an asynchronous service discovery that keeps searching for +/// the instance in the background. init() succeeds as long as the instance specifier is +/// valid and the service discovery could be started; the proxy may only become available +/// later. Until it does, method calls return kCommunicationError. +/// +/// @tparam Traits Binds the class to a proxy implementation. Defaults to +/// MwComProxyTraits (the real mw::com proxy). Tests inject a +/// traits type whose Proxy is a fake, giving full control +/// for testing. +template +class BasicLmControlImpl final : public ILmControl +{ + public: + /// @brief Creates an inert, unconnected instance. Call init() to put it to use. + BasicLmControlImpl() noexcept = default; + + /// @brief Validate the instance specifier and start the asynchronous service discovery. + /// @param[in] instance_specifier The mw::com instance specifier identifying the Launch + /// Manager service. Must be a non-empty, valid specifier + /// string. + /// @pre Must be called exactly once, before any other method. + /// @return void once discovery is running, or the error that prevented setup. + /// @error kInvalidArguments instance_specifier is empty or malformed. + /// @error kCommunicationError the service discovery could not be started. + score::Result init(std::string_view instance_specifier) noexcept + { + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(!find_handle_.has_value(), "LmControl::init() called more than once"); + + auto specifier_result = score::mw::com::InstanceSpecifier::Create(std::string{instance_specifier}); + if (!specifier_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: Invalid instance specifier" << instance_specifier + << "Error: " << specifier_result.error(); + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + + const auto start_result = Traits::StartFindService( + [this](score::mw::com::ServiceHandleContainer handles, FindServiceHandle) noexcept { + onServiceFound(std::move(handles)); + }, + std::move(specifier_result).value()); + + if (!start_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: StartFindService failed with error" << start_result.error(); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + find_handle_ = start_result.value(); + return {}; + } + + ~BasicLmControlImpl() noexcept override + { + // Stop discovery first: once StopFindService returns, onServiceFound() can no longer run, + // so the remaining teardown races nothing. + if (find_handle_.has_value()) + { + const auto stop_result = Traits::StopFindService(find_handle_.value()); + if (!stop_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: StopFindService failed with error:" << stop_result.error(); + } + } + + if (proxy_.has_value()) + { + // Unsubscribe also unsets the receive handler, no need for explicit UnsetReceiveHandler() call. + proxy_->activation_result.Unsubscribe(); + } + } + + score::Result activate_run_target(RunTargetName runTargetName, bool force = false) override + { + auto* const proxy = connectedProxy(); + if (proxy == nullptr) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + LM_LOG_DEBUG() << "LmControl: activate_run_target:" << runTargetName << " force=" << force; + + const ActivateRunTargetRequest request{ + runTargetName, force ? ActivationMode::kForced : ActivationMode::kQueued}; + + const auto result = proxy->activate_run_target(request); + if (!result.has_value()) + { + LM_LOG_ERROR() << "LmControl: activate_run_target: proxy method call failed with error:" << result.error(); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == RequestStatus::kRejected) + { + LM_LOG_DEBUG() << "LmControl: activate_run_target: rejected by LM with code" << response.rejection_reason; + return score::MakeUnexpected(response.rejection_reason); + } + + return {}; + } + + score::Result register_run_target_activation_callback(ActivationCallback callback) override + { + if (!callback) + { + return score::MakeUnexpected(ExecErrc::kInvalidArguments); + } + std::lock_guard lock{callback_mutex_}; + callback_ = std::move(callback); + return {}; + } + + score::Result get_active_run_target() override + { + auto* const proxy = connectedProxy(); + if (proxy == nullptr) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto result = proxy->get_active_run_target(); + if (!result.has_value()) + { + LM_LOG_ERROR() << "LmControl: get_active_run_target: proxy method call failed with error:" + << result.error(); + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + + const auto& response = *result.value(); + if (response.status == QueryStatus::kNotAvailable) + { + LM_LOG_DEBUG() << "LmControl: get_active_run_target: Status not available, reason:" + << response.rejection_reason; + return score::MakeUnexpected(response.rejection_reason); + } + + LM_LOG_DEBUG() << "LmControl: get_active_run_target:" << response.run_target; + return response.run_target; + } + + private: + using ProxyType = typename Traits::Proxy; + using HandleType = typename Traits::HandleType; + using FindServiceHandle = typename Traits::FindServiceHandle; + + /// @brief Allow to read small number of samples at once + static constexpr std::size_t kActivationResultSampleCount = 4U; + + /// @brief Returns the proxy instance or nullptr if not connected + ProxyType* connectedProxy() noexcept + { + std::lock_guard lock{mutex_}; + return proxy_.has_value() ? &proxy_.value() : nullptr; + } + + /// @brief Invoked by mw::com whenever service availability changes. On the first matching + /// instance it creates the proxy and subscribes to activation results. + void onServiceFound(score::mw::com::ServiceHandleContainer handles) noexcept + { + std::lock_guard lock{mutex_}; + + // Ignore "service went away" notifications and any callbacks after we are set up. + if (handles.empty() || proxy_.has_value()) + { + return; + } + + if (createProxy(handles.front())) + { + static_cast(subscribeToActivationResults()); + } + } + + /// @brief Create the proxy for the discovered instance and publish it into proxy_. + /// @pre Caller holds mutex_ and proxy_ is empty. + /// @return true if the proxy was created, false on error (proxy_ left empty). + bool createProxy(const HandleType& handle) noexcept + { + SCORE_LANGUAGE_FUTURECPP_ASSERT_DBG_MESSAGE(!proxy_.has_value(), "proxy already created"); + + auto create_result = Traits::Create(handle); + if (!create_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: Proxy::Create failed with error:" << create_result.error(); + return false; + } + proxy_.emplace(std::move(create_result).value()); + LM_LOG_DEBUG() << "LmControl: proxy created successfully"; + return true; + } + + /// @brief Subscribe to the activation_result event and install the receive handler. + /// @pre Caller holds mutex_ and proxy_ has a value. + /// @return true on success; on failure proxy_ is reset so we stay unconnected. + bool subscribeToActivationResults() noexcept + { + SCORE_LANGUAGE_FUTURECPP_ASSERT_DBG_MESSAGE(proxy_.has_value(), "proxy must not be null"); + + const auto subscribe_result = proxy_->activation_result.Subscribe(kActivationResultSampleCount); + if (!subscribe_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: activation_result.Subscribe failed with error:" << subscribe_result.error(); + proxy_.reset(); + return false; + } + + const auto receivehandler_result = proxy_->activation_result.SetReceiveHandler([this]() { + onActivationResult(); + }); + if (!receivehandler_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: activation_result.SetReceiveHandler failed with error:" + << receivehandler_result.error(); + proxy_.reset(); + return false; + } + return true; + } + + /// @brief mw::com receive handler for the activation_result event. + /// @details mw::com may invoke this concurrently with itself. Since proxy-event + /// API calls must not run concurrently on the same event, the handler + /// is serialized against itself + /// @note Deliberately a separate mutex from mutex_, not a reuse of it: this path invokes the + /// user activation callback, which is free to call back into activate_run_target() and + /// therefore into mutex_. Guarding both with one non-recursive mutex would deadlock. + void onActivationResult() + { + std::lock_guard processing_lock{sample_processing_mutex_}; + readActivationEvents(); + } + + /// @brief Ready and forwards all pending activation-result samples. + /// @pre The caller holds sample_processing_mutex_ and proxy_ has a value. + void readActivationEvents() + { + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(proxy_.has_value(), "proxy must not be null"); + + const ActivationCallback callback = currentCallback(); + if (!callback) + { + LM_LOG_WARN() << "LmControl: activation_result: no callback registered — samples dropped"; + } + + std::size_t processed = 0U; + for (;;) + { + const auto get_result = proxy_->activation_result.GetNewSamples( + [&callback](const auto& sample) noexcept { + forwardSample(callback, sample); + }, + kActivationResultSampleCount); + if (!get_result.has_value()) + { + LM_LOG_ERROR() << "LmControl: GetNewSamples failed with error:" << get_result.error(); + break; + } + + processed += get_result.value(); + + // All samples consumed, if the batch was smaller than the max. + if (get_result.value() < kActivationResultSampleCount) + { + break; + } + } + + LM_LOG_DEBUG() << "LmControl: GetNewSamples: processed" << processed << "sample(s)"; + } + + /// @brief Returns a copy of the currently registered callback, empty if there is none. + /// @details Copied out under the lock so that the caller can invoke it after unlocking: + /// holding callback_mutex_ across the invocation would deadlock a callback that + /// re-registers itself. + ActivationCallback currentCallback() noexcept + { + std::lock_guard lock{callback_mutex_}; + return callback_; + } + + /// @brief Forwards a single activation-result sample to the given callback. + /// @details The sample parameter is intentionally a template: in production it + /// is a score::mw::com::SamplePtr, while a fake proxy + /// in tests can hand in any pointer-like sample. Nothing here depends + /// on the concrete mw::com sample type. + /// @param[in] callback The callback to invoke, may be empty in which case the sample is dropped. + /// @param[in] sample The received activation result. + template + static void forwardSample(const ActivationCallback& callback, const SamplePtrType& sample) noexcept + { + LM_LOG_DEBUG() << "LmControl: activation_result received: run_target=" << sample->activated_run_target + << "source=" << toStringView(sample->activation_source); + if (callback) + { + callback(sample->activation_source, sample->activated_run_target); + LM_LOG_DEBUG() << "LmControl: activation_result: user callback invoked"; + } + } + + // Guards proxy_ + std::mutex mutex_; + std::optional proxy_; + + // Handle of the running discovery search, stopped by the destructor. Written once by init() + // and never touched again, so it needs no lock. + std::optional find_handle_; + + ActivationCallback callback_; + std::mutex callback_mutex_; + + // Serializes onActivationResult() against itself: mw::com may dispatch the + // receive handler concurrently, but proxy-event API calls on a single event + // must not overlap. + std::mutex sample_processing_mutex_; +}; + +/// @brief Production alias: BasicLmControlImpl wired to the real mw::com proxy. +using LmControlImpl = BasicLmControlImpl; + +} // namespace score::mw::lifecycle + +#endif // SCORE_MW_LIFECYCLE_LM_CONTROL_IMPL_HPP diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp new file mode 100644 index 000000000..1cc66cd68 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_impl_UT.cpp @@ -0,0 +1,747 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "score/mw/lifecycle/details/lm_control_impl.hpp" + +namespace score::mw::lifecycle +{ +namespace +{ + +using ::testing::_; +using ::testing::AllOf; +using ::testing::DoAll; +using ::testing::Field; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; + +// Opaque stand-ins for the mw::com discovery handle types. +struct FakeHandle +{ +}; +struct FakeFindHandle +{ +}; + +using FindHandler = std::function, FakeFindHandle)>; + +/// @brief The single mock all framework calls are routed to. Owned by the test. +class MwComMock +{ + public: + MOCK_METHOD(score::Result, StartFindService, (FindHandler handler)); + MOCK_METHOD(score::Result, StopFindService, ()); + MOCK_METHOD(score::Result, CreateProxy, ()); + + MOCK_METHOD(score::Result, Subscribe, (std::size_t max_sample_count)); + MOCK_METHOD(score::Result, SetReceiveHandler, (std::function handler)); + MOCK_METHOD(void, Unsubscribe, ()); + MOCK_METHOD((score::Result>), GetNewSamples, (std::size_t max_count)); + + MOCK_METHOD(score::Result, ActivateRunTarget, (const ActivateRunTargetRequest& request)); + MOCK_METHOD(score::Result, GetActiveRunTarget, ()); +}; + +/// @brief Delegating adapter for LmControlProxy::activation_result. +struct FakeActivationEvent +{ + MwComMock* mock; + + // Mirrors mw::com: a single GetNewSamples() call never hands out more than the + // max_sample_count agreed in Subscribe(), no matter how large the backlog is. + std::size_t subscribed_max_count{0U}; + + score::Result Subscribe(std::size_t max_sample_count) + { + auto result = mock->Subscribe(max_sample_count); + if (result.has_value()) + { + subscribed_max_count = max_sample_count; + } + return result; + } + + score::Result SetReceiveHandler(std::function handler) + { + return mock->SetReceiveHandler(std::move(handler)); + } + + void Unsubscribe() + { + mock->Unsubscribe(); + } + + template + score::Result GetNewSamples(Callback&& callback, std::size_t max_count) + { + const std::size_t effective_max_count = std::min(max_count, subscribed_max_count); + auto samples_result = mock->GetNewSamples(effective_max_count); + if (!samples_result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + auto samples = std::move(samples_result).value(); + + // Uphold the mw::com contract even if a test stubs a larger batch than requested. + const std::size_t delivered = std::min(samples.size(), effective_max_count); + for (std::size_t i = 0U; i < delivered; ++i) + { + callback(&samples[i]); + } + return delivered; + } +}; + +/// @brief Delegating adapter for LmControlProxy. +struct FakeProxy +{ + MwComMock* mock; + FakeActivationEvent activation_result; + + explicit FakeProxy(MwComMock* m) : mock{m}, activation_result{m} + { + } + + score::Result> activate_run_target(const ActivateRunTargetRequest& req) + { + auto result = mock->ActivateRunTarget(req); + if (!result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return std::make_shared(std::move(result).value()); + } + + score::Result> get_active_run_target() + { + auto result = mock->GetActiveRunTarget(); + if (!result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return std::make_shared(std::move(result).value()); + } +}; + +/// @brief Compile-time seam to inject the fake proxy into BasicLmControlImpl and to mock mw::com API calls. +struct FakeProxyTraits +{ + using Proxy = FakeProxy; + using HandleType = FakeHandle; + using FindServiceHandle = FakeFindHandle; + + static inline MwComMock* mock{nullptr}; + + template + static score::Result StartFindService(Handler&& handler, score::mw::com::InstanceSpecifier) + { + return mock->StartFindService(FindHandler{std::forward(handler)}); + } + + static score::Result StopFindService(FindServiceHandle) + { + return mock->StopFindService(); + } + + static score::Result Create(HandleType) + { + auto result = mock->CreateProxy(); + if (!result.has_value()) + { + return score::MakeUnexpected(ExecErrc::kCommunicationError); + } + return FakeProxy{mock}; + } +}; + +using LmControlImplType = BasicLmControlImpl; + +constexpr std::string_view kValidSpecifier = "StateManager/LaunchManager/Instance"; + +ActivateRunTargetResponse Accepted() +{ + return ActivateRunTargetResponse{RequestStatus::kAccepted, ExecErrc::kGeneralError}; +} + +GetActiveRunTargetResponse Available(RunTargetName name) +{ + return GetActiveRunTargetResponse{QueryStatus::kAvailable, name, ExecErrc::kGeneralError}; +} + +/// @brief Builds a batch of activation results, one per run target name. +std::vector MakeSamples(std::initializer_list run_target_names) +{ + std::vector samples{}; + samples.reserve(run_target_names.size()); + for (const auto name : run_target_names) + { + samples.push_back(ActivationResult{RunTargetName{name}, RunTargetActivationSource::kStateManagerRequest}); + } + return samples; +} + +class LmControlUT : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + + FakeProxyTraits::mock = &mock_; + + // Sensible defaults so the "connected and healthy" path needs no per-test + // wiring; tests override only the calls they care about. + ON_CALL(mock_, StartFindService(_)) + .WillByDefault(DoAll(SaveArg<0>(&find_handler_), Return(score::Result{FakeFindHandle{}}))); + ON_CALL(mock_, StopFindService()).WillByDefault(Return(score::Result{})); + ON_CALL(mock_, CreateProxy()).WillByDefault(Return(score::Result{})); + ON_CALL(mock_, Subscribe(_)).WillByDefault(Return(score::Result{})); + ON_CALL(mock_, SetReceiveHandler(_)) + .WillByDefault(DoAll(SaveArg<0>(&receive_handler_), Return(score::Result{}))); + ON_CALL(mock_, GetNewSamples(_)).WillByDefault(Invoke([](std::size_t) { + return score::Result>{std::vector{}}; + })); + ON_CALL(mock_, ActivateRunTarget(_)).WillByDefault(Invoke([](const ActivateRunTargetRequest&) { + return score::Result{Accepted()}; + })); + ON_CALL(mock_, GetActiveRunTarget()).WillByDefault(Invoke([] { + return score::Result{Available(RunTargetName{"Running"})}; + })); + } + + void TearDown() override + { + FakeProxyTraits::mock = nullptr; + } + + /// @brief Simulate mw::com discovering one matching instance. + void ConnectService() + { + ASSERT_TRUE(find_handler_) << "StartFindService was never called"; + find_handler_(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); + } + + /// @brief Build a successfully-constructed (but not yet connected) SUT. + std::unique_ptr MakeLmControl() + { + auto sut = std::make_unique(); + EXPECT_TRUE(sut->init(kValidSpecifier).has_value()); + return sut; + } + + /// @brief Build an already-connected SUT using the fixture defaults. + std::unique_ptr MakeConnected() + { + auto sut = MakeLmControl(); + ConnectService(); + return sut; + } + + NiceMock mock_; + FindHandler find_handler_{}; + std::function receive_handler_{}; +}; + +// --------------------------------------------------------------------------- +// Construction / discovery lifecycle +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, InvalidSpecifierIsRejectedByInit) +{ + RecordProperty( + "Description", "init() with an empty instance specifier returns kInvalidArguments and never starts discovery."); + + EXPECT_CALL(mock_, StartFindService(_)).Times(0); + + LmControlImplType sut{}; + const auto result = sut.init(""); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); +} + +TEST_F(LmControlUT, StartFindServiceFailureIsReportedByInit) +{ + RecordProperty( + "Description", + "When StartFindService fails, init() returns kCommunicationError and the instance can be destroyed " + "without stopping a search it never started."); + + EXPECT_CALL(mock_, StartFindService(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(mock_, StopFindService()).Times(0); + EXPECT_CALL(mock_, Unsubscribe()).Times(0); + + LmControlImplType sut{}; + const auto result = sut.init(kValidSpecifier); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, InitStartsBackgroundDiscovery) +{ + RecordProperty( + "Description", "init() with a valid specifier starts asynchronous service discovery via StartFindService."); + + EXPECT_CALL(mock_, StartFindService(_)); + + LmControlImplType sut{}; + EXPECT_TRUE(sut.init(kValidSpecifier).has_value()); + EXPECT_TRUE(find_handler_); +} + +TEST_F(LmControlUT, MethodsFailWhileNotYetConnected) +{ + RecordProperty( + "Description", + "Before the service is discovered, activate_run_target and get_active_run_target return " + "kCommunicationError without issuing any proxy calls."); + + EXPECT_CALL(mock_, ActivateRunTarget(_)).Times(0); + EXPECT_CALL(mock_, GetActiveRunTarget()).Times(0); + + auto sut = MakeLmControl(); + + auto activate = sut->activate_run_target("Running"); + ASSERT_FALSE(activate.has_value()); + EXPECT_EQ(activate.error(), ExecErrc::kCommunicationError); + + auto get = sut->get_active_run_target(); + ASSERT_FALSE(get.has_value()); + EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, DiscoveryCreatesProxySubscribesAndSetsHandler) +{ + RecordProperty( + "Description", + "On discovering the service, the SUT creates the proxy, subscribes to activation results and sets the " + "receive handler."); + + EXPECT_CALL(mock_, CreateProxy()); + EXPECT_CALL(mock_, Subscribe(4U)); // must match kActivationResultSampleCount + EXPECT_CALL(mock_, SetReceiveHandler(_)); + + auto sut = MakeConnected(); + EXPECT_TRUE(receive_handler_); +} + +TEST_F(LmControlUT, EmptyHandlesAreIgnored) +{ + RecordProperty( + "Description", "A discovery callback with an empty handle container is ignored and no proxy is created."); + + EXPECT_CALL(mock_, CreateProxy()).Times(0); + + auto sut = MakeLmControl(); + ASSERT_TRUE(find_handler_); + find_handler_(score::mw::com::ServiceHandleContainer{}, FakeFindHandle{}); + + EXPECT_FALSE(sut->get_active_run_target().has_value()); +} + +TEST_F(LmControlUT, SecondDiscoveryCallbackIsIgnored) +{ + RecordProperty( + "Description", "A second discovery callback after the proxy is already set up does not create another proxy."); + + EXPECT_CALL(mock_, CreateProxy()).Times(1); + + auto sut = MakeConnected(); + // A second notification after setup must not create another proxy. + find_handler_(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); +} + +TEST_F(LmControlUT, SynchronousDiscoveryDuringConstructionStopsFindingOnce) +{ + RecordProperty( + "Description", + "When discovery completes synchronously while init() is still running, the service discovery is still " + "stopped exactly once, in the destructor."); + + EXPECT_CALL(mock_, StartFindService(_)).WillOnce(Invoke([](FindHandler handler) { + handler(score::mw::com::ServiceHandleContainer{FakeHandle{}}, FakeFindHandle{}); + return score::Result{FakeFindHandle{}}; + })); + // The destructor is the only place that stops discovery. The synchronous handler leaves the + // search untouched, so init() can store the handle unconditionally. + EXPECT_CALL(mock_, StopFindService()).Times(1); + + auto sut = MakeLmControl(); + EXPECT_TRUE(sut->get_active_run_target().has_value()); +} + +TEST_F(LmControlUT, SubscribeFailureLeavesInstanceDisconnected) +{ + RecordProperty( + "Description", + "When Subscribe fails during discovery, the instance stays disconnected and method calls return " + "kCommunicationError."); + + EXPECT_CALL(mock_, Subscribe(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + auto sut = MakeConnected(); + + auto get = sut->get_active_run_target(); + ASSERT_FALSE(get.has_value()); + EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, SetReceiveHandlerFailureLeavesInstanceDisconnected) +{ + RecordProperty( + "Description", + "When installing the receive handler fails during discovery, the instance stays disconnected and method " + "calls return kCommunicationError."); + + EXPECT_CALL(mock_, SetReceiveHandler(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + auto sut = MakeConnected(); + + auto get = sut->get_active_run_target(); + ASSERT_FALSE(get.has_value()); + EXPECT_EQ(get.error(), ExecErrc::kCommunicationError); +} + +TEST_F(LmControlUT, ProxyCreateFailureLeavesInstanceDisconnected) +{ + RecordProperty( + "Description", + "When proxy creation fails during discovery, no subscription is attempted and the instance stays " + "disconnected."); + + EXPECT_CALL(mock_, CreateProxy()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(mock_, Subscribe(_)).Times(0); + + auto sut = MakeConnected(); + + EXPECT_FALSE(sut->get_active_run_target().has_value()); +} + +TEST_F(LmControlUT, StopFindServiceFailureOnDestructionIsTolerated) +{ + RecordProperty( + "Description", + "A StopFindService failure while tearing down is only logged; destruction still completes cleanly."); + + EXPECT_CALL(mock_, StopFindService()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kInvalidArguments))); + + { + auto sut = MakeConnected(); + EXPECT_TRUE(sut->get_active_run_target().has_value()); + } // The failure inside the destructor is only logged. +} + +// --------------------------------------------------------------------------- +// activate_run_target +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, ActivateForwardsNameAndQueuedModeByDefault) +{ + RecordProperty( + "Description", "activate_run_target forwards the run target name and defaults to queued activation mode."); + + auto sut = MakeConnected(); + + EXPECT_CALL( + mock_, + ActivateRunTarget(AllOf( + Field(&ActivateRunTargetRequest::run_target_name, RunTargetName{"Driving"}), + Field(&ActivateRunTargetRequest::mode, ActivationMode::kQueued)))) + .WillOnce(Return(Accepted())); + + EXPECT_TRUE(sut->activate_run_target("Driving").has_value()); +} + +TEST_F(LmControlUT, ActivateForcedMapsToForcedMode) +{ + RecordProperty("Description", "activate_run_target with force=true maps to forced activation mode."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, ActivateRunTarget(Field(&ActivateRunTargetRequest::mode, ActivationMode::kForced))) + .WillOnce(Return(Accepted())); + + EXPECT_TRUE(sut->activate_run_target("Driving", /*force=*/true).has_value()); +} + +TEST_F(LmControlUT, ActivateRejectionSurfacesRejectionReason) +{ + RecordProperty("Description", "A rejected activation surfaces the rejection reason as the returned error."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, ActivateRunTarget(_)) + .WillOnce(Return(ActivateRunTargetResponse{RequestStatus::kRejected, ExecErrc::kRequestQueueIsFull})); + + auto result = sut->activate_run_target("Driving"); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kRequestQueueIsFull); +} + +TEST_F(LmControlUT, ActivateTransportFailureReturnsCommunicationError) +{ + RecordProperty("Description", "A transport failure during activate_run_target is reported as kCommunicationError."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, ActivateRunTarget(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kFailed))); + + auto result = sut->activate_run_target("Driving"); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +// --------------------------------------------------------------------------- +// get_active_run_target +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, GetActiveRunTargetReturnsName) +{ + RecordProperty("Description", "get_active_run_target returns the currently active run target name."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetActiveRunTarget()).WillOnce(Return(Available(RunTargetName{"Running"}))); + + auto result = sut->get_active_run_target(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "Running"); +} + +TEST_F(LmControlUT, GetActiveRunTargetNotAvailableSurfacesReason) +{ + RecordProperty( + "Description", + "When no active run target is available, get_active_run_target surfaces the reason as the error."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetActiveRunTarget()) + .WillOnce(Return( + GetActiveRunTargetResponse{QueryStatus::kNotAvailable, RunTargetName{}, ExecErrc::kActivationInProgress})); + + auto result = sut->get_active_run_target(); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kActivationInProgress); +} + +TEST_F(LmControlUT, GetActiveRunTargetTransportFailureReturnsCommunicationError) +{ + RecordProperty( + "Description", "A transport failure during get_active_run_target is reported as kCommunicationError."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetActiveRunTarget()).WillOnce(Return(score::MakeUnexpected(ExecErrc::kFailed))); + + auto result = sut->get_active_run_target(); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +// --------------------------------------------------------------------------- +// register_run_target_activation_callback + activation_result dispatch +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, RegisterEmptyCallbackRejected) +{ + RecordProperty("Description", "Registering a null activation callback is rejected with kInvalidArguments."); + + auto sut = MakeLmControl(); + auto result = sut->register_run_target_activation_callback(nullptr); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kInvalidArguments); +} + +TEST_F(LmControlUT, ActivationResultInvokesRegisteredCallback) +{ + RecordProperty( + "Description", + "A received activation_result sample is forwarded to the registered callback with its source and " + "run target."); + + auto sut = MakeConnected(); + + std::optional source{}; + std::optional name{}; + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource s, RunTargetName n) { + source = s; + name = n; + }) + .has_value()); + + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { + std::vector samples; + samples.push_back(ActivationResult{RunTargetName{"Recovery"}, RunTargetActivationSource::kRecoveryAction}); + return score::Result>{std::move(samples)}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + ASSERT_TRUE(name.has_value()); + EXPECT_EQ(name.value(), "Recovery"); + EXPECT_EQ(source.value(), RunTargetActivationSource::kRecoveryAction); +} + +TEST_F(LmControlUT, ActivationResultBacklogExceedingSubscriptionIsDrainedInSeveralCalls) +{ + RecordProperty( + "Description", + "Number of available samples larger than subscribed max_sample_count is read with repeated GetNewSamples " + "calls until a batch smaller than the maximum signals an empty queue; every sample is forwarded exactly once."); + + auto sut = MakeConnected(); + + std::vector received{}; + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource, RunTargetName n) { + received.push_back(n); + }) + .has_value()); + + // Nine pending samples, but Subscribe(kActivationResultSampleCount == 4) caps every single + // GetNewSamples call at four: two full batches, then a short one that ends the drain. + EXPECT_CALL(mock_, GetNewSamples(4U)) + .WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T0", "T1", "T2", "T3"})}; + })) + .WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T4", "T5", "T6", "T7"})}; + })) + .WillOnce(Invoke([](std::size_t) { + return score::Result>{MakeSamples({"T8"})}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + EXPECT_THAT(received, ::testing::ElementsAre("T0", "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8")); +} + +TEST_F(LmControlUT, ActivationResultDrainStopsOnEmptyBatch) +{ + RecordProperty( + "Description", + "A receive handler firing with nothing retrievable issues exactly one GetNewSamples call and does not spin."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetNewSamples(4U)).WillOnce(Invoke([](std::size_t) { + return score::Result>{std::vector{}}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); +} + +TEST_F(LmControlUT, ActivationResultWithoutCallbackIsDroppedSafely) +{ + RecordProperty( + "Description", "An activation_result received with no registered callback is dropped without crashing."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { + std::vector samples; + samples.push_back(ActivationResult{RunTargetName{"Running"}, RunTargetActivationSource::kStateManagerRequest}); + return score::Result>{std::move(samples)}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); // no callback registered — must not crash +} + +TEST_F(LmControlUT, ActivationResultGetNewSamplesFailureIsTolerated) +{ + RecordProperty( + "Description", "A GetNewSamples failure while reading activation results is tolerated without crashing."); + + auto sut = MakeConnected(); + + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Return(score::MakeUnexpected(ExecErrc::kCommunicationError))); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); // error branch — must not crash +} + +TEST_F(LmControlUT, LatestRegisteredCallbackWins) +{ + RecordProperty("Description", "The most recently registered activation callback replaces any earlier ones."); + + auto sut = MakeConnected(); + + int first_calls{0}; + int second_calls{0}; + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource, RunTargetName) { + ++first_calls; + }) + .has_value()); + ASSERT_TRUE(sut->register_run_target_activation_callback([&](RunTargetActivationSource, RunTargetName) { + ++second_calls; + }) + .has_value()); + + EXPECT_CALL(mock_, GetNewSamples(_)).WillOnce(Invoke([](std::size_t) { + std::vector samples; + samples.push_back(ActivationResult{RunTargetName{"Running"}, RunTargetActivationSource::kStateManagerRequest}); + return score::Result>{std::move(samples)}; + })); + + ASSERT_TRUE(receive_handler_); + receive_handler_(); + + EXPECT_EQ(first_calls, 0); + EXPECT_EQ(second_calls, 1); +} + +// --------------------------------------------------------------------------- +// Destruction +// --------------------------------------------------------------------------- + +TEST_F(LmControlUT, DestructorStopsDiscoveryWhenNeverConnected) +{ + RecordProperty("Description", "Destroying a never-connected instance stops discovery and does not unsubscribe."); + + EXPECT_CALL(mock_, StopFindService()).Times(1); + EXPECT_CALL(mock_, Unsubscribe()).Times(0); + + { + auto sut = MakeLmControl(); + } +} + +TEST_F(LmControlUT, DestructorUnsubscribesWhenConnected) +{ + RecordProperty("Description", "Destroying a connected instance unsubscribes from the activation_result event."); + + EXPECT_CALL(mock_, Unsubscribe()).Times(1); + + MakeConnected().reset(); +} + +} // namespace +} // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/details/lm_control_service.h b/score/launch_manager/src/lm_control/src/details/lm_control_service.h new file mode 100644 index 000000000..b4797651b --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/lm_control_service.h @@ -0,0 +1,152 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_MW_LIFECYCLE_LM_CONTROL_SERVICE_H_ +#define SCORE_MW_LIFECYCLE_LM_CONTROL_SERVICE_H_ + +#include + +#include "score/mw/com/types.h" +#include "score/mw/lifecycle/ilm_control.hpp" + +namespace score::mw::lifecycle +{ + +// ============================================================================ +// activate_run_target +// ============================================================================ + +/// @brief Controls how an activation request interacts with any in-progress activation. +/// +/// @details Mirrors the `force` parameter of `ILmControl::activate_run_target`. +enum class ActivationMode : uint8_t +{ + kQueued = 0, ///< Queue this request behind any in-progress activation. + kForced = 1, ///< Cancel any in-progress activation and the pending queue, + ///< then start this activation immediately. +}; + +/// @brief Request sent by the State Manager to activate a Run Target. +struct ActivateRunTargetRequest +{ + /// @brief Name of the Run Target to activate. + RunTargetName run_target_name; + + /// @brief Whether to queue or force this activation. + ActivationMode mode{ActivationMode::kQueued}; +}; + +/// @brief Indicates whether the request was accepted or rejected by the Launch Manager. +enum class RequestStatus : uint8_t +{ + kAccepted = 0, ///< The request was accepted. + kRejected = 1, ///< The request was rejected; see the accompanying rejection_reason field. +}; + +/// @brief Synchronous response to an activate_run_target request. +/// +/// Carries only the immediate acceptance/rejection decision. Activation +/// completion is notified separately via the `activation_result` event. +struct ActivateRunTargetResponse +{ + /// @brief Whether the Launch Manager accepted or rejected the request. + RequestStatus status{RequestStatus::kRejected}; + + /// @brief Reason for rejection. Only meaningful when status == RequestStatus::kRejected. + ExecErrc rejection_reason{ExecErrc::kGeneralError}; +}; + +// ============================================================================ +// get_active_run_target +// ============================================================================ + +/// @brief Indicates whether the Launch Manager can provide the currently active Run Target. +enum class QueryStatus : uint8_t +{ + kAvailable = 0, ///< An active Run Target exists; see the run_target field. + kNotAvailable = 1, ///< No active Run Target; an activation is in progress. +}; + +/// @brief Synchronous response to a get_active_run_target query. +struct GetActiveRunTargetResponse +{ + /// @brief Whether an active Run Target is currently available. + QueryStatus status{QueryStatus::kNotAvailable}; + + /// @brief The currently active Run Target. + /// Only meaningful when status == QueryStatus::kAvailable. + RunTargetName run_target; + + /// @brief Reason the active Run Target is not available. + /// Only meaningful when status == QueryStatus::kNotAvailable. + /// Expected value: kActivationInProgress. + ExecErrc rejection_reason{ExecErrc::kGeneralError}; +}; + +// ============================================================================ +// activation_result event +// ============================================================================ + +/// @brief Event sent by the Launch Manager when a Run Target activation completes. +/// +/// Activation always resolves into some Run Target — it cannot fail at the graph +/// level. The activated Run Target may differ from the one originally requested if a +/// preempting request or recovery action redirected the transition. +struct ActivationResult +{ + /// @brief The Run Target the Launch Manager activated. + RunTargetName activated_run_target; + + /// @brief What caused this activation to occur. + RunTargetActivationSource activation_source{RunTargetActivationSource::kStateManagerRequest}; +}; + +// ============================================================================ +// Service definition +// ============================================================================ + +/// @brief mw::com service definition for the Launch Manager control interface. +template +class LmControlService : public Trait::Base +{ + public: + using Trait::Base::Base; + + /// @brief Request Run Target activation. + /// + /// Returns an `ActivateRunTargetResponse` synchronously indicating whether + /// the request was accepted or rejected (e.g. queue full, unknown Run Target). + /// If accepted, the actual activation result arrives asynchronously via + /// the `activation_result` event. + typename Trait::template Method activate_run_target{ + *this, + "ActivateRunTarget"}; + + /// @brief Query the currently active Run Target. + /// + /// Returns a `GetActiveRunTargetResponse` synchronously. If an activation is + /// in progress, status is QueryStatus::kNotAvailable and rejection_reason + /// carries kActivationInProgress. The caller should wait for the + /// activation_result event and retry if needed. + typename Trait::template Method get_active_run_target{*this, "GetActiveRunTarget"}; + + /// @brief Event fired by the Launch Manager when a Run Target activation completes. + typename Trait::template Event activation_result{*this, "ActivationResult"}; +}; + +using LmControlProxy = score::mw::com::AsProxy; +using LmControlSkeleton = score::mw::com::AsSkeleton; + +} // namespace score::mw::lifecycle + +#endif // SCORE_MW_LIFECYCLE_LM_CONTROL_SERVICE_H_ diff --git a/score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json b/score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json new file mode 100644 index 000000000..a478be5d7 --- /dev/null +++ b/score/launch_manager/src/lm_control/src/details/test_lm_mw_com_config.json @@ -0,0 +1,100 @@ +{ + "serviceTypes": [ + { + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "bindings": [ + { + "binding": "SHM", + "serviceId": 7101, + "events": [ + { + "eventName": "ActivationResult", + "eventId": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "methodId": 2 + }, + { + "methodName": "GetActiveRunTarget", + "methodId": 3 + } + ] + } + ] + } + ], + "serviceInstances": [ + { + "instanceSpecifier": "LaunchManager/StateManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult", + "numberOfSampleSlots": 8, + "maxSubscribers": 1 + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ] + } + ] + }, + { + "instanceSpecifier": "StateManager/LaunchManager/Instance", + "serviceTypeName": "/score/mw/lifecycle/LmControlService", + "version": { + "major": 1, + "minor": 0 + }, + "instances": [ + { + "instanceId": 1, + "asil-level": "QM", + "binding": "SHM", + "events": [ + { + "eventName": "ActivationResult" + } + ], + "methods": [ + { + "methodName": "ActivateRunTarget", + "queueSize": 1 + }, + { + "methodName": "GetActiveRunTarget", + "queueSize": 1 + } + ] + } + ] + } + ], + "global": { + "asil-level": "QM" + } +} diff --git a/score/launch_manager/src/lm_control/src/fixed_string.hpp b/score/launch_manager/src/lm_control/src/fixed_string.hpp index af67ca02f..094cd9b67 100644 --- a/score/launch_manager/src/lm_control/src/fixed_string.hpp +++ b/score/launch_manager/src/lm_control/src/fixed_string.hpp @@ -364,7 +364,7 @@ bool operator!=(const FixedString& lhs, const FixedString& rhs) noexcept template Stream& operator<<(Stream& os, const FixedString& fs) { - return os << fs.data(); + return os << fs.as_string_view(); } } // namespace score::mw::lifecycle diff --git a/score/launch_manager/src/lm_control/src/ilm_control.hpp b/score/launch_manager/src/lm_control/src/ilm_control.hpp index 8427a088c..b72608391 100644 --- a/score/launch_manager/src/lm_control/src/ilm_control.hpp +++ b/score/launch_manager/src/lm_control/src/ilm_control.hpp @@ -66,7 +66,7 @@ class ILmControl using ActivationCallback = std::function; - /// @brief Factory method - create a connected ILmControl instance. + /// @brief Factory method - create a ILmControl instance. /// /// @warning **Work in progress API shape not yet finalised.** /// The signature and behaviour of this method may change. Open @@ -132,17 +132,13 @@ class ILmControl /// Only a single subscriber is supported. The expected usage is /// register-once during setup, leaving the callback in place until /// the ILmControl instance is destroyed. Re-registration is - /// supported defensively (a second call overrides the previous - /// setting) but callers that re-register must be prepared to - /// handle kCallbackInProgress (transient, retry). There is no - /// un-register path; empty callbacks are rejected. + /// supported defensively: a second call overrides the previous + /// setting. There is no un-register path; empty callbacks are rejected. /// /// @param[in] callback The callback to invoke when Run Target activation finishes. /// /// @returns void on success. /// - /// @error kCallbackInProgress A new callback cannot be registered because the - /// current callback is still executing. Retry after it returns. /// @error kInvalidArguments The callback is empty. virtual score::Result register_run_target_activation_callback(ActivationCallback callback) = 0; @@ -158,7 +154,7 @@ class ILmControl /// /// @error kActivationInProgress Launch Manager is currently executing a Run Target /// activation and thus there is no single Run Target active. - /// @error kCommunicationError Connection with Launch Manager cannot be established + /// @error kCommunicationError Connection with Launch Manager cannot be established /// and information about active Run Target cannot be retrieved. virtual score::Result get_active_run_target() = 0; diff --git a/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp b/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp index 7204c930e..36a60d389 100644 --- a/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp +++ b/score/launch_manager/src/lm_control/src/run_target_activation_source.hpp @@ -14,6 +14,8 @@ #ifndef SCORE_MW_LIFECYCLE_RUN_TARGET_ACTIVATION_SOURCE_HPP #define SCORE_MW_LIFECYCLE_RUN_TARGET_ACTIVATION_SOURCE_HPP +#include + namespace score::mw::lifecycle { @@ -25,7 +27,7 @@ namespace score::mw::lifecycle /// @note Activation cannot fail and always resolves into some /// Run Target. This enum describes *why* it resolved, /// not whether it succeeded. -enum class RunTargetActivationSource +enum class RunTargetActivationSource : std::uint8_t { /// @brief Activation was explicitly requested by a State Manager. kStateManagerRequest = 0, diff --git a/tests/utils/bazel/runfiles.bzl b/tests/utils/bazel/runfiles.bzl new file mode 100644 index 000000000..8f31853df --- /dev/null +++ b/tests/utils/bazel/runfiles.bzl @@ -0,0 +1,45 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Bazel rules for placing files at a chosen location in a target's runfiles.""" + +def _runfiles_at_impl(ctx): + src = ctx.file.src + return DefaultInfo( + files = depset([src]), + runfiles = ctx.runfiles( + files = [src], + # root_symlinks are relative to the runfiles root, one level above the + # main repository directory - hence the explicit "_main/" prefix, which + # makes the path relative to the working directory of the executable. + root_symlinks = {"_main/" + ctx.attr.path: src}, + ), + ) + +runfiles_at = rule( + implementation = _runfiles_at_impl, + doc = """Exposes a single file at a fixed path in the runfiles of every target depending on it. + +Use it for files that a library expects at a hard-coded, working-directory-relative +location and that therefore cannot be passed as a command line argument.""", + attrs = { + "path": attr.string( + mandatory = True, + doc = "Destination path, relative to the working directory of the executable.", + ), + "src": attr.label( + allow_single_file = True, + mandatory = True, + doc = "The file to expose.", + ), + }, +)