diff --git a/.bazelrc b/.bazelrc index 99a7fa14b..a53df161a 100644 --- a/.bazelrc +++ b/.bazelrc @@ -104,6 +104,10 @@ common:qnx_arm64 --test_env=QEMU_CPU=Cascadelake-Server-v5 common:qnx --config=qnx_x86_64 +# Enables FlatBuffers-based mw::com configuration tooling (disabled by default, +# see score/mw/com/impl/configuration/flatbuffers_flags.bzl). +common:flatbuffers --//score/mw/com/impl/configuration:enable_flatbuffers=true + # unshare /dev/shm and /tmp test --sandbox_tmpfs_path=/dev/shm test --sandbox_tmpfs_path=/tmp diff --git a/MODULE.bazel b/MODULE.bazel index 8748511e7..f0b63f7ca 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -17,6 +17,7 @@ bazel_dep(name = "bazel_skylib", version = "1.9.0") bazel_dep(name = "boost.interprocess", version = "1.83.0.bcr.4") bazel_dep(name = "boost.program_options", version = "1.83.0.bcr.4") bazel_dep(name = "download_utils", version = "1.2.2") +bazel_dep(name = "flatbuffers", version = "25.12.19") bazel_dep(name = "googletest", version = "1.17.0.bcr.2") bazel_dep(name = "nlohmann_json", version = "3.11.3") bazel_dep(name = "platforms", version = "1.0.0") diff --git a/quality/visibility_guard/public_targets.golden b/quality/visibility_guard/public_targets.golden index 1eda8f498..e91b7bb05 100644 --- a/quality/visibility_guard/public_targets.golden +++ b/quality/visibility_guard/public_targets.golden @@ -87,6 +87,8 @@ //score/mw/com/dependability:mw_com_rst //score/mw/com/example/com-api-example:com-api-example //score/mw/com/impl/bindings/mock_binding:generic_skeleton_event +//score/mw/com/impl/configuration/converter:json_to_flatbuffer +//score/mw/com/impl/configuration/converter:mw_com_config.bin //score/mw/com/impl/configuration:mw_com_config.json //score/mw/com/impl/configuration:mw_com_config_disabled_trace_config.json //score/mw/com/impl/configuration:mw_com_config_invalid_trace_config_path.json diff --git a/score/mw/com/impl/configuration/BUILD b/score/mw/com/impl/configuration/BUILD index 54604e663..5e913dd40 100644 --- a/score/mw/com/impl/configuration/BUILD +++ b/score/mw/com/impl/configuration/BUILD @@ -11,10 +11,12 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@flatbuffers//:build_defs.bzl", "flatbuffer_cc_library") load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") load("//bazel/tools:json_schema_validator.bzl", "validate_json_schema_test") load("//quality/unit_testing:unit_testing.bzl", "cc_unit_test") load("//score/mw:common_features.bzl", "COMPILER_WARNING_FEATURES") +load(":flatbuffers_flags.bzl", "FLATBUFFERS_TARGET_COMPATIBLE_WITH", "flatbuffers_build_settings") validate_json_schema_test( name = "validate_mw_com_config_schema", @@ -24,6 +26,34 @@ validate_json_schema_test( target_compatible_with = ["@platforms//os:linux"], ) +# Feature flag gating availability of FlatBuffers-based configuration tooling. +# Disabled by default; enable with: +# bazel build --config=flatbuffers //... +flatbuffers_build_settings( + visibility = ["//score/mw/com/impl/configuration:__subpackages__"], +) + +# FlatBuffers schema mirroring mw_com_config_schema.json. Compiling it in-build +# validates the schema and provides the generated C++ header (mw_com_config_generated.h) +# to consumers that read the binary configuration produced by the converter tool +# (//score/mw/com/impl/configuration/converter:json_to_flatbuffer). +flatbuffer_cc_library( + name = "mw_com_config_fbs", + srcs = ["mw_com_config.fbs"], + target_compatible_with = FLATBUFFERS_TARGET_COMPATIBLE_WITH, + visibility = ["//score/mw/com:__subpackages__"], +) + +# Exported so the converter tool package can reference the schema (as the flatc +# input) and the example config (in its rule/test). +exports_files( + [ + "mw_com_config.fbs", + "example/mw_com_config.json", + ], + visibility = ["//score/mw/com/impl/configuration/converter:__pkg__"], +) + filegroup( name = "mw_com_config_schema", srcs = ["mw_com_config_schema.json"], diff --git a/score/mw/com/impl/configuration/converter/BUILD b/score/mw/com/impl/configuration/converter/BUILD new file mode 100644 index 000000000..a570516ea --- /dev/null +++ b/score/mw/com/impl/configuration/converter/BUILD @@ -0,0 +1,59 @@ +# ******************************************************************************* +# 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_python//python:defs.bzl", "py_binary", "py_test") +load("//score/mw/com/impl/configuration:flatbuffers_flags.bzl", "FLATBUFFERS_TARGET_COMPATIBLE_WITH") +load(":json_to_flatbuffer_rule.bzl", "mw_com_config_to_flatbuffer") + +py_binary( + name = "json_to_flatbuffer", + srcs = [ + "json_to_flatbuffer.py", + ], + imports = ["."], + main = "json_to_flatbuffer.py", + visibility = ["//visibility:public"], +) + +# Requires FlatBuffers support (see //score/mw/com/impl/configuration:flatbuffers_flags.bzl): +# bazel test --config=flatbuffers :json_to_flatbuffer_test +py_test( + name = "json_to_flatbuffer_test", + size = "small", + srcs = [ + "json_to_flatbuffer.py", + "json_to_flatbuffer_test.py", + ], + data = [ + "//score/mw/com/impl/configuration:example/mw_com_config.json", + "//score/mw/com/impl/configuration:mw_com_config.fbs", + "@flatbuffers//:flatc", + ], + env = { + "FLATC_PATH": "$(rootpath @flatbuffers//:flatc)", + }, + imports = ["."], + main = "json_to_flatbuffer_test.py", + target_compatible_with = FLATBUFFERS_TARGET_COMPATIBLE_WITH, +) + +# Example: build the FlatBuffer binary for the example configuration as part of +# the build graph. Requires FlatBuffers support to be enabled: +# bazel build --config=flatbuffers //score/mw/com/impl/configuration/converter:mw_com_config.bin +mw_com_config_to_flatbuffer( + name = "mw_com_config.bin", + out = "mw_com_config.bin", + config_json = "//score/mw/com/impl/configuration:example/mw_com_config.json", + target_compatible_with = FLATBUFFERS_TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) diff --git a/score/mw/com/impl/configuration/converter/json_to_flatbuffer.py b/score/mw/com/impl/configuration/converter/json_to_flatbuffer.py new file mode 100644 index 000000000..82c0de42d --- /dev/null +++ b/score/mw/com/impl/configuration/converter/json_to_flatbuffer.py @@ -0,0 +1,310 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Convert an mw::com configuration JSON file into a FlatBuffer binary. + +The runtime configuration JSON (validated against ``mw_com_config_schema.json``) +uses ``camelCase``/``kebab-case`` keys and lowercase enum strings, which do not +match the ``snake_case`` field names and enum symbols of ``mw_com_config.fbs``. + +This tool performs the semantic mapping (key rename, enum-string -> enum-symbol, +dropping absent optionals) to produce a "flatc-ingestible" JSON, then delegates +serialization to ``flatc --binary`` so the resulting ``.bin`` is guaranteed to +conform to the schema. This keeps the non-trivial conversion logic here in Python +while letting flatc own the binary encoding. +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile + + +class ConversionError(Exception): + """Raised when the input JSON does not match the expected configuration shape.""" + + +# ----------------------------------------------------------------------------- +# Enum string -> FlatBuffers enum symbol mappings (see mw_com_config.fbs). +# ----------------------------------------------------------------------------- +_BINDING_ENUM = {"SHM": "SHM"} +_ASIL_LEVEL_ENUM = {"QM": "QM", "B": "B"} +_PERMISSION_CHECKS_ENUM = { + "file-permissions-on-empty": "FILE_PERMISSIONS_ON_EMPTY", + "strict": "STRICT", +} +_SHM_SIZE_CALC_MODE_ENUM = {"SIMULATION": "SIMULATION"} + + +# ----------------------------------------------------------------------------- +# Field descriptors. Each describes how a single JSON key maps to a FlatBuffers +# field: the target field name and how to convert its value. +# ----------------------------------------------------------------------------- +class _Field: + def __init__(self, name, convert): + self.name = name + self._convert = convert + + def convert(self, value, path): + return self._convert(value, path) + + +def _scalar(name): + """A value copied verbatim (string / number / bool).""" + return _Field(name, lambda value, path: value) + + +def _enum(name, mapping): + """A string value translated to its FlatBuffers enum symbol.""" + + def convert(value, path): + if value not in mapping: + raise ConversionError( + f"{path}: invalid enum value {value!r}; expected one of " + f"{sorted(mapping)}" + ) + return mapping[value] + + return _Field(name, convert) + + +def _object(name, spec): + """A nested object converted against ``spec``.""" + return _Field(name, lambda value, path: _convert_object(value, spec, path)) + + +def _object_list(name, spec): + """An array of objects, each converted against ``spec``.""" + + def convert(value, path): + if not isinstance(value, list): + raise ConversionError(f"{path}: expected an array") + return [_convert_object(item, spec, f"{path}[{i}]") for i, item in enumerate(value)] + + return _Field(name, convert) + + +def _scalar_list(name): + """An array of scalars copied verbatim.""" + + def convert(value, path): + if not isinstance(value, list): + raise ConversionError(f"{path}: expected an array") + return list(value) + + return _Field(name, convert) + + +def _convert_object(node, spec, path): + """Convert a JSON object ``node`` using ``spec`` (json_key -> _Field). + + Absent optional keys are simply left out, so optional FlatBuffers scalars stay + unset. Unknown keys are a hard error: they usually indicate drift between the + JSON schema and the FlatBuffers schema, which should fail loudly. + """ + if not isinstance(node, dict): + raise ConversionError(f"{path}: expected an object") + result = {} + for key, value in node.items(): + field = spec.get(key) + if field is None: + raise ConversionError(f"{path}: unknown key {key!r}") + result[field.name] = field.convert(value, f"{path}.{key}") + return result + + +# ----------------------------------------------------------------------------- +# Configuration schema specs (mirror mw_com_config.fbs / mw_com_config_schema.json). +# ----------------------------------------------------------------------------- +_VERSION_SPEC = { + "major": _scalar("major"), + "minor": _scalar("minor"), +} + +# serviceTypes[] +_SERVICE_TYPE_EVENT_SPEC = { + "eventName": _scalar("event_name"), + "eventId": _scalar("event_id"), +} +_SERVICE_TYPE_FIELD_SPEC = { + "fieldName": _scalar("field_name"), + "fieldId": _scalar("field_id"), + "Get": _scalar("get"), + "Set": _scalar("set"), +} +_SERVICE_TYPE_METHOD_SPEC = { + "methodName": _scalar("method_name"), + "methodId": _scalar("method_id"), +} +_SERVICE_TYPE_BINDING_SPEC = { + "binding": _enum("binding", _BINDING_ENUM), + "serviceId": _scalar("service_id"), + "events": _object_list("events", _SERVICE_TYPE_EVENT_SPEC), + "fields": _object_list("fields", _SERVICE_TYPE_FIELD_SPEC), + "methods": _object_list("methods", _SERVICE_TYPE_METHOD_SPEC), +} +_SERVICE_TYPE_SPEC = { + "serviceTypeName": _scalar("service_type_name"), + "version": _object("version", _VERSION_SPEC), + "bindings": _object_list("bindings", _SERVICE_TYPE_BINDING_SPEC), +} + +# serviceInstances[] +_UID_LIST_SPEC = { + "QM": _scalar_list("qm"), + "B": _scalar_list("b"), +} +_INSTANCE_EVENT_SPEC = { + "eventName": _scalar("event_name"), + "maxSamples": _scalar("max_samples"), + "numberOfSampleSlots": _scalar("number_of_sample_slots"), + "maxSubscribers": _scalar("max_subscribers"), + "enforceMaxSamples": _scalar("enforce_max_samples"), + "numberOfIpcTracingSlots": _scalar("number_of_ipc_tracing_slots"), +} +_INSTANCE_FIELD_SPEC = { + "fieldName": _scalar("field_name"), + "numberOfSampleSlots": _scalar("number_of_sample_slots"), + "maxSubscribers": _scalar("max_subscribers"), + "enforceMaxSamples": _scalar("enforce_max_samples"), + "numberOfIpcTracingSlots": _scalar("number_of_ipc_tracing_slots"), + "useGetIfAvailable": _scalar("use_get_if_available"), + "useSetIfAvailable": _scalar("use_set_if_available"), +} +_INSTANCE_METHOD_SPEC = { + "methodName": _scalar("method_name"), + "queueSize": _scalar("queue_size"), + "use": _scalar("use"), +} +_SERVICE_INSTANCE_BINDING_SPEC = { + "instanceId": _scalar("instance_id"), + "asil-level": _enum("asil_level", _ASIL_LEVEL_ENUM), + "binding": _enum("binding", _BINDING_ENUM), + "shm-size": _scalar("shm_size"), + "control-asil-b-shm-size": _scalar("control_asil_b_shm_size"), + "control-qm-shm-size": _scalar("control_qm_shm_size"), + "permission-checks": _enum("permission_checks", _PERMISSION_CHECKS_ENUM), + "allowedConsumer": _object("allowed_consumer", _UID_LIST_SPEC), + "allowedProvider": _object("allowed_provider", _UID_LIST_SPEC), + "events": _object_list("events", _INSTANCE_EVENT_SPEC), + "fields": _object_list("fields", _INSTANCE_FIELD_SPEC), + "methods": _object_list("methods", _INSTANCE_METHOD_SPEC), + "interVmSupport": _scalar("inter_vm_support"), + "interVmForwarded": _scalar("inter_vm_forwarded"), +} +_SERVICE_INSTANCE_SPEC = { + "instanceSpecifier": _scalar("instance_specifier"), + "serviceTypeName": _scalar("service_type_name"), + "version": _object("version", _VERSION_SPEC), + "instances": _object_list("instances", _SERVICE_INSTANCE_BINDING_SPEC), +} + +# global +_QUEUE_SIZE_SPEC = { + "QM-receiver": _scalar("qm_receiver"), + "B-receiver": _scalar("b_receiver"), + "B-sender": _scalar("b_sender"), +} +_GLOBAL_SPEC = { + "asil-level": _enum("asil_level", _ASIL_LEVEL_ENUM), + "applicationID": _scalar("application_id"), + "queue-size": _object("queue_size", _QUEUE_SIZE_SPEC), + "shm-size-calc-mode": _enum("shm_size_calc_mode", _SHM_SIZE_CALC_MODE_ENUM), +} + +# tracing +_TRACING_SPEC = { + "enable": _scalar("enable"), + "applicationInstanceID": _scalar("application_instance_id"), + "traceFilterConfigPath": _scalar("trace_filter_config_path"), +} + +# root +_ROOT_SPEC = { + "serviceTypes": _object_list("service_types", _SERVICE_TYPE_SPEC), + "serviceInstances": _object_list("service_instances", _SERVICE_INSTANCE_SPEC), + "global": _object("global", _GLOBAL_SPEC), + "tracing": _object("tracing", _TRACING_SPEC), +} + + +def normalize(config): + """Transform a parsed configuration dict into flatc-ingestible form.""" + return _convert_object(config, _ROOT_SPEC, "$") + + +def load_json(path): + with open(path, "r", encoding="utf-8") as json_file: + return json.load(json_file) + + +def run_flatc(flatc, schema, normalized_json_path, out_dir): + """Invoke flatc to serialize ``normalized_json_path`` into ``out_dir``.""" + command = [flatc, "--binary", "-o", out_dir, schema, normalized_json_path] + subprocess.run(command, check=True) + + +def convert(input_path, schema_path, output_path, flatc="flatc"): + """Convert the config JSON at ``input_path`` into a FlatBuffer at ``output_path``.""" + normalized = normalize(load_json(input_path)) + + output_dir = os.path.dirname(os.path.abspath(output_path)) + os.makedirs(output_dir, exist_ok=True) + + # flatc derives the output name from the input basename (.bin), so name + # the temporary normalized JSON after the requested output stem. + stem = os.path.splitext(os.path.basename(output_path))[0] + with tempfile.TemporaryDirectory() as work_dir: + normalized_json_path = os.path.join(work_dir, stem + ".json") + with open(normalized_json_path, "w", encoding="utf-8") as normalized_file: + json.dump(normalized, normalized_file) + + run_flatc(flatc, schema_path, normalized_json_path, work_dir) + + produced = os.path.join(work_dir, stem + ".bin") + if not os.path.exists(produced): + raise ConversionError( + f"flatc did not produce the expected output {produced!r}" + ) + shutil.move(produced, output_path) + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Convert an mw::com configuration JSON file into a FlatBuffer binary." + ) + parser.add_argument("--input", required=True, help="Path to the configuration JSON file.") + parser.add_argument("--schema", required=True, help="Path to the mw_com_config.fbs schema.") + parser.add_argument("--output", required=True, help="Path of the FlatBuffer binary to write.") + parser.add_argument( + "--flatc", + default="flatc", + help="Path to the flatc compiler (default: %(default)s, looked up on PATH).", + ) + args = parser.parse_args(argv) + + try: + convert(args.input, args.schema, args.output, args.flatc) + except ConversionError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + except subprocess.CalledProcessError as error: + print(f"error: flatc failed with exit code {error.returncode}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/score/mw/com/impl/configuration/converter/json_to_flatbuffer_rule.bzl b/score/mw/com/impl/configuration/converter/json_to_flatbuffer_rule.bzl new file mode 100644 index 000000000..3db08e2dd --- /dev/null +++ b/score/mw/com/impl/configuration/converter/json_to_flatbuffer_rule.bzl @@ -0,0 +1,69 @@ +# ******************************************************************************* +# 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 rule that converts an mw::com config JSON into a FlatBuffer binary. + +The rule runs the ``json_to_flatbuffer`` Python tool as a build action, passing +the pinned ``@flatbuffers//:flatc`` compiler, so ``.bin`` artifacts become part +of the build graph and can be consumed by other targets (e.g. as ``data``). +""" + +def _mw_com_config_to_flatbuffer_impl(ctx): + output = ctx.actions.declare_file(ctx.attr.out) + + args = ctx.actions.args() + args.add("--input", ctx.file.config_json.path) + args.add("--schema", ctx.file._schema.path) + args.add("--output", output.path) + args.add("--flatc", ctx.executable._flatc.path) + + ctx.actions.run( + executable = ctx.executable._tool, + inputs = [ctx.file.config_json, ctx.file._schema], + outputs = [output], + tools = [ctx.executable._flatc], + arguments = [args], + mnemonic = "MwComConfigToFlatBuffer", + progress_message = "Converting %s to FlatBuffer" % ctx.file.config_json.short_path, + ) + + return [DefaultInfo(files = depset([output]))] + +mw_com_config_to_flatbuffer = rule( + implementation = _mw_com_config_to_flatbuffer_impl, + doc = "Converts an mw::com configuration JSON file into a FlatBuffer binary.", + attrs = { + "config_json": attr.label( + allow_single_file = [".json"], + mandatory = True, + doc = "The mw::com configuration JSON file to convert.", + ), + "out": attr.string( + mandatory = True, + doc = "Name of the FlatBuffer binary to produce (e.g. 'mw_com_config.bin').", + ), + "_schema": attr.label( + allow_single_file = [".fbs"], + default = "//score/mw/com/impl/configuration:mw_com_config.fbs", + ), + "_tool": attr.label( + default = "//score/mw/com/impl/configuration/converter:json_to_flatbuffer", + executable = True, + cfg = "exec", + ), + "_flatc": attr.label( + default = "@flatbuffers//:flatc", + executable = True, + cfg = "exec", + ), + }, +) diff --git a/score/mw/com/impl/configuration/converter/json_to_flatbuffer_test.py b/score/mw/com/impl/configuration/converter/json_to_flatbuffer_test.py new file mode 100644 index 000000000..e0d4e5cd0 --- /dev/null +++ b/score/mw/com/impl/configuration/converter/json_to_flatbuffer_test.py @@ -0,0 +1,185 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Tests for the JSON-to-FlatBuffer configuration converter.""" + +import json +import os +import shutil +import subprocess +import tempfile +import unittest + +import json_to_flatbuffer + + +_CONVERTER_DIR = os.path.dirname(os.path.abspath(__file__)) +_CONFIG_DIR = os.path.dirname(_CONVERTER_DIR) +_SCHEMA = os.path.join(_CONFIG_DIR, "mw_com_config.fbs") +_EXAMPLE = os.path.join(_CONFIG_DIR, "example", "mw_com_config.json") + + +def _flatc(): + """Locate the flatc binary provided via the FLATC_PATH runfiles env or PATH.""" + from_env = os.environ.get("FLATC_PATH") + if from_env: + return os.path.abspath(from_env) + return shutil.which("flatc") + + +class NormalizeTest(unittest.TestCase): + """Unit tests for the in-memory JSON normalization (no flatc involved).""" + + def test_renames_keys_and_maps_enums(self): + config = { + "serviceTypes": [], + "serviceInstances": [ + { + "instanceSpecifier": "abc/port", + "serviceTypeName": "/svc", + "version": {"major": 1, "minor": 2}, + "instances": [ + { + "asil-level": "B", + "binding": "SHM", + "shm-size": 42, + "permission-checks": "file-permissions-on-empty", + "allowedConsumer": {"QM": [1], "B": [2]}, + } + ], + } + ], + "global": { + "asil-level": "QM", + "queue-size": {"QM-receiver": 8, "B-receiver": 5, "B-sender": 12}, + "shm-size-calc-mode": "SIMULATION", + }, + } + + normalized = json_to_flatbuffer.normalize(config) + + instance = normalized["service_instances"][0]["instances"][0] + self.assertEqual(instance["asil_level"], "B") + self.assertEqual(instance["binding"], "SHM") + self.assertEqual(instance["shm_size"], 42) + self.assertEqual(instance["permission_checks"], "FILE_PERMISSIONS_ON_EMPTY") + self.assertEqual(instance["allowed_consumer"], {"qm": [1], "b": [2]}) + self.assertEqual(normalized["global"]["queue_size"]["qm_receiver"], 8) + self.assertEqual(normalized["global"]["shm_size_calc_mode"], "SIMULATION") + + def test_omits_absent_optional_keys(self): + config = { + "serviceTypes": [], + "serviceInstances": [], + "tracing": {"applicationInstanceID": "APP"}, + } + normalized = json_to_flatbuffer.normalize(config) + # Absent optional keys must not appear, so optional scalars stay unset. + self.assertEqual(normalized["tracing"], {"application_instance_id": "APP"}) + self.assertNotIn("global", normalized) + + def test_unknown_key_is_rejected(self): + with self.assertRaises(json_to_flatbuffer.ConversionError): + json_to_flatbuffer.normalize( + {"serviceTypes": [], "serviceInstances": [], "bogus": 1} + ) + + def test_invalid_enum_value_is_rejected(self): + config = { + "serviceTypes": [], + "serviceInstances": [ + { + "instanceSpecifier": "p", + "serviceTypeName": "/svc", + "version": {"major": 1, "minor": 0}, + "instances": [{"asil-level": "NOPE", "binding": "SHM"}], + } + ], + } + with self.assertRaises(json_to_flatbuffer.ConversionError): + json_to_flatbuffer.normalize(config) + + +class ConvertExampleTest(unittest.TestCase): + """End-to-end test: convert the example config and round-trip it via flatc.""" + + def setUp(self): + self.flatc = _flatc() + if not self.flatc or not os.path.exists(self.flatc): + self.skipTest("flatc binary not available") + self.work_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.work_dir, ignore_errors=True) + + def _roundtrip_to_json(self, bin_path, defaults=False): + """Re-emit a FlatBuffer binary back to a JSON dict using flatc.""" + out_dir = os.path.join(self.work_dir, "rt") + os.makedirs(out_dir, exist_ok=True) + command = [self.flatc, "--json", "--strict-json", "--raw-binary"] + if defaults: + command.append("--defaults-json") + command += ["-o", out_dir, _SCHEMA, "--", bin_path] + subprocess.run(command, check=True) + stem = os.path.splitext(os.path.basename(bin_path))[0] + with open(os.path.join(out_dir, stem + ".json"), "r", encoding="utf-8") as result: + return json.load(result) + + def test_example_converts_and_roundtrips(self): + out_bin = os.path.join(self.work_dir, "mw_com_config.bin") + json_to_flatbuffer.convert(_EXAMPLE, _SCHEMA, out_bin, self.flatc) + + self.assertTrue(os.path.exists(out_bin)) + self.assertGreater(os.path.getsize(out_bin), 0) + + data = self._roundtrip_to_json(out_bin) + + service_type = data["service_types"][0] + self.assertEqual( + service_type["service_type_name"], + "/score/ncar/services/TirePressureService", + ) + binding = service_type["bindings"][0] + self.assertEqual(binding["binding"], "SHM") + self.assertEqual(binding["service_id"], 1234) + + instance = data["service_instances"][0]["instances"][0] + self.assertEqual(instance["asil_level"], "B") + self.assertEqual(instance["shm_size"], 10000) + self.assertEqual(instance["allowed_consumer"]["qm"], [42, 43]) + self.assertTrue(instance["inter_vm_support"]) + + event = instance["events"][0] + self.assertEqual(event["number_of_sample_slots"], 50) + # enforceMaxSamples was absent in the input, so the optional stays unset. + self.assertNotIn("enforce_max_samples", event) + + self.assertEqual(data["global"]["queue_size"]["qm_receiver"], 8) + self.assertEqual(data["global"]["asil_level"], "B") + # shm-size-calc-mode is SIMULATION, which equals the enum default, so + # flatc legitimately omits it unless defaults are emitted (see below). + self.assertNotIn("shm_size_calc_mode", data["global"]) + self.assertEqual( + data["tracing"]["application_instance_id"], "ara_com_example" + ) + + def test_schema_defaults_applied_when_emitted(self): + # permission-checks and shm-size-calc-mode resolve to their schema + # defaults; flatc should surface them when defaults are emitted. + out_bin = os.path.join(self.work_dir, "mw_com_config.bin") + json_to_flatbuffer.convert(_EXAMPLE, _SCHEMA, out_bin, self.flatc) + data = self._roundtrip_to_json(out_bin, defaults=True) + instance = data["service_instances"][0]["instances"][0] + self.assertEqual(instance["permission_checks"], "FILE_PERMISSIONS_ON_EMPTY") + self.assertEqual(data["global"]["shm_size_calc_mode"], "SIMULATION") + + +if __name__ == "__main__": + unittest.main() diff --git a/score/mw/com/impl/configuration/flatbuffers_flags.bzl b/score/mw/com/impl/configuration/flatbuffers_flags.bzl new file mode 100644 index 000000000..8c972bc3c --- /dev/null +++ b/score/mw/com/impl/configuration/flatbuffers_flags.bzl @@ -0,0 +1,63 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +"""Feature flag gating availability of the FlatBuffers-based mw::com configuration tooling. + +FlatBuffers support (the `.fbs` schema library and the JSON-to-FlatBuffer converter) +is opt-in: it is disabled by default so plain `bazel build //...` / CI runs do not +require the `flatbuffers` toolchain unless explicitly requested. + +Usage in BUILD files: + load("//score/mw/com/impl/configuration:flatbuffers_flags.bzl", "FLATBUFFERS_TARGET_COMPATIBLE_WITH", "flatbuffers_build_settings") + flatbuffers_build_settings(visibility = ["//score/mw/com/impl/configuration:__subpackages__"]) + + some_target( + ... + target_compatible_with = FLATBUFFERS_TARGET_COMPATIBLE_WITH, + ) + +Enable at the command line with: + bazel build --//score/mw/com/impl/configuration:enable_flatbuffers=true //... +""" + +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") + +# Shared `target_compatible_with` value for any target that depends on FlatBuffers. +# Defined once here (rather than repeated per-target) so all gated targets stay in +# sync; the labels are fully qualified so this resolves correctly from any package. +FLATBUFFERS_TARGET_COMPATIBLE_WITH = select({ + "//score/mw/com/impl/configuration:flatbuffers_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + +def flatbuffers_build_settings(name = "flatbuffers_flags", visibility = None): + """Declares the enable_flatbuffers flag and its corresponding config_setting. + + Args: + name: Unused; kept for symmetry with similar *_build_settings macros. + visibility: Visibility list applied to the generated flag/config_setting targets. + """ + + # Feature flag gating FlatBuffers-based configuration tooling. Disabled by + # default: the flatbuffers toolchain is opt-in. + bool_flag( + name = "enable_flatbuffers", + build_setting_default = False, + visibility = visibility, + ) + + native.config_setting( + name = "flatbuffers_enabled", + flag_values = {":enable_flatbuffers": "True"}, + visibility = visibility, + ) diff --git a/score/mw/com/impl/configuration/mw_com_config.fbs b/score/mw/com/impl/configuration/mw_com_config.fbs new file mode 100644 index 000000000..879fd5711 --- /dev/null +++ b/score/mw/com/impl/configuration/mw_com_config.fbs @@ -0,0 +1,232 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* +// +// FlatBuffers schema mirroring the mw::com runtime configuration. +// +// This schema is the binary-serialization counterpart of the JSON configuration +// schema `mw_com_config_schema.json`. Field structure, cardinality and semantics +// are kept 1:1 with that JSON schema. Field names use idiomatic FlatBuffers +// `snake_case`; where the corresponding JSON key differs (JSON uses `camelCase` +// and `kebab-case`), the original key is given in a comment next to the field. +// +// Integer widths are chosen to match the concrete C++ types used by the +// configuration data model (see e.g. lola_service_id.h, lola_event_instance_ +// deployment.h, lola_service_instance_deployment.h, global_configuration.h). +// +// Notes on FlatBuffers vs. JSON schema: +// * Scalar fields that are OPTIONAL in the JSON schema use the optional-scalar +// form (`= null`) so that "not configured" can be distinguished from a value. +// The documented JSON default (if any) is stated in a comment; applying that +// default is left to the reader/converter, exactly like the JSON case. +// * FlatBuffers scalars/enums cannot carry the `(required)` attribute; enum +// fields that are mandatory in the JSON schema (asil-level, binding) use a +// leading INVALID/0 value so that "unset" remains detectable. Mandatory +// strings, tables and vectors do use `(required)`. +// * FlatBuffers does not support default values for strings; such defaults are +// documented in comments only. + +namespace score.mw.com.impl.configuration; + +// ----------------------------------------------------------------------------- +// Enumerations +// ----------------------------------------------------------------------------- + +/// Binding technology (JSON: "binding" enum -> ["SHM"]). +/// INVALID is a schema-only sentinel to detect an unset mandatory field. +enum Binding : ubyte { + INVALID = 0, + SHM = 1, +} + +/// ASIL level (JSON: "asil-level" enum -> ["QM", "B"]). +/// Values mirror score::mw::com::impl::QualityType (quality_type.h). +enum AsilLevel : ushort { + INVALID = 0, + QM = 1, + B = 2, +} + +/// Strategy for empty user-id lists (JSON: "permission-checks"). +enum PermissionChecks : ubyte { + FILE_PERMISSIONS_ON_EMPTY = 0, // JSON: "file-permissions-on-empty" (default) + STRICT = 1, // JSON: "strict" +} + +/// Calculation mode for shared-memory size (JSON: "shm-size-calc-mode"). +/// Mirrors score::mw::com::impl::ShmSizeCalculationMode (shm_size_calc_mode.h). +enum ShmSizeCalcMode : ubyte { + SIMULATION = 0, // JSON: "SIMULATION" (default) +} + +// ----------------------------------------------------------------------------- +// Shared definitions +// ----------------------------------------------------------------------------- + +/// JSON: $defs/serviceVersion. Mirrors ServiceVersionType (service_version_type.h). +table ServiceVersion { + major:uint32; // std::uint32_t, mandatory in JSON + minor:uint32; // std::uint32_t, mandatory in JSON +} + +// ----------------------------------------------------------------------------- +// serviceTypes[] +// ----------------------------------------------------------------------------- + +/// JSON: serviceTypes[].bindings[].events[] +table ServiceTypeEvent { + event_name:string (required); // JSON: "eventName" + event_id:uint16; // JSON: "eventId"; LolaServiceElementId (LoLa uses 8 bit) +} + +/// JSON: serviceTypes[].bindings[].fields[] +table ServiceTypeField { + field_name:string (required); // JSON: "fieldName" + field_id:uint16; // JSON: "fieldId"; LolaServiceElementId + get:bool = false; // JSON: "Get" (default false) + set:bool = false; // JSON: "Set" (default false) +} + +/// JSON: serviceTypes[].bindings[].methods[] +table ServiceTypeMethod { + method_name:string (required); // JSON: "methodName" + method_id:uint16; // JSON: "methodId"; LolaServiceElementId +} + +/// JSON: serviceTypes[].bindings[] +table ServiceTypeBinding { + binding:Binding; // mandatory in JSON; INVALID marks "unset" + service_id:uint16; // JSON: "serviceId"; LolaServiceId (16 bit for LoLa/Shm) + events:[ServiceTypeEvent]; // optional + fields:[ServiceTypeField]; // optional + methods:[ServiceTypeMethod]; // optional +} + +/// JSON: serviceTypes[] +table ServiceType { + service_type_name:string (required); // JSON: "serviceTypeName" + version:ServiceVersion (required); + bindings:[ServiceTypeBinding] (required); +} + +// ----------------------------------------------------------------------------- +// serviceInstances[] +// ----------------------------------------------------------------------------- + +/// User-id lists grouped by ASIL level (JSON: "allowedConsumer" / "allowedProvider"). +/// uid values map to uid_t -> uint32. +table UidList { + qm:[uint32]; // JSON: "QM" + b:[uint32]; // JSON: "B" +} + +/// JSON: serviceInstances[].instances[].events[] +table InstanceEvent { + event_name:string (required); // JSON: "eventName" + max_samples:uint16 = null; // JSON: "maxSamples"; DEPRECATED (use number_of_sample_slots); 1..65535 + number_of_sample_slots:uint16 = null; // JSON: "numberOfSampleSlots"; 1..65535 + max_subscribers:uint8 = null; // JSON: "maxSubscribers"; SubscriberCountType (uint8) + enforce_max_samples:bool = null; // JSON: "enforceMaxSamples"; default true + number_of_ipc_tracing_slots:uint8 = null; // JSON: "numberOfIpcTracingSlots"; 0..255, default 0 +} + +/// JSON: serviceInstances[].instances[].fields[] +table InstanceField { + field_name:string (required); // JSON: "fieldName" + number_of_sample_slots:uint16 = null; // JSON: "numberOfSampleSlots"; 1..65535 + max_subscribers:uint8 = null; // JSON: "maxSubscribers"; SubscriberCountType (uint8) + enforce_max_samples:bool = null; // JSON: "enforceMaxSamples"; default true + number_of_ipc_tracing_slots:uint8 = null; // JSON: "numberOfIpcTracingSlots"; 0..255, default 0 + use_get_if_available:bool = null; // JSON: "useGetIfAvailable"; default false + use_set_if_available:bool = null; // JSON: "useSetIfAvailable"; default false +} + +/// JSON: serviceInstances[].instances[].methods[] +table InstanceMethod { + method_name:string (required); // JSON: "methodName" + queue_size:uint8 = null; // JSON: "queueSize"; QueueSize (uint8), 1..255 + use:bool = null; // JSON: "use"; default true +} + +/// JSON: serviceInstances[].instances[] (a service instance bound to a technology). +table ServiceInstanceBinding { + instance_id:uint16 = null; // JSON: "instanceId"; absent => "any" + asil_level:AsilLevel; // JSON: "asil-level", mandatory; INVALID marks "unset" + binding:Binding; // mandatory in JSON; INVALID marks "unset" + shm_size:uint64 = null; // JSON: "shm-size"; std::size_t + control_asil_b_shm_size:uint64 = null; // JSON: "control-asil-b-shm-size"; std::size_t + control_qm_shm_size:uint64 = null; // JSON: "control-qm-shm-size"; std::size_t + permission_checks:PermissionChecks = FILE_PERMISSIONS_ON_EMPTY; // JSON: "permission-checks" + allowed_consumer:UidList; // JSON: "allowedConsumer"; optional + allowed_provider:UidList; // JSON: "allowedProvider"; optional + events:[InstanceEvent]; // optional + fields:[InstanceField]; // optional + methods:[InstanceMethod]; // optional + inter_vm_support:bool = null; // JSON: "interVmSupport"; default false + inter_vm_forwarded:bool = null; // JSON: "interVmForwarded"; default false +} + +/// JSON: serviceInstances[] +table ServiceInstance { + instance_specifier:string (required); // JSON: "instanceSpecifier" + service_type_name:string (required); // JSON: "serviceTypeName" + version:ServiceVersion (required); + instances:[ServiceInstanceBinding] (required); +} + +// ----------------------------------------------------------------------------- +// global +// ----------------------------------------------------------------------------- + +/// JSON: global."queue-size". Values map to std::int32_t (global_configuration.h). +table QueueSize { + qm_receiver:int32 = 10; // JSON: "QM-receiver" (default 10) + b_receiver:int32 = 10; // JSON: "B-receiver" (default 10) + b_sender:int32 = 20; // JSON: "B-sender" (default 20) +} + +/// JSON: global +table GlobalConfiguration { + asil_level:AsilLevel = QM; // JSON: "asil-level" (default QM) + application_id:uint32 = null; // JSON: "applicationID"; ApplicationId (uint32), 0..4294967295 + queue_size:QueueSize; // JSON: "queue-size" + shm_size_calc_mode:ShmSizeCalcMode = SIMULATION; // JSON: "shm-size-calc-mode" +} + +// ----------------------------------------------------------------------------- +// tracing +// ----------------------------------------------------------------------------- + +/// JSON: tracing +table TracingConfiguration { + enable:bool = true; // JSON: "enable"; default true + application_instance_id:string (required); // JSON: "applicationInstanceID" + // JSON: "traceFilterConfigPath"; default "./etc/mw_com_trace_filter.json" + // (FlatBuffers has no string defaults; default is applied by the reader). + trace_filter_config_path:string; +} + +// ----------------------------------------------------------------------------- +// Root +// ----------------------------------------------------------------------------- + +/// Top-level mw::com configuration (JSON root object). +table Configuration { + service_types:[ServiceType] (required); // JSON: "serviceTypes" + service_instances:[ServiceInstance] (required); // JSON: "serviceInstances" + global:GlobalConfiguration; // optional + tracing:TracingConfiguration; // optional +} + +root_type Configuration; + +file_identifier "MWCC";