diff --git a/.cmake-format.py b/.cmake-format.py index 1037c5fd..2bef5bc6 100644 --- a/.cmake-format.py +++ b/.cmake-format.py @@ -4,143 +4,163 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Config file that specifies how cmake-format should behave.""" # ---------------------------------- # Options affecting listfile parsing # ---------------------------------- -with section("parse"): +with section("parse"): # noqa: F821 # Specify structure for custom cmake functions - additional_commands = {'add_sen_integration_test': {'kwargs': {'WORKING_DIRECTORY': 1, - 'REQ_COMPONENTS': 3, - 'REQ_DEPS': 3}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'add_sen_run_smoke_test': {'kwargs': {'NAME': 1, - 'CONFIG_FILE': 1, - 'WORKING_DIRECTORY': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'add_sen_cli_gen_smoke_test': {'kwargs': {'NAME': 1, - 'COMMAND': 1, - 'WORKING_DIRECTORY': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'add_sen_smoke_test': {'kwargs': {'NAME': 1, - 'COMMAND': 1, - 'WORKING_DIRECTORY': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_add_dependency': {'pargs': {'nargs': 3}}, - 'sen_configure_target': {'pargs': {'nargs': 1}}, - 'sen_enable_static_analysis': {'pargs': {'nargs': 1}}, - 'sen_generate_code': {'kwargs': {'TARGET': 1, - 'OUTPUT_DIR': 1, - 'BASE_PATH': 1, - 'LANG': 1, - 'CODEGEN_SETTINGS': 1, - 'GEN_HDR_FILES': 1, - 'SCHEMA_FILE': 1, - 'SCHEMA_COMPONENT_NAME': 1, - 'HLA_OUTPUT_DIR': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+'}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_cpp': {'kwargs': {'TARGET': 1, - 'OUTPUT_DIR': 1, - 'BASE_PATH': 1, - 'LANG': 1, - 'CODEGEN_SETTINGS': 1, - 'GEN_HDR_FILES': 1, - 'SCHEMA_FILE': 1, - 'SCHEMA_COMPONENT_NAME': 1, - 'HLA_OUTPUT_DIR': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+'}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_python': {'kwargs': {'TARGET': 1, - 'OUTPUT_DIR': 1, - 'BASE_PATH': 1, - 'LANG': 1, - 'CODEGEN_SETTINGS': 1, - 'GEN_HDR_FILES': 1, - 'SCHEMA_FILE': 1, - 'SCHEMA_COMPONENT_NAME': 1, - 'HLA_OUTPUT_DIR': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+'}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_uml': {'kwargs': {'BASE_PATH': 1, - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'OUT': 1}, - 'pargs': {'flags': ['CLASSES_ONLY', - 'TYPES_ONLY', - 'TYPES_ONLY_NO_ENUMS'], - 'nargs': '*'}}, - 'add_sen_package': {'kwargs': {'TARGET': 1, - 'MAINTAINER': 1, - 'DESCRIPTION': 1, - 'VERSION': 1, - 'BASE_PATH': 1, - 'EXPORT_NAME': 1, - 'SCHEMA_PATH': 1, - 'GEN_HDR_FILES': 1, - 'CODEGEN_SETTINGS': 1, - 'HLA_OUTPUT_DIR': 1, - 'TEST_TARGET': 1, - 'SOURCES': '+', - 'DEPS': '+', - 'PRIVATE_DEPS': '+', - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+', - 'EXPORTED_CLASSES': '+'}, - 'pargs': {'flags': ['NO_SCHEMA', 'IS_COMPONENT', 'PUBLIC_SYMBOLS'], 'nargs': '*'}}, - 'sen_generate_package': {'kwargs': {'TARGET': 1, - 'MAINTAINER': 1, - 'DESCRIPTION': 1, - 'VERSION': 1, - 'BASE_PATH': 1, - 'EXPORT_NAME': 1, - 'SCHEMA_PATH': 1, - 'GEN_HDR_FILES': 1, - 'CODEGEN_SETTINGS': 1, - 'HLA_OUTPUT_DIR': 1, - 'TEST_TARGET': 1, - 'SOURCES': '+', - 'DEPS': '+', - 'PRIVATE_DEPS': '+', - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+', - 'EXPORTED_CLASSES': '+'}, - 'pargs': {'flags': ['NO_SCHEMA', 'IS_COMPONENT'], 'nargs': '*'}}, - 'add_sen_component': {'kwargs': {'TARGET': 1, - 'MAINTAINER': 1, - 'DESCRIPTION': 1, - 'VERSION': 1, - 'BASE_PATH': 1, - 'EXPORT_NAME': 1, - 'SCHEMA_PATH': 1, - 'GEN_HDR_FILES': 1, - 'CODEGEN_SETTINGS': 1, - 'HLA_OUTPUT_DIR': 1, - 'TEST_TARGET': 1, - 'SOURCES': '+', - 'DEPS': '+', - 'PRIVATE_DEPS': '+', - 'STL_FILES': '+', - 'HLA_FOM_DIRS': '+', - 'HLA_MAPPINGS_FILE': '+', - 'EXPORTED_CLASSES': '+'}, - 'pargs': {'flags': ['NO_SCHEMA', 'PUBLIC_SYMBOLS'], 'nargs': '*'}}, - 'sen_combine_schemas': {'kwargs': {'SCHEMAS': '+', - 'OUTPUT': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}, - 'sen_generate_yaml': {'kwargs': {'DEPS': '+', - 'OUTPUT': 1, - 'SCRIPT': 1, - 'TARGET': 1}, - 'pargs': {'flags': [], 'nargs': '*'}}} + additional_commands = { + "add_sen_integration_test": { + "kwargs": {"WORKING_DIRECTORY": 1, "REQ_COMPONENTS": 3, "REQ_DEPS": 3}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "add_sen_run_smoke_test": { + "kwargs": {"NAME": 1, "CONFIG_FILE": 1, "WORKING_DIRECTORY": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "add_sen_cli_gen_smoke_test": { + "kwargs": {"NAME": 1, "COMMAND": 1, "WORKING_DIRECTORY": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "add_sen_smoke_test": { + "kwargs": {"NAME": 1, "COMMAND": 1, "WORKING_DIRECTORY": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_add_dependency": {"pargs": {"nargs": 3}}, + "sen_configure_target": {"pargs": {"nargs": 1}}, + "sen_enable_static_analysis": {"pargs": {"nargs": 1}}, + "sen_generate_code": { + "kwargs": { + "TARGET": 1, + "OUTPUT_DIR": 1, + "BASE_PATH": 1, + "LANG": 1, + "CODEGEN_SETTINGS": 1, + "GEN_HDR_FILES": 1, + "SCHEMA_FILE": 1, + "SCHEMA_COMPONENT_NAME": 1, + "HLA_OUTPUT_DIR": 1, + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + }, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_generate_cpp": { + "kwargs": { + "TARGET": 1, + "OUTPUT_DIR": 1, + "BASE_PATH": 1, + "LANG": 1, + "CODEGEN_SETTINGS": 1, + "GEN_HDR_FILES": 1, + "SCHEMA_FILE": 1, + "SCHEMA_COMPONENT_NAME": 1, + "HLA_OUTPUT_DIR": 1, + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + }, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_generate_python": { + "kwargs": { + "TARGET": 1, + "OUTPUT_DIR": 1, + "BASE_PATH": 1, + "LANG": 1, + "CODEGEN_SETTINGS": 1, + "GEN_HDR_FILES": 1, + "SCHEMA_FILE": 1, + "SCHEMA_COMPONENT_NAME": 1, + "HLA_OUTPUT_DIR": 1, + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + }, + "pargs": {"flags": [], "nargs": "*"}, + }, + "sen_generate_uml": { + "kwargs": {"BASE_PATH": 1, "STL_FILES": "+", "HLA_FOM_DIRS": "+", "OUT": 1}, + "pargs": {"flags": ["CLASSES_ONLY", "TYPES_ONLY", "TYPES_ONLY_NO_ENUMS"], "nargs": "*"}, + }, + "add_sen_package": { + "kwargs": { + "TARGET": 1, + "MAINTAINER": 1, + "DESCRIPTION": 1, + "VERSION": 1, + "BASE_PATH": 1, + "EXPORT_NAME": 1, + "SCHEMA_PATH": 1, + "GEN_HDR_FILES": 1, + "CODEGEN_SETTINGS": 1, + "HLA_OUTPUT_DIR": 1, + "TEST_TARGET": 1, + "SOURCES": "+", + "DEPS": "+", + "PRIVATE_DEPS": "+", + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + "EXPORTED_CLASSES": "+", + }, + "pargs": {"flags": ["NO_SCHEMA", "IS_COMPONENT", "PUBLIC_SYMBOLS"], "nargs": "*"}, + }, + "sen_generate_package": { + "kwargs": { + "TARGET": 1, + "MAINTAINER": 1, + "DESCRIPTION": 1, + "VERSION": 1, + "BASE_PATH": 1, + "EXPORT_NAME": 1, + "SCHEMA_PATH": 1, + "GEN_HDR_FILES": 1, + "CODEGEN_SETTINGS": 1, + "HLA_OUTPUT_DIR": 1, + "TEST_TARGET": 1, + "SOURCES": "+", + "DEPS": "+", + "PRIVATE_DEPS": "+", + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + "EXPORTED_CLASSES": "+", + }, + "pargs": {"flags": ["NO_SCHEMA", "IS_COMPONENT"], "nargs": "*"}, + }, + "add_sen_component": { + "kwargs": { + "TARGET": 1, + "MAINTAINER": 1, + "DESCRIPTION": 1, + "VERSION": 1, + "BASE_PATH": 1, + "EXPORT_NAME": 1, + "SCHEMA_PATH": 1, + "GEN_HDR_FILES": 1, + "CODEGEN_SETTINGS": 1, + "HLA_OUTPUT_DIR": 1, + "TEST_TARGET": 1, + "SOURCES": "+", + "DEPS": "+", + "PRIVATE_DEPS": "+", + "STL_FILES": "+", + "HLA_FOM_DIRS": "+", + "HLA_MAPPINGS_FILE": "+", + "EXPORTED_CLASSES": "+", + }, + "pargs": {"flags": ["NO_SCHEMA", "PUBLIC_SYMBOLS"], "nargs": "*"}, + }, + "sen_combine_schemas": {"kwargs": {"SCHEMAS": "+", "OUTPUT": 1}, "pargs": {"flags": [], "nargs": "*"}}, + "sen_generate_yaml": { + "kwargs": {"DEPS": "+", "OUTPUT": 1, "SCRIPT": 1, "TARGET": 1}, + "pargs": {"flags": [], "nargs": "*"}, + }, + } # Override configurations per-command where available override_spec = {} @@ -154,7 +174,7 @@ # ----------------------------- # Options affecting formatting. # ----------------------------- -with section("format"): +with section("format"): # noqa: F821 # Disable formatting entirely, making cmake-format a no-op disable = False @@ -175,7 +195,7 @@ # 'use-space', fractional indentation is left as spaces (utf-8 0x20). If set # to `round-up` fractional indentation is replaced with a single tab character # (utf-8 0x09) effectively shifting the column to the next tabstop - fractional_tab_policy = 'use-space' + fractional_tab_policy = "use-space" # If an argument group contains more than this many sub-groups (parg or kwarg # groups) then force it to a vertical layout. @@ -203,7 +223,7 @@ # to this reference: `prefix`: the start of the statement, `prefix-indent`: # the start of the statement, plus one indentation level, `child`: align to # the column of the arguments - dangle_align = 'prefix' + dangle_align = "prefix" # If the statement spelling length (including space and parenthesis) is # smaller than this amount, then force reject nested layouts. @@ -219,13 +239,13 @@ max_lines_hwrap = 2 # What style line endings to use in the output. - line_ending = 'unix' + line_ending = "unix" # Format command names consistently as 'lower' or 'upper' case - command_case = 'canonical' + command_case = "canonical" # Format keywords consistently as 'lower' or 'upper' case - keyword_case = 'upper' + keyword_case = "upper" # A list of command names which should always be wrapped always_wrap = [] @@ -251,12 +271,12 @@ # ------------------------------------------------ # Options affecting comment reflow and formatting. # ------------------------------------------------ -with section("markup"): +with section("markup"): # noqa: F821 # What character to use for bulleted lists - bullet_char = '-' + bullet_char = "-" # What character to use as punctuation after numerals in an enumerated list - enum_char = '.' + enum_char = "." # If comment markup is enabled, don't reflow the first comment block in each # listfile. Use this to preserve formatting of your copyright/license @@ -269,15 +289,15 @@ # Regular expression to match preformat fences in comments default= # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` - fence_pattern = '^\\s*([`~]{3}[`~]*)(.*)$' + fence_pattern = "^\\s*([`~]{3}[`~]*)(.*)$" # Regular expression to match rulers in comments default= # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` - ruler_pattern = '^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' + ruler_pattern = "^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$" # If a comment line matches starts with this pattern then it is explicitly a # trailing comment for the preceeding argument. Default is '#<' - explicit_trailing_pattern = '#<' + explicit_trailing_pattern = "#<" # If a comment line starts with at least this many consecutive hash # characters, then don't lstrip() them off. This allows for lazy hash rulers @@ -294,43 +314,43 @@ # ---------------------------- # Options affecting the linter # ---------------------------- -with section("lint"): +with section("lint"): # noqa: F821 # a list of lint codes to disable disabled_codes = [] # regular expression pattern describing valid function names - function_pattern = '[0-9a-z_]+' + function_pattern = "[0-9a-z_]+" # regular expression pattern describing valid macro names - macro_pattern = '[0-9A-Z_]+' + macro_pattern = "[0-9A-Z_]+" # regular expression pattern describing valid names for variables with global # (cache) scope - global_var_pattern = '[A-Z][0-9A-Z_]+' + global_var_pattern = "[A-Z][0-9A-Z_]+" # regular expression pattern describing valid names for variables with global # scope (but internal semantic) - internal_var_pattern = '_[A-Z][0-9A-Z_]+' + internal_var_pattern = "_[A-Z][0-9A-Z_]+" # regular expression pattern describing valid names for variables with local # scope - local_var_pattern = '[a-z][a-z0-9_]+' + local_var_pattern = "[a-z][a-z0-9_]+" # regular expression pattern describing valid names for privatedirectory # variables - private_var_pattern = '_[0-9a-z_]+' + private_var_pattern = "_[0-9a-z_]+" # regular expression pattern describing valid names for public directory # variables - public_var_pattern = '[A-Z][0-9A-Z_]+' + public_var_pattern = "[A-Z][0-9A-Z_]+" # regular expression pattern describing valid names for function/macro # arguments and loop variables. - argument_var_pattern = '[a-z][a-z0-9_]+' + argument_var_pattern = "[a-z][a-z0-9_]+" # regular expression pattern describing valid names for keywords used in # functions or macros - keyword_pattern = '[A-Z][0-9A-Z_]+' + keyword_pattern = "[A-Z][0-9A-Z_]+" # In the heuristic for C0201, how many conditionals to match within a loop in # before considering the loop a parser. @@ -350,21 +370,21 @@ # ------------------------------- # Options affecting file encoding # ------------------------------- -with section("encode"): +with section("encode"): # noqa: F821 # If true, emit the unicode byte-order mark (BOM) at the start of the file emit_byteorder_mark = False # Specify the encoding of the input file. Defaults to utf-8 - input_encoding = 'utf-8' + input_encoding = "utf-8" # Specify the encoding of the output file. Defaults to utf-8. Note that cmake # only claims to support utf-8 so be careful when using anything else - output_encoding = 'utf-8' + output_encoding = "utf-8" # ------------------------------------- # Miscellaneous configurations options. # ------------------------------------- -with section("misc"): +with section("misc"): # noqa: F821 # A dictionary containing any per-command configuration overrides. Currently # only `command_case` is supported. per_command = {} diff --git a/.conan/test_packages/package/conanfile.py b/.conan/test_packages/package/conanfile.py index dcaa6699..4f65febe 100644 --- a/.conan/test_packages/package/conanfile.py +++ b/.conan/test_packages/package/conanfile.py @@ -1,35 +1,40 @@ - # === conanfile.py ===================================================================================================== # Sen Infrastructure # Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== - -# test to consume conan package in conan v2 way +"""Module that defines a test_package test to consume conan package in conan v2 way.""" from conan import ConanFile -from conan.tools.cmake import CMakeToolchain, CMake from conan.tools.build import cross_building +from conan.tools.cmake import CMake, CMakeToolchain + class TestPackageConan(ConanFile): + """Conan file that specifies how a project is setup that uses Sen.""" + settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps", "VirtualRunEnv" test_type = "explicit" def requirements(self): + """Defines the dependencies of Sen.""" self.requires(self.tested_reference_str) def generate(self): + """Generate the toolchain files.""" tc = CMakeToolchain(self, generator="Ninja") tc.generate() def build(self): + """Configure and build Sen test_package.""" cmake = CMake(self) cmake.configure() cmake.build() def test(self): + """Defines a few test calls to ensure Sen works.""" if not cross_building(self): self.run("sen --version", env="conanrun") # TODO(SEN-1185): enable tests when possible diff --git a/.conan/test_packages/package/my_package/CMakeLists.txt b/.conan/test_packages/package/my_package/CMakeLists.txt index 4ec7b1b7..92dfe5fb 100644 --- a/.conan/test_packages/package/my_package/CMakeLists.txt +++ b/.conan/test_packages/package/my_package/CMakeLists.txt @@ -24,7 +24,6 @@ add_sen_package( target_sources(my_package PRIVATE ${_combined_schema}) target_link_libraries(my_package PRIVATE $) set_target_properties(my_package PROPERTIES FOLDER "examples/basic") -set_target_properties(my_package PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # TODO(SEN-1185): enable tests when possible configure_file(config/my_package.yaml ${CMAKE_BINARY_DIR}/test_configs/my_package.yaml COPYONLY) diff --git a/.github/scripts/generate_matrix_jobs.py b/.github/scripts/generate_matrix_jobs.py index 0fb680ba..77169b08 100644 --- a/.github/scripts/generate_matrix_jobs.py +++ b/.github/scripts/generate_matrix_jobs.py @@ -4,30 +4,27 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== -""" -Script to generate various forms of job matrices. -""" -import os +"""Script to generate various forms of job matrices.""" + +import argparse import json +import os import typing as tp -import argparse -from dataclasses import dataclass, is_dataclass, asdict +from dataclasses import asdict, dataclass, is_dataclass def convert_dataclass_to_json(obj) -> dict: """Converts the given dataclass to json.""" if is_dataclass(obj): - return { - key: convert_dataclass_to_json(value) - for key, value in asdict(obj).items() - } + return {key: convert_dataclass_to_json(value) for key, value in asdict(obj).items()} return obj @dataclass(frozen=True, order=True) class Compiler: - """Compiler specification""" + """Compiler specification.""" + name: str version: int cc: str @@ -36,13 +33,15 @@ class Compiler: @dataclass(frozen=True, order=True) class Container: - """Container specification""" + """Container specification.""" + image: str @dataclass(frozen=True, order=True) class JobSpecification: """Pipeline job specification that defines the configuration options.""" + name: str os: str runner: tp.Literal["ubuntu-latest", "windows-2022", "self-hosted", "ubuntu-24.04-arm"] @@ -56,47 +55,65 @@ def as_json(self) -> dict: return convert_dataclass_to_json(self) -def compute_jobs(release: bool, conan: bool) -> list[JobSpecification]: +def compute_jobs(release: bool, conan: bool) -> list[JobSpecification]: # noqa: ARG001 """Computes the list of pipeline jobs that should run.""" jobs = [] # Add gcc jobs if not release: jobs.append( - JobSpecification("Basic GCC", "ubuntu-22.04", "self-hosted", None, - Compiler("gcc", 12, "gcc-12", "g++-12"), 17, - "Debug")) + JobSpecification( + "Basic GCC", "ubuntu-22.04", "self-hosted", None, Compiler("gcc", 12, "gcc-12", "g++-12"), 17, "Debug" + ) + ) else: jobs.append( - JobSpecification("Basic GCC", "ubuntu-22.04", "self-hosted", None, - Compiler("gcc", 12, "gcc-12", "g++-12"), 17, - "Release")) + JobSpecification( + "Basic GCC", "ubuntu-22.04", "self-hosted", None, Compiler("gcc", 12, "gcc-12", "g++-12"), 17, "Release" + ) + ) # Add clang jobs if not release: jobs.append( - JobSpecification("Basic Clang", "ubuntu-24.04", "self-hosted", - None, - Compiler("clang", 20, "clang-20", - "clang++-20"), 17, "Debug")) + JobSpecification( + "Basic Clang", + "ubuntu-24.04", + "self-hosted", + None, + Compiler("clang", 20, "clang-20", "clang++-20"), + 17, + "Debug", + ) + ) # Add msvc jobs if release: jobs.append( - JobSpecification("Basic Windows", "windows", "windows-2022", None, - Compiler("msvc", 194, "cl", "cl"), 17, "Release")) + JobSpecification( + "Basic Windows", "windows", "windows-2022", None, Compiler("msvc", 194, "cl", "cl"), 17, "Release" + ) + ) else: jobs.append( - JobSpecification("Basic Windows", "windows", "windows-2022", None, - Compiler("msvc", 194, "cl", "cl"), 17, "Debug")) + JobSpecification( + "Basic Windows", "windows", "windows-2022", None, Compiler("msvc", 194, "cl", "cl"), 17, "Debug" + ) + ) # Add amd64 jobs if not release: jobs.append( - JobSpecification("Basic Ubuntu arm", "ubuntu-24.04", - "ubuntu-24.04-arm", None, - Compiler("gcc_arm64", 12, "gcc-14", - "g++-14"), 17, "Debug")) + JobSpecification( + "Basic Ubuntu arm", + "ubuntu-24.04", + "ubuntu-24.04-arm", + None, + Compiler("gcc_arm64", 12, "gcc-14", "g++-14"), + 17, + "Debug", + ) + ) return sorted(jobs) @@ -106,9 +123,8 @@ def generate_jobs_file(release: bool, conan: bool) -> None: jobs = compute_jobs(release, conan) if output_file := os.environ.get("GITHUB_OUTPUT"): - with open(output_file, "wt", encoding='utf-8') as matrix_file: - matrix_file.write( - f"jobs={json.dumps([j.as_json() for j in jobs])}") + with open(output_file, "w", encoding="utf-8") as matrix_file: + matrix_file.write(f"jobs={json.dumps([j.as_json() for j in jobs])}") else: print("Error: No output file specified to write to.") @@ -117,18 +133,18 @@ def main() -> None: """Runs the job matrix generator.""" parser = argparse.ArgumentParser( prog="generate_matrix_jobs", - description= - "Generates the list of required matrix jobs for various building needs." + description="Generates the list of required matrix jobs for various building needs.", ) parser.add_argument( "--release", action=argparse.BooleanOptionalAction, - help="Generate the jobs needed for building release artifacts.") + help="Generate the jobs needed for building release artifacts.", + ) parser.add_argument( "--conan", action=argparse.BooleanOptionalAction, - help= - "Generate the jobs needed to ensure all required conan packages work.") + help="Generate the jobs needed to ensure all required conan packages work.", + ) args = parser.parse_args() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43048800..5ebaf292 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,5 +38,19 @@ repos: hooks: - id: cmake-format +- repo: https://github.com/jackdewinter/pymarkdown + rev: v0.9.20 + hooks: + - id: pymarkdown + name: Checking markdown files + args: [--config, .pymarkdown.yaml, fix] + +- repo: https://github.com/astral-sh/ruff-pre-commit.git + rev: v0.4.8 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + # Global Configuration exclude: (examples/packages/hla_fom/.*|test/util/dummy_entities/hla/.*|.*.svg|.*.excalidraw|stl.tmLanguage.json) diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..5b0e6e41 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,34 @@ +line-length = 120 +indent-width = 4 +include = ["*.py"] + +[format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" + +[lint] +select = [ + "ARG", + "B", + "C", + "D", + "E", + "F", + "I", + "I", + "PLC", + "PLE", + "PLW", + "UP", + "W", +] +ignore = [ + "D105", # The meaning of special member functions can be found in the python documentation + "D203", # Conflicts with D211(blank-line-before-class) + "D212", # Conflicts with D213(multi-line-summary-second-line) +] +preview = true + +[lint.pydocstyle] +convention = "google" diff --git a/CMakeLists.txt b/CMakeLists.txt index 14b41f34..ea123f33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ if(SEN_BUILD_TESTS) add_subdirectory(test/util/calls_on_removal) add_subdirectory(test/util/publish_types_manually) add_subdirectory(test/util/query_test) + add_subdirectory(test/support) endif() # coverage diff --git a/README.md b/README.md index 6a22793c..cb8bff25 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ class MyProjectConan(ConanFile): self.requires("sen/[>=1.0]") ``` -2. Run `conan install . --profile --build=missing` to download Sen before running CMake. +1. Run `conan install . --profile --build=missing` to download Sen before running CMake. To ensure your system is able to find all paths, add the `bin` folder to your `PATH` environment variable (in Linux also add it to the `LD_LIBRARY_PATH`). Check that everything works by running `sen shell`. You can play with the objects in the `local` session. diff --git a/apps/cli_gen/src/cpp/templates/struct_impl.j2 b/apps/cli_gen/src/cpp/templates/struct_impl.j2 index ae77007a..c6a83491 100644 --- a/apps/cli_gen/src/cpp/templates/struct_impl.j2 +++ b/apps/cli_gen/src/cpp/templates/struct_impl.j2 @@ -204,3 +204,16 @@ sen::ConstTypeHandle<::sen::StructType> sen::MetaTypeTrait<{{ qualType }}>::meta }); return type; } + +std::string sen::SerializationTraits<{{ qualType }}>::toJsonString(const {{ qualType }}& val) +{ + ::sen::Var var; + ::sen::VariantTraits<{{ qualType }}>::valueToVariant(val, var); + return ::sen::toJson(var); +} + +void sen::SerializationTraits<{{ qualType }}>::fromJsonString(const std::string& str, {{ qualType }}& val) +{ + const ::sen::Var var = ::sen::fromJson(str); + ::sen::VariantTraits<{{ qualType }}>::variantToValue(var, val); +} diff --git a/apps/cli_gen/src/cpp/templates/variant_impl.j2 b/apps/cli_gen/src/cpp/templates/variant_impl.j2 index a7eb32c8..876e51f6 100644 --- a/apps/cli_gen/src/cpp/templates/variant_impl.j2 +++ b/apps/cli_gen/src/cpp/templates/variant_impl.j2 @@ -173,3 +173,16 @@ std::function<::sen::lang::Value(const void*)> sen::VariantTraits<{{ qualType }} }); return type; } + +std::string sen::SerializationTraits<{{ qualType }}>::toJsonString(const {{ qualType }}& val) +{ + ::sen::Var var; + ::sen::VariantTraits<{{ qualType }}>::valueToVariant(val, var); + return ::sen::toJson(var); +} + +void sen::SerializationTraits<{{ qualType }}>::fromJsonString(const std::string& str, {{ qualType }}& val) +{ + const ::sen::Var var = ::sen::fromJson(str); + ::sen::VariantTraits<{{ qualType }}>::variantToValue(var, val); +} diff --git a/apps/cli_gen/test/CMakeLists.txt b/apps/cli_gen/test/CMakeLists.txt index 1e10a197..25a54e20 100644 --- a/apps/cli_gen/test/CMakeLists.txt +++ b/apps/cli_gen/test/CMakeLists.txt @@ -6,7 +6,7 @@ # ====================================================================================================================== set(hla_fom_gen_test_sources ${CMAKE_CURRENT_SOURCE_DIR}) -set(working_dir ${CMAKE_BINARY_DIR}/bin) +set(working_dir $) # Test case 1 # @@ -272,3 +272,20 @@ add_sen_integration_test( # link them so the checker only runs if the generator succeeds set_tests_properties(hla_fom_gen_integration_test_19 PROPERTIES ENVIRONMENT "PYTHONUNBUFFERED=1") + +# Test case 20 +# +# ModuleA-20 defines interactions with optional and required parameters +# +# Tests that the cli_gen generates MaybeXX types for optional parameters and standard types for required parameters +add_sen_integration_test( + hla_fom_gen_integration_test_20 + COMMAND + ${Python3_EXECUTABLE} + ${hla_fom_gen_test_sources}/test20/test.py + WORKING_DIRECTORY ${working_dir} + REQ_DEPS cli_gen +) + +# link them so the checker only runs if the generator succeeds +set_tests_properties(hla_fom_gen_integration_test_20 PROPERTIES ENVIRONMENT "PYTHONUNBUFFERED=1") diff --git a/apps/cli_gen/test/test19/test.py b/apps/cli_gen/test/test19/test.py index 4720b6e1..d8ee2cab 100755 --- a/apps/cli_gen/test/test19/test.py +++ b/apps/cli_gen/test/test19/test.py @@ -4,14 +4,19 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Encodes a cli_gen test to ensure code gen settings are correctly handled.""" +import os import subprocess import sys -import os -# integration test to check that the codegen settings work well in FOM code generation -if __name__ == "__main__": +def main() -> None: + """ + Encodes the setup and testing logic of running cli_gen. + + Integration test to check that the codegen settings work well in FOM code generation. + """ # input arguments exe_path = "./cli_gen" source_dir = os.path.dirname(os.path.abspath(__file__)) @@ -23,12 +28,12 @@ "cpp", "fom", f"--directories={source_dir}/fom", - f"--settings={source_dir}/codegen_settings.json" + f"--settings={source_dir}/codegen_settings.json", ] print(f"Executing: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True) + result = subprocess.run(cmd, capture_output=True, text=True, check=False) if result.returncode != 0: print(f"Error: cli_gen failed with exit code {result.returncode}") @@ -40,7 +45,7 @@ print(f"Error: Generated file NOT FOUND at {output_file}") sys.exit(1) - with open(output_file, 'r') as f: + with open(output_file, encoding="utf-8") as f: if target_word in f.read(): # the test passes if the skeleton method for the checked property is present print(f"Success: Found '{target_word}' in {output_file}") @@ -48,3 +53,7 @@ else: print(f"Failure: '{target_word}' not found in the file.") sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/apps/cli_gen/test/test20/fom/moduleA-20.xml b/apps/cli_gen/test/test20/fom/moduleA-20.xml new file mode 100644 index 00000000..f7512f77 --- /dev/null +++ b/apps/cli_gen/test/test20/fom/moduleA-20.xml @@ -0,0 +1,49 @@ + + + + New Module + FOM + 1.0 + unclassified + + + Description of New Module + + + + + + HLAobjectRoot + + + + + HLAinteractionRoot + + MyInteraction + PublishSubscribe + HLAreliable + Receive + Test interaction + + MyOptionalParam + IntModuleA + Optional. This is optional. + + + MyRequiredParam + IntModuleA + This is required. + + + + + + + + IntModuleA + HLAinteger32BE + + + + diff --git a/apps/cli_gen/test/test20/test.py b/apps/cli_gen/test/test20/test.py new file mode 100755 index 00000000..d45d96c5 --- /dev/null +++ b/apps/cli_gen/test/test20/test.py @@ -0,0 +1,66 @@ +# === test.py ========================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== +"""Encodes a cli_gen test to ensure FOM generations works correctly.""" + +import os +import subprocess +import sys + + +def main() -> None: + """ + Encodes the setup and testing logic of running cli_gen. + + Integration test to check that the FOM generator creates Maybe types for optional parameters + """ + # input arguments + exe_path = "./cli_gen" + source_dir = os.path.dirname(os.path.abspath(__file__)) + output_file = os.path.join(os.getcwd(), "fom", "modulea-20.xml.h") + + target_optional = "MaybeI32 myOptionalParam" + target_required = "IntModuleA myRequiredParam" + + cmd = [exe_path, "cpp", "fom", f"--directories={source_dir}/fom"] + + print(f"Executing: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True, check=False) + + if result.returncode != 0: + print(f"Error: cli_gen failed with exit code {result.returncode}") + print(f"Stdout: {result.stdout}") + print(f"Stderr: {result.stderr}") + sys.exit(1) + + if not os.path.exists(output_file): + print(f"Error: Generated file NOT FOUND at {output_file}") + sys.exit(1) + + with open(output_file, encoding="utf-8") as f: + content = f.read() + + if target_optional not in content: + print( + f"Failure: Optional parameter generation failed. '{target_optional}' " + f"not found in the file {output_file}." + ) + sys.exit(1) + + if target_required not in content: + print( + f"Failure: Required parameter generation failed. '{target_required}' " + f"not found in the file {output_file}." + ) + sys.exit(1) + + print(f"Success: Found correctly generated parameters in {output_file}") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/cmake/util/color_cmake_target_graph.py b/cmake/util/color_cmake_target_graph.py index a253a387..cc036941 100755 --- a/cmake/util/color_cmake_target_graph.py +++ b/cmake/util/color_cmake_target_graph.py @@ -4,12 +4,11 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Script to generate a colored cmake target graph.""" import re import sys -input_path, output_path = sys.argv[1], sys.argv[2] - COLORS_DATA = { "egg": {"outline": "#f9a825", "fill": "#fbc02d"}, # executables "doubleoctagon": {"outline": "#7e57c2", "fill": "#9575cd"}, # shared libs @@ -17,27 +16,43 @@ "hexagon": {"outline": "#e57373", "fill": "#ef9a9a"}, # object libs "tripleoctagon": {"outline": "#fb8c00", "fill": "#ffcc80"}, # module libs "pentagon": {"outline": "#4caf50", "fill": "#81c784"}, # interface libs - "box": {"outline": "#66bb6a", "fill": "#a5d6a7"} # custom target + "box": {"outline": "#66bb6a", "fill": "#a5d6a7"}, # custom target } -shape_re = re.compile(r'(shape\s*=\s*\"?([a-zA-z0-9_]+)\"?)') +SHAPE_REGEX = re.compile(r"(shape\s*=\s*\"?([a-zA-z0-9_]+)\"?)") + + +def main() -> None: + """ + Defines the core logic of the script. + + 1. read inputs from cmake + 2. match shapes and color them + 3. write out the resulting graph + """ + input_path, output_path = sys.argv[1], sys.argv[2] + + with open(input_path, encoding="utf-8") as f: + lines = f.readlines() + + out_lines = [] -with open(input_path, "r", encoding="utf-8") as f: - lines = f.readlines() + for raw_line in lines: + line = raw_line + if "[" in line and "shape" in line and "]" in line: + m = SHAPE_REGEX.search(line) + if m: + shape = m.group(2) + data = COLORS_DATA.get(shape) + if data: + color = data.get("outline") + fillcolor = data.get("fill") + line = line.replace("]", f', color="{color}", fillcolor="{fillcolor}"]', 1) + out_lines.append(line) -out_lines = [] + with open(output_path, "w", encoding="utf-8") as f: + f.writelines(out_lines) -for line in lines: - if "[" in line and "shape" in line and "]" in line: - m = shape_re.search(line) - if m: - shape = m.group(2) - data = COLORS_DATA.get(shape) - if data: - color = data.get("outline") - fillcolor = data.get("fill") - line = line.replace(']', f', color="{color}", fillcolor="{fillcolor}"]', 1) - out_lines.append(line) -with open(output_path, "w", encoding="utf-8") as f: - f.writelines(out_lines) +if __name__ == "__main__": + main() diff --git a/cmake/util/generate_coverage_report.py b/cmake/util/generate_coverage_report.py index 651852f6..74ab5fb7 100755 --- a/cmake/util/generate_coverage_report.py +++ b/cmake/util/generate_coverage_report.py @@ -9,7 +9,6 @@ """Script for generating llvm-cov based coverage reports.""" -import typing as tp import argparse import glob import os @@ -17,7 +16,7 @@ def merge_coverage_profiles(llvm_profdata, profile_data_dir): - """Merge separate coverage prof data into one merged profile with `llvm-profdata merge`""" + """Merge separate coverage prof data into one merged profile with `llvm-profdata merge`.""" print(" ├ Merging raw profiles...") raw_profiles = glob.glob(os.path.join(profile_data_dir, "*.profraw")) @@ -25,7 +24,7 @@ def merge_coverage_profiles(llvm_profdata, profile_data_dir): input_files = os.path.join(profile_data_dir, "profile_input_files") profdata_path = os.path.join(profile_data_dir, "merged_coverage.profdata") - with open(input_files, "w") as manifest: + with open(input_files, "w", encoding="utf-8") as manifest: manifest.write("\n".join(raw_profiles)) cmd = [llvm_profdata, "merge"] @@ -42,7 +41,7 @@ def merge_coverage_profiles(llvm_profdata, profile_data_dir): return profdata_path -def transform_to_binary_args(binaries) -> tp.List[str]: +def transform_to_binary_args(binaries) -> list[str]: """Expands the binaries into a arg list that the llvm tools expect.""" binary_cmd_line = [binaries[0]] for binary in binaries[1:]: @@ -52,7 +51,7 @@ def transform_to_binary_args(binaries) -> tp.List[str]: return binary_cmd_line -def generate_html_report(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): +def generate_html_report(llvm_cov, profile, report_dir, binaries, ignore_regex: str | None): """Generates a html report for the given coverage data.""" print(" ├ Generating html report files...") @@ -71,7 +70,7 @@ def generate_html_report(llvm_cov, profile, report_dir, binaries, ignore_regex: subprocess.check_call(cmd) -def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): +def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_regex: str | None): """Generates a coverage report for the given coverage data.""" print(" ├ Generating coverage report...") @@ -88,7 +87,8 @@ def generate_coverage_report(llvm_cov, profile, report_dir, binaries, ignore_reg stdout=output_file, ) -def generate_coverage_summary(llvm_cov, profile, report_dir, binaries, ignore_regex: tp.Optional[str]): + +def generate_coverage_summary(llvm_cov, profile, report_dir, binaries, ignore_regex: str | None): """Generates a coverage summary for the given coverage data.""" print(" ├ Generating coverage summary...") diff --git a/cmake/util/sen_codegen_utils.cmake b/cmake/util/sen_codegen_utils.cmake index efd6ff52..7339a2d4 100644 --- a/cmake/util/sen_codegen_utils.cmake +++ b/cmake/util/sen_codegen_utils.cmake @@ -426,6 +426,12 @@ function(sen_generate_code) _all_mappings ) set(_mapping_opt "--mappings=${_all_mappings}") + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY HLA_MAPPINGS ${_abs_mappings} + ) + endif() target_sources(${_arg_TARGET} PRIVATE ${_xml_files}) @@ -475,6 +481,121 @@ function(sen_generate_code) endfunction() +# Takes a target and adds Sen properties to it with the fom/stl files. +# +# sen_generate_interface_package( +# TARGET +# [BASE_PATH base_path] +# [HLA_OUTPUT_DIR path] +# [STL_FILES [files...]] +# [HLA_FOM_DIRS [dirs...]] +# [HLA_MAPPINGS_FILE [files...]] +function(sen_generate_interface_package) + + set(_one_value_args TARGET BASE_PATH) + set(_multi_value_args STL_FILES HLA_FOM_DIRS HLA_MAPPINGS_FILE) + + cmake_parse_arguments( + _arg + "${_options}" + "${_one_value_args}" + "${_multi_value_args}" + ${ARGN} + ) + + if(NOT _arg_TARGET) + message(FATAL_ERROR " sen_generate_interface_package: no TARGET set") + endif() + + if(NOT TARGET ${_arg_TARGET}) + message( + FATAL_ERROR + " sen_generate_interface_package: specified TARGET has not been created. Define the target before calling this function" + ) + endif() + + if(_arg_STL_FILES AND _arg_HLA_FOM_DIRS) + message( + FATAL_ERROR + " sen_generate_interface_package: STL_FILES and HLA_FOM_DIRS cannot be present at the same time" + ) + endif() + + if(_arg_HLA_MAPPINGS_FILE AND NOT _arg_HLA_FOM_DIRS) + message( + FATAL_ERROR + " sen_generate_interface_package: HLA_MAPPINGS_FILE is defined, but no HLA_FOM_DIRS were specified" + ) + endif() + + # get the absolute base path + if(DEFINED _arg_BASE_PATH AND NOT (_arg_BASE_PATH STREQUAL "")) + get_filename_component(_abs_base_path ${_arg_BASE_PATH} ABSOLUTE) + else() + set(_abs_base_path ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + set_property(TARGET ${_arg_TARGET} PROPERTY BASE_PATH ${_abs_base_path}) + + # set the include path as extra options for the code generation + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY SEN_IMPORT_DIRS -i ${_abs_base_path} + ) + + if(_arg_STL_FILES) + foreach(_stl_file ${_arg_STL_FILES}) + # get the absolute path to the sen file + get_filename_component(_abs_stl_file ${_stl_file} ABSOLUTE) + get_filename_component(_stl_file_name ${_stl_file} NAME) + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY STL_FILES ${_abs_stl_file} + ) + endforeach() + endif(_arg_STL_FILES) + + # generate from HLA FOM XMLs + if(_arg_HLA_FOM_DIRS) + set(_abs_fom_dirs) + set(_fom_generated_files) + + foreach(_fom_dir ${_arg_HLA_FOM_DIRS}) + get_filename_component(_abs_fom_dir ${_fom_dir} ABSOLUTE) + list(APPEND _abs_fom_dirs ${_abs_fom_dir}) + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY HLA_FOM_DIRS ${_abs_fom_dir} + ) + + # get the xml files + file( + GLOB _xml_files + LIST_DIRECTORIES false + "${_fom_dir}/*.xml" + ) + endforeach() + + if(_arg_HLA_MAPPINGS_FILE) + set(_abs_mappings) + foreach(_mappings_file ${_arg_HLA_MAPPINGS_FILE}) + get_filename_component(_abs_mapping_file ${_mappings_file} ABSOLUTE) + list(APPEND _abs_mappings ${_abs_mapping_file}) + endforeach() + + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY HLA_MAPPINGS ${_abs_mappings} + ) + endif() + endif() + +endfunction() + # Convenience wrapper for sen_generate_code() that generates C++ code (the default). # Accepts the same arguments as sen_generate_code() except LANG, which is fixed to cpp. macro(sen_generate_cpp) diff --git a/cmake/util/sen_misc_utils.cmake b/cmake/util/sen_misc_utils.cmake index fff0d109..77ec8307 100644 --- a/cmake/util/sen_misc_utils.cmake +++ b/cmake/util/sen_misc_utils.cmake @@ -313,6 +313,7 @@ function(get_external_interfaces) get_target_property(_build_stl_files ${_arg_TARGET} STL_FILES) get_target_property(_build_hla_fom_dirs ${_arg_TARGET} HLA_FOM_DIRS) + get_target_property(_build_hla_mappings ${_arg_TARGET} HLA_MAPPINGS) get_target_property(_build_base_path ${_arg_TARGET} BASE_PATH) string( REGEX MATCH @@ -326,6 +327,12 @@ function(get_external_interfaces) _hla_fom_dirs_present ${_build_hla_fom_dirs} ) + string( + REGEX MATCH + NOTFOUND + _hla_mappings_present + ${_build_hla_mappings} + ) # set base_path that should be common to all interfaces set_property(TARGET ${_arg_TARGET} PROPERTY INSTALL_BASE_PATH ${_abs_install_dir}/interfaces/) @@ -382,7 +389,7 @@ function(get_external_interfaces) if(NOT EXISTS ${installation_fom_dir}) message( FATAL_ERROR - "get_external_interfaces: Could not obtain external interfaces for TARGET ${_arg_TARGET}: Directory ${installation_stl} could not be found on system.\ + "get_external_interfaces: Could not obtain external interfaces for TARGET ${_arg_TARGET}: Directory ${installation_fom_dir} could not be found on system.\ Please, contact with the maintainers of the target. \n\ Possible reasons of the failure: \n\ \tWrongly inputted BASE_PATH on original package generation.\n\ @@ -390,8 +397,8 @@ function(get_external_interfaces) \tINSTALL directory not compliant with the BASE_PATH. \n\ Report this information to the developer when suggesting a fix: \n\ - \tFile was originally built in ${stl_file}\n\ - \tFile was expected to be in ${_abs_install_dir}/${relative_stl_path}\ + \tFOM folder ${fom_dir}\n\ + \tFolder was expected to be in ${_abs_install_dir}/${relative_fom_dir_path}\ Please check the interfaces consumption guide on Sen's official documentation for further information. " @@ -405,6 +412,43 @@ function(get_external_interfaces) endif() endforeach() endif() + # set hla mappings if found + if(_build_hla_mappings) + foreach(mappings_file ${_build_hla_mappings}) + string( + REPLACE ${_build_base_path} + "" + relative_mappings_file_path + ${mappings_file} + ) + set(installation_mappings_file "${_abs_install_dir}/${relative_mappings_file_path}") + + if(NOT EXISTS ${installation_mappings_file}) + message( + FATAL_ERROR + "get_external_interfaces: Could not obtain external interfaces for TARGET ${_arg_TARGET}: Directory ${installation_mappings_file} could not be found on system.\ + Please, contact with the maintainers of the target. \n\ + Possible reasons of the failure: \n\ + \tWrongly inputted BASE_PATH on original package generation.\n\ + \tNo INSTALL directive on the FOM directories.\n\ + \tINSTALL directory not compliant with the BASE_PATH. \n\ + + Report this information to the developer when suggesting a fix: \n\ + \tMappings file was originally built in ${mappings_file}\n\ + \tFile was expected to be in ${_abs_install_dir}/${relative_mappings_file_path}\ + + Please check the interfaces consumption guide on Sen's official documentation for further information. + " + ) + else() + set_property( + TARGET ${_arg_TARGET} + APPEND + PROPERTY INSTALL_HLA_MAPPINGS ${installation_mappings_file} + ) + endif() + endforeach() + endif() endfunction() # Parse all the -config.cmake.in files in the current directory and install the generated -config.cmake files into the target @@ -480,6 +524,7 @@ function(copy_target_properties to_target from_target) copy_target_property(${to_target} ${from_target} SEN_IMPORT_DIRS) copy_target_property(${to_target} ${from_target} STL_FILES) copy_target_property(${to_target} ${from_target} HLA_FOM_DIRS) + copy_target_property(${to_target} ${from_target} HLA_MAPPINGS) copy_target_property(${to_target} ${from_target} EXPORT_FILES) copy_target_property(${to_target} ${from_target} SEN_EXPORTS_TYPES) copy_target_property(${to_target} ${from_target} SEN_IS_PYTHON) @@ -639,12 +684,20 @@ function(sen_configure_target target_name) endif() endif() - set_property(TARGET ${target_name} PROPERTY LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" - )# .exe and .dll - set_property(TARGET ${target_name} PROPERTY RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" - )# .so and .dylib - set_property(TARGET ${target_name} PROPERTY ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib" - )# .a and .lib + if(NOT DEFINED CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set_property(TARGET ${target_name} PROPERTY LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" + )# .exe and .dll + endif() + + if(NOT DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set_property(TARGET ${target_name} PROPERTY RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin" + )# .so and .dylib + endif() + + if(NOT DEFINED CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set_property(TARGET ${target_name} PROPERTY ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib" + )# .a and .lib + endif() # link coverage flags if coverage was enabled (interface target created in root CMakeLists.txt) if(TARGET sen_coverage_flags) diff --git a/cmake/util/sen_package_utils.cmake b/cmake/util/sen_package_utils.cmake index 9bd3b7a4..60e5aae5 100644 --- a/cmake/util/sen_package_utils.cmake +++ b/cmake/util/sen_package_utils.cmake @@ -344,6 +344,133 @@ function(add_sen_package) endfunction() +# Creates a Sen package: an INTERFACE library that contains STL or HLA FOM files. +# +# add_sen_interface_package( +# TARGET +# Name of the CMake target to create. +# +# [MAINTAINER ] +# Person or team responsible for this package. Defaults to "unknown". +# Note: AUTHOR is a deprecated alias for MAINTAINER. +# +# [DESCRIPTION ] +# Human-readable description embedded in the package metadata. +# +# [VERSION ] +# Package version string. Defaults to the CMake project version. +# +# [BASE_PATH ] +# Root directory used to compute relative paths for generated files and +# interface resolution. Defaults to CMAKE_CURRENT_SOURCE_DIR. +# +# [DEPS ] +# Public dependencies. Linked with PUBLIC linkage; their exported Sen types +# are re-exported by this package. +# +# [STL_FILES ] +# Sen Type Language (.stl) files from which C++ code is generated. +# Mutually exclusive with HLA_FOM_DIRS. +# +# [HLA_FOM_DIRS ] +# Directories containing HLA FOM XML files from which C++ code is generated. +# Mutually exclusive with STL_FILES. +# +# [HLA_MAPPINGS_FILE ] +# HLA mapping files that customise FOM-to-C++ translation. +# Requires HLA_FOM_DIRS. +# +function(add_sen_interface_package) + + set(_one_value_args + TARGET + MAINTAINER + DESCRIPTION + VERSION + BASE_PATH + ) + + set(_multi_value_args + DEPS + STL_FILES + HLA_FOM_DIRS + HLA_MAPPINGS_FILE + ) + + cmake_parse_arguments( + _arg + "${_options}" + "${_one_value_args}" + "${_multi_value_args}" + ${ARGN} + ) + + if(NOT _arg_TARGET) + message(FATAL_ERROR "add_sen_interface_package: no TARGET set") + endif() + + if(NOT _arg_MAINTAINER AND NOT _arg_AUTHOR) + set(_arg_MAINTAINER "unknown") + endif() + + if(NOT _arg_VERSION) + set(_arg_VERSION ${CMAKE_PROJECT_VERSION}) + endif() + + if(_arg_BASE_PATH) + get_filename_component(_abs_base_path ${_arg_BASE_PATH} ABSOLUTE) + else() + set(_abs_base_path ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + + add_library(${_arg_TARGET} INTERFACE) + + get_git_head_revision(git_ref_spec git_hash ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) + git_local_changes(git_status_str) + target_compile_definitions( + ${_arg_TARGET} + INTERFACE SEN_TARGET_NAME="${_arg_TARGET}" + SEN_TARGET_MAINTAINER="${_arg_MAINTAINER}" + SEN_TARGET_DESCRIPTION="${_arg_DESCRIPTION}" + SEN_TARGET_VERSION="${_arg_VERSION}" + GIT_REF_SPEC="${git_ref_spec}" + GIT_HASH="${git_hash}" + GIT_STATUS="${git_status_str}" + ) + + # Copy dependency import paths + if(_arg_DEPS) + foreach(_item ${_arg_DEPS}) + copy_target_property(${_arg_TARGET} ${_item} SEN_IMPORT_DIRS) + endforeach() + endif() + + # generate code if needed + if(_arg_STL_FILES OR _arg_HLA_FOM_DIRS) + if(_arg_STL_FILES) + sen_generate_interface_package( + TARGET + ${_arg_TARGET} + BASE_PATH + ${_arg_BASE_PATH} + STL_FILES + ${_arg_STL_FILES} + ) + else() + sen_generate_interface_package( + TARGET + ${_arg_TARGET} + BASE_PATH + ${_arg_BASE_PATH} + HLA_FOM_DIRS + ${_arg_HLA_FOM_DIRS} + HLA_MAPPINGS_FILE + ${_arg_HLA_MAPPINGS_FILE} + ) + endif() + endif() +endfunction() + # Deprecated wrapper for add_sen_package(... PUBLIC_SYMBOLS). # Kept for backwards compatibility — prefer add_sen_package() with PUBLIC_SYMBOLS directly. macro(sen_generate_package) diff --git a/cmake/util/test.cmake b/cmake/util/test.cmake index 0ee3c982..082d0bf9 100644 --- a/cmake/util/test.cmake +++ b/cmake/util/test.cmake @@ -160,7 +160,7 @@ endfunction() # [REQ_DEPS ] # ) function(add_sen_integration_test test_name) - set(_options) + set(_options FLAKY) set(_one_value_args) set(_multi_value_args REQ_COMPONENTS REQ_DEPS) @@ -220,20 +220,21 @@ function(add_sen_run_smoke_test test_name) message(FATAL_ERROR "add_sen_run_smoke_test: no CONFIG_FILE set") endif() - if(NOT - CMAKE_GENERATOR - STREQUAL - "Ninja" + if(_arg_WORKING_DIRECTORY) + set(_working_dir ${_arg_WORKING_DIRECTORY}) + elseif(DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set(_working_dir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + elseif( + NOT + CMAKE_GENERATOR + STREQUAL + "Ninja" ) set(_working_dir ${PROJECT_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE}) else() set(_working_dir ${PROJECT_BINARY_DIR}/bin) endif() - if(_arg_WORKING_DIRECTORY) - set(_working_dir ${_arg_WORKING_DIRECTORY}) - endif() - get_filename_component(_abs_config ${_arg_CONFIG_FILE} ABSOLUTE) if(_arg_NO_START_STOP) @@ -343,7 +344,7 @@ function(add_sen_cli_gen_smoke_test test_name) ) endfunction() -# add_sen_cli_gen_smoke_test( +# append_test_env_modification( # # [list of modifications] # ) diff --git a/cmake/util/tsan_ignorelist.txt b/cmake/util/tsan_ignorelist.txt index c9a81565..8af263ee 100644 --- a/cmake/util/tsan_ignorelist.txt +++ b/cmake/util/tsan_ignorelist.txt @@ -43,3 +43,19 @@ race:moodycamel::ConcurrentQueue REQ_DEPS cli_run ) diff --git a/components/py/test/src/modify_objects.py b/components/py/test/src/modify_objects.py index a98755cd..87f1bc48 100644 --- a/components/py/test/src/modify_objects.py +++ b/components/py/test/src/modify_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to test the modification of objects through the py component.""" import sen @@ -15,23 +16,29 @@ static_value = 56 dynamic_value = 567 -def dynamic_prop_changed(): - global test_object, dynamic_value, cb_fired - assert test_object.dynamicProp == dynamic_value, f"Error in dynamicProp [value: {test_object.dynamicProp}, expectation: {dynamic_value}]" +def dynamic_prop_changed(): + """Calback to react when a property changed.""" + assert ( + test_object.dynamicProp == dynamic_value + ), f"Error in dynamicProp [value: {test_object.dynamicProp}, expectation: {dynamic_value}]" # stopping after checking the value of the property sen.api.requestKernelStop() + def run(): - global test_object, test_bus, dynamic_value, static_value + """Sen run: to setup the initial component state.""" + global test_object, test_bus # noqa: PLW0603 - test_object = sen.api.make("py_test_package.TestObject", "test_object", staticProp = static_value) + test_object = sen.api.make("py_test_package.TestObject", "test_object", staticProp=static_value) test_bus = sen.api.getBus("my.tutorial") test_bus.add(test_object) # check the value of the static property - assert test_object.staticProp == static_value, f"Error in staticProp [value: {test_object.staticProp}, expectation: {static_value}]" + assert ( + test_object.staticProp == static_value + ), f"Error in staticProp [value: {test_object.staticProp}, expectation: {static_value}]" # react to changes in the dynamicProp test_object.onDynamicPropChanged(dynamic_prop_changed) @@ -39,16 +46,20 @@ def run(): # set the dynamic prop to a known value test_object.dynamicProp = dynamic_value + def update(): - global cycle + """Sen update: triggers test execution.""" + global cycle # noqa: PLW0603 if cycle > 1: print("Callback for dynamicProp did not trigger when expected") sen.api.requestKernelStop(1) cycle += 1 + def stop(): - global test_bus, test_object + """Sen stop: trigger that the execution stops.""" + global test_bus, test_object # noqa: PLW0603 test_bus.remove(test_object) test_object, test_bus = None, None diff --git a/components/recorder/test/CMakeLists.txt b/components/recorder/test/CMakeLists.txt index a31f1f56..a9fd1f75 100644 --- a/components/recorder/test/CMakeLists.txt +++ b/components/recorder/test/CMakeLists.txt @@ -5,7 +5,7 @@ # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== -set(recorder_tests recorder_crash_test.cpp recorder_test.cpp) +set(recorder_tests recorder_crash_test.cpp) add_sen_unit_test_suite( recorder_test @@ -16,6 +16,7 @@ add_sen_unit_test_suite( sen::db recorder_for_testing spdlog::spdlog + archive_test_helpers ) sen_generate_cpp( diff --git a/components/recorder/test/data/gen_crash_record.py b/components/recorder/test/data/gen_crash_record.py index 14a22cbd..e662a2f2 100644 --- a/components/recorder/test/data/gen_crash_record.py +++ b/components/recorder/test/data/gen_crash_record.py @@ -4,23 +4,27 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to generate crash records.""" -import sen -import signal import os +import signal + +import sen # to store the object obj = None execution_counter = 0 + def run(): - global obj # refer to the global variable defined above + """Sen run: to setup the initial component state.""" + global obj # refer to the global variable defined above # noqa: PLW0603 obj = sen.api.open("SELECT * FROM my.tutorial") def update(): - global obj # refer to the global variable defined above - global execution_counter + """Sen update: triggers test execution.""" + global execution_counter # refer to the global variable defined above # noqa: PLW0603 execution_counter += 1 # if the object is present diff --git a/components/recorder/test/recorder_crash_test.cpp b/components/recorder/test/recorder_crash_test.cpp index 80c4df55..f095d694 100644 --- a/components/recorder/test/recorder_crash_test.cpp +++ b/components/recorder/test/recorder_crash_test.cpp @@ -5,6 +5,9 @@ // © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. // ===================================================================================================================== +// shared test helpers +#include "archive_test_helpers.h" + // sen #include "sen/core/base/numbers.h" #include "sen/core/meta/property.h" @@ -20,7 +23,6 @@ // std #include -#include #include #include #include @@ -33,20 +35,6 @@ namespace class RecorderCrashResilienceTest: public ::testing::Test { protected: - void SetUp() override - { - testDir_ = std::filesystem::temp_directory_path() / ("recorder_test_" + std::to_string(std::time(nullptr))); - std::filesystem::create_directories(testDir_); - } - - void TearDown() override - { - if (std::filesystem::exists(testDir_)) - { - std::filesystem::remove_all(testDir_); - } - } - static void truncateFile(const std::filesystem::path& path, std::uintmax_t newSize) { std::filesystem::resize_file(path, newSize); @@ -54,10 +42,10 @@ class RecorderCrashResilienceTest: public ::testing::Test static std::uintmax_t getFileSize(const std::filesystem::path& path) { return std::filesystem::file_size(path); } - [[nodiscard]] const std::filesystem::path& getTestDir() const { return testDir_; } + [[nodiscard]] const std::filesystem::path& getTestDir() const { return tempDir_.path(); } private: - std::filesystem::path testDir_; + sen::test::TempDir tempDir_ {"recorder_test_"}; }; /// @test @@ -66,12 +54,8 @@ TEST_F(RecorderCrashResilienceTest, RecordingCreatesValidArchive) { auto kernel = sen::kernel::TestKernel::fromYamlString(""); - sen::db::OutSettings settings; - settings.name = "test_recording"; - settings.folder = getTestDir().string(); - settings.indexKeyframes = true; - - const auto archivePath = getTestDir() / settings.name; + auto settings = sen::test::makeArchiveSettings("test_recording", getTestDir()); + const auto archivePath = sen::test::makeArchivePath("test_recording", getTestDir()); { sen::db::Output output(std::move(settings), []() {}); @@ -95,12 +79,8 @@ TEST_F(RecorderCrashResilienceTest, TruncatedRecording_CanStillBeOpened) { auto kernel = sen::kernel::TestKernel::fromYamlString(""); - sen::db::OutSettings settings; - settings.name = "test_recording"; - settings.folder = getTestDir().string(); - settings.indexKeyframes = true; - - const auto archivePath = getTestDir() / settings.name; + auto settings = sen::test::makeArchiveSettings("test_recording", getTestDir()); + const auto archivePath = sen::test::makeArchivePath("test_recording", getTestDir()); const auto runtimePath = archivePath / "runtime"; { @@ -137,12 +117,8 @@ TEST_F(RecorderCrashResilienceTest, TypesFileCreatedWithArchive) { auto kernel = sen::kernel::TestKernel::fromYamlString(""); - sen::db::OutSettings settings; - settings.name = "test_recording"; - settings.folder = getTestDir().string(); - settings.indexKeyframes = true; - - const auto archivePath = getTestDir() / settings.name; + auto settings = sen::test::makeArchiveSettings("test_recording", getTestDir()); + const auto archivePath = sen::test::makeArchivePath("test_recording", getTestDir()); { sen::db::Output output(std::move(settings), []() {}); diff --git a/components/replayer/CMakeLists.txt b/components/replayer/CMakeLists.txt index 91b97c63..18938b1f 100644 --- a/components/replayer/CMakeLists.txt +++ b/components/replayer/CMakeLists.txt @@ -29,6 +29,7 @@ add_sen_package( STL_FILES ${_replayer_stl_files} DEPS sen::db PRIVATE_DEPS spdlog::spdlog + TEST_TARGET replayer_for_testing IS_COMPONENT ) @@ -40,3 +41,8 @@ install(FILES ${_replayer_stl_files} DESTINATION interfaces/stl/sen/components/r sen_internal_install(replayer) install(FILES ${PROJECT_SOURCE_DIR}/schemas/replayer.json DESTINATION schemas) + +# tests +if(SEN_BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/components/replayer/src/replay.cpp b/components/replayer/src/replay.cpp index 2ba6f1bd..977fa643 100644 --- a/components/replayer/src/replay.cpp +++ b/components/replayer/src/replay.cpp @@ -191,6 +191,11 @@ void ReplayImpl::seekImpl(TimeStamp time) applyCursor(); setNextPlaybackTime(cursor_.get().time); flushObjectsActivity(); + + if (auto diff = time - cursor_.get().time; diff.get() > 0) + { + advanceCursor(diff); + } } void ReplayImpl::applyCursor() diff --git a/components/replayer/test/CMakeLists.txt b/components/replayer/test/CMakeLists.txt new file mode 100644 index 00000000..7f393731 --- /dev/null +++ b/components/replayer/test/CMakeLists.txt @@ -0,0 +1,37 @@ +# === CMakeLists.txt =================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +set(replayer_tests replayer_test.cpp replay_test.cpp replayed_object_test.cpp) + +add_sen_unit_test_suite( + replayer_test + ${replayer_tests} + LINK_DEPS + sen::core + sen::kernel + sen::db + replayer_for_testing + archive_test_helpers +) + +sen_generate_cpp( + TARGET replayer_test + STL_FILES stl/replay_test_class.stl + GEN_HDR_FILES public_generated_files +) + +target_include_directories( + replayer_test SYSTEM + INTERFACE + PRIVATE ${CMAKE_SOURCE_DIR}/components/replayer/src +) + +target_compile_definitions(replayer_test PRIVATE TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") + +set_target_properties( + replayer_test PROPERTIES FOLDER "test" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin" +) diff --git a/components/replayer/test/replay_test.cpp b/components/replayer/test/replay_test.cpp new file mode 100644 index 00000000..bd7ee393 --- /dev/null +++ b/components/replayer/test/replay_test.cpp @@ -0,0 +1,456 @@ +// === replay_test.cpp ================================================================================================= +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// component +#include "replay.h" +#include "replayer_test_helpers.h" + +// sen +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/duration.h" +#include "sen/core/base/timestamp.h" +#include "sen/core/obj/interest.h" +#include "sen/core/obj/object.h" +#include "sen/core/obj/object_list.h" +#include "sen/core/obj/object_source.h" +#include "sen/core/obj/subscription.h" +#include "sen/db/input.h" +#include "sen/db/output.h" +#include "sen/kernel/component.h" +#include "sen/kernel/component_api.h" +#include "sen/kernel/test_kernel.h" + +// generated code +#include "stl/replay_test_class.stl.h" +#include "stl/sen/components/replayer/replayer.stl.h" +#include "stl/sen/db/basic_types.stl.h" + +// google test +#include + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace replayer::test +{ +using sen::components::replayer::ReplayStatus; + +class TestReplayImpl final: public sen::components::replayer::ReplayImpl +{ +public: + SEN_NOCOPY_NOMOVE(TestReplayImpl) + using ReplayImpl::ReplayImpl; + ~TestReplayImpl() override = default; + + void playDirect() { playImpl(); } + void pauseDirect() { pauseImpl(); } + void stopDirect() { stopImpl(); } + void advanceDirect(sen::Duration time) { advanceImpl(time); } + void seekDirect(sen::TimeStamp time) { seekImpl(time); } +}; + +struct ReplaySetup +{ + SEN_NOCOPY_NOMOVE(ReplaySetup) + TempDir tempDir; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr controlBus; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr replay; // NOLINT(misc-non-private-member-variables-in-classes) + sen::kernel::TestComponent component; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr kernel; // NOLINT(misc-non-private-member-variables-in-classes) + + std::filesystem::path archivePath; // NOLINT(misc-non-private-member-variables-in-classes) + + explicit ReplaySetup(const std::string& archiveName = "test_replay", + std::function populateDb = nullptr) + { + archivePath = makeArchivePath(archiveName, tempDir); + auto settings = makeArchiveSettings(archiveName, tempDir); + + { + sen::db::Output output(std::move(settings), []() {}); + if (populateDb) + { + sen::kernel::TestComponent dummyComponent; + sen::kernel::TestKernel dummyKernel(&dummyComponent); + populateDb(output, dummyKernel); + } + else + { + sen::TimeStamp t(std::chrono::seconds(0)); + output.keyframe(t, {}); + t += std::chrono::seconds(10); + output.keyframe(t, {}); + t += std::chrono::seconds(10); + output.keyframe(t, {}); + } + } + + component.onInit( + [this](sen::kernel::InitApi&& api) -> sen::kernel::PassResult + { + controlBus = api.getSource("local.replay_test"); + return sen::kernel::done(); + }); + + component.onRun( + [this, archiveName](sen::kernel::RunApi& api) + { + auto input = std::make_unique(archivePath.string(), api.getTypes()); + replay = std::make_shared(archiveName, archivePath.string(), std::move(input), api); + controlBus->add(replay); + return api.execLoop(std::chrono::seconds(1), + [this, &api]() + { + if (replay) + { + replay->update(api); + } + }); + }); + + kernel = std::make_unique(&component); + } + + ~ReplaySetup() + { + if (controlBus != nullptr && replay != nullptr) + { + replay->stopDirect(); + controlBus->remove(replay); + + step(2); + replay.reset(); + } + + controlBus.reset(); + kernel.reset(); + } + + void step(std::size_t count = 1) const { kernel->step(count); } +}; + +void registerDummyReplayType(ReplaySetup& setup) +{ + setup.kernel->getTypes().add(replayer_test::DummyReplayObjBase::meta()); +} + +[[nodiscard]] TestReplayImpl& startReplay(ReplaySetup& setup) +{ + setup.step(); + if (!setup.replay) + { + throw std::runtime_error("startReplay: replay was not created"); + } + return *setup.replay; +} + +[[nodiscard]] sen::Subscription observeReplayObjects(const ReplaySetup& setup) +{ + if (setup.controlBus == nullptr) + { + throw std::runtime_error("observeReplayObjects: control bus was not created"); + } + + sen::Subscription subscription; + subscription.attachTo( + setup.controlBus, sen::Interest::make("SELECT * FROM local.replay_test", sen::CustomTypeRegistry()), false); + return subscription; +} + +[[nodiscard]] bool hasObjectNamed(const sen::ObjectList& objects, std::string_view name) +{ + for (const auto* object: objects.getObjects()) + { + if (object != nullptr && object->getName() == name) + { + return true; + } + } + return false; +} + +[[nodiscard]] std::size_t countObjectsNamed(const sen::ObjectList& objects, std::string_view name) +{ + std::size_t count = 0; + for (const auto* object: objects.getObjects()) + { + if (object != nullptr && object->getName() == name) + { + ++count; + } + } + return count; +} + +/// @test +/// Keeps playback time coherent across play, pause, stop, and reset +/// requirements(SEN-364) +TEST(ReplayTest, StopChangesStatusAndResetsTime) +{ + ReplaySetup setup; + auto& replay = startReplay(setup); + + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::stopped); + + auto initialTime = replay.getNextPlaybackTime(); + + replay.playDirect(); + setup.step(); + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::playing); + EXPECT_NO_THROW(replay.playDirect()); + + replay.pauseDirect(); + setup.step(); + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::paused); + EXPECT_NO_THROW(replay.pauseDirect()); + + replay.advanceDirect(std::chrono::seconds(10)); + + EXPECT_GT(replay.getNextPlaybackTime(), initialTime); + + replay.stopDirect(); + setup.step(); + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::stopped); + EXPECT_EQ(replay.getNextPlaybackTime(), initialTime); + + EXPECT_NO_THROW(replay.stopDirect()); +} + +/// @test +/// Advances when idle, ignores manual advance while playing and pauses at the end +/// requirements(SEN-364) +TEST(ReplayTest, AdvanceForwardWhenPaused) +{ + ReplaySetup setup; + auto& replay = startReplay(setup); + + auto initialTime = replay.getNextPlaybackTime(); + replay.advanceDirect(std::chrono::seconds(5)); + setup.step(); + + EXPECT_EQ(replay.getNextPlaybackTime(), initialTime + std::chrono::seconds(5)); + + replay.playDirect(); + setup.step(); + auto playingTime = replay.getNextPlaybackTime(); + + replay.advanceDirect(std::chrono::seconds(10)); + setup.step(); + + EXPECT_LT(replay.getNextPlaybackTime(), playingTime + std::chrono::seconds(5)); + + ReplaySetup eofSetup; + auto& eofReplay = startReplay(eofSetup); + + eofReplay.advanceDirect(std::chrono::seconds(40)); + EXPECT_EQ(eofReplay.getNextStatus(), ReplayStatus::paused); +} + +/// @test +/// Seeks within the archive window and rejects times outside it +/// requirements(SEN-364) +TEST(ReplayTest, SeekingToValidTimeUpdatesTime) +{ + ReplaySetup setup; + auto& replay = startReplay(setup); + + auto initialTime = replay.getNextPlaybackTime(); + sen::TimeStamp seekTarget = initialTime; + + EXPECT_NO_THROW(replay.seekDirect(seekTarget)); + setup.step(); + + EXPECT_EQ(replay.getNextPlaybackTime(), seekTarget); + + sen::TimeStamp outOfBounds = initialTime + std::chrono::hours(1); + EXPECT_ANY_THROW(replay.seekDirect(outOfBounds)); +} + +/// @test +/// Seeks to a time between two indexed keyframes +/// requirements(SEN-364) +TEST(ReplayTest, SeekToIntermediateTime) +{ + ReplaySetup setup("test_seek", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto t = makeTime(0); + output.keyframe(t, {}); + + auto dummyObj = std::make_shared("dummy", sen::VarMap {}); + auto info = makeObjectInfo(dummyObj, "local", "dummy_registry"); + + t += std::chrono::seconds(10); + output.creation(t, info, true); + output.propertyChange(t, dummyObj->getId(), firstPropertyId(*dummyObj), {}); + output.keyframe(t, {info}); + + t += std::chrono::seconds(10); + output.keyframe(t, {info}); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + + auto initialTime = replay.getNextPlaybackTime(); + + sen::TimeStamp seekTarget = initialTime + std::chrono::seconds(15); + + EXPECT_NO_THROW(replay.seekDirect(seekTarget)); + setup.step(); + + EXPECT_EQ(replay.getNextPlaybackTime(), seekTarget); +} + +/// @test +/// Handles keyframe replacement and awkward lifecycle entries without breaking playback +/// requirements(SEN-364) +TEST(ReplayTest, KeyframeHandling) +{ + ReplaySetup setup("test_keyframe", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + auto obj2 = std::make_shared("obj2", sen::VarMap {}); + auto info2 = makeObjectInfo(obj2, "local", "replay_test"); + + auto t = makeTime(0); + output.keyframe(t, {info1}); + + t += std::chrono::seconds(10); + output.keyframe(t, {info2}); + + t += std::chrono::seconds(10); + output.keyframe(t, {info2}); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + auto objects = observeReplayObjects(setup); + + EXPECT_FALSE(hasObjectNamed(objects.list, "obj1")); + EXPECT_FALSE(hasObjectNamed(objects.list, "obj2")); + + replay.advanceDirect(std::chrono::seconds(10)); + setup.step(); + + EXPECT_FALSE(hasObjectNamed(objects.list, "obj1")); + EXPECT_TRUE(hasObjectNamed(objects.list, "obj2")); + EXPECT_EQ(countObjectsNamed(objects.list, "obj2"), 1U); + + replay.advanceDirect(std::chrono::seconds(10)); + setup.step(); + + EXPECT_FALSE(hasObjectNamed(objects.list, "obj1")); + EXPECT_TRUE(hasObjectNamed(objects.list, "obj2")); + EXPECT_EQ(countObjectsNamed(objects.list, "obj2"), 1U); + + ReplaySetup lifecycleSetup("test_lifecycle", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + + auto t = makeTime(0); + output.creation(t, info1, true); + + t += std::chrono::seconds(5); + output.creation(t, info1, true); + + t += std::chrono::seconds(5); + output.deletion(t, obj1->getId()); + + t += std::chrono::seconds(5); + output.deletion(t, sen::ObjectId(999)); + }); + + registerDummyReplayType(lifecycleSetup); + auto& lifecycleReplay = startReplay(lifecycleSetup); + auto lifecycleObjects = observeReplayObjects(lifecycleSetup); + lifecycleReplay.advanceDirect(std::chrono::seconds(1)); + + lifecycleSetup.step(1); + EXPECT_TRUE(hasObjectNamed(lifecycleObjects.list, "obj1")); + + lifecycleReplay.advanceDirect(std::chrono::seconds(10)); + lifecycleSetup.step(1); + EXPECT_FALSE(hasObjectNamed(lifecycleObjects.list, "obj1")); +} + +/// @test +/// Applies payloads for existing objects and ignores the ones that arrive too late +/// requirements(SEN-364) +TEST(ReplayTest, PayloadApplication) +{ + ReplaySetup setup("test_payloads", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + auto obj2 = std::make_shared("obj2", sen::VarMap {}); + auto info2 = makeObjectInfo(obj2, "local", "replay_test"); + + auto t = makeTime(0); + output.creation(t, info1, true); + output.creation(t, info2, true); + + t += std::chrono::seconds(5); + output.propertyChange(t, obj1->getId(), firstPropertyId(*obj1), {}); + + t += std::chrono::seconds(5); + output.deletion(t, obj2->getId()); + + t += std::chrono::seconds(5); + output.propertyChange(t, obj2->getId(), firstPropertyId(*obj1), {}); + output.event(t, obj2->getId(), firstEventId(*obj1), {}); + + t += std::chrono::seconds(5); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + + replay.advanceDirect(std::chrono::seconds(40)); + setup.step(); + + EXPECT_EQ(replay.getNextStatus(), ReplayStatus::paused); +} + +/// @test +/// Rejects seeks that fall inside the time window but outside the keyframe index +/// requirements(SEN-364) +TEST(ReplayTest, SeekNoIndexThrows) +{ + ReplaySetup setup("test_no_index", + [](sen::db::Output& output, sen::kernel::TestKernel&) + { + auto obj1 = std::make_shared("obj1", sen::VarMap {}); + auto info1 = makeObjectInfo(obj1, "local", "replay_test"); + + auto t = makeTime(5); + output.creation(t, info1, false); + + t = makeTime(20); + output.deletion(t, obj1->getId()); + }); + + registerDummyReplayType(setup); + auto& replay = startReplay(setup); + + EXPECT_ANY_THROW(replay.seekDirect(makeTime(10))); +} + +} // namespace replayer::test diff --git a/components/replayer/test/replayed_object_test.cpp b/components/replayer/test/replayed_object_test.cpp new file mode 100644 index 00000000..62f6188b --- /dev/null +++ b/components/replayer/test/replayed_object_test.cpp @@ -0,0 +1,711 @@ +// === replayed_object_test.cpp ======================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// component +#include "replayed_object.h" +#include "replayed_object_proxy.h" +#include "replayer_test_helpers.h" + +// sen +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/memory_block.h" +#include "sen/core/base/numbers.h" +#include "sen/core/base/span.h" +#include "sen/core/base/timestamp.h" +#include "sen/core/io/buffer_writer.h" +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" +#include "sen/core/meta/class_type.h" +#include "sen/core/meta/event.h" +#include "sen/core/meta/native_types.h" +#include "sen/core/meta/property.h" +#include "sen/core/meta/sequence_traits.h" +#include "sen/core/meta/type.h" +#include "sen/core/meta/var.h" +#include "sen/core/obj/callback.h" +#include "sen/core/obj/connection_guard.h" +#include "sen/core/obj/detail/work_queue.h" +#include "sen/core/obj/native_object.h" +#include "sen/db/creation.h" +#include "sen/db/event.h" +#include "sen/db/input.h" +#include "sen/db/keyframe.h" +#include "sen/db/output.h" +#include "sen/db/property_change.h" +#include "sen/db/snapshot.h" +#include "sen/kernel/component.h" +#include "sen/kernel/component_api.h" +#include "sen/kernel/test_kernel.h" + +// generated code +#include "stl/sen/db/basic_types.stl.h" +#include "stl/sen/kernel/basic_types.stl.h" + +// google test +#include + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace replayer::test +{ + +class TestReplayedObject final: public sen::components::replayer::ReplayedObject +{ +public: + SEN_NOCOPY_NOMOVE(TestReplayedObject) + using ReplayedObject::ReplayedObject; + ~TestReplayedObject() override = default; + + using ReplayedObject::commit; + using ReplayedObject::senImplComputeMaxReliableSerializedPropertySizeImpl; + using ReplayedObject::senImplGetFieldValueGetter; + using ReplayedObject::senImplRemoveTypedConnection; + using ReplayedObject::senImplStreamCall; + using ReplayedObject::senImplVariantCall; + using ReplayedObject::senImplWriteAllPropertiesToStream; + using ReplayedObject::senImplWriteChangedPropertiesToStream; + using ReplayedObject::senImplWriteDynamicPropertiesToStream; + using ReplayedObject::senImplWriteStaticPropertiesToStream; +}; + +class TestReplayedObjectProxy final: public sen::components::replayer::ReplayedObjectProxy +{ +public: + SEN_NOCOPY_NOMOVE(TestReplayedObjectProxy) + TestReplayedObjectProxy(sen::components::replayer::ReplayedObject* owner, std::string_view localPrefix) + : ReplayedObjectProxy(owner, localPrefix) + { + eventQueue_.enable(); + + for (const auto& prop: owner->getAllProps()) + { + guards.push_back(onPropertyChangedUntyped( + prop.get(), + sen::EventCallback(&eventQueue_, + [this, id = prop->getId()](const sen::EventInfo&, const sen::VarList&) + { emittedEvents.push_back(id); }))); + } + } + + ~TestReplayedObjectProxy() override = default; + + using ReplayedObjectProxy::drainInputsImpl; + using ReplayedObjectProxy::senImplGetPropertyImpl; + using ReplayedObjectProxy::senImplRemoveTypedConnection; + using ReplayedObjectProxy::senImplWriteAllPropertiesToStream; + using ReplayedObjectProxy::senImplWriteDynamicPropertiesToStream; + using ReplayedObjectProxy::senImplWriteStaticPropertiesToStream; + + void executeCallbacks() + { + while (eventQueue_.executeAll()) + { + } + } + // intercept events for validation + std::vector emittedEvents; // NOLINT(misc-non-private-member-variables-in-classes) + std::vector guards; // NOLINT(misc-non-private-member-variables-in-classes) + +private: + sen::impl::WorkQueue eventQueue_ {256U, false}; +}; + +[[nodiscard]] const sen::Property* findPropertyByName(const TestReplayedObject& obj, std::string_view propertyName) +{ + const auto* property = obj.getClass().type()->searchPropertyByName(std::string(propertyName)); + if (!property) + { + throw std::runtime_error("findPropertyByName: property not found"); + } + return property; +} + +[[nodiscard]] const sen::Event* findEventByName(const TestReplayedObject& obj, std::string_view eventName) +{ + const auto* event = obj.getClass().type()->searchEventByName(std::string(eventName)); + if (!event) + { + throw std::runtime_error("findEventByName: event not found"); + } + return event; +} + +void executeQueue(sen::impl::WorkQueue& queue) +{ + while (queue.executeAll()) + { + } +} + +void executeObjectQueue(sen::NativeObject* object) +{ + auto* queue = sen::impl::getWorkQueue(object); + if (!queue) + { + throw std::runtime_error("executeObjectQueue: object queue is null"); + } + queue->enable(); + executeQueue(*queue); +} + +[[nodiscard]] std::shared_ptr makeObject(const sen::db::Snapshot& snapshot, + sen::TimeStamp timeStamp) +{ + return std::make_shared(snapshot, timeStamp); +} + +template +void injectFlushCommit(TestReplayedObject& object, sen::TimeStamp timeStamp, const T& entry) +{ + object.inject(timeStamp, entry); + object.flushPendingChanges(timeStamp); + object.commit(timeStamp); +} + +void flushCommit(TestReplayedObject& object, sen::TimeStamp timeStamp) +{ + object.flushPendingChanges(timeStamp); + object.commit(timeStamp); +} + +struct ReplayedObjectSetup +{ + TempDir tempDir; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr object; // NOLINT(misc-non-private-member-variables-in-classes) + sen::kernel::TestComponent component; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr kernel; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr input; // NOLINT(misc-non-private-member-variables-in-classes) + std::filesystem::path archivePath; // NOLINT(misc-non-private-member-variables-in-classes) + + template + void writePropertyChange(sen::db::Output& output, + sen::TimeStamp timeStamp, + std::string_view propertyName, + Writer&& writer) + { + const auto* property = object->getClass().type()->searchPropertyByName(std::string(propertyName)); + if (!property) + { + throw std::runtime_error("writePropertyChange: property not found"); + } + + ::sen::kernel::Buffer buffer; + sen::ResizableBufferWriter bufferWriter(buffer); + sen::OutputStream out(bufferWriter); + writer(out); + output.propertyChange(timeStamp, object->getId(), property->getId(), std::move(buffer)); + } + + template + void writeEvent(sen::db::Output& output, sen::TimeStamp timeStamp, std::string_view eventName, Writer&& writer) + { + const auto* event = object->getClass().type()->searchEventByName(std::string(eventName)); + if (!event) + { + throw std::runtime_error("writeEvent: event not found"); + } + + ::sen::kernel::Buffer buffer; + sen::ResizableBufferWriter bufferWriter(buffer); + sen::OutputStream out(bufferWriter); + writer(out); + output.event(timeStamp, object->getId(), event->getId(), std::move(buffer)); + } + + ReplayedObjectSetup() + { + // register an object + object = std::make_shared("dummyObj", sen::VarMap {}); + component.onInit( + [this](sen::kernel::InitApi&& api) -> sen::kernel::PassResult + { + auto source = api.getSource("local.test"); + source->add(object); + return sen::kernel::done(); + }); + component.onRun([](auto& api) { return api.execLoop(std::chrono::seconds(1), []() {}); }); + kernel = std::make_unique(&component); + kernel->step(); + + // write archive to disk + archivePath = makeArchivePath("replayed_obj_test", tempDir); + auto settings = makeArchiveSettings("replayed_obj_test", tempDir); + + { + sen::db::Output output(std::move(settings), []() {}); + + auto info = makeObjectInfo(object); + output.creation(kernel->getTime(), info, true); + + kernel->step(); + const auto entryTime = kernel->getTime(); + + writePropertyChange(output, + entryTime, + "testProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 12.3); }); + writePropertyChange( + output, entryTime, "uniProp", [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 1); }); + writePropertyChange(output, + entryTime, + "multiProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 2); }); + writePropertyChange(output, + entryTime, + "enumProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 1U); }); + writePropertyChange(output, + entryTime, + "distProp", + [](sen::OutputStream& out) { sen::SerializationTraits::write(out, 2.5F); }); + writePropertyChange(output, + entryTime, + "seqProp", + [](sen::OutputStream& out) + { + std::vector values {1, 2, 3}; + sen::SequenceTraitsBase>::write(out, values); + }); + writePropertyChange(output, entryTime, "emptyStructProp", [](sen::OutputStream& out) { out.writeUInt8(0U); }); + + writeEvent(output, + entryTime, + "testEvent", + [](sen::OutputStream& out) + { + sen::SerializationTraits::write(out, 99); + sen::SerializationTraits::write(out, "hello"); + }); + + kernel->step(); + output.keyframe(kernel->getTime(), {info}); + } + + // open the archive for reading + input = std::make_unique(archivePath.string(), kernel->getTypes()); + } + + [[nodiscard]] sen::db::Snapshot findCreationSnapshot() const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + ++cursor; + if (cursor.atEnd()) + { + break; + } + + const auto& entry = cursor.get(); + if (std::holds_alternative(entry.payload)) + { + return std::get(entry.payload).getSnapshot(); + } + } + throw std::runtime_error("no Creation entry found in the archive"); + } + + [[nodiscard]] sen::db::PropertyChange findPropertyChange() const { return findPropertyChangeNamed("testProp"); } + + [[nodiscard]] sen::db::PropertyChange findPropertyChangeNamed(const std::string& name) const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + if (std::holds_alternative(cursor.get().payload)) + { + const auto& pc = std::get(cursor.get().payload); + if (pc.getProperty()->getName() == name) + { + return pc; + } + } + ++cursor; + } + throw std::runtime_error("no PropertyChange entry found in the archive"); + } + + [[nodiscard]] sen::db::Event findEvent() const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + if (std::holds_alternative(cursor.get().payload)) + { + return std::get(cursor.get().payload); + } + ++cursor; + } + throw std::runtime_error("no Event entry found in the archive"); + } + + [[nodiscard]] sen::db::Snapshot findKeyframeSnapshot() const + { + auto cursor = input->begin(); + while (!cursor.atEnd()) + { + ++cursor; + if (cursor.atEnd()) + { + break; + } + + if (const auto& entry = cursor.get(); std::holds_alternative(entry.payload)) + { + if (const auto& kf = std::get(entry.payload); !kf.getSnapshots().empty()) + { + return kf.getSnapshots()[0]; + } + } + } + throw std::runtime_error("no Keyframe with snapshots found in the archive"); + } +}; + +/// @test +/// Builds a replayed object from a snapshot and preserves its initial state +/// requirements(SEN-364) +TEST(ReplayedObjectTest, ConstructionFromSnapshotSetsName) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + const auto t0 = makeTime(3); + auto obj = makeObject(snapshot, t0); + + EXPECT_EQ(obj->getName(), "dummyObj"); + ASSERT_NE(obj->getClass().type(), nullptr); + EXPECT_EQ(obj->getClass().type()->getQualifiedName(), snapshot.getType().type()->getQualifiedName()); + EXPECT_EQ(obj->getLastCommitTime(), t0); + EXPECT_FALSE(obj->getAllProps().empty()); + + const auto* prop = findPropertyByName(*obj, "testProp"); + EXPECT_FALSE(obj->getPropertyUntyped(prop).isEmpty()); + EXPECT_EQ(obj->getPropertyLastTime(prop), t0); +} + +/// @test +/// Applies injected changes only after flush and commit +/// requirements(SEN-364) +TEST(ReplayedObjectTest, FlushAndCommitAppliesPropertyChange) +{ + ReplayedObjectSetup setup; + auto creationSnapshot = setup.findCreationSnapshot(); + auto keyframeSnapshot = setup.findKeyframeSnapshot(); + const auto t0 = makeTime(1); + auto obj = makeObject(creationSnapshot, t0); + const auto* prop = findPropertyByName(*obj, "testProp"); + auto originalValue = obj->getPropertyUntyped(prop).getCopyAs(); + + auto propChange = setup.findPropertyChange(); + const auto t1 = makeTime(5); + obj->inject(t1, propChange); + EXPECT_EQ(obj->getPropertyUntyped(prop).getCopyAs(), originalValue); + + flushCommit(*obj, t1); + EXPECT_DOUBLE_EQ(obj->getPropertyUntyped(prop).getCopyAs(), 12.3); + EXPECT_EQ(obj->getPropertyLastTime(prop), t1); + + const auto t2 = makeTime(9); + injectFlushCommit(*obj, t2, keyframeSnapshot); + EXPECT_EQ(obj->getLastCommitTime(), t2); + + const auto t3 = makeTime(12); + injectFlushCommit(*obj, t3, propChange); + EXPECT_EQ(obj->getLastCommitTime(), t3); + EXPECT_EQ(obj->getPropertyLastTime(prop), t3); + EXPECT_DOUBLE_EQ(obj->getPropertyUntyped(prop).getCopyAs(), 12.3); +} + +/// @test +/// Rejects mutable operations on replayed objects +/// requirements(SEN-364) +TEST(ReplayedObjectTest, RemoveTypedConnectionThrowsLogicError) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + auto obj = makeObject(snapshot, makeTime(1)); + const auto* prop = findPropertyByName(*obj, "testProp"); + + EXPECT_THROW(obj->senImplRemoveTypedConnection(sen::ConnId {1}), std::logic_error); + EXPECT_ANY_THROW(std::ignore = obj->getNextPropertyUntyped(prop)); + EXPECT_ANY_THROW(obj->setNextPropertyUntyped(prop, sen::Var(1.0))); +} + +/// @test +/// Serializes replayed state and processes injected changes +/// requirements(SEN-364) +TEST(ReplayedObjectTest, SerializesStateAndProcessesInjectedChanges) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + const auto t0 = makeTime(1); + auto obj = makeObject(snapshot, t0); + + { + ::sen::kernel::Buffer allBuffer; + sen::ResizableBufferWriter writer(allBuffer); + sen::OutputStream out(writer); + obj->senImplWriteAllPropertiesToStream(out); + EXPECT_FALSE(allBuffer.empty()); + } + { + ::sen::kernel::Buffer staticBuffer; + sen::ResizableBufferWriter writer(staticBuffer); + sen::OutputStream out(writer); + obj->senImplWriteStaticPropertiesToStream(out); + EXPECT_TRUE(staticBuffer.empty()); + } + { + ::sen::kernel::Buffer dynamicBuffer; + sen::ResizableBufferWriter writer(dynamicBuffer); + sen::OutputStream out(writer); + obj->senImplWriteDynamicPropertiesToStream(out); + EXPECT_FALSE(dynamicBuffer.empty()); + } + + auto propChange = setup.findPropertyChangeNamed("testProp"); + const auto t1 = makeTime(4); + injectFlushCommit(*obj, t1, propChange); + auto* prop = findPropertyByName(*obj, "testProp"); + EXPECT_DOUBLE_EQ(obj->getPropertyUntyped(prop).getCopyAs(), 12.3); + + // Fresh object to isolate serialization state. + auto serializationObj = makeObject(snapshot, t0); + + auto uniChange = setup.findPropertyChangeNamed("uniProp"); + auto multiChange = setup.findPropertyChangeNamed("multiProp"); + serializationObj->inject(t1, propChange); + serializationObj->inject(t1, uniChange); + serializationObj->inject(t1, multiChange); + serializationObj->flushPendingChanges(t1); + + auto maxSize = serializationObj->senImplComputeMaxReliableSerializedPropertySizeImpl(); + EXPECT_GE(maxSize, 0U); + + auto pool = sen::FixedMemoryBlockPool<1024>::make(); + auto uniBlock = pool->getBlockPtr(); + auto multiBlock = pool->getBlockPtr(); + uint32_t uniCalls = 0; + uint32_t multiCalls = 0; + auto uniProvider = [&](uint32_t size) + { + ++uniCalls; + uniBlock->resize(size); + return sen::ResizableBufferWriter(*uniBlock); + }; + auto multiProvider = [&](uint32_t size) + { + ++multiCalls; + multiBlock->resize(size); + return sen::ResizableBufferWriter(*multiBlock); + }; + + std::vector confirmedBuf; + sen::ResizableBufferWriter confirmedWriter(confirmedBuf); + sen::OutputStream confirmedOut(confirmedWriter); + serializationObj->senImplWriteChangedPropertiesToStream(confirmedOut, uniProvider, multiProvider); + + const auto serializedDestinations = uniCalls + multiCalls + static_cast(!confirmedBuf.empty()); + EXPECT_GE(serializedDestinations, 1U); + + auto* objQueue = sen::impl::getWorkQueue(obj.get()); + ASSERT_NE(objQueue, nullptr); + objQueue->enable(); + + auto evt = setup.findEvent(); + const auto t2 = makeTime(7); + obj->inject(t2, evt); + obj->flushPendingChanges(t2); + executeQueue(*objQueue); + objQueue->clear(); + objQueue->disable(); +} + +/// @test +/// Allows getter calls and rejects unknown method ids +/// requirements(SEN-364) +TEST(ReplayedObjectTest, StreamCallToGetterSucceeds) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + auto obj = makeObject(snapshot, makeTime(1)); + const auto* prop = findPropertyByName(*obj, "testProp"); + const auto methodId = prop->getGetterMethod().getId(); + + std::vector emptyBuf; + sen::InputStream in(emptyBuf); + + bool streamCallbackInvoked = false; + obj->senImplStreamCall(methodId, + in, + [&streamCallbackInvoked](sen::StreamCall&& streamWriter) + { + ::sen::kernel::Buffer resultBuf; + sen::ResizableBufferWriter writer(resultBuf); + sen::OutputStream out(writer); + streamWriter(out); + streamCallbackInvoked = true; + }); + EXPECT_TRUE(streamCallbackInvoked); + + bool variantCallbackInvoked = false; + sen::Var result; + obj->senImplVariantCall(methodId, + sen::VarList {}, + [&variantCallbackInvoked, &result](sen::VariantCall&& variantWriter) + { + variantWriter(result); + variantCallbackInvoked = true; + }); + EXPECT_TRUE(variantCallbackInvoked); + EXPECT_FALSE(result.isEmpty()); + + sen::MemberHash wrongStreamId(1234); + sen::MemberHash wrongVariantId(4321); + EXPECT_ANY_THROW(obj->senImplStreamCall(wrongStreamId, in, [](sen::StreamCall&&) {})); + EXPECT_ANY_THROW(obj->senImplVariantCall(wrongVariantId, sen::VarList {}, [](sen::VariantCall&&) {})); +} + +/// @test +/// Resolves supported field getters and rejects invalid paths +/// requirements(SEN-364) +TEST(ReplayedObjectTest, FieldGetterHandlesSupportedAndInvalidPaths) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findKeyframeSnapshot(); + auto obj = makeObject(snapshot, makeTime(1)); + + auto expectFieldGetterWorks = + [&](const std::string& name, std::vector path = {}, bool runtimeErrorIsExpected = false) + { + const auto* property = obj->getClass().type()->searchPropertyByName(name); + ASSERT_NE(property, nullptr); + try + { + auto result = obj->senImplGetFieldValueGetter(property->getId(), sen::Span(path.data(), path.size())); + EXPECT_TRUE(result.getterFunc != nullptr); + } + catch (const std::runtime_error& err) + { + if (runtimeErrorIsExpected) + { + SUCCEED(); + } + else + { + ADD_FAILURE() << "Unexpected runtime_error in expectFieldGetterWorks(" << name << "): " << err.what(); + } + } + }; + + expectFieldGetterWorks("bProp"); + expectFieldGetterWorks("u8Prop"); + expectFieldGetterWorks("i16Prop"); + expectFieldGetterWorks("u16Prop"); + expectFieldGetterWorks("i32Prop"); + expectFieldGetterWorks("u32Prop"); + expectFieldGetterWorks("i64Prop"); + expectFieldGetterWorks("u64Prop"); + expectFieldGetterWorks("f32Prop"); + expectFieldGetterWorks("testProp"); + expectFieldGetterWorks("strProp"); + expectFieldGetterWorks("durProp"); + expectFieldGetterWorks("timeProp"); + expectFieldGetterWorks("enumProp", {}, true); + expectFieldGetterWorks("distProp", {}, true); + expectFieldGetterWorks("structProp", {0}); + expectFieldGetterWorks("structProp", {1, 0}); + + const auto* variantProp = findPropertyByName(*obj, "variantProp"); + auto canReadVariantField = [&](uint16_t index) + { + std::vector path {index}; + try + { + auto result = + obj->senImplGetFieldValueGetter(variantProp->getId(), sen::Span(path.data(), path.size())); + return result.getterFunc != nullptr; + } + catch (...) + { + return false; + } + }; + EXPECT_TRUE(canReadVariantField(0) || canReadVariantField(1)); + + EXPECT_ANY_THROW(static_cast( + obj->senImplGetFieldValueGetter(findPropertyByName(*obj, "seqProp")->getId(), sen::Span()))); + + std::vector badStructPath {99U}; + EXPECT_ANY_THROW(static_cast(obj->senImplGetFieldValueGetter( + findPropertyByName(*obj, "structProp")->getId(), sen::Span(badStructPath.data(), badStructPath.size())))); + + std::vector badVariantPath {99U}; + EXPECT_ANY_THROW(static_cast( + obj->senImplGetFieldValueGetter(findPropertyByName(*obj, "variantProp")->getId(), + sen::Span(badVariantPath.data(), badVariantPath.size())))); + + std::vector emptyStructPath {0U}; + EXPECT_ANY_THROW(static_cast( + obj->senImplGetFieldValueGetter(findPropertyByName(*obj, "emptyStructProp")->getId(), + sen::Span(emptyStructPath.data(), emptyStructPath.size())))); +} + +/// @test +/// Proxy mirrors committed changes and blocks invalid native operations +/// requirements(SEN-364) +TEST(ReplayedObjectTest, ProxyDrainInputsDetectsChanges) +{ + ReplayedObjectSetup setup; + auto snapshot = setup.findCreationSnapshot(); + const auto t0 = makeTime(1); + auto obj = makeObject(snapshot, t0); + TestReplayedObjectProxy proxy(obj.get(), "local"); + + const auto* prop = findPropertyByName(*obj, "testProp"); + auto val = proxy.senImplGetPropertyImpl(prop->getId()); + EXPECT_FALSE(val.isEmpty()); + EXPECT_EQ(val.getCopyAs(), obj->getPropertyUntyped(prop).getCopyAs()); + + proxy.drainInputsImpl(t0); + proxy.executeCallbacks(); + EXPECT_TRUE(proxy.emittedEvents.empty()); + + auto propChange = setup.findPropertyChangeNamed("testProp"); + const auto t1 = makeTime(5); + injectFlushCommit(*obj, t1, propChange); + + proxy.drainInputsImpl(t1); + proxy.executeCallbacks(); + ASSERT_EQ(proxy.emittedEvents.size(), 1); + EXPECT_EQ(proxy.emittedEvents.front(), prop->getId()); + + proxy.emittedEvents.clear(); + proxy.drainInputsImpl(t1); + proxy.executeCallbacks(); + EXPECT_TRUE(proxy.emittedEvents.empty()); + + std::vector buf; + sen::ResizableBufferWriter writer(buf); + sen::OutputStream out(writer); + EXPECT_THROW(proxy.senImplWriteAllPropertiesToStream(out), std::logic_error); + EXPECT_THROW(proxy.senImplWriteStaticPropertiesToStream(out), std::logic_error); + EXPECT_THROW(proxy.senImplWriteDynamicPropertiesToStream(out), std::logic_error); + EXPECT_THROW(proxy.senImplRemoveTypedConnection(sen::ConnId {1}), std::logic_error); +} + +} // namespace replayer::test diff --git a/components/replayer/test/replayer_test.cpp b/components/replayer/test/replayer_test.cpp new file mode 100644 index 00000000..5c4dc5d2 --- /dev/null +++ b/components/replayer/test/replayer_test.cpp @@ -0,0 +1,218 @@ +// === replayer_test.cpp =============================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// components +#include "replayer.h" +#include "replayer_test_helpers.h" + +// sen +#include "sen/core/obj/object_source.h" +#include "sen/db/output.h" +#include "sen/kernel/component.h" +#include "sen/kernel/component_api.h" +#include "sen/kernel/test_kernel.h" + +// generated code +#include "stl/sen/db/basic_types.stl.h" + +// google test +#include + +// std +#include +#include +#include +#include +#include +#include + +namespace replayer::test +{ + +class TestReplayerImpl final: public sen::components::replayer::ReplayerImpl +{ +public: + using ReplayerImpl::ReplayerImpl; + + TestReplayerImpl(const TestReplayerImpl&) = delete; + TestReplayerImpl& operator=(const TestReplayerImpl&) = delete; + TestReplayerImpl(TestReplayerImpl&&) = delete; + TestReplayerImpl& operator=(TestReplayerImpl&&) = delete; + ~TestReplayerImpl() override = default; + + void openDirect(const std::string& name, const std::string& path) { openImpl(name, path); } + void closeDirect(const std::string& name) { closeImpl(name); } + void closeAllDirect() { closeAllImpl(); } +}; + +struct ReplayerSetup +{ + TempDir tempDir; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr controlBus; // NOLINT(misc-non-private-member-variables-in-classes) + std::shared_ptr replayer; // NOLINT(misc-non-private-member-variables-in-classes) + sen::kernel::TestComponent component; // NOLINT(misc-non-private-member-variables-in-classes) + std::unique_ptr kernel; // NOLINT(misc-non-private-member-variables-in-classes) + + explicit ReplayerSetup(bool autoPlay = false) + { + component.onInit( + [this](sen::kernel::InitApi&& api) -> sen::kernel::PassResult + { + controlBus = api.getSource("local.replayer"); + return sen::kernel::done(); + }); + + component.onRun( + [this, autoPlay](sen::kernel::RunApi& api) + { + replayer = std::make_shared("test_replayer", autoPlay, controlBus, api); + controlBus->add(replayer); + return api.execLoop(std::chrono::seconds(1), []() {}); + }); + + kernel = std::make_unique(&component); + } + + ReplayerSetup(const ReplayerSetup&) = delete; + ReplayerSetup& operator=(const ReplayerSetup&) = delete; + ReplayerSetup(ReplayerSetup&&) = delete; + ReplayerSetup& operator=(ReplayerSetup&&) = delete; + + ~ReplayerSetup() + { + if (controlBus != nullptr && replayer != nullptr) + { + replayer->closeAllDirect(); + controlBus->remove(replayer); + step(2); + replayer.reset(); + } + controlBus.reset(); + kernel.reset(); + } + + void step(std::size_t count = 1) const { kernel->step(count); } + + [[nodiscard]] std::string createValidArchive(const std::string& name) const + { + const auto fullPath = makeArchivePath(name, tempDir); + auto settings = makeArchiveSettings(name, tempDir); + + sen::db::Output output(std::move(settings), []() {}); + output.keyframe(kernel->getTime(), {}); + + return fullPath.string(); + } +}; + +/// @test +/// Opening a non-exist recording throws error +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningNonExistentRecordingThrows) +{ + ReplayerSetup setup; + setup.step(); + + ASSERT_NE(setup.replayer, nullptr); + + // try to open a non-existing path + const std::string fakePath = (setup.tempDir.path() / "missing_dir").string(); + + EXPECT_ANY_THROW(setup.replayer->openDirect("bad_session", fakePath)); +} + +/// @test +/// Opening a valid recording succeeds +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningValidRecordingSucceeds) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath = setup.createValidArchive("valid_rec"); + + EXPECT_NO_THROW(setup.replayer->openDirect("session_1", archivePath)); + EXPECT_NO_THROW(setup.replayer->closeDirect("session_1")); +} + +/// @test +/// Opening with a duplicate name throws an error +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningWithDuplicateNameThrows) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath1 = setup.createValidArchive("valid_rec1"); + const std::string archivePath2 = setup.createValidArchive("valid_rec2"); + + setup.replayer->openDirect("session_same", archivePath1); + EXPECT_ANY_THROW(setup.replayer->openDirect("session_same", archivePath2)); +} + +/// @test +/// Opening the same archive path twice throws an error +/// requirements(SEN-364) +TEST(ReplayerTest, OpeningWithDuplicatePathThrows) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath = setup.createValidArchive("valid_rec"); + + setup.replayer->openDirect("session_1", archivePath); + EXPECT_ANY_THROW(setup.replayer->openDirect("session_2", archivePath)); +} + +/// @test +/// Closing a non-existent replay throws an error +/// requirements(SEN-364) +TEST(ReplayerTest, ClosingNonExistentReplayThrows) +{ + ReplayerSetup setup; + setup.step(); + + EXPECT_ANY_THROW(setup.replayer->closeDirect("does_not_exist")); +} + +/// @test +/// CloseAll completely clears all managed replays +/// requirements(SEN-364) +TEST(ReplayerTest, CloseAllClearsReplays) +{ + ReplayerSetup setup; + setup.step(); + + const std::string archivePath1 = setup.createValidArchive("valid_rec1"); + const std::string archivePath2 = setup.createValidArchive("valid_rec2"); + + setup.replayer->openDirect("session_1", archivePath1); + setup.replayer->openDirect("session_2", archivePath2); + + setup.replayer->closeAllDirect(); + + // Now they can be reopened because they were cleared, and trying to close throws + EXPECT_ANY_THROW(setup.replayer->closeDirect("session_1")); + EXPECT_ANY_THROW(setup.replayer->closeDirect("session_2")); + + // Adding them again should not throw duplicate path/name errors + EXPECT_NO_THROW(setup.replayer->openDirect("session_1", archivePath1)); +} + +/// @test +/// AutoPlay configuration does not crash when opening an archive +/// requirements(SEN-364) +TEST(ReplayerTest, AutoPlayDoesNotCrash) +{ + ReplayerSetup setup(true); + setup.step(); + + const std::string archivePath = setup.createValidArchive("auto_rec"); + EXPECT_NO_THROW(setup.replayer->openDirect("auto_session", archivePath)); +} + +} // namespace replayer::test diff --git a/components/replayer/test/replayer_test_helpers.h b/components/replayer/test/replayer_test_helpers.h new file mode 100644 index 00000000..8853a77e --- /dev/null +++ b/components/replayer/test/replayer_test_helpers.h @@ -0,0 +1,41 @@ +// === replayer_test_helpers.h ========================================================================================= +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +#ifndef SEN_COMPONENTS_REPLAYER_TEST_REPLAYER_TEST_HELPERS_H +#define SEN_COMPONENTS_REPLAYER_TEST_REPLAYER_TEST_HELPERS_H + +// shared test helpers +#include "archive_test_helpers.h" + +// generated code +#include "stl/replay_test_class.stl.h" + +// sen +#include "sen/core/base/compiler_macros.h" + +namespace replayer::test +{ + +using sen::test::firstEventId; +using sen::test::firstPropertyId; +using sen::test::makeArchivePath; +using sen::test::makeArchiveSettings; +using sen::test::makeObjectInfo; +using sen::test::makeTime; +using sen::test::TempDir; + +class DummyReplayObjImpl: public replayer_test::DummyReplayObjBase +{ +public: + SEN_NOCOPY_NOMOVE(DummyReplayObjImpl) + using DummyReplayObjBase::DummyReplayObjBase; + ~DummyReplayObjImpl() override = default; +}; + +} // namespace replayer::test + +#endif // SEN_COMPONENTS_REPLAYER_TEST_REPLAYER_TEST_HELPERS_H diff --git a/components/replayer/test/stl/replay_test_class.stl b/components/replayer/test/stl/replay_test_class.stl new file mode 100644 index 00000000..92c08630 --- /dev/null +++ b/components/replayer/test/stl/replay_test_class.stl @@ -0,0 +1,59 @@ +package replayer_test; + +struct NestedStruct +{ + val : i32 +} + +struct TestStruct +{ + f1 : i32, + f2 : NestedStruct +} + +variant TestVariant +{ + i32, + string +} + +enum TestEnum: u8 +{ + e0, + e1 +} + +quantity TestDistance; +sequence IntSequence; + +struct EmptyStruct {} + +// Dummy class to inject properties +class DummyReplayObj +{ + var testProp : f64 [confirmed]; + var bProp : bool; + var u8Prop : u8; + var i16Prop : i16; + var u16Prop : u16; + var i32Prop : i32; + var u32Prop : u32; + var i64Prop : i64; + var u64Prop : u64; + var f32Prop : f32; + var strProp : string [confirmed]; + var durProp : Duration; + var timeProp : TimeStamp; + var enumProp : TestEnum [confirmed]; + var distProp : TestDistance [confirmed]; + var seqProp : IntSequence [confirmed]; + var emptyStructProp : EmptyStruct [confirmed]; + + var structProp : TestStruct; + var variantProp : TestVariant [confirmed]; + + var uniProp : i32 [bestEffort]; + var multiProp : i32; + + event testEvent(v1: i32, v2: string) [confirmed]; +} diff --git a/components/rest/src/response_adapter.cpp b/components/rest/src/response_adapter.cpp index 2f8a8416..c390c855 100644 --- a/components/rest/src/response_adapter.cpp +++ b/components/rest/src/response_adapter.cpp @@ -83,7 +83,7 @@ void adaptForJsonResponse(sen::Var& var, const sen::Type* type) switch (var_.getCopyAs()) { case 0: - var_ = "event"; + var_ = "evt"; break; case 1: diff --git a/components/rest/src/sen_router.cpp b/components/rest/src/sen_router.cpp index a4ebf685..9e580962 100644 --- a/components/rest/src/sen_router.cpp +++ b/components/rest/src/sen_router.cpp @@ -17,6 +17,7 @@ #include "locators.h" #include "notification_loop.h" #include "object_interests_manager.h" +#include "response_adapter.h" #include "types.h" #include "utils.h" @@ -31,6 +32,7 @@ #include "sen/core/meta/callable.h" #include "sen/core/meta/event.h" #include "sen/core/meta/method.h" +#include "sen/core/meta/property.h" #include "sen/core/meta/var.h" #include "sen/core/obj/callback.h" #include "sen/core/obj/object.h" @@ -156,18 +158,9 @@ std::shared_ptr SenRouter::getObject(const InterestSubscription& in return *objectIt; } -std::optional SenRouter::getObjectDefinition(const InterestSubscription& interestSubscription, - const std::string& urlPath, - const std::string& objectName) const +Object SenRouter::getObjectDefinition(const std::string& urlPath, const sen::Object& object) const { - auto object = getObject(interestSubscription, objectName); - if (!object) - { - return std::nullopt; - } - - auto objectClass = object->getClass(); - + auto objectClass = object.getClass(); Links links; for (const auto& method: objectClass->getMethods(sen::ClassType::SearchMode::includeParents)) { @@ -179,13 +172,18 @@ std::optional SenRouter::getObjectDefinition(const InterestSubscription& for (const auto& prop: objectClass->getProperties(sen::ClassType::SearchMode::includeParents)) { // prop getters - const std::string getterRelUrl = urlPath + "/methods/" + std::string(prop->getGetterMethod().getName()); + const auto& getter = prop->getGetterMethod(); + std::string getterRelUrl = urlPath + "/methods/" + std::string(getter.getName()); links.emplace_back(Link {RelType::getter, getterRelUrl + "/invoke", HttpMethod::httpPost}); + links.emplace_back(Link {RelType::def, std::move(getterRelUrl), HttpMethod::httpGet}); // prop setters - const std::string setterRelUrl = urlPath + "/methods/" + std::string(prop->getSetterMethod().getName()); - links.emplace_back(Link {RelType::setter, setterRelUrl + "/invoke", HttpMethod::httpPost}); - links.emplace_back(Link {RelType::def, setterRelUrl, HttpMethod::httpGet}); + if (const auto& setter = prop->getSetterMethod(); prop->getCategory() == PropertyCategory::dynamicRW) + { + std::string setterRelUrl = urlPath + "/methods/" + std::string(setter.getName()); + links.emplace_back(Link {RelType::setter, setterRelUrl + "/invoke", HttpMethod::httpPost}); + links.emplace_back(Link {RelType::def, std::move(setterRelUrl), HttpMethod::httpGet}); + } // prop change subscription const std::string propRelUrlEvent = urlPath + "/properties/" + std::string(prop->getName()); @@ -202,10 +200,10 @@ std::optional SenRouter::getObjectDefinition(const InterestSubscription& } return Object { - object->getId().get(), - object->getName(), + object.getId().get(), + object.getName(), std::string(objectClass->getQualifiedName()), - object->getLocalName(), + object.getLocalName(), std::string(objectClass->getDescription()), links, }; @@ -222,17 +220,8 @@ std::optional SenRouter::getMethodDefinition(const InterestSubscri auto objectClass = object->getClass(); - const auto& methods = objectClass->getMethods(sen::ClassType::SearchMode::includeParents); - const auto methodIt = std::find_if(methods.cbegin(), - methods.cend(), - [&methodLocator](const std::shared_ptr methodPtr) - { return methodPtr && methodPtr->getName() == methodLocator.method(); }); - if (methodIt == methods.cend()) - { - return std::nullopt; - } - - auto method = methodIt->get(); + const auto method = + objectClass->searchMethodByName(methodLocator.method(), sen::ClassType::SearchMode::includeParents); if (!method) { return std::nullopt; @@ -381,7 +370,7 @@ SenRouter::SenRouter(kernel::RunApi& api): api_(api) addStreamRoute( HttpMethod::httpGet, "/api/sse", bindAuthStreamRouteCallback(this, &SenRouter::getNotificationsHandler)); - // types instrospection + // types introspection addRoute(HttpMethod::httpGet, "/api/types/:type", bindAuthRouteCallback(this, &SenRouter::getTypeIntrospection)); } @@ -494,10 +483,10 @@ JsonResponse SenRouter::createInterestHandler(ClientSession& clientSession, { logClientSession(clientSession, "createInterest"); - const auto payload = Json::parse(httpSession.getRequest().body()); - try { + const auto payload = Json::parse(httpSession.getRequest().body()); + Interest interest; interest.query = payload.at("query").get(); interest.name = payload.at("name").get(); @@ -614,13 +603,32 @@ JsonResponse SenRouter::getObjectHandler(ClientSession& clientSession, } const auto& interestSubscription = interestSubscriptionRes.getValue(); - auto object = getObjectDefinition(interestSubscription, httpSession.getRequest().path(), urlParams[1]); - if (!object) + auto objectReference = getObject(interestSubscription, urlParams[1]); + + if (!objectReference) { return getErrorNotFound(); } - return JsonResponse {httpSuccess, *object}; + auto object = getObjectDefinition(httpSession.getRequest().path(), *objectReference); + auto var = sen::toVariant(object); + adaptForJsonResponse(var, sen::MetaTypeTrait::meta().type()); + auto jsonObject = nlohmann::json::parse(toJson(var)); + + if (!queryParams.empty() && queryParams.find("includeValues")->second == "true") + { + nlohmann::json values; + + for (const auto& prop: objectReference->getClass()->getProperties(sen::ClassType::SearchMode::includeParents)) + { + auto value = objectReference->getPropertyUntyped(prop.get()); + values[prop->getName()] = nlohmann::json::parse(toJson(value)); + } + + jsonObject["properties"] = values; + } + + return JsonResponse {httpSuccess, jsonObject.dump()}; } JsonResponse SenRouter::getPropertyHandler(ClientSession& clientSession, @@ -843,7 +851,8 @@ JsonResponse SenRouter::invokeMethodHandler(ClientSession& clientSession, return getErrorNotFound(); } - const sen::Method* method = object->getClass()->searchMethodByName(locator.method()); + const sen::Method* method = + object->getClass()->searchMethodByName(locator.method(), sen::ClassType::SearchMode::includeParents); if (!method) { return JsonResponse(httpNotFoundError, Error {"method not found"}); diff --git a/components/rest/src/sen_router.h b/components/rest/src/sen_router.h index 0eb9f244..a73ebe60 100644 --- a/components/rest/src/sen_router.h +++ b/components/rest/src/sen_router.h @@ -185,9 +185,7 @@ class SenRouter: public BaseRouter [[nodiscard]] std::shared_ptr getObject(const InterestSubscription& interestSubscription, const std::string& objectName) const; - [[nodiscard]] std::optional getObjectDefinition(const InterestSubscription& interestSubscription, - const std::string& urlPath, - const std::string& objectName) const; + [[nodiscard]] Object getObjectDefinition(const std::string& urlPath, const sen::Object& object) const; [[nodiscard]] std::optional getMethodDefinition(const InterestSubscription& interestSubscription, const MethodLocator& methodLocator) const; [[nodiscard]] std::optional getEventDefinition(const InterestSubscription& interestSubscription, diff --git a/components/rest/stl/types.stl b/components/rest/stl/types.stl index 542dae00..f11b0ae9 100644 --- a/components/rest/stl/types.stl +++ b/components/rest/stl/types.stl @@ -86,8 +86,8 @@ struct ObjectSummary { objectId : u32, name : string, - classname : string, - localname : string, + className : string, + localName : string, link : Link } @@ -127,8 +127,8 @@ struct Object { objectId : u32, name : string, - classname : string, - localname : string, + className : string, + localName : string, description : string, links : Links } diff --git a/components/rest/test/e2e_test.cpp b/components/rest/test/e2e_test.cpp index f7a984dd..20fc8bbc 100644 --- a/components/rest/test/e2e_test.cpp +++ b/components/rest/test/e2e_test.cpp @@ -233,6 +233,20 @@ TEST(Rest, e2e_create_interest_invalid_query) ASSERT_EQ(ret.statusCode, 400); } +/// @test +/// End-to-end test for malformed request to create interest +/// @requirements(SEN-1061) +TEST(Rest, e2e_create_interest_malformed_request) +{ + Server server; + + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + auto ret = request(HttpMethod::httpPost, "127.0.0.1", "12345", "/api/interests", std::nullopt, token.value()); + ASSERT_EQ(ret.statusCode, 400); +} + /// @test /// End-to-end test for getting an existing interest /// @requirements(SEN-1061) @@ -428,11 +442,11 @@ TEST(Rest, e2e_get_existing_object) auto interests = Json::parse(ret.body); ASSERT_TRUE(interests.is_object()); - ASSERT_EQ(interests["localname"], "rest.local.kernel.api"); + ASSERT_EQ(interests["localName"], "rest.local.kernel.api"); } /// @test -/// End-to-end test for getting an non-existing object +/// End-to-end test for getting a non-existing object /// @requirements(SEN-1061) TEST(Rest, e2e_get_non_existing_object) { @@ -461,6 +475,77 @@ TEST(Rest, e2e_get_non_existing_object) ASSERT_EQ(ret.statusCode, 404); } +/// @test +/// End-to-end test getting existing object with its properties when optional query param is true +TEST(Rest, e2e_get_existing_object_including_properties) +{ + Server server; + + // Authenticate + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + // Create interest + auto createRet = request(HttpMethod::httpPost, + "127.0.0.1", + "12345", + "/api/interests", + Json {{"name", "test_interest"}, {"query", "SELECT * FROM local.kernel"}}, + token.value()); + ASSERT_EQ(createRet.statusCode, 200); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Try to get an object properties + auto ret = request(HttpMethod::httpGet, + "127.0.0.1", + "12345", + "/api/interests/test_interest/objects/api?includeValues=true", + Json(), + token.value()); + ASSERT_EQ(ret.statusCode, 200); + + auto interests = Json::parse(ret.body); + ASSERT_TRUE(interests.is_object()); + ASSERT_TRUE(interests.contains("properties")); + ASSERT_TRUE(interests["properties"].is_object()); +} + +/// @test +/// End-to-end test getting existing object without its properties when optional query param is not true +TEST(Rest, e2e_get_existing_object_not_including_properties) +{ + Server server; + + // Authenticate + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + // Create interest + auto createRet = request(HttpMethod::httpPost, + "127.0.0.1", + "12345", + "/api/interests", + Json {{"name", "test_interest"}, {"query", "SELECT * FROM local.kernel"}}, + token.value()); + ASSERT_EQ(createRet.statusCode, 200); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Try to not get an object properties + auto ret = request(HttpMethod::httpGet, + "127.0.0.1", + "12345", + "/api/interests/test_interest/objects/api?includeValues=false", + Json(), + token.value()); + ASSERT_EQ(ret.statusCode, 200); + + auto interests = Json::parse(ret.body); + ASSERT_TRUE(interests.is_object()); + ASSERT_FALSE(interests.contains("properties")); +} + /// @test /// End-to-end test for getting a method definition /// @requirements(SEN-1061) @@ -499,6 +584,51 @@ TEST(Rest, e2e_get_method_definition) ASSERT_EQ(res["name"], "shutdown"); } +/// @test +/// End-to-end test to verify all returned definition links are accessible +/// @requirements(SEN-1061) +TEST(Rest, e2e_get_all_method_definitions) +{ + Server server; + + // Authenticate + auto token = authenticate(); + ASSERT_TRUE(token.has_value()); + + // Create interest + auto createRet = request(HttpMethod::httpPost, + "127.0.0.1", + "12345", + "/api/interests", + Json {{"name", "test_interest"}, {"query", "SELECT * FROM local.kernel"}}, + token.value()); + ASSERT_EQ(createRet.statusCode, 200); + + // Retrieve object definition + HttpResponse ret = retryUntil( + 200, + [&token]() + { + return request( + HttpMethod::httpGet, "127.0.0.1", "12345", "/api/interests/test_interest/objects/api", Json(), token.value()); + }); + ASSERT_EQ(ret.statusCode, 200); + + auto res = Json::parse(ret.body); + ASSERT_TRUE(res.is_object()); + + // Walk all definition links + for (const auto& link: res["links"]) + { + if (link["rel"] != "def") + { + continue; + } + auto defRet = request(HttpMethod::httpGet, "127.0.0.1", "12345", link["href"], Json(), token.value()); + ASSERT_EQ(defRet.statusCode, 200); + } +} + /// @test /// End-to-end test for getting property definition /// @requirements(SEN-1061) diff --git a/components/rest/test/request.cpp b/components/rest/test/request.cpp index 13b76642..ebbb61a9 100644 --- a/components/rest/test/request.cpp +++ b/components/rest/test/request.cpp @@ -41,12 +41,11 @@ HttpResponse request(const HttpMethod& method, const std::string& host, const std::string& port, const std::string& path, - const Json& data, + const std::optional data, const std::string& token, bool isSSE) { HttpResponse result {0, ""}; - asio::io_context context; asio::ip::tcp::resolver resolver(context); asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, port); @@ -61,8 +60,11 @@ HttpResponse request(const HttpMethod& method, { case HttpMethod::httpPost: request = "POST"; - payload = data.dump(); - payloadSize = payload.size(); + if (data.has_value()) + { + payload = data->dump(); + payloadSize = payload.size(); + } break; case HttpMethod::httpDelete: request = "DELETE"; diff --git a/components/rest/test/request.h b/components/rest/test/request.h index 333445ea..2215cce2 100644 --- a/components/rest/test/request.h +++ b/components/rest/test/request.h @@ -31,7 +31,7 @@ HttpResponse request(const HttpMethod& method, const std::string& host, const std::string& port, const std::string& path, - const Json& data = Json(), + const std::optional data = Json(), const std::string& token = "", bool isSSE = false); diff --git a/conanfile.py b/conanfile.py index 48457971..5362c608 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,16 +4,21 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to define how to setup conan packages for Sen.""" + +from os import getenv +from os.path import isdir, join from conan import ConanFile -from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout +from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout from conan.tools.files import copy from conan.tools.scm.git import Git -from conan.tools.system.package_manager import Apt, Yum, Dnf, Zypper -from os.path import isdir, join -from os import getenv +from conan.tools.system.package_manager import Apt, Dnf, Yum, Zypper + class SenConan(ConanFile): + """Conan file that specifies how Sen can be build and packaged with conan.""" + name = "sen" author = "Enrique Parodi Spalazzi (enrique.parodi@airbus.com)" # Main PoC url = "https://github.com/airbus/sen" @@ -26,11 +31,13 @@ class SenConan(ConanFile): settings = "os", "compiler", "build_type", "arch" def build_requirements(self): + """Defines the dependencies only need for building of Sen.""" self.tool_requires("cmake/3.28.1") self.tool_requires("ninja/1.13.2") self.test_requires("gtest/1.17.0") def requirements(self): + """Defines the dependencies of Sen.""" # non-visible dependencies self.requires("asio/1.36.0", visible=False) self.requires("cli11/2.3.2", visible=False) @@ -57,11 +64,14 @@ def requirements(self): # our usage of imgui in Linux has an implicit dependency on SDL. # SDL has a missing dependency on libext-dev, so we need to install it. if self.settings.os == "Linux": - self.requires("sdl/2.24.0", - options={"alsa": False, "pulse": False, "shared": True, "wayland": False, "libunwind": False}, - visible=False) + self.requires( + "sdl/2.24.0", + options={"alsa": False, "pulse": False, "shared": True, "wayland": False, "libunwind": False}, + visible=False, + ) def system_requirements(self): + """Defines the system requirements of Sen.""" # our usage of imgui on Linux has an implicit dependency on SDL, # which itself requires libXext. Install it via the system package manager. if self.settings.os == "Linux": @@ -71,9 +81,22 @@ def system_requirements(self): Zypper(self).install(["libxext-devel"], update=True) # opensuse, sles, ... def export_sources(self): + """Defines the set of files that should be exported for building Sen.""" # Sources are located in the same place as this recipe, copy them to the recipe - include_patterns = ["apps/*", "cmake/*", "components/*", "examples/*", "libs/*", "test/*", - "CMakeLists.txt", ".clang-tidy", ".clang-format", "LICENSE.txt", "util/*", "resources/*"] + include_patterns = [ + "apps/*", + "cmake/*", + "components/*", + "examples/*", + "libs/*", + "test/*", + "CMakeLists.txt", + ".clang-tidy", + ".clang-format", + "LICENSE.txt", + "util/*", + "resources/*", + ] exclude_patterns = ["*/__pycache__/*", "*/.mypy_cache/*", "doc/*", "*/schema.json"] @@ -84,7 +107,8 @@ def export_sources(self): no_copy_source = True def set_version(self): - if isdir(join(self.recipe_folder, '.git')): + """Defines the version number that should be used.""" + if isdir(join(self.recipe_folder, ".git")): # sets project version either to the current tag or the # last tag with with the diff-commit addition. # For example: @@ -107,9 +131,11 @@ def set_version(self): self.version = fictive_version def layout(self): + """Define the folder layout for building Sen.""" cmake_layout(self) def generate(self): + """Generate the cmake dependency and toolchain files.""" deps = CMakeDeps(self) deps.generate() @@ -138,15 +164,18 @@ def generate(self): tc.generate() def build(self): + """Configure and build Sen.""" cmake = CMake(self) cmake.configure() cmake.build() def package(self): + """Package Sen into a conan package.""" cmake = CMake(self) cmake.install() def package_info(self): + """Calculate the conan package info.""" self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.builddirs = [join("cmake", "sen")] self.cpp_info.set_property("cmake_target_name", "sen::core sen::kernel sen::db sen::util") @@ -160,11 +189,14 @@ def package_info(self): # Windows: PATH is already prepended above; no additional loader path needed. + def env_var_to_bool(env_var_name): """Returns True if the environment variable is set to a truthy value, False otherwise.""" return getenv(env_var_name, "").lower() in ("true", "on", "1", "yes") + def select_sanitizer() -> str: + """Determine which sanitizer should be enabled.""" if env_var_to_bool("ENABLE_ASAN"): print("Configuring address and undefined-behavior sanitizers...") return "ASanUBSan" diff --git a/docs/components/openapi.yaml b/docs/components/openapi.yaml index 57452739..02e5a4e9 100644 --- a/docs/components/openapi.yaml +++ b/docs/components/openapi.yaml @@ -227,6 +227,12 @@ paths: required: true schema: type: string + - in: query + name: includeValues + description: Whether to include property values + required: false + schema: + type: string get: summary: Get object details description: Returns information about a specific object including available @@ -783,6 +789,9 @@ components: items: $ref: '#/components/schemas/Link' description: Links to available methods, properties, and events + properties: + type: object + description: Properties with its values required: - objectId - name @@ -811,8 +820,8 @@ components: method: type: string enum: - - httpGet - - httpPost + - get + - post description: HTTP method to use required: - rel @@ -827,7 +836,7 @@ components: description: type: string description: Method description - ret_type: + retType: type: string description: Return type name args: @@ -838,7 +847,7 @@ components: required: - name - description - - ret_type + - retType - args Argument: type: object @@ -897,10 +906,10 @@ components: type: string enum: - evt - - property - invoke - - objectAdded - - objectRemoved + - property + - object_added + - object_removed description: Notification type data: type: object diff --git a/docs/examples/index.md b/docs/examples/index.md index 5bc871ed..8e1676fa 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -17,26 +17,36 @@ compile it, and run it under a Sen kernel. The [examples](../examples/index.md) are numbered and ordered by complexity. Follow them in sequence: -| # | Example | What you learn | -|---|---------|---------------| -| 1 | [Calculators](../../examples/config/1_calculators/readme.md) | Basic package, multiple implementations, shell interaction | -| 2 | [Inheritance](../../examples/config/2_inheritance/readme.md) | STL inheritance, template injection | -| 3 | [Aircraft](../../examples/config/3_aircraft/readme.md) | HLA FOMs, `update()` loop, virtual time | -| 4 | [School](../../examples/config/4_school/readme.md) | Object discovery, events, multi-component | -| 6 | [Recorder](../../examples/config/6_recorder/readme.md) | Recording, Python post-processing | -| 7 | [Replayer](../../examples/config/7_replayer/readme.md) | Replay with real-time and stepped execution | -| 8 | [InfluxDB](../../examples/config/8_influx/readme.md) | Grafana visualisation | -| 9 | [HLA Servers](../../examples/config/9_hla_servers/readme.md) | Request/response servers | -| 10 | [Python](../../examples/config/10_python/readme.md) | Embedded Python scripting | -| 11 | [Shapes](../../examples/config/11_shapes/readme.md) | Interest management, Sen Query Language | -| 12 | [Fibonacci](../../examples/config/12_fibonacci/readme.md) | Deferred methods, load balancing | -| 13 | [Timer](../../examples/config/13_timer/readme.md) | Checked properties, state validation | +| # | Example | What you learn | +| --- | ------------------------------------------------------------ | ---------------------------------------------------------- | +| 1 | [Calculators](../../examples/config/1_calculators/readme.md) | Basic package, multiple implementations, shell interaction | +| 2 | [Inheritance](../../examples/config/2_inheritance/readme.md) | STL inheritance, template injection | +| 3 | [Aircraft](../../examples/config/3_aircraft/readme.md) | HLA FOMs, `update()` loop, virtual time | +| 4 | [School](../../examples/config/4_school/readme.md) | Object discovery, events, multi-component | +| 6 | [Recorder](../../examples/config/6_recorder/readme.md) | Recording, Python post-processing | +| 7 | [Replayer](../../examples/config/7_replayer/readme.md) | Replay with real-time and stepped execution | +| 8 | [InfluxDB](../../examples/config/8_influx/readme.md) | Grafana visualisation | +| 9 | [HLA Servers](../../examples/config/9_hla_servers/readme.md) | Request/response servers | +| 10 | [Python](../../examples/config/10_python/readme.md) | Embedded Python scripting | +| 11 | [Shapes](../../examples/config/11_shapes/readme.md) | Interest management, Sen Query Language | +| 12 | [Fibonacci](../../examples/config/12_fibonacci/readme.md) | Deferred methods, load balancing | +| 13 | [Timer](../../examples/config/13_timer/readme.md) | Checked properties, state validation | ### 4. Go deeper with the how-to guides Once you are comfortable with the examples, the [How-To Guides](../howto_guides/objects.md) cover specific topics in depth: working with objects, generated code, logging, dead reckoning, and more. +## Example applications + +Included in the same [examples](../examples/) directory, you can find a set of example applications: + +| Application | Description | +| -------------------------------------------------------------- | ---------------------------------------- | +| [Web explorer](../../examples/apps/web_explorer/readme.md) | Basic Sen explorer for the web browser | +| [REST Python](../../examples/apps/rest_python/readme.md) | Python client for the Sen REST component | +| [Recording inspector](../../examples/apps/recording_inspector) | | + ## Reference material - [STL language reference](../users_guide/stl.md) - full syntax of the Sen Type Language diff --git a/docs/howto_guides/components.md b/docs/howto_guides/components.md index a8cc259d..c9e01a45 100644 --- a/docs/howto_guides/components.md +++ b/docs/howto_guides/components.md @@ -545,3 +545,7 @@ We just need to be aware of the following: Object names must be unique within the bus in which they are published. Otherwise, an exception will be raised. + +### Object's naming convention + +Sen supports the use of all special characters for published object naming, with the only exception of literal space characters (" "), which are restricted. diff --git a/examples/apps/rest_python/readme.md b/examples/apps/rest_python/readme.md index e2ff1946..f6f137e6 100644 --- a/examples/apps/rest_python/readme.md +++ b/examples/apps/rest_python/readme.md @@ -29,8 +29,8 @@ Prepare the Python environment ```bash python3 -m venv .env -source env/bin/activate -pip install -r requirements +source .env/bin/activate +pip install -r requirements.txt ``` Now run the client example: @@ -38,3 +38,6 @@ Now run the client example: ```bash python sen_client.py ``` + +This example works as a simple python client for the Sen REST component. It instantiates a SenClient object that starts +a session, enable notifications and does some basic operations to exemplify how it can be used. diff --git a/examples/apps/rest_python/sen_client.py b/examples/apps/rest_python/sen_client.py index 4228a9a7..66531793 100644 --- a/examples/apps/rest_python/sen_client.py +++ b/examples/apps/rest_python/sen_client.py @@ -4,17 +4,39 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +""" +This program contains an example of how users can interact with the REST API using Python. + +It shows how to sign in, create interests, display details and interact with remote objects. +""" -import requests import json -import sseclient +import threading import time -from typing import List, Dict, Any +import requests +import sseclient + class SenClient: - def __init__(self, base_url = "http://localhost"): - self.base_url = base_url.rstrip('/') + """ + Represents the client for the Sen REST component. + + Attributes: + base_url (str): URL of the Sen REST server. + session (requests.Session): Active HTTP session. + sse_event_types (list): List of notifiable event types. + token (str): Token needed for authentication. + """ + + def __init__(self, base_url="http://localhost"): + """ + Initialize SenClient. + + Args: + base_url: base url to use (defaults to localhost) + """ + self.base_url = base_url.rstrip("/") self.session = requests.Session() self.sse_event_types = [ "object_added", @@ -25,8 +47,15 @@ def __init__(self, base_url = "http://localhost"): ] self.token = None - def get_headers(self, is_sse = False): - header = {"Content-Type": "application/json", } + def get_headers(self, is_sse=False): + """Gets the headers needed for REST API requests. + + Args: + is_sse (bool): Whether notifications are enabled. + """ + header = { + "Content-Type": "application/json", + } if is_sse: header["Accept"] = "text/event-stream" else: @@ -37,12 +66,20 @@ def get_headers(self, is_sse = False): return header def get(self, href): + """Makes GET request to the REST API. + + Args: + href (str): Endpoint route. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ try: - url = href if href.startswith('http') else f"{self.base_url}{href}" - response = self.session.get( - url, - headers=self.get_headers() - ) + url = href if href.startswith("http") else f"{self.base_url}{href}" + response = self.session.get(url, headers=self.get_headers()) response.raise_for_status() return response.json() @@ -51,12 +88,20 @@ def get(self, href): raise def post(self, href): + """Makes POST request to the REST API. + + Args: + href (str): Endpoint route. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ try: - url = href if href.startswith('http') else f"{self.base_url}{href}" - response = self.session.post( - url, - headers=self.get_headers() - ) + url = href if href.startswith("http") else f"{self.base_url}{href}" + response = self.session.post(url, headers=self.get_headers()) response.raise_for_status() return response.json() @@ -65,13 +110,21 @@ def post(self, href): raise def post_json(self, href, body): + """Makes POST request to the REST API with a body attached. + + Args: + href (str): Endpoint route. + body (str): Body of the request. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ try: - url = href if href.startswith('http') else f"{self.base_url}{href}" - response = self.session.post( - url, - headers= self.get_headers(), - json=body - ) + url = href if href.startswith("http") else f"{self.base_url}{href}" + response = self.session.post(url, headers=self.get_headers(), json=body) response.raise_for_status() return response.json() @@ -80,12 +133,24 @@ def post_json(self, href, body): raise def post_args(self, href, args_str): + """Makes POST request to the REST API when some arguments are needed. + + Args: + href (str): Endpoint route. + args_str (str): Arguments sent to the endpoint. + + Returns: + dict: Response of the request. + + Raises: + requests.RequestException: If there is any error in the request. + """ args_str = args_str.strip() if args_str: args = [] - for v in args_str.split(','): - v = v.strip() + for raw_v in args_str.split(","): + v = raw_v.strip() try: args.append(int(v)) except ValueError: @@ -97,9 +162,14 @@ def post_args(self, href, args_str): return data def enable_notifications(self): - import threading + """Enable notifications that are displayed on standard output.""" def listen_sse(): + """Tries to enable notifications and displays them on the standard output. + + Raises: + Exception: If there is any error in the request. + """ try: notifications = sseclient.SSEClient(f"{self.base_url}/api/sse", headers=self.get_headers(True)) for notification in notifications: @@ -114,17 +184,37 @@ def listen_sse(): print("Notifications enabled") def disable_notifications(self): + """Disable notifications if they are enabled.""" if self.sse_thread: self.sse_thread = None print("SSE disabled") def signin(self, client_id): + """Sign in to the REST API to get an API token. + + Args: + client_id (str): Client id. + + Returns: + dict: Response containing the API token. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.post_json("/api/auth", {"id": client_id}) print("Signed in successfully") self.token = data["token"] return data def start_client_session(self, client_id): + """Start a client session and get all the sessions if a client id is provided. + + Args: + client_id (str): Client id. + + Raises: + requests.RequestException: If there is any error in the request. + """ if not client_id: print("Client ID is required") return @@ -134,34 +224,89 @@ def start_client_session(self, client_id): self.get_sessions() def get_sessions(self): + """Get all the sessions. + + Returns: + list: List containing all the sessions. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.get("/api/sessions") return data def print_sessions(self, sessions): + """Display all the sessions on standard output. + + Args: + sessions (list): List with all the sessions. + """ print("\n=== Sessions ===") for session in sessions: print(f" {session}") print() def create_interest(self, name, query): + """Create an interest based on a query. + + Args: + name (str): Name of the interest. + query (str): Query used to create the interest. + + Returns: + dict: Dictionary containing the created interest name. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.post_json("/api/interests", {"name": name, "query": query}) return data def reload_interests(self): + """Get all the interests created. + + Returns: + list[dict]: List containing all the interests with their name, bus and session. + + Raises: + requests.RequestException: If there is any error in the request. + """ interests = self.get("/api/interests") return interests def print_interests(self, interests): + """Display all the interests on standard output. + + Args: + interests (list[dict]): List containing all the interests. + """ print("\n=== Interests ===") for interest in interests: print(f" {json.dumps(interest)}") print() def get_objects(self, interest_name): + """Get all the objects contained on an interest. + + Args: + interest_name (str): Name of the interest. + + Returns: + list[dict]: List containing all the objects with their class name, link, local name, + name and object id. + + Raises: + requests.RequestException: If there is any error in the request. + """ objects = self.get(f"/api/interests/{interest_name}/objects") return objects def print_objects(self, objects): + """Display all the objects contained on an interest on standard output. + + Args: + objects (list[dict]): List containing all the objects. + """ print("\n=== Objects ===") for obj in objects: print(f" {obj.get('name')} [ {obj.get('localname')} ]") @@ -169,54 +314,99 @@ def print_objects(self, objects): print() def get_object(self, interest_name, object_name): + """ + Get all the object details. + + Args: + interest_name: Name of the interest + object_name (str): Name of the object. + + Returns: + dict: Dictionary with all the object associated with its class name, description, links, + local name, name and object id. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.get(f"/api/interests/{interest_name}/objects/{object_name}") return data def print_object(self, data): + """ + Display all the object details. + + Args: + data (dict): Dictionary containing all the object details. + """ methods = [] properties = [] events = [] - for link in data.get('links', []): - rel = link.get('rel') - href = link.get('href') + for link in data.get("links", []): + rel = link.get("rel") + href = link.get("href") - if rel == 'method': + if rel == "method": methods.append(href) - elif rel == 'def': + elif rel == "def": methods.append(f"{href} (definition)") - elif rel == 'property': + elif rel == "property": properties.append(href) - elif rel == 'property_subscribe': + elif rel == "property_subscribe": properties.append(f"{href} (subscribe)") - elif rel == 'property_unsubscribe': + elif rel == "property_unsubscribe": properties.append(f"{href} (unsubscribe)") print("\n=== Object Details ===") if methods: - print("\nMethods:") - for method in methods: - print(f" {method}") + print("\nMethods:" + "\n".join(f" {method}" for method in methods)) if properties: - print("\nProperties:") - for prop in properties: - print(f" {prop}") + print("\nProperties:" + "\n".join(f" {prop}" for prop in properties)) if events: - print("\nEvents:") - for event in events: - print(f" {event}") + print("\nEvents:" + "\n".join(f" {event}" for event in events)) print() def get_method_info(self, interest_name, object_name, method): + """Get all the method details. + + Args: + interest_name (str): Name of the interest. + object_name (str): Name of the object. + method (str): Name of the method. + + Returns: + dict: Dictionary containing all the method arguments, description, name and return type of the result. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.get(f"/api/interests/{interest_name}/objects/{object_name}/methods/{method}") return data def print_method_def(self, method_def): + """Display all the method details. + + Args: + method_def (dict): Dictionary containing all the method details. + """ print(f"Method definition: {method_def}") def invoke_method(self, interest_name, object_name, method): + """Invoke a method. + + Args: + interest_name (str): Name of the interest. + object_name (str): Name of the object. + method (str): Name of the method. + + Returns: + dict: Dictionary with the method invocation id and its state. + + Raises: + requests.RequestException: If there is any error in the request. + """ data = self.post_args(f"/api/interests/{interest_name}/objects/{object_name}/methods/{method}/invoke", "") return data diff --git a/examples/apps/web_explorer/readme.md b/examples/apps/web_explorer/readme.md index 23ff3910..b997edd9 100644 --- a/examples/apps/web_explorer/readme.md +++ b/examples/apps/web_explorer/readme.md @@ -5,11 +5,16 @@ real-time updates through Server-Sent Events (SSE). ## Prerequisites -- Docker +--- + +- Docker engine - make sure to follow the [installation guide](https://docs.docker.com/engine/install/) + and the [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/) - A running Sen REST component ## Example structure +--- + ``` web_explorer/ ├── index.html # Web page @@ -21,6 +26,8 @@ web_explorer/ ## How to run +--- + Create a **sen** configuration file (e.g. config.yaml) to load the REST component: ```yaml @@ -49,3 +56,45 @@ Open the browser (http://localhost:8000) and developer console to see: - SSE event streams - Error messages - Object interaction logs + +## Usage + +### Start session + +First of all, start session with the Client ID of choice and click `Start Session`. +This step is needed in order to interact with the component. + +Next to `Start Session` button there is a checkbox with the name `SSE`. This checkbox enables the browser to receive +push notifications from the REST component when Sen objects are updated. + +### Create interest + +To create an interest, fill both fields in the `Interest` section. The first field corresponds to the interest name and +the second field is for the interest query. The interest query must be written on Sen Query Language. There is a section +called `Sessions` which shows all the available sessions. + +Interests are saved until the Sen REST component is ended. To show all the created interests, click `Reload` on the +`Available Interests` section. + +NOTE: To show newly created interests after `Reload` has already been clicked, refresh the page and click `Reload` +again. + +### Retrieve objects + +Once an interest is created, click `Retrieve objects` in the `Available Interests` section to show all the objects in +that interest. + +The `Objects` section is where object elements will be shown. These include methods, properties and events. To do that, +click +`Get Object`. This will show all the object elements classified by type. + +For each type of element, different options are available: + +- In `Object Methods` you can get every method definition and invoke them. + You can do that by clicking `Definition` + and `Invoke` buttons respectively. For invoking a method, the arguments must be defined in the `Invoke Method Args` + section. +- In `Object Properties` you can get every property value and subscribe or unsubscribe to them. To do that, click + `Get Value`, `Subscribe` and `Unsubscribe` buttons respectively. +- In `Object Events` you can subscribe or unsubscribe to an event by clicking `Subscribe` and `Unsubscribe` buttons + respectively. diff --git a/examples/config/10_python/scripts/creating_objects.py b/examples/config/10_python/scripts/creating_objects.py index 86cf3108..f2db9ab3 100644 --- a/examples/config/10_python/scripts/creating_objects.py +++ b/examples/config/10_python/scripts/creating_objects.py @@ -4,13 +4,16 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to create objects.""" import sen myObject = testBus = None + def run(): - global myObject, testBus # refer to the globals defined above + """Sen run: to setup the initial component state.""" + global myObject, testBus # refer to the globals defined above # noqa: PLW0603 type = { "entityKind": 1, @@ -19,30 +22,30 @@ def run(): "category": 1, "subcategory": 3, "specific": 0, - "extra": 0 + "extra": 0, } - id = { - "entityNumber": 1, - "federateIdentifier": { - "siteID": 1, - "applicationID": 1 - } - } + id = {"entityNumber": 1, "federateIdentifier": {"siteID": 1, "applicationID": 1}} - print(f"Python: creating and publishing the object") - myObject = sen.api.make("aircrafts.DummyAircraft", "myAircraft", entityType=type, alternateEntityType=type, entityIdentifier = id) + print("Python: creating and publishing the object") + myObject = sen.api.make( + "aircrafts.DummyAircraft", "myAircraft", entityType=type, alternateEntityType=type, entityIdentifier=id + ) testBus = sen.api.getBus("my.tutorial") testBus.add(myObject) # setting the speed property to 150 myObject.speed = 150 + def update(): + """Sen update: triggers test execution.""" print(myObject) + def stop(): - global testBus, myObject # refer to the globals defined above + """Sen stop: trigger that the execution stops.""" + global testBus, myObject # refer to the globals defined above # noqa: PLW0603 print("Python: deleting the object") testBus.remove(myObject) diff --git a/examples/config/10_python/scripts/hello_python.py b/examples/config/10_python/scripts/hello_python.py index 3a8951f2..9b04526e 100644 --- a/examples/config/10_python/scripts/hello_python.py +++ b/examples/config/10_python/scripts/hello_python.py @@ -4,20 +4,26 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to use the python component.""" import sen + # this is executed only once (at the start of the component execution) def run(): - print(f"Python: run") + """Sen run: to setup the initial component state.""" + print("Python: run") print(f"Python: the config is: {sen.api.config}") print(f"Python: the app name is: {sen.api.appName}") # this is executed every cycle def update(): + """Sen update: triggers test execution.""" print(f"Python: update (current time: {sen.api.time})") + # this is executed only once (at the end of the component execution) def stop(): - print(f"Python: stop called") + """Sen stop: trigger that the execution stops.""" + print("Python: stop called") diff --git a/examples/config/10_python/scripts/inspecting_objects.py b/examples/config/10_python/scripts/inspecting_objects.py index 314b28ee..8d120dab 100644 --- a/examples/config/10_python/scripts/inspecting_objects.py +++ b/examples/config/10_python/scripts/inspecting_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to inspect objects.""" import sen @@ -12,19 +13,18 @@ def run(): - global list # refer to the global variable defined above + """Sen run: to setup the initial component state.""" + global list # refer to the global variable defined above # noqa: PLW0603 list = sen.api.open("SELECT * FROM local.kernel") # open it # register some callbacks to show changes in the list - list.onAdded(lambda obj: print(f'Python: object added {obj}')) - list.onRemoved(lambda obj: print(f'Python: object removed {obj}')) + list.onAdded(lambda obj: print(f"Python: object added {obj}")) + list.onRemoved(lambda obj: print(f"Python: object removed {obj}")) def update(): - # refer to the global variable defined above - global list - + """Sen update: triggers test execution.""" print(f"Python: printing the list at: {sen.api.time})") print(list) diff --git a/examples/config/10_python/scripts/interacting_with_objects.py b/examples/config/10_python/scripts/interacting_with_objects.py index 6f4ef895..c4eb94a6 100644 --- a/examples/config/10_python/scripts/interacting_with_objects.py +++ b/examples/config/10_python/scripts/interacting_with_objects.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to interact with objects.""" import sen @@ -12,12 +13,13 @@ def run(): - global obj # refer to the global variable defined above - obj = sen.api.open("SELECT * FROM local.shell WHERE name = \"shell_impl\"") + """Sen run: to setup the initial component state.""" + global obj # refer to the global variable defined above # noqa: PLW0603 + obj = sen.api.open('SELECT * FROM local.shell WHERE name = "shell_impl"') def update(): - global obj # refer to the global variable defined above + """Sen update: triggers test execution.""" print("Python: update") # if the object is present, do something with it diff --git a/examples/config/10_python/scripts/reacting_to_events_and_changes.py b/examples/config/10_python/scripts/reacting_to_events_and_changes.py index f1b95ade..342afb45 100644 --- a/examples/config/10_python/scripts/reacting_to_events_and_changes.py +++ b/examples/config/10_python/scripts/reacting_to_events_and_changes.py @@ -4,6 +4,7 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to react on events and changes (e.g., object added).""" import sen @@ -11,16 +12,18 @@ teachers = None -def teacherDetected(teacher): +def teacherDetected(teacher) -> None: + """Prints information about the detected teacher.""" teacher.onStressLevelPeaked(lambda args: print(f"Python: {teacher.name} stress level peaking to {args}")) teacher.onStatusChanged(lambda: print(f"Python: {teacher.name} status changed to {teacher.status}")) -def run(): - global teachers # refer to the global variable defined above +def run() -> None: + """Sen run: to setup the initial component state.""" + global teachers # refer to the global variable defined above # noqa: PLW0603 print("Python: run") # select the object and install some callbacks teachers = sen.api.open("SELECT school.Teacher FROM school.primary") - teachers.onAdded(lambda obj: teacherDetected(obj)) + teachers.onAdded(teacherDetected) diff --git a/examples/config/6_recorder/3_recorder_school_print.py b/examples/config/6_recorder/3_recorder_school_print.py index 97b2380c..916d717b 100644 --- a/examples/config/6_recorder/3_recorder_school_print.py +++ b/examples/config/6_recorder/3_recorder_school_print.py @@ -4,17 +4,19 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Example module that demonstrates how to handle recording.""" -import sen_db_python as sen from datetime import datetime -epoch = datetime(1970,1,1) +import sen_db_python as sen + +epoch = datetime(1970, 1, 1) try: input = sen.Input("school_recording") print(f"Opened archive '{input.path}' with the following summary:") - print(f" - firstTime: {epoch+input.summary.firstTime}") - print(f" - lastTime: {epoch+input.summary.lastTime}") + print(f" - firstTime: {epoch + input.summary.firstTime}") + print(f" - lastTime: {epoch + input.summary.lastTime}") print(f" - keyframeCount: {input.summary.keyframeCount}") print(f" - objectCount: {input.summary.objectCount}") print(f" - typeCount: {input.summary.typeCount}") @@ -30,31 +32,31 @@ entry = cursor.entry if type(entry.payload) is sen.Keyframe: - print(f"{epoch+entry.time} -> Keyframe:") + print(f"{epoch + entry.time} -> Keyframe:") for obj in entry.payload.snapshots: print(f" - object: {obj.name}") print(f" class: {obj.className}") print("") elif type(entry.payload) is sen.Creation: - print(f"{epoch+entry.time} -> Object Creation:") + print(f"{epoch + entry.time} -> Object Creation:") print(f" - name: {entry.payload.name}") print(f" - class: {entry.payload.className}") print("") elif type(entry.payload) is sen.Deletion: - print(f"{epoch+entry.time} -> Object Deletion:") + print(f"{epoch + entry.time} -> Object Deletion:") print(f" - object id: {entry.payload.objectId}") print("") elif type(entry.payload) is sen.PropertyChange: - print(f"{epoch+entry.time} -> Property Changed:") + print(f"{epoch + entry.time} -> Property Changed:") print(f" - object id: {entry.payload.objectId}") print(f" - property: {entry.payload.name}") print("") elif type(entry.payload) is sen.Event: - print(f"{epoch+entry.time} -> Event:") + print(f"{epoch + entry.time} -> Event:") print(f" - object id: {entry.payload.objectId}") print(f" - event: {entry.payload.name}") print("") diff --git a/examples/config/9_hla_servers/readme.md b/examples/config/9_hla_servers/readme.md index f41f862a..30c5c9bc 100644 --- a/examples/config/9_hla_servers/readme.md +++ b/examples/config/9_hla_servers/readme.md @@ -83,5 +83,5 @@ In this case you need to populate the parameters in a more involved manner due t data model: ``` -my.tutorial.weatherServer.reqWeather {"type": "GeodeticLocation", "value": { "lat": 0, "lon": 0}}, false +my.tutorial.weatherServer.reqWeather {"type": "GeodeticLocation", "value": { "latitude": 0, "longitude": 0}}, false ``` diff --git a/examples/packages/weather_server/src/randomize.h b/examples/packages/weather_server/src/randomize.h index d2044864..beeca90c 100644 --- a/examples/packages/weather_server/src/randomize.h +++ b/examples/packages/weather_server/src/randomize.h @@ -9,6 +9,7 @@ #define SEN_WEATHER_SERVER_RANDOMIZE_H // generated code +#include "hla_fom/hla.stl.h" #include "netn/netn-metoc.xml.h" // sen @@ -32,17 +33,26 @@ template void randomizeData(T& result, const netn::GeoReferenceVariant& ref) { result.geoReference = ref; - result.barometricPressure = getRand(0.0f, 50.0f); - result.humidity = getRand(0.0f, 100.0f); + result.barometricPressure = hla::MaybeF32(getRand(0.0f, 50.0f)); + result.humidity = hla::MaybeF32(getRand(0.0f, 100.0f)); result.temperature = getRand(-40.0f, 40.0f); result.visibilityRange = getRand(0.0f, 50000.0f); - result.haze.density = getRand(0.0f, 1.0f); - result.haze.type = netn::HazeTypeEnum32::fog; - result.precipitation.intensity = getRand(0.0f, 1.0f); - result.precipitation.type = netn::PrecipitationTypeEnum32::rain; - result.wind.direction = getRand(0.0f, 360.0f); - result.wind.horizontalSpeed = getRand(0.0f, 500.0f); - result.wind.verticalSpeed = getRand(0.0f, 500.0f); + + netn::HazeStruct haze; + haze.density = getRand(0.0f, 1.0f); + haze.type = netn::HazeTypeEnum32::fog; + result.haze = haze; + + netn::PrecipitationStruct precipitation; + precipitation.intensity = getRand(0.0f, 1.0f); + precipitation.type = netn::PrecipitationTypeEnum32::rain; + result.precipitation = precipitation; + + netn::WindStruct wind; + wind.direction = getRand(0.0f, 360.0f); + wind.horizontalSpeed = getRand(0.0f, 500.0f); + wind.verticalSpeed = getRand(0.0f, 500.0f); + result.wind = wind; } template @@ -68,9 +78,13 @@ struct MakeRandom { netn::TroposphereLayerCondition elem {}; randomizeData(elem, ref); - elem.cloud.coverage = getRand(0.0f, 1.0f); - elem.cloud.density = getRand(0.0f, 1.0f); - elem.cloud.type = netn::CloudTypeEnum32::cirrostratus; + + netn::CloudStruct cloud; + cloud.coverage = getRand(0.0f, 1.0f); + cloud.density = getRand(0.0f, 1.0f); + cloud.type = netn::CloudTypeEnum32::cirrostratus; + elem.cloud = cloud; + return elem; } }; @@ -82,12 +96,19 @@ struct MakeRandom { netn::WaterSurfaceCondition elem {}; randomizeData(elem, ref); - elem.ice.coverage = getRand(0.0f, 100.0f); - elem.ice.thickness = getRand(0.0f, 3000.0f); - elem.ice.type = netn::IceTypeEnum16::ice; - elem.current.direction = getRand(0.0f, 360.0f); - elem.current.speed = getRand(0.0f, 50.0f); - elem.salinity = getRand(0.0f, 20.0f); + + netn::IceStruct ice; + ice.coverage = getRand(0.0f, 100.0f); + ice.thickness = getRand(0.0f, 3000.0f); + ice.type = netn::IceTypeEnum16::ice; + elem.ice = ice; + + netn::CurrentStruct current; + current.direction = getRand(0.0f, 360.0f); + current.speed = getRand(0.0f, 50.0f); + elem.current = current; + + elem.salinity = hla::MaybeF32(getRand(0.0f, 20.0f)); return elem; } }; @@ -99,11 +120,15 @@ struct MakeRandom { netn::SubsurfaceLayerCondition elem {}; elem.geoReference = ref; - elem.current.direction = getRand(0.0f, 360.0f); - elem.current.speed = getRand(0.0f, 50.0f); - elem.salinity = getRand(0.0f, 20.0f); + + netn::CurrentStruct current; + current.direction = getRand(0.0f, 360.0f); + current.speed = getRand(0.0f, 50.0f); + elem.current = current; + + elem.salinity = hla::MaybeF32(getRand(0.0f, 20.0f)); elem.temperature = getRand(-20.0f, 400.0f); - elem.bottomType = netn::SedimentTypeEnum32::mud; + elem.bottomType = netn::MaybeSedimentTypeEnum32(netn::SedimentTypeEnum32::mud); return elem; } }; @@ -115,8 +140,8 @@ struct MakeRandom { netn::LandSurfaceCondition elem {}; randomizeData(elem, ref); - elem.moisture = netn::SurfaceMoistureEnum16::wet; - elem.iceCondition = netn::RoadIceConditionEnum16::patches; + elem.moisture = netn::MaybeSurfaceMoistureEnum16(netn::SurfaceMoistureEnum16::wet); + elem.iceCondition = netn::MaybeRoadIceConditionEnum16(netn::RoadIceConditionEnum16::patches); return elem; } }; diff --git a/libs/core/include/sen/core/meta/basic_traits.h b/libs/core/include/sen/core/meta/basic_traits.h index ee4005d5..9f847e58 100644 --- a/libs/core/include/sen/core/meta/basic_traits.h +++ b/libs/core/include/sen/core/meta/basic_traits.h @@ -8,11 +8,15 @@ #ifndef SEN_CORE_META_BASIC_TRAITS_H #define SEN_CORE_META_BASIC_TRAITS_H -#include "sen/core/base/duration.h" -#include "sen/core/base/timestamp.h" +// sen #include "sen/core/io/detail/serialization_traits.h" #include "sen/core/io/util.h" #include "sen/core/meta/type.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -38,6 +42,19 @@ struct BasicTraits static void variantToValue(const Var& var, T& val) { val = getCopyAs(var); } static void valueToVariant(T val, Var& var) { var = val; } static constexpr uint32_t serializedSize(T val) noexcept { return impl::getSerializedSize(val); } + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/detail/native_types_impl.h b/libs/core/include/sen/core/meta/detail/native_types_impl.h index 0c6bc6ba..381a654d 100644 --- a/libs/core/include/sen/core/meta/detail/native_types_impl.h +++ b/libs/core/include/sen/core/meta/detail/native_types_impl.h @@ -108,6 +108,8 @@ class RealTypeBase: public Parent static inline void write(OutputStream& out, native val) { out.writefunc(val); } \ static inline void read(InputStream& in, native& val) { in.readfunc(val); } \ using BasicTraits::serializedSize; \ + using BasicTraits::toJsonString; \ + using BasicTraits::fromJsonString; \ } //-------------------------------------------------------------------------------------------------------------- diff --git a/libs/core/include/sen/core/meta/enum_traits.h b/libs/core/include/sen/core/meta/enum_traits.h index 4dccd304..eb4b94ac 100644 --- a/libs/core/include/sen/core/meta/enum_traits.h +++ b/libs/core/include/sen/core/meta/enum_traits.h @@ -8,7 +8,16 @@ #ifndef SEN_CORE_META_ENUM_TRAITS_H #define SEN_CORE_META_ENUM_TRAITS_H +// sen +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" #include "sen/core/meta/enum_type.h" +#include "sen/core/meta/var.h" + +// std +#include +#include +#include namespace sen { @@ -30,6 +39,19 @@ struct EnumTraitsBase static void valueToVariant(const T& val, Var& var) { var = static_cast(val); } static void variantToValue(const Var& var, T& val) { impl::enumVariantToValue(var, val); } [[nodiscard]] static uint32_t serializedSize(T val) noexcept { return impl::enumSerializedSize(val); } + + static std::string toJsonString(T val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/optional_traits.h b/libs/core/include/sen/core/meta/optional_traits.h index f0d5248e..7c786207 100644 --- a/libs/core/include/sen/core/meta/optional_traits.h +++ b/libs/core/include/sen/core/meta/optional_traits.h @@ -14,9 +14,12 @@ #include "sen/core/io/output_stream.h" #include "sen/core/io/util.h" #include "sen/core/meta/optional_type.h" +#include "sen/core/meta/var.h" // std +#include #include +#include namespace sen { @@ -37,6 +40,19 @@ struct OptionalTraitsBase static void valueToVariant(const T& val, Var& var); static void variantToValue(const Var& var, T& val); [[nodiscard]] static uint32_t serializedSize(T val) noexcept; + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/quantity_traits.h b/libs/core/include/sen/core/meta/quantity_traits.h index c111e9f5..4bd6db6b 100644 --- a/libs/core/include/sen/core/meta/quantity_traits.h +++ b/libs/core/include/sen/core/meta/quantity_traits.h @@ -8,8 +8,17 @@ #ifndef SEN_CORE_META_QUANTITY_TRAITS_H #define SEN_CORE_META_QUANTITY_TRAITS_H +// sen +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" #include "sen/core/io/util.h" #include "sen/core/meta/quantity_type.h" +#include "sen/core/meta/type_traits.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -35,6 +44,19 @@ struct QuantityTraitsBase { return SerializationTraits::serializedSize(val.get()); } + + static std::string toJsonString(T val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/sequence_traits.h b/libs/core/include/sen/core/meta/sequence_traits.h index 00837074..1bf9bfd8 100644 --- a/libs/core/include/sen/core/meta/sequence_traits.h +++ b/libs/core/include/sen/core/meta/sequence_traits.h @@ -9,8 +9,15 @@ #define SEN_CORE_META_SEQUENCE_TRAITS_H // sen +#include "sen/core/io/input_stream.h" +#include "sen/core/io/output_stream.h" #include "sen/core/io/util.h" #include "sen/core/meta/sequence_type.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -31,6 +38,19 @@ struct SequenceTraitsBase static void valueToVariant(const T& val, Var& var) { impl::sequenceToVariant(val, var); } static void variantToValue(const Var& var, T& val) { impl::variantToSequence(var, val); } [[nodiscard]] static uint32_t serializedSize(const T& val) noexcept { return impl::sequenceSerializedSize(val); } + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// Base class for sequence traits. @@ -44,6 +64,19 @@ struct ArrayTraitsBase static void valueToVariant(const T& val, Var& var) { impl::arrayToVariant(val, var); } static void variantToValue(const Var& var, T& val) { impl::variantToArray(var, val); } [[nodiscard]] static uint32_t serializedSize(const T& val) noexcept { return impl::arraySerializedSize(val); } + + static std::string toJsonString(const T& val) + { + Var var; + valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, T& val) + { + const Var var = fromJson(str); + variantToValue(var, val); + } }; /// @} diff --git a/libs/core/include/sen/core/meta/time_types.h b/libs/core/include/sen/core/meta/time_types.h index 6a9c913c..3c30c896 100644 --- a/libs/core/include/sen/core/meta/time_types.h +++ b/libs/core/include/sen/core/meta/time_types.h @@ -11,11 +11,19 @@ // sen #include "sen/core/base/duration.h" #include "sen/core/base/timestamp.h" +#include "sen/core/io/detail/serialization_traits.h" +#include "sen/core/io/input_stream.h" #include "sen/core/meta/basic_traits.h" #include "sen/core/meta/quantity_type.h" #include "sen/core/meta/type.h" +#include "sen/core/meta/type_traits.h" #include "sen/core/meta/unit.h" #include "sen/core/meta/unit_registry.h" +#include "sen/core/meta/var.h" + +// std +#include +#include namespace sen { @@ -131,6 +139,19 @@ struct SerializationTraits { return impl::getSerializedSize(val.get()); } + + static std::string toJsonString(const Duration val) + { + Var var; + VariantTraits::valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, Duration& val) + { + const Var var = fromJson(str); + VariantTraits::variantToValue(var, val); + } }; std::ostream& operator<<(std::ostream& out, const Duration& val); @@ -165,6 +186,19 @@ struct SerializationTraits { return impl::getSerializedSize(val); } + + static std::string toJsonString(const TimeStamp val) + { + Var var; + VariantTraits::valueToVariant(val, var); + return toJson(var); + } + + static void fromJsonString(const std::string& str, TimeStamp& val) + { + const Var var = fromJson(str); + VariantTraits::variantToValue(var, val); + } }; template <> diff --git a/libs/core/include/sen/core/meta/type_traits.h b/libs/core/include/sen/core/meta/type_traits.h index 15698a31..d022db6c 100644 --- a/libs/core/include/sen/core/meta/type_traits.h +++ b/libs/core/include/sen/core/meta/type_traits.h @@ -49,6 +49,8 @@ struct SerializationTraits // static void write(OutputStream& out, const T& val); // static void read(InputStream& in, T& val); // static uint32_t serializedSize(const T& val) noexcept; + // static std::string toJsonString(const T& val); + // static void fromJsonString(const std::string& str, T& val); static_assert(true, "SerializationTraits is not defined for type T."); }; diff --git a/libs/core/include/sen/core/obj/detail/gen.h b/libs/core/include/sen/core/obj/detail/gen.h index fab1ff59..20bcb334 100644 --- a/libs/core/include/sen/core/obj/detail/gen.h +++ b/libs/core/include/sen/core/obj/detail/gen.h @@ -293,6 +293,8 @@ protected: using OptionalTraitsBase::write; \ using OptionalTraitsBase::read; \ using OptionalTraitsBase::serializedSize; \ + using OptionalTraitsBase::toJsonString; \ + using OptionalTraitsBase::fromJsonString; \ }; /// Used by the code generator NOLINTNEXTLINE @@ -314,6 +316,8 @@ protected: using EnumTraitsBase::write; \ using EnumTraitsBase::read; \ using EnumTraitsBase::serializedSize; \ + using EnumTraitsBase::toJsonString; \ + using EnumTraitsBase::fromJsonString; \ }; \ template <> \ struct SEN_MAYBE_EXPORT(doExport) StringConversionTraits \ @@ -343,6 +347,8 @@ protected: using SequenceTraitsBase::write; \ using SequenceTraitsBase::read; \ using SequenceTraitsBase::serializedSize; \ + using SequenceTraitsBase::toJsonString; \ + using SequenceTraitsBase::fromJsonString; \ }; /// Used by the code generator NOLINTNEXTLINE @@ -364,6 +370,8 @@ protected: using ArrayTraitsBase::write; \ using ArrayTraitsBase::read; \ using ArrayTraitsBase::serializedSize; \ + using ArrayTraitsBase::toJsonString; \ + using ArrayTraitsBase::fromJsonString; \ }; /// Used by the code generator NOLINTNEXTLINE @@ -386,6 +394,8 @@ protected: static void write(OutputStream& out, const classname& val); \ static void read(InputStream& in, classname& val); \ [[nodiscard]] static uint32_t serializedSize(const classname& val) noexcept; \ + static std::string toJsonString(const classname& val); \ + static void fromJsonString(const std::string& str, classname& val); \ }; /// Used by the code generator NOLINTNEXTLINE @@ -409,6 +419,8 @@ protected: static void write(OutputStream& out, const classname& val); \ static void read(InputStream& in, classname& val); \ [[nodiscard]] static uint32_t serializedSize(const classname& val) noexcept; \ + static std::string toJsonString(const classname& val); \ + static void fromJsonString(const std::string& str, classname& val); \ }; /// Used by the code generator NOLINTNEXTLINE @@ -520,6 +532,8 @@ protected: using QuantityTraitsBase::write; \ using QuantityTraitsBase::read; \ using QuantityTraitsBase::serializedSize; \ + using QuantityTraitsBase::toJsonString; \ + using QuantityTraitsBase::fromJsonString; \ }; \ template <> \ struct SEN_MAYBE_EXPORT(doExport) QuantityTraits \ diff --git a/libs/core/src/lang/fom_document_set.cpp b/libs/core/src/lang/fom_document_set.cpp index 1d4759cb..b4daeacc 100644 --- a/libs/core/src/lang/fom_document_set.cpp +++ b/libs/core/src/lang/fom_document_set.cpp @@ -1670,9 +1670,20 @@ std::vector FomDocumentSet::collectArgsFromInteractionNode(const pugi::xpat continue; } - nodeArgs.emplace_back(toLowerCamelCase(paramName), - formatSemantics(param.node().child_value("semantics")), - getOrCreateTypeFromFomName(param.node().child_value("dataType"), doc).first); + std::string semanticsString = param.node().child_value("semantics"); + + auto argType = [&]() + { + if (startsWith(semanticsString, "Optional.") || startsWith(semanticsString, "Optional (") || + startsWith(semanticsString, "Optional:") || startsWith(semanticsString, "Optional,")) + { + return getOptionalPropertyType(param.node().child_value("dataType"), doc); + } + + return getOrCreateTypeFromFomName(param.node().child_value("dataType"), doc).first; + }(); + + nodeArgs.emplace_back(toLowerCamelCase(paramName), formatSemantics(semanticsString), argType); } result = prependArgs(result, nodeArgs); diff --git a/libs/core/src/lang/stl_parser.cpp b/libs/core/src/lang/stl_parser.cpp index 6333169c..84921ab1 100644 --- a/libs/core/src/lang/stl_parser.cpp +++ b/libs/core/src/lang/stl_parser.cpp @@ -194,6 +194,17 @@ StlStructStatement StlParser::structDeclaration() while (!check(StlTokenType::rightBrace) && !isAtEnd()) { + previousComment_ = {}; + while (check(StlTokenType::comment)) + { + previousComment_.push_back(consume(StlTokenType::comment, {"expecting comment"})); + } + + if (!previousComment_.empty() && (check(StlTokenType::rightBrace) || isAtEnd())) + { + break; + } + statement.fields.push_back(structFieldStatement()); } @@ -207,6 +218,9 @@ StlStructFieldStatement StlParser::structFieldStatement() StlStructFieldStatement statement; + statement.description = previousComment_; + previousComment_.clear(); + // maybe we have a comment before the field while (check(StlTokenType::comment)) { @@ -270,6 +284,17 @@ StlEnumStatement StlParser::enumDeclaration() while (!check(StlTokenType::rightBrace) && !isAtEnd()) { + previousComment_ = {}; + while (check(StlTokenType::comment)) + { + previousComment_.push_back(consume(StlTokenType::comment, {"expecting comment"})); + } + + if (!previousComment_.empty() && (check(StlTokenType::rightBrace) || isAtEnd())) + { + break; + } + statement.enumerators.push_back(enumeratorDeclaration()); // If next one is a comment and not a comma, we should be at the end of the enumeration @@ -307,6 +332,9 @@ StlEnumeratorStatement StlParser::enumeratorDeclaration() StlEnumeratorStatement statement; + statement.description = previousComment_; + previousComment_.clear(); + // maybe we have a comment before the enumerator while (check(StlTokenType::comment)) { @@ -450,6 +478,17 @@ StlVariantStatement StlParser::variantDeclaration() while (!check(StlTokenType::rightBrace) && !isAtEnd()) { + previousComment_ = {}; + while (check(StlTokenType::comment)) + { + previousComment_.push_back(consume(StlTokenType::comment, {"expecting comment"})); + } + + if (!previousComment_.empty() && (check(StlTokenType::rightBrace) || isAtEnd())) + { + break; + } + statement.elements.push_back(variantElementDeclaration()); } @@ -464,6 +503,9 @@ StlVariantElement StlParser::variantElementDeclaration() StlVariantElement statement; + statement.description = previousComment_; + previousComment_.clear(); + // maybe we have a comment before the variant element while (check(StlTokenType::comment)) { diff --git a/libs/core/src/meta/property.cpp b/libs/core/src/meta/property.cpp index 3cfd2912..11f03e6f 100644 --- a/libs/core/src/meta/property.cpp +++ b/libs/core/src/meta/property.cpp @@ -13,7 +13,6 @@ // sen #include "sen/core/base/assert.h" #include "sen/core/base/hash32.h" -#include "sen/core/base/span.h" #include "sen/core/meta/callable.h" #include "sen/core/meta/event.h" #include "sen/core/meta/method.h" @@ -49,90 +48,14 @@ std::string makeExplanation(const PropertySpec& spec, std::string_view message) sen::throwRuntimeError(makeExplanation(spec, message)); } -/// Throws if the property does not have all the required fields -void checkRequiredFields(const PropertySpec& spec) { impl::checkMemberName(spec.name); } - -/// Throws if the getter method is not valid -void checkGetterMethod(const PropertySpec& spec, const Method& getter) -{ - // getters shall have no arguments - if (!getter.getArgs().empty()) - { - throwError(spec, "getter method takes arguments"); - } - - // getters shall be constant - if (getter.getConstness() != Constness::constant) - { - throwError(spec, "getter method is not constant"); - } - - // getters shall return something - if (getter.getReturnType()->isVoidType()) - { - throwError(spec, "getter method does not return anything"); - } - - // getters shall return the same type as the property - if (getter.getReturnType() != spec.type) - { - throwError(spec, "getter method returns a different type"); - } - - // getters naming shall be consistent with our expectations - if (getter.getName() != Property::makeGetterMethodName(spec)) - { - throwError(spec, "getter method has an invalid name"); - } -} - -/// Throws if the setter method is not valid -void checkSetterMethod(const PropertySpec& spec, const Method& setter) +/// Throws if the property does not have all the required fields or has invalid types +void checkRequiredFields(const PropertySpec& spec) { - // setters shall have only one argument - if (setter.getArgs().size() != 1U) - { - throwError(spec, "setter does not have one argument"); - } - - // setters shall be non-constant - if (setter.getConstness() == Constness::constant) - { - throwError(spec, "setter method is constant"); - } - - // setters shall not return - if (!setter.getReturnType()->isVoidType()) - { - throwError(spec, "setter method returns something"); - } - - // setters shall take the same type as the property - if (setter.getArgs()[0U].type != spec.type) - { - throwError(spec, "setter method takes a different type"); - } - - // setters naming shall be consistent with our expectations - if (setter.getName() != Property::makeSetterMethodName(spec)) - { - throwError(spec, "setter method has an invalid name"); - } -} - -/// Throws if the change notification event is not valid -void checkChangeNotificationEvent(const PropertySpec& spec, const Event& changeEvent) -{ - // change events shall have no arguments - if (!changeEvent.getArgs().empty()) - { - throwError(spec, "change event has arguments"); - } + impl::checkMemberName(spec.name); - // event naming shall be consistent with our expectations - if (changeEvent.getName() != Property::makeChangeNotificationEventName(spec)) + if (spec.type->isVoidType()) { - throwError(spec, "change event has an invalid name"); + throwError(spec, "property cannot be of void type"); } } @@ -180,11 +103,6 @@ Property::Property(PropertySpec spec, Private /*priv*/) changeEventSpec.callableSpec.transportMode = spec_.transportMode; changeEvent_ = Event::make(changeEventSpec); } - - // just to be safe - checkGetterMethod(spec_, *getterMethod_); // check the getter method - checkSetterMethod(spec_, *setterMethod_); // check the setter method - checkChangeNotificationEvent(spec_, *changeEvent_); // check the event } std::shared_ptr Property::make(PropertySpec spec) diff --git a/libs/core/test/CMakeLists.txt b/libs/core/test/CMakeLists.txt index 5730b2fa..93a3f909 100644 --- a/libs/core/test/CMakeLists.txt +++ b/libs/core/test/CMakeLists.txt @@ -128,6 +128,7 @@ add_sen_unit_test_suite( sen::util PRIVATE cpptrace::cpptrace + nlohmann_json::nlohmann_json core_test_io_generated core_test_meta_generated core_test_obj_generated diff --git a/libs/core/test/io/serialization_traits_test.cpp b/libs/core/test/io/serialization_traits_test.cpp index 278d7bd6..9d24a5df 100644 --- a/libs/core/test/io/serialization_traits_test.cpp +++ b/libs/core/test/io/serialization_traits_test.cpp @@ -6,19 +6,39 @@ // ===================================================================================================================== // sen +#include "sen/core/base/duration.h" #include "sen/core/base/numbers.h" #include "sen/core/base/timestamp.h" #include "sen/core/io/detail/serialization_traits.h" +#include "sen/core/meta/enum_traits.h" +#include "sen/core/meta/native_types.h" +#include "sen/core/meta/optional_traits.h" +#include "sen/core/meta/sequence_traits.h" +#include "sen/core/meta/time_types.h" +#include "sen/core/meta/type_traits.h" + +// generated code +#include "stl/test_struct_traits.stl.h" +#include "stl/test_variant_traits.stl.h" // google test #include +// json +#include + // std +#include +#include #include +#include #include +#include #include #include +#include #include +#include #include using sen::impl::allowsContiguousIO; @@ -27,6 +47,9 @@ using sen::impl::isNumeric; using sen::impl::isPureIntegral; using sen::impl::IsStreamable; +namespace +{ + struct TrivialType { int32_t x; @@ -37,11 +60,40 @@ enum class TestEnum8 : uint8_t { value = 1 }; + enum class TestEnum32 : uint32_t { value = 100 }; +} // namespace + +namespace sen +{ + +template <> +struct StringConversionTraits +{ + static std::string_view toString(TestEnum8 val) + { + if (val == TestEnum8::value) + { + return "value"; + } + return "unknown"; + } + static TestEnum8 fromString(std::string_view val) + { + if (val == "value") + { + return TestEnum8::value; + } + throw std::runtime_error("Invalid enum"); + } +}; + +} // namespace sen + /// @test /// Check pure integral types /// @requirements(SEN-1052) @@ -240,3 +292,208 @@ TEST(SerializationTraits, getSerializedSizeTimeStamp) const sen::TimeStamp ts; EXPECT_EQ(sen::impl::getSerializedSize(ts), sen::impl::getSerializedSize(int64_t {0})); } + +/// @test +/// Checks JSON string conversion for native numeric types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, NativeTypeConversion) +{ + constexpr uint32_t original = 8080U; + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_number_unsigned()); + EXPECT_EQ(jsonObj.get(), 8080U); + + uint32_t recovered = 0U; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for string types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, StringTypeConversion) +{ + const std::string original = "String Test"; + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_string()); + EXPECT_EQ(jsonObj.get(), "String Test"); + + std::string recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for sequence types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, SequenceConversion) +{ + const std::vector original = {256, 512, 1024}; + const std::string jsonStr = sen::SequenceTraitsBase>::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_array()); + ASSERT_EQ(jsonObj.size(), 3U); + EXPECT_EQ(jsonObj[0].get(), 256); + EXPECT_EQ(jsonObj[1].get(), 512); + EXPECT_EQ(jsonObj[2].get(), 1024); + + std::vector recovered; + sen::SequenceTraitsBase>::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for Duration type +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, DurationConversion) +{ + const sen::Duration original(5000); + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_number_integer()); + EXPECT_EQ(jsonObj.get(), 5000); + + sen::Duration recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for TimeStamp type +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, TimeStampConversion) +{ + const sen::TimeStamp original(sen::Duration(0)); + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_string()); + EXPECT_EQ(jsonObj.get(), "1970-01-01 00:00:00 000000"); + + sen::TimeStamp recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + + const auto timeDifference = + std::abs(original.sinceEpoch().getNanoseconds() - recovered.sinceEpoch().getNanoseconds()); + constexpr auto maxTimezoneOffset = + std::chrono::duration_cast(std::chrono::hours(24)).count(); + EXPECT_LE(timeDifference, maxTimezoneOffset); +} + +/// @test +/// Checks JSON string conversion for optional types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, OptionalConversion) +{ + constexpr std::optional originalWithValue = 128; + const std::string jsonStrWithValue = sen::OptionalTraitsBase>::toJsonString(originalWithValue); + + auto jsonObjWithValue = nlohmann::json::parse(jsonStrWithValue); + EXPECT_TRUE(jsonObjWithValue.is_number_integer()); + EXPECT_EQ(jsonObjWithValue.get(), 128); + + std::optional recoveredWithValue; + sen::OptionalTraitsBase>::fromJsonString(jsonStrWithValue, recoveredWithValue); + EXPECT_TRUE(recoveredWithValue.has_value()); + EXPECT_EQ(originalWithValue.value(), recoveredWithValue.value()); + + constexpr std::optional originalEmpty = std::nullopt; + const std::string jsonStrEmpty = sen::OptionalTraitsBase>::toJsonString(originalEmpty); + + auto jsonObjEmpty = nlohmann::json::parse(jsonStrEmpty); + EXPECT_TRUE(jsonObjEmpty.is_null()); + + std::optional recoveredEmpty; + sen::OptionalTraitsBase>::fromJsonString(jsonStrEmpty, recoveredEmpty); + EXPECT_FALSE(recoveredEmpty.has_value()); +} + +/// @test +/// Checks JSON string conversion for enum types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, EnumConversion) +{ + constexpr auto original = TestEnum8::value; + const std::string jsonStr = sen::EnumTraitsBase::toJsonString(original); + + const auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_number_unsigned()); + EXPECT_EQ(jsonObj.get(), static_cast(TestEnum8::value)); + + TestEnum8 recovered; + sen::EnumTraitsBase::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for array and sequence types +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, ArrayConversion) +{ + constexpr std::array original = {192, 168, 1}; + const std::string jsonStr = sen::ArrayTraitsBase>::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_array()); + ASSERT_EQ(jsonObj.size(), 3U); + EXPECT_EQ(jsonObj[1].get(), 168); + + std::array recovered {}; + sen::ArrayTraitsBase>::fromJsonString(jsonStr, recovered); + EXPECT_EQ(original, recovered); +} + +/// @test +/// Checks JSON string conversion for structs +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, StructConversion) +{ + test_struct_traits::MyStructWithNativeFieldsOnly original; + original.field1 = 4096U; + original.field2 = "StructTest"; + original.field3 = 3.14f; + + const std::string jsonStr = + sen::SerializationTraits::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_object()); + EXPECT_EQ(jsonObj["field1"].get(), 4096U); + EXPECT_EQ(jsonObj["field2"].get(), "StructTest"); + EXPECT_FLOAT_EQ(jsonObj["field3"].get(), 3.14f); + + test_struct_traits::MyStructWithNativeFieldsOnly recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + + EXPECT_EQ(original.field1, recovered.field1); + EXPECT_EQ(original.field2, recovered.field2); + EXPECT_FLOAT_EQ(original.field3, recovered.field3); +} + +/// @test +/// Checks JSON string conversion for variants +/// @requirements(SEN-1052) +TEST(SerializationTraitsJsonTest, VariantConversion) +{ + test_variant_traits::MockVariant original; + original.emplace<2>(9999U); + + const std::string jsonStr = sen::SerializationTraits::toJsonString(original); + + auto jsonObj = nlohmann::json::parse(jsonStr); + EXPECT_TRUE(jsonObj.is_object()); + EXPECT_EQ(jsonObj["type"].get(), 2U); + EXPECT_EQ(jsonObj["value"].get(), 9999U); + + test_variant_traits::MockVariant recovered; + sen::SerializationTraits::fromJsonString(jsonStr, recovered); + + ASSERT_EQ(recovered.index(), 2U); + EXPECT_EQ(std::get<2>(recovered), 9999U); +} diff --git a/libs/core/test/lang/parser_test.cpp b/libs/core/test/lang/parser_test.cpp index 2834621b..7db60d7b 100644 --- a/libs/core/test/lang/parser_test.cpp +++ b/libs/core/test/lang/parser_test.cpp @@ -437,3 +437,131 @@ class Person EXPECT_ANY_THROW(auto statements = parser.parse()); } } + +/// @test +/// Checks that the last field of structs, enums and variants can end with a comma. +/// It also checks that they can have a comment line before the closing brace +/// @requirements(SEN-903) +TEST(Parser, trailingCommasAndFloatingComments) +{ + const std::string program = + R"( +// A struct whose last field has a comma, and which has a line containing a comment before the closing brace +struct CPrint +{ + flags : u32, + color : u32, + text : string, // last element ends with a coma + + // extra : string +} + +// An enum whose last enumerator has a comma, and which has a line containing a comment before the closing brace +enum OsKind : u8 +{ + windowsOs, // Microsoft Windows + linuxOs, // Linux + androidOs, // Android Linux + appleOs, // Apple OS (iOS, tvOS, etc..) + unixOs, // all unices not caught above + posixOs, // Posix + otherOs, // other, unknown + // myFirstOs + // mySecondOs +} + +// A variant whose last element has a comma, and which has a line containing a comment before the closing brace +variant TerminalCommand +{ + HideCursor, + ShowCursor, + SaveCursorPosition, + RestoreCursorPosition, + MoveCursorLeft, + MoveCursorRight, + MoveCursorUp, + MoveCursorDown, + Print, + CPrint, // last element ends with a coma and its description should not include 'customCommand' next comment + +// customCommand +} + + )"; + + StlScanner scanner(program); + const auto tokens = scanner.scanTokens(); + EXPECT_FALSE(tokens.empty()); + + StlParser parser(tokens); + const auto statements = parser.parse(); + + ASSERT_EQ(3U, statements.size()); + + // Struct + ASSERT_TRUE(std::holds_alternative(statements[0])); + const auto& structure = std::get(statements[0]); + ASSERT_EQ(3U, structure.fields.size()); + EXPECT_EQ("flags", structure.fields[0].identifier.lexeme()); + EXPECT_EQ("text", structure.fields[2].identifier.lexeme()); + ASSERT_EQ(0U, structure.fields[1].description.size()); + EXPECT_EQ("last element ends with a coma", structure.fields[2].description[0].lexeme()); + + // Enum + ASSERT_TRUE(std::holds_alternative(statements[1])); + const auto& enumeration = std::get(statements[1]); + ASSERT_EQ(7U, enumeration.enumerators.size()); + EXPECT_EQ("windowsOs", enumeration.enumerators[0].identifier.lexeme()); + EXPECT_EQ("linuxOs", enumeration.enumerators[1].identifier.lexeme()); + EXPECT_EQ("otherOs", enumeration.enumerators[6].identifier.lexeme()); + + // Variant + ASSERT_TRUE(std::holds_alternative(statements[2])); + const auto& variant = std::get(statements[2]); + ASSERT_EQ(10U, variant.elements.size()); + EXPECT_EQ("last element ends with a coma and its description should not include 'customCommand' next comment", + variant.elements[9].description[0].lexeme()); +} + +/// @test +/// Checks that structures, enums and variants can be empty with only a comment inside +/// @requirements(SEN-903) +TEST(Parser, emptyBlocksWithFloatingComments) +{ + const std::string program = + R"( +struct EmptyStruct { + // Test comment +} + +enum EmptyEnum : u8 { + // Test comment +} + +variant EmptyVariant { + // Test comment +} + + )"; + + StlScanner scanner(program); + const auto tokens = scanner.scanTokens(); + EXPECT_FALSE(tokens.empty()); + + StlParser parser(tokens); + const auto statements = parser.parse(); + + ASSERT_EQ(3U, statements.size()); + + // Struct + ASSERT_TRUE(std::holds_alternative(statements[0])); + EXPECT_EQ(0U, std::get(statements[0]).fields.size()); + + // Enum + ASSERT_TRUE(std::holds_alternative(statements[1])); + EXPECT_EQ(0U, std::get(statements[1]).enumerators.size()); + + // Variant + ASSERT_TRUE(std::holds_alternative(statements[2])); + EXPECT_EQ(0U, std::get(statements[2]).elements.size()); +} diff --git a/libs/core/test/meta/property_test.cpp b/libs/core/test/meta/property_test.cpp index 688dea6c..0abc426f 100644 --- a/libs/core/test/meta/property_test.cpp +++ b/libs/core/test/meta/property_test.cpp @@ -400,3 +400,40 @@ TEST(Property, setter) EXPECT_EQ(result, "setNextEventSetter"); } } + +/// @test +/// Checks rejection of void type property +/// @requirements(SEN-355) +TEST(Property, voidTypeProperty) +{ + auto spec = getValidSpec(); + spec.type = sen::VoidType::get(); + EXPECT_THROW(std::ignore = Property::make(spec), std::exception); +} + +/// @test +/// Checks the internal structure, arguments, and return types of the generated getter, setter, and event +/// @requirements(SEN-355) +TEST(Property, internalGenerators) +{ + const auto& spec = getValidSpec(); + const auto instance = Property::make(spec); + + const auto& getter = instance->getGetterMethod(); + EXPECT_TRUE(getter.getArgs().empty()); + EXPECT_EQ(getter.getConstness(), Constness::constant); + EXPECT_FALSE(getter.getReturnType()->isVoidType()); + EXPECT_EQ(getter.getReturnType(), spec.type); + EXPECT_EQ(getter.getName(), Property::makeGetterMethodName(spec)); + + const auto& setter = instance->getSetterMethod(); + EXPECT_EQ(setter.getArgs().size(), 1U); + EXPECT_EQ(setter.getConstness(), Constness::nonConstant); + EXPECT_TRUE(setter.getReturnType()->isVoidType()); + EXPECT_EQ(setter.getArgs()[0U].type, spec.type); + EXPECT_EQ(setter.getName(), Property::makeSetterMethodName(spec)); + + const auto& changeEvent = instance->getChangeEvent(); + EXPECT_TRUE(changeEvent.getArgs().empty()); + EXPECT_EQ(changeEvent.getName(), Property::makeChangeNotificationEventName(spec)); +} diff --git a/libs/db/src/input.cpp b/libs/db/src/input.cpp index 897cf942..242c6c31 100644 --- a/libs/db/src/input.cpp +++ b/libs/db/src/input.cpp @@ -315,6 +315,11 @@ class Input::Impl { readIndexes(); + if (keyframeIndexes_.empty()) + { + return std::nullopt; + } + auto iterGeq = std::lower_bound(keyframeIndexes_.begin(), keyframeIndexes_.end(), time, @@ -325,6 +330,11 @@ class Input::Impl return *iterGeq; } + if (iterGeq == keyframeIndexes_.end()) + { + return keyframeIndexes_.back(); + } + const auto& prev = *(iterGeq - 1); const auto a = prev.time.sinceEpoch().get(); const auto b = iterGeq->time.sinceEpoch().get(); diff --git a/libs/db/test/CMakeLists.txt b/libs/db/test/CMakeLists.txt index 37feaf7d..45143478 100644 --- a/libs/db/test/CMakeLists.txt +++ b/libs/db/test/CMakeLists.txt @@ -25,6 +25,7 @@ add_sen_unit_test_suite( sen::core sen::kernel sen::db + archive_test_helpers ) sen_generate_cpp( diff --git a/libs/db/test/creation_test.cpp b/libs/db/test/creation_test.cpp index c2bd4934..2958fde8 100644 --- a/libs/db/test/creation_test.cpp +++ b/libs/db/test/creation_test.cpp @@ -35,17 +35,13 @@ TEST(CreationTest, WriteAndReadCreationWithRealObject) TempDir tempDir; SingleClassSetup setup; - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {setup.object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(setup.object); output.creation(setup.kernel->getTime(), info, true); setup.kernel->step(); @@ -92,17 +88,13 @@ TEST(CreationTest, IndexedCreationAppearsInObjectIndex) TempDir tempDir; SingleClassSetup setup; - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {setup.object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(setup.object); output.creation(setup.kernel->getTime(), info, true); setup.kernel->step(); @@ -139,17 +131,13 @@ TEST(CreationTest, NonIndexedCreationNotInObjectIndex) sen::kernel::TestKernel kernel(&component); kernel.step(); - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(object); output.creation(kernel.getTime(), info, false); kernel.step(); output.keyframe(kernel.getTime(), {}); diff --git a/libs/db/test/db_test_helpers.h b/libs/db/test/db_test_helpers.h index e89c597c..891227fa 100644 --- a/libs/db/test/db_test_helpers.h +++ b/libs/db/test/db_test_helpers.h @@ -10,56 +10,33 @@ #ifndef SEN_DB_TEST_HELPERS_H #define SEN_DB_TEST_HELPERS_H -#include "stl/db_test_class.stl.h" +// shared test helpers +#include "archive_test_helpers.h" // sen #include "sen/core/base/compiler_macros.h" -#include "sen/core/base/uuid.h" #include "sen/kernel/component.h" #include "sen/kernel/component_api.h" #include "sen/kernel/test_kernel.h" +// generated code +#include "stl/db_test_class.stl.h" + // std #include -#include #include #include namespace sen::db::test { -class TempDir -{ -public: - TempDir(): path_(std::filesystem::temp_directory_path() / ("db_test_" + getRandomPathPostFix())) - { - std::filesystem::create_directories(path_); - } - - // prevent object copy and movable type - TempDir(const TempDir&) = delete; - TempDir& operator=(const TempDir&) = delete; - - TempDir(TempDir&&) = delete; - TempDir& operator=(TempDir&&) = delete; - - ~TempDir() - { - if (std::filesystem::exists(path_)) - { - std::filesystem::remove_all(path_); - } - } - - [[nodiscard]] const std::filesystem::path& path() const { return path_; } - -private: - /// Returns a random post fix for temporary files paths. - static std::string getRandomPathPostFix() { return sen::UuidRandomGenerator()().toString(); } - -private: - std::filesystem::path path_; -}; +using sen::test::firstEventId; +using sen::test::firstPropertyId; +using sen::test::makeArchivePath; +using sen::test::makeArchiveSettings; +using sen::test::makeObjectInfo; +using sen::test::makeTime; +using sen::test::TempDir; class TestObjImpl: public db_test::TestObjBase { diff --git a/libs/db/test/input_test.cpp b/libs/db/test/input_test.cpp index e2da2695..5caa6178 100644 --- a/libs/db/test/input_test.cpp +++ b/libs/db/test/input_test.cpp @@ -38,12 +38,8 @@ namespace sen::db::test /// Helper function to create a valir archive with one keyframe for corruption tests static std::filesystem::path createValidArchive(const std::filesystem::path& baseDir, sen::kernel::TestKernel& kernel) { - OutSettings settings; - settings.name = "test"; - settings.folder = baseDir.string(); - settings.indexKeyframes = true; - - const auto archivePath = baseDir / settings.name; + auto settings = makeArchiveSettings("test", baseDir); + const auto archivePath = makeArchivePath("test", baseDir); { Output output(std::move(settings), []() {}); @@ -103,17 +99,13 @@ TEST(InputTest, OpenRecordingWithOneKeyframe) const auto* propMeta = classType.type()->searchPropertyByName("speed"); ASSERT_NE(propMeta, nullptr); - OutSettings settings; - settings.name = "test"; - settings.folder = tempDir.path().string(); - settings.indexKeyframes = true; - - const auto archivePath = tempDir.path() / settings.name; + auto settings = makeArchiveSettings("test", tempDir); + const auto archivePath = makeArchivePath("test", tempDir); { Output output(std::move(settings), []() {}); - ObjectInfo info = {object.get(), "test_session", "test_bus"}; + auto info = makeObjectInfo(object); output.creation(kernel.getTime(), info, true); ::sen::kernel::Buffer propBuf; diff --git a/libs/kernel/src/bus/bus.cpp b/libs/kernel/src/bus/bus.cpp index d83ab082..ec71b4a9 100644 --- a/libs/kernel/src/bus/bus.cpp +++ b/libs/kernel/src/bus/bus.cpp @@ -206,20 +206,20 @@ void Bus::removeRemotesFromProcess(ProcessId processId) void Bus::remoteMessageReceived(ObjectOwnerId to, Span msg) { Lock lock(remotesMutex_); - if (auto remotesItr = remotes_.find(to); remotesItr != remotes_.end()) - { - InputStream in(msg); - // read the header - uint8_t categoryVal = 0; - in.readUInt8(categoryVal); + InputStream in(msg); - sendMessageToParticipant(static_cast(categoryVal), in, *(*remotesItr).second); - } - else + // read the header + uint8_t categoryVal = 0; + in.readUInt8(categoryVal); + + if (const auto remotesItr = remotes_.find(to); remotesItr != remotes_.end()) { - logger_->debug("Bus {}.{}: remote message lost"); + sendMessageToParticipant(static_cast(categoryVal), in, *remotesItr->second); + return; } + + logger_->debug("Bus {}.{}: remote {} message lost.", address_.sessionName, address_.busName, categoryVal); } void Bus::remoteBroadcastMessageReceived(Span msg) diff --git a/libs/kernel/src/crash_reporter.cpp b/libs/kernel/src/crash_reporter.cpp index 04f4b094..4b0852a7 100644 --- a/libs/kernel/src/crash_reporter.cpp +++ b/libs/kernel/src/crash_reporter.cpp @@ -72,8 +72,8 @@ #include // with Apple we need to explicitly declare this as an external symbol -#ifdef __APPLE__ -extern "C" char** environ; +#if defined(__APPLE__) || defined(__linux__) +# include #endif namespace sen::kernel::impl diff --git a/libs/kernel/src/kernel_impl.cpp b/libs/kernel/src/kernel_impl.cpp index 3aa7a13f..1f91e5bd 100644 --- a/libs/kernel/src/kernel_impl.cpp +++ b/libs/kernel/src/kernel_impl.cpp @@ -279,7 +279,7 @@ void KernelImpl::sessionUnavailable(const std::string& name) const // NOSONAR std::shared_ptr KernelImpl::getKernelLogger() { - static std::shared_ptr logger = spdlog::stderr_color_mt("kernel"); + static std::shared_ptr logger = spdlog::stdout_color_mt("kernel"); return logger; } diff --git a/libs/kernel/src/posix/posix_api.cpp b/libs/kernel/src/posix/posix_api.cpp index 331b2e2a..6c26fee0 100644 --- a/libs/kernel/src/posix/posix_api.cpp +++ b/libs/kernel/src/posix/posix_api.cpp @@ -8,10 +8,7 @@ #include "./posix_api.h" // system -#ifdef __linux__ -# include -# include -#elif defined(__APPLE__) +#if defined(__APPLE__) # include "mach/mach.h" # include "mach/thread_act.h" # include "mach/thread_policy.h" diff --git a/libs/kernel/src/posix/thread_impl.cpp b/libs/kernel/src/posix/thread_impl.cpp index ef065383..562e55f0 100644 --- a/libs/kernel/src/posix/thread_impl.cpp +++ b/libs/kernel/src/posix/thread_impl.cpp @@ -16,11 +16,6 @@ // generated code #include "stl/sen/kernel/basic_types.stl.h" -// system -#ifdef __linux__ -# include -#endif - // other posix #include diff --git a/libs/kernel/test/CMakeLists.txt b/libs/kernel/test/CMakeLists.txt index f863b7e4..79b11ab3 100644 --- a/libs/kernel/test/CMakeLists.txt +++ b/libs/kernel/test/CMakeLists.txt @@ -14,4 +14,9 @@ if(NOT MSVC) add_subdirectory(integration/runtime_compatibility) add_subdirectory(integration/crash_report) add_subdirectory(integration/type_clash) + + # integration tests that use containers (only available in Linux) + if(LINUX) + add_subdirectory(integration/object_sync) + endif() endif() diff --git a/libs/kernel/test/integration/crash_report/CMakeLists.txt b/libs/kernel/test/integration/crash_report/CMakeLists.txt index f1f0bf3c..56d025cd 100644 --- a/libs/kernel/test/integration/crash_report/CMakeLists.txt +++ b/libs/kernel/test/integration/crash_report/CMakeLists.txt @@ -19,14 +19,13 @@ add_sen_package( ) set_target_properties(crash_report PROPERTIES FOLDER "test") -set_target_properties(crash_report PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_integration_test( kernel_crash_report_stacktrace_test COMMAND ${PYTEST_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/crash_report_tester.py - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_DEPS crash_report ) diff --git a/libs/kernel/test/integration/crash_report/crash_report_tester.py b/libs/kernel/test/integration/crash_report/crash_report_tester.py index 81f877ec..761ca30f 100644 --- a/libs/kernel/test/integration/crash_report/crash_report_tester.py +++ b/libs/kernel/test/integration/crash_report/crash_report_tester.py @@ -4,18 +4,23 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to specify the crash reporter test cases.""" -import subprocess import json import os -import re import platform +import re +import subprocess + def test_crash_reporter_generates_stacktrace(): + """Test to ensure that the crash reporter generates a stacktrace on failure.""" config_path = os.path.join(os.path.dirname(__file__), "config", "config.yaml") - sen_executable = 'sen' if platform.system() == 'Windows' else './sen' + sen_executable = "sen" if platform.system() == "Windows" else "./sen" - process = subprocess.Popen([sen_executable, 'run', config_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + process = subprocess.Popen( + [sen_executable, "run", config_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) _, stderr = process.communicate() match = re.search(r"Crash report written to (.*\.json)", stderr) @@ -25,7 +30,7 @@ def test_crash_reporter_generates_stacktrace(): assert os.path.exists(report_path), f"Crash report file does not exist: {report_path}" try: - with open(report_path, 'r') as f: + with open(report_path, encoding="utf-8") as f: data = json.load(f) error_data = data.get("errorData", {}) diff --git a/libs/kernel/test/integration/object_sync/CMakeLists.txt b/libs/kernel/test/integration/object_sync/CMakeLists.txt new file mode 100644 index 00000000..8990a8ff --- /dev/null +++ b/libs/kernel/test/integration/object_sync/CMakeLists.txt @@ -0,0 +1,433 @@ +# === CMakeLists.txt =================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +add_sen_package( + TARGET object_sync + STL_FILES stl/object_sync.stl + SOURCES src/object_sync.cpp + NO_SCHEMA + PRIVATE_DEPS spdlog::spdlog +) + +find_package(Python REQUIRED COMPONENTS Interpreter) + +function(generate_config_files DESTINATION) + file( + GLOB + templates + CONFIGURE_DEPENDS + config/*.in + ) + + set(template_names "") + foreach(full_path ${templates}) + get_filename_component(name_only ${full_path} NAME) + list(APPEND template_names ${name_only}) + endforeach() + + foreach(file ${template_names}) + string( + REPLACE ".in" + "" + output_file + ${file} + ) + configure_file(config/${file} ${DESTINATION}/${output_file} @ONLY) + endforeach() +endfunction() + +set(RYUK_IMAGE "docker-proxy.pforgeipt-docker.intra.airbusds.corp/testcontainers/ryuk:0.8.1") + +set(COMMON_TEST_ENV + "PYTHONUNBUFFERED=set:1;RYUK_CONTAINER_IMAGE=set:${RYUK_IMAGE};ASAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/asan_ignorelist.txt:fast_unwind_on_malloc=0:malloc_context_size=100;TSAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/tsan_ignorelist.txt;LSAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/lsan_ignorelist.txt:report_objects=1;UBSAN_OPTIONS=set:suppressions=/home/builder/sen/cmake/util/ubsan_ignorelist.txt" +) + +set(_working_dir $) + +set(LISTENER_TYPE ListenerStaticProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/1_static_props) + +add_sen_integration_test( + object_sync_static_props_local_test + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_static_props_local_test ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_static_props_single_process_test + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_static_props_single_process_test ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_static_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/1_static_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_static_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerBestEffortProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props) + +add_sen_integration_test( + object_sync_best_effort_props_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_props_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_props_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_props_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/2_best_effort_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerConfirmedProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props) + +add_sen_integration_test( + object_sync_confirmed_props_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_props_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_props_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_props_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/3_confirmed_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerWritableProps) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/4_writable_props) + +add_sen_integration_test( + object_sync_writable_props_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_writable_props_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_writable_props_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_writable_props_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_writable_props_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/4_writable_props/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_writable_props_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerBestEffortEvent) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events) + +add_sen_integration_test( + object_sync_best_effort_events_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_events_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_events_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_events_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_best_effort_events_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/5_best_effort_events/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_best_effort_events_two_processes ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerConfirmedEvent) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events) + +add_sen_integration_test( + object_sync_confirmed_events_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_events_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_events_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_events_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_confirmed_events_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/6_confirmed_events/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_confirmed_events_two_processes ${COMMON_TEST_ENV}) + +configure_file( + config/single_component.yaml.in ${CMAKE_CURRENT_BINARY_DIR}/7_local_method/single_component.yaml @ONLY +) + +add_sen_integration_test( + object_sync_local_method + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/7_local_method/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_local_method ${COMMON_TEST_ENV}) + +set(LISTENER_TYPE ListenerConstMethod) +generate_config_files(${CMAKE_CURRENT_BINARY_DIR}/8_const_method) + +add_sen_integration_test( + object_sync_const_method_local + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/single_component.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_const_method_local ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_const_method_single_process + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/single_process.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_const_method_single_process ${COMMON_TEST_ENV}) + +add_sen_integration_test( + object_sync_const_method_two_processes + COMMAND + ${Python_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/run.py + ${_working_dir} + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/publisher.yaml + ${CMAKE_CURRENT_BINARY_DIR}/8_const_method/listener.yaml + REQ_DEPS + object_sync py ether + kernel + FLAKY # marked as flaky (sanitizer tickets have been created and the test can fail around once every 100 runs) +) + +# link them so the checker only runs if the generator succeeds +append_test_env_modification(object_sync_const_method_two_processes ${COMMON_TEST_ENV}) diff --git a/libs/kernel/test/integration/object_sync/config/listener.yaml.in b/libs/kernel/test/integration/object_sync/config/listener.yaml.in new file mode 100644 index 00000000..a99268a0 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/listener.yaml.in @@ -0,0 +1,52 @@ +kernel: + crashReportDisabled: true + +load: + - name: ether + group: 10 + discovery: + type: TcpDiscovery + value: + beamPeriod: 100 ms + beamExpiryTime: 1 s + hubAddress: + host: sen-hub + port: 65454 + +build: + - name: listenerComp1 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener1 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp2 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener2 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp3 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener3 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE name = "testObject" + bus: session.bus + - name: listenerComp4 + group: 1 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener4 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE staticProp = 15 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/config/publisher.yaml.in b/libs/kernel/test/integration/object_sync/config/publisher.yaml.in new file mode 100644 index 00000000..afeccfab --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/publisher.yaml.in @@ -0,0 +1,26 @@ +kernel: + crashReportDisabled: true + +load: + - name: ether + group: 10 + runDiscoveryHub: 65454 + discovery: + type: TcpDiscovery + value: + beamPeriod: 100 ms + beamExpiryTime: 1 s + hubAddress: + host: sen-hub + port: 65454 + +build: + - name: myComponent + group: 1 + freqHz: 30 + imports: [object_sync] + objects: + - name: publisher + class: object_sync.PublisherImpl + numOfListeners: 4 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/config/single_component.yaml.in b/libs/kernel/test/integration/object_sync/config/single_component.yaml.in new file mode 100644 index 00000000..73bf895c --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/single_component.yaml.in @@ -0,0 +1,29 @@ +kernel: + crashReportDisabled: true + +build: + - name: myComponent + group: 3 + freqHz: 50 + imports: [object_sync] + objects: + - name: publisher + class: object_sync.PublisherImpl + numOfListeners: 4 + bus: session.bus + - name: listener1 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listener2 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listener3 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE name = "testObject" + bus: session.bus + - name: listener4 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE staticProp = 15 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/config/single_process.yaml.in b/libs/kernel/test/integration/object_sync/config/single_process.yaml.in new file mode 100644 index 00000000..ca14df89 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/config/single_process.yaml.in @@ -0,0 +1,62 @@ +kernel: + crashReportDisabled: true + +load: + - name: ether + group: 10 + runDiscoveryHub: 65454 + discovery: + type: TcpDiscovery + value: + beamPeriod: 100 ms + beamExpiryTime: 1 s + hubAddress: + host: sen-hub + port: 65454 + +build: + - name: myComponent + group: 3 + freqHz: 30 + imports: [object_sync] + objects: + - name: publisher + class: object_sync.PublisherImpl + numOfListeners: 4 + bus: session.bus + - name: listenerComp1 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener1 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp2 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener2 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus + bus: session.bus + - name: listenerComp3 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener3 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE name = "testObject" + bus: session.bus + - name: listenerComp4 + group: 3 + freqHz: 60 + imports: [object_sync] + objects: + - name: listener4 + class: object_sync.@LISTENER_TYPE@ + query: SELECT object_sync.TestObject FROM session.bus WHERE staticProp = 15 + bus: session.bus diff --git a/libs/kernel/test/integration/object_sync/readme.md b/libs/kernel/test/integration/object_sync/readme.md new file mode 100644 index 00000000..5e4100ae --- /dev/null +++ b/libs/kernel/test/integration/object_sync/readme.md @@ -0,0 +1,77 @@ +# Object Synchronization integration tests + +These tests check data correctness in the communication between kernel participants in different +network configurations. + +The following objects are involved: + +- _publisher_: This object is published in the kernel pipeline configuration (yaml) and is in charge + of publishing the object under test, which will be detected by the listeners. It is used to + correctly sync with the listeners and avoid timing errors when running the listeners in a separate + component. + +- _testObject_: Contains all properties, events and methods whose correctness will be tested. It is + added and removed from the bus by the publisher. It contains a method called `doUpdate()`, which + will be called by the publisher when it certifies that the listeners have all detected the object. + This `doUpdate()` method enables automatic property updates and event emissions on every update of + the object (implemented in the `update()` method). + +- _listeners_: Collection of listener objects, four in the current version of the test, which + detect the test object and check whether the property updates, events or method responses received + are the expected ones. Each test checks a certain class member (e.g. confirmed property updates), + and in order to implement the specific checks for each test, various listener classes are derived + from a base `ListenerImpl` class. Additionally, each of the listeners have a configurable query + for the interest in the object, allowing to test different local participants with the same, or + different interests. + +The tests perform each check in three network configurations: + +- `Local`: The publisher, testObject and listeners all run in the same component. Local method + calling can be checked in this configuration. +- `Single process`: The publisher and the listeners run all in separated components, but all in the + same process. This asserts the correct functioning of the kernel with several local participants + in multiple threads (components). +- `Two processes`: The listeners are all in a different process, and also in different components + themselves, testing the detection of a remote object published by a remote participant by several + local participants in different threads. + +The implementation of the publisher and listeners contains logic that shuts down the kernels if the +checks pass. `run.py` just launches the processes, spits the logs through stdout, and exits with a +timeout (5 seconds) if processes do not exit automatically before. Any communication error between +the participants results in a timeout error in the integration test. If the error is in the +correctness of the data, assert statements will indicate precisely what failed. Each process is +executed in an isolated container spawned from `run.py` using the `test_containers` python library. + +The publisher and listeners interact in the following way (in a successful test): + +``` + ┌───────────┐ ┌─────────────┐ + │ Publisher │ │ Listeners │ + └─────┬─────┘ └──────┬──────┘ + │ │ + ▼ │ +(1) all listeners detected? │ + (add testObject) │ + │ ▼ + │ (2) test object detected? + │ (state -> ready) + ▼ │ + (3) all listeners ready? │ + (update testObject) │ + │ ▼ + │ (4) 10 updates received? + │ (state -> inSync) + ▼ │ + (5) all listeners inSync? │ + (remove testObject) │ + │ ▼ + │ (6) testObject removed? + │ (check correctness, state -> finished and kernel shutdown) + ▼ + (7) all listeners finished? + (kernel shutdown) + +``` + +As you can see from the diagram, both the listeners and publisher shutdown the kernel in case of a +correct execution. diff --git a/libs/kernel/test/integration/object_sync/run.py b/libs/kernel/test/integration/object_sync/run.py new file mode 100644 index 00000000..1d4083e6 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/run.py @@ -0,0 +1,159 @@ +# === run.py =========================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== +"""Module that implements the container test harness runner.""" + +# std +import os +import sys +import time +from pathlib import Path +from threading import Thread + +import docker +from testcontainers.core.container import DockerContainer + +# testcontainers +from testcontainers.core.network import Network + +# constants +# TODO (SEN-1681) replace with a lighter runtime image +IMAGE_NAME = "sim-csr-docker.pforgeipt-docker.intra.airbusds.corp/sen-build-debian12:0.1.0-31-ge5dd492" +TIMEOUT = 5 + + +def stream_logs(cont: DockerContainer) -> None: + """Formats and streams logs from the containers.""" + w = cont.get_wrapped_container() + for line in w.logs(stream=True, follow=True): + if line: + print(f"[{w.short_id}] {line.decode('utf-8', 'replace').strip()}") + + +def is_container() -> bool: + """Checks whether the script is running inside a container.""" + try: + return os.path.exists("/.dockerenv") or any( + k in open("/proc/self/cgroup", encoding="utf-8").read() for k in ("docker", "containerd") + ) + except (FileNotFoundError, PermissionError): + return False + + +def get_repo_root(start_path: Path) -> Path: + """Returns the repo root path from a given path.""" + for p in [start_path] + list(start_path.parents): + if (p / ".git").exists(): + return p + raise FileNotFoundError(f"Error: '{start_path}' is not inside a Git repo.") + + +def find_host_mount(container_path: Path) -> Path | None: + """Finds the directory in the host where a certain container path is mounted.""" + if not (mount_file := Path("/proc/self/mountinfo")).exists(): + raise FileNotFoundError("Could not access /proc/self/mountinfo. Host might not be a docker") + + ws = os.environ.get("WORKSPACE") + + mounts = list( + { + Path(ws if ws else parts[3]) + for line in open(mount_file, encoding="utf-8") + if str(container_path) in line and (parts := line.split()) + } + ) + + if len(mounts) > 1: + raise RuntimeError(f"Error: Multiple Sen repository mounts detected! {mounts}.") + return mounts[0] if mounts else (print("No Sen repository mounts found.") or None) + + +def check_image_availability(image: str) -> None: + """Reports an error if the container image is not found in the docker cache.""" + try: + client = docker.from_env() + client.images.get(image) + except docker.errors.ImageNotFound as err: + raise RuntimeError("Container Image not found in the local cache. Please pull the image first.") from err + except docker.errors.DockerException as e: + raise RuntimeError("Could not connect to the local Docker daemon.") from e + + +def abort(container_list: list[DockerContainer], thread_list: list[Thread]) -> None: + """Stops containers and joins log threads before aborting with error.""" + for container in container_list: + container.stop() + + for t in thread_list: + if isinstance(t, Thread): + t.join() + + sys.exit(1) + + +if __name__ == "__main__": + if len(sys.argv) < 3: + sys.exit("Usage: python run.py ...") + + cmake_workdir = sys.argv[1] + configs = sys.argv[2:] + + check_image_availability(IMAGE_NAME) + + # repo root dir in the environment used in cmake (it can be the host machine or a container) + cmake_repo_root = get_repo_root(Path(__file__).parent) + + # repo root dir in the host machine (if cmake is being configured in a container) + host_repo_root = find_host_mount(cmake_repo_root) if is_container() else cmake_repo_root + + # repo root dir in the container used to execute the integration tests + test_repo_root = Path("/home/builder/sen") + test_workdir = test_repo_root / Path(cmake_workdir).relative_to(cmake_repo_root) + + # container paths + with Network() as network: + containers = [] + log_threads = [] + + for i, config in enumerate(configs): + aliases = ["sen-hub"] if i == 0 else [] + + # container definition + container = ( + DockerContainer(IMAGE_NAME) + .with_network(network) + .with_network_aliases(*aliases) + .with_volume_mapping(host_repo_root, "/home/builder/sen", mode="rw") + .with_command(f"./cli_run {test_repo_root / Path(config).relative_to(cmake_repo_root)}") + .with_kwargs(working_dir=str(test_workdir), cap_add=["SYS_ADMIN"], security_opt=["seccomp=unconfined"]) + ) + + # start the container (with the host environment) and the log thread + container.env.update(os.environ) + container.start() + log_threads.append(Thread(target=stream_logs, args=(container,), daemon=True).start()) + containers.append(container) + + deadline = time.time() + TIMEOUT + while time.time() < deadline: + wrapped = [c.get_wrapped_container() for c in containers] + for w in wrapped: + w.reload() + + # abort if any of the processes has exited with error + if any(w.status == "exited" and w.wait()["StatusCode"] != 0 for w in wrapped): + abort(containers, log_threads) + + # pass the test if all processes have exited successfully + if all(w.status == "exited" for w in wrapped): + # join the logger threads + list(map(Thread.join, [t for t in log_threads if isinstance(t, Thread)])) + break + + time.sleep(0.2) + else: + print(f"\nError: timeout of {TIMEOUT}s reached!") + abort(containers, log_threads) diff --git a/libs/kernel/test/integration/object_sync/src/object_sync.cpp b/libs/kernel/test/integration/object_sync/src/object_sync.cpp new file mode 100644 index 00000000..88b353b0 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/src/object_sync.cpp @@ -0,0 +1,863 @@ +// === object_sync.cpp ================================================================================================= +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +#include "stl/object_sync.stl.h" + +// sen +#include "sen/core/base/assert.h" +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/numbers.h" +#include "sen/core/meta/class_type.h" +#include "sen/core/meta/var.h" +#include "sen/core/obj/connection_guard.h" +#include "sen/core/obj/interest.h" +#include "sen/core/obj/object.h" +#include "sen/core/obj/object_list.h" +#include "sen/core/obj/object_source.h" +#include "sen/core/obj/subscription.h" +#include "sen/kernel/component_api.h" + +// spdlog +#include +#include + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace object_sync +{ + +namespace +{ + +constexpr std::size_t generatorSeed = 2155648U; +constexpr uint32_t numOfChecks = 10U; +constexpr uint8_t staticPropValue = 15U; +constexpr auto staticNoConfigPropValue = TestEnum::second; + +[[nodiscard]] std::string generateString(std::mt19937& gen, const int length = 10) +{ + static constexpr std::string_view charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + std::string result; + result.reserve(length); + std::uniform_int_distribution dist(0, charset.size() - 1); + + std::generate_n(std::back_inserter(result), length, [&]() { return charset[dist(gen)]; }); + + return result; +} + +[[nodiscard]] TestStruct generateStruct(std::mt19937& gen) +{ + std::uniform_int_distribution distInteger; + std::uniform_real_distribution distFloat(0.0f, 1000.0f); + std::uniform_int_distribution distStrLen(1U, 6U); + + return {distInteger(gen), generateString(gen, static_cast(distStrLen(gen))), distFloat(gen)}; +} + +} // namespace + +/// Object used when testing class member synchronization +class TestObjectImpl final: public TestObjectBase +{ +public: + SEN_NOCOPY_NOMOVE(TestObjectImpl) + +public: + TestObjectImpl(const std::string& name, const u8 staticProp) + : TestObjectBase(name, staticProp) + , logger_(spdlog::stdout_color_mt(name)) + , gen_(generatorSeed) + , localMethodGen_(generatorSeed) + { + // set static no config property + setNextStaticNoConfigProp(staticNoConfigPropValue); + } + + ~TestObjectImpl() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + + if (!doUpdate_) + { + return; + } + + // keeps track of the number of updates + setNextUpdateId(++updateCounter_); + + // update best effort prop + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + setNextBestEffortProp(std::uniform_real_distribution()(gen_)); + + // update confirmed prop + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + setNextConfirmedProp(generateStruct(gen_)); + + // best effort event + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + bestEffortEvent(updateCounter_, generateStruct(gen_)); + + // confirmed event + gen_.seed(generatorSeed); + gen_.discard(updateCounter_); + confirmedEvent(updateCounter_, generateString(gen_)); + } + +protected: + [[nodiscard]] u32 constMethodImpl(const TestEnum& arg) const override { return static_cast(arg); } + [[nodiscard]] u8 confirmedMethodImpl(u64 arg) override { return static_cast(arg); } + [[nodiscard]] bool bestEffortMethodImpl(const std::string& arg) override + { + std::ignore = arg; + return true; + } + [[nodiscard]] u16 localMethod() override { return localMethodDist_(gen_); } + void doUpdateImpl() override { doUpdate_ = true; } + +private: + std::shared_ptr logger_; + uint32_t updateCounter_ = 0U; + std::mt19937 gen_; + bool doUpdate_ = false; + + // method distributions + std::mt19937 localMethodGen_; + std::uniform_int_distribution localMethodDist_; +}; + +/// Publishes/Unpublishes the TestObject +class PublisherImpl final: public PublisherBase +{ +public: + SEN_NOCOPY_NOMOVE(PublisherImpl) + +public: + PublisherImpl(std::string name, const sen::VarMap& args) + : PublisherBase(name, args), logger_(spdlog::stdout_color_mt(name)) + { + } + + ~PublisherImpl() override = default; + +public: + void registered(sen::kernel::RegistrationApi& api) override + { + listenerStates_.reserve(getNumOfListeners()); + + // publish the test object once all listeners have been detected + guards_.emplace_back(onListenersReadyChanged({this, + [this]() + { + // publish the test object + object_->doUpdate(); + }})); + + // detect listeners (used for test shutdown) + listenerSub_ = api.selectAllFrom( + "session.bus", + [this, &api](const auto& addedObjects) + { + detectedListeners_ += std::distance(addedObjects.begin(), addedObjects.end()); + for (auto* listener: addedObjects) + { + listenerStates_[listener->asObject().getId()] = listener->getState(); + guards_.emplace_back( + listener->onStateChanged({this, + [this, &api, listener]() + { + std::ignore = api; + const auto& id = listener->asObject().getId(); + const auto& name = listener->asObject().getName(); + const auto& state = listener->getState(); + + logger_->info("{} received on state changed from {} to {}", + getName(), + name, + sen::StringConversionTraits::toString(state)); + + listenerStates_[id] = state; + + // remove the test object from the bus if all listeners are in sync + if (allListenersWithState(ListenerState::inSync)) + { + listenerStates_.clear(); + + logger_->info("{} removing {}", getName(), object_->getName()); + bus_->remove(object_); + } + + // shutdown the process kernel if all listeners are finished + if (allListenersWithState(ListenerState::finished)) + { + logger_->info("{} commanding kernel stop", getName()); + api.requestKernelStop(); + } + + if (allListenersWithState(ListenerState::ready)) + { + setNextListenersReady(true); + logger_->info("listeners ready"); + } + }})); + } + + // publish the test object when all expected listeners have been detected + if (detectedListeners_ == getNumOfListeners()) + { + // publish the test object + bus_ = api.getSource("session.bus"); + object_ = std::make_shared("testObject", staticPropValue); + logger_->info("publishing test object"); + bus_->add(object_); + } + }); + } + +private: + [[nodiscard]] bool allListenersWithState(const ListenerState state) + { + return listenerStates_.size() == getNumOfListeners() && + std::all_of(listenerStates_.begin(), + listenerStates_.end(), + [state](const auto& pair) { return pair.second == state; }); + } + +private: + std::shared_ptr logger_; + std::shared_ptr bus_; + std::shared_ptr object_; + std::shared_ptr> listenerSub_; + std::unordered_map listenerStates_; + std::vector guards_; + uint32_t detectedListeners_ = 0U; +}; + +SEN_EXPORT_CLASS(PublisherImpl) + +/// Contains all data from TestObject received on the listeners +struct ListenedData +{ + // static props + uint8_t staticProp; + TestEnum staticNoConfigProp; + + // dynamic props + std::vector bestEffortPropUpdates; + std::vector confirmedPropUpdates; + std::vector writablePropUpdates; + + // events + std::unordered_map bestEffortEventData; + std::unordered_map confirmedEventData; +}; + +/// Detects changes in the TestObject members (from the same, or a different component or process) +class ListenerImpl: public ListenerBase +{ +public: + SEN_NOCOPY_NOMOVE(ListenerImpl) + +public: + ListenerImpl(std::string name, const sen::VarMap& args) + : ListenerBase(name, args), logger_(spdlog::stdout_color_mt(name)) + { + } + + ~ListenerImpl() override = default; + +public: + void registered(sen::kernel::RegistrationApi& api) override + { + // detect all other listeners + listenerSub_ = api.selectAllFrom("session.bus"); + + // listen to test objects + bus_ = api.getSource("session.bus"); + bus_->addSubscriber(sen::Interest::make(getQuery(), api.getTypes()), &objList_, true); + + std::ignore = objList_.onAdded( + [this](const auto& addedObjects) + { + if (addedObjects.begin() != addedObjects.end()) + { + testObject_ = *addedObjects.begin(); + + logger_->info("{} detected {}", getName(), testObject_->asObject().getName()); + + // store static data + data_.staticProp = testObject_->getStaticProp(); + data_.staticNoConfigProp = testObject_->getStaticNoConfigProp(); + doSync(); + + // property update callbacks + objGuards_.emplace_back(testObject_->onBestEffortPropChanged( + {this, + [this]() + { + doSync(testObject_->getUpdateId()); + + if (data_.bestEffortPropUpdates.size() < numOfChecks) + { + data_.bestEffortPropUpdates.push_back(testObject_->getBestEffortProp()); + } + }})); + objGuards_.emplace_back(testObject_->onConfirmedPropChanged( + {this, + [this]() + { + doSync(testObject_->getUpdateId()); + if (data_.confirmedPropUpdates.size() < numOfChecks) + { + data_.confirmedPropUpdates.push_back(testObject_->getConfirmedProp()); + } + }})); + objGuards_.emplace_back( + testObject_->onWritablePropChanged({this, + [this]() + { + doSync(testObject_->getUpdateId()); + if (data_.writablePropUpdates.size() < numOfChecks) + { + data_.writablePropUpdates.push_back(testObject_->getWritableProp()); + } + }})); + + objGuards_.emplace_back( + testObject_->onBestEffortEvent({this, + [this](const uint32_t id, const TestStruct& arg) + { + if (data_.bestEffortEventData.size() < numOfChecks) + { + if (!data_.bestEffortEventData.insert({id, arg}).second) + { + // Key was repeated; result.first points to the existing + // element + repeatedEventsReceived_ = true; + } + } + }})); + objGuards_.emplace_back( + testObject_->onConfirmedEvent({this, + [this](const uint32_t id, const std::string& arg) + { + if (data_.confirmedEventData.size() < numOfChecks) + { + if (!data_.confirmedEventData.insert({id, arg}).second) + { + // Key was repeated; result.first points to the existing + // element + repeatedEventsReceived_ = true; + } + } + }})); + + setNextState(ListenerState::ready); + } + }); + + std::ignore = objList_.onRemoved( + [this](const auto& removedObjects) + { + if (removedObjects.begin() != removedObjects.end()) + { + // clear callbacks associated to the object + testObject_ = nullptr; + doChecks(); + setNextState(ListenerState::finished); + logger_->info("{} moved to finished state", getName()); + } + }); + + // detect when the listener has finished and stop the kernel if all other listeners are finished + listenersGuard_ = + onStateChanged({this, + [this, &api]() + { + // if all listeners are finished, stop the process + if (std::all_of(listenerSub_->list.getObjects().begin(), + listenerSub_->list.getObjects().end(), + [](const auto* elem) { return elem->getState() == ListenerState::finished; })) + { + logger_->info("stopping listener process", getName()); + api.requestKernelStop(); + } + }}); + } + +protected: + std::shared_ptr& getLogger() { return logger_; } + [[nodiscard]] const ListenedData& getData() const noexcept { return data_; } + [[nodiscard]] TestObjectInterface* getTestObject() const noexcept { return testObject_; } + [[nodiscard]] uint32_t getFirstUpdateId() const noexcept { return firstUpdateId_; } + [[nodiscard]] bool getRepeatedEventsReceived() const noexcept { return repeatedEventsReceived_; } + +protected: + /// Synchronizes received updates and change the state to onSync when the required updates are received + virtual void doSync(uint32_t updateId = 0) // NOLINT [google-default-arguments] + { + if (firstUpdateId_ == 0) + { + firstUpdateId_ = updateId; + } + } + +private: + /// Asserts the correctness of the property updates received + virtual void doChecks() {} + +private: + std::shared_ptr bus_; + sen::ObjectList objList_; + TestObjectInterface* testObject_ = nullptr; + std::shared_ptr> listenerSub_; + std::vector objGuards_; + sen::ConnectionGuard listenersGuard_; + std::shared_ptr logger_; + ListenedData data_ {}; + uint32_t firstUpdateId_ = 0; // first update ID received by the listener + bool repeatedEventsReceived_ = + false; // true if the events where received more than once in each participant (previous bug) + std::vector listeners_; +}; + +/// Listener that checks if static props are synchronized correctly +class ListenerStaticProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerStaticProps) + +public: + using ListenerImpl::ListenerImpl; + ~ListenerStaticProps() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + + if (getData().staticProp != 0U) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + + SEN_ASSERT(data.staticProp == staticPropValue); + SEN_ASSERT(data.staticNoConfigProp == staticNoConfigPropValue); + } +}; + +SEN_EXPORT_CLASS(ListenerStaticProps) + +/// Listener that checks if static props are synchronized correctly +class ListenerBestEffortProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerBestEffortProps) + +public: + using ListenerImpl::ListenerImpl; + ~ListenerBestEffortProps() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + ListenerImpl::doSync(updateId); + + if (getData().bestEffortPropUpdates.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + + SEN_ASSERT(data.bestEffortPropUpdates.size() == numOfChecks); + + // generator for the updates + for (uint32_t i = 0; i < numOfChecks; ++i) + { + gen_.seed(generatorSeed); + gen_.discard(getFirstUpdateId() + i); + SEN_ASSERT(data.bestEffortPropUpdates[i] - std::uniform_real_distribution()(gen_) < 1e-6); + } + } + +private: + std::mt19937 gen_ {generatorSeed}; +}; + +SEN_EXPORT_CLASS(ListenerBestEffortProps) + +/// Listener that checks if static props are synchronized correctly +class ListenerConfirmedProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConfirmedProps) + +public: + using ListenerImpl::ListenerImpl; + ~ListenerConfirmedProps() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + ListenerImpl::doSync(updateId); + + if (getData().confirmedPropUpdates.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + SEN_ASSERT(data.confirmedPropUpdates.size() == numOfChecks); + + // generator for the updates + for (uint32_t i = 0; i < numOfChecks; ++i) + { + std::mt19937 gen(generatorSeed); + gen.discard(getFirstUpdateId() + i); + SEN_ASSERT(data.confirmedPropUpdates[i] == generateStruct(gen)); + } + } +}; + +SEN_EXPORT_CLASS(ListenerConfirmedProps) + +/// Listener that checks if writable props are synchronized . We just send the update ID in the writable prop directly +class ListenerWritableProps final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerWritableProps) + +public: + ListenerWritableProps(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + } + + ~ListenerWritableProps() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + + // set the writable property + if (auto* obj = getTestObject(); obj != nullptr) + { + obj->setNextWritableProp({counter_++, updateDist_(gen_)}); + } + } + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + + if (getData().writablePropUpdates.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& updates = getData().writablePropUpdates; + + for (const auto& [id, value]: updates) + { + gen_.seed(generatorSeed); + updateDist_.reset(); + gen_.discard(id); + SEN_ASSERT(value == updateDist_(gen_)); + } + } + +private: + uint32_t counter_ = 0U; + std::mt19937_64 gen_; + std::uniform_int_distribution updateDist_; +}; + +SEN_EXPORT_CLASS(ListenerWritableProps) + +/// Listener that checks if best effort events are transmitted correctly +class ListenerBestEffortEvent final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerBestEffortEvent) + +public: + ListenerBestEffortEvent(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + } + + ~ListenerBestEffortEvent() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + if (getData().bestEffortEventData.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + SEN_ASSERT(data.bestEffortEventData.size() == numOfChecks); + + // generator for the updates + for (const auto& [id, value]: data.bestEffortEventData) + { + gen_.seed(generatorSeed); + gen_.discard(id); + SEN_ASSERT(value == generateStruct(gen_)); + } + // check that the event was not received multiple times + SEN_ASSERT(!getRepeatedEventsReceived()); + } + +private: + std::mt19937 gen_; +}; + +SEN_EXPORT_CLASS(ListenerBestEffortEvent) + +/// Listener that checks if confirmed events are transmitted correctly +class ListenerConfirmedEvent final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConfirmedEvent) + +public: + ListenerConfirmedEvent(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + } + + ~ListenerConfirmedEvent() override = default; + +private: // implements ListenerImpl + void doSync(uint32_t updateId) override + { + std::ignore = updateId; + if (getData().confirmedEventData.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + + void doChecks() override + { + const auto& data = getData(); + SEN_ASSERT(data.confirmedEventData.size() == numOfChecks); + + // generator for the updates + for (const auto& [id, value]: data.confirmedEventData) + { + gen_.seed(generatorSeed); + gen_.discard(id); + SEN_ASSERT(value == generateString(gen_)); + } + + // check that the event was not received multiple times + SEN_ASSERT(!getRepeatedEventsReceived()); + } + +private: + std::mt19937 gen_; +}; + +SEN_EXPORT_CLASS(ListenerConfirmedEvent) + +/// Listener that checks if confirmed events are transmitted correctly +class ListenerLocalMethod final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerLocalMethod) + +public: + ListenerLocalMethod(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed) + { + returnValues_.reserve(numOfChecks); + } + ~ListenerLocalMethod() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + if (auto* testObject = getTestObject(); testObject != nullptr) + { + returnValues_.push_back(testObject->localMethod()); + + if (returnValues_.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + } + +private: // implements ListenerImpl + void doChecks() override + { + // generator for the updates + for (const auto value: returnValues_) + { + SEN_ASSERT(value == distribution_(gen_)); + } + } + +private: + std::mt19937 gen_; + std::uniform_int_distribution distribution_; + std::vector returnValues_; +}; + +SEN_EXPORT_CLASS(ListenerLocalMethod) + +/// Listener that checks if confirmed return correctly when called +class ListenerConstMethod final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConstMethod) + +public: + ListenerConstMethod(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed), distribution_(0U, 2U) + { + returnValues_.reserve(numOfChecks); + } + + ~ListenerConstMethod() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + if (const auto* testObject = getTestObject(); testObject != nullptr) + { + testObject->constMethod(static_cast(distribution_(gen_)), + {this, + [this](const auto& response) + { + if (response) + { + returnValues_.push_back(response.getValue()); + + if (returnValues_.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + }}); + } + } + +private: // implements ListenerImpl + void doChecks() override + { + // check that we have collected responses within range (expected values) + SEN_ASSERT(std::all_of(returnValues_.begin(), returnValues_.end(), [](uint32_t val) { return val <= 2U; })); + } + +private: + std::mt19937 gen_; + std::uniform_int_distribution distribution_; + std::vector returnValues_; +}; + +SEN_EXPORT_CLASS(ListenerConstMethod) + +/// Listener that checks if confirmed methods return correctly when called +class ListenerConfirmedMethod final: public ListenerImpl +{ +public: + SEN_NOCOPY_NOMOVE(ListenerConfirmedMethod) + +public: + ListenerConfirmedMethod(std::string name, const sen::VarMap& args) + : ListenerImpl(std::move(name), args), gen_(generatorSeed), distribution_(0U, 2U) + { + returnValues_.reserve(numOfChecks); + } + + ~ListenerConfirmedMethod() override = default; + +public: + void update(sen::kernel::RunApi& runApi) override + { + std::ignore = runApi; + if (const auto* testObject = getTestObject(); testObject != nullptr) + { + testObject->constMethod(static_cast(distribution_(gen_)), + {this, + [this](const auto& response) + { + if (response) + { + returnValues_.push_back(response.getValue()); + + if (returnValues_.size() == numOfChecks) + { + setNextState(ListenerState::inSync); + } + } + }}); + } + } + +private: // implements ListenerImpl + void doChecks() override + { + // check that we have collected responses within range (expected values) + SEN_ASSERT(std::all_of(returnValues_.begin(), returnValues_.end(), [](uint32_t val) { return val <= 2U; })); + } + +private: + std::mt19937 gen_; + std::uniform_int_distribution distribution_; + std::vector returnValues_; +}; + +SEN_EXPORT_CLASS(ListenerConfirmedMethod) + +} // namespace object_sync diff --git a/libs/kernel/test/integration/object_sync/stl/object_sync.stl b/libs/kernel/test/integration/object_sync/stl/object_sync.stl new file mode 100644 index 00000000..eb63cec7 --- /dev/null +++ b/libs/kernel/test/integration/object_sync/stl/object_sync.stl @@ -0,0 +1,63 @@ +package object_sync; + +enum ListenerState : u8 +{ + connecting, + ready, + inSync, + finished +} + +class Publisher +{ + var numOfListeners : u32 [static]; + var listenersReady : bool; +} + +class Listener +{ + var query : string [static, confirmed]; + var state : ListenerState [confirmed]; +} + +struct TestStruct +{ + field1 : u32, + field2 : string, + field3 : f32 +} + +enum TestEnum : u8 +{ + first, + second, + third +} + +// Type used to evaluate correctness of writable properties through the network. Id is incremented and it is used to generate the value (which is then checked when reading ) +struct WritablePropType +{ + id : u32, + value : u64 +} + +class TestObject +{ + var staticProp : u8 [static]; + var staticNoConfigProp : TestEnum [static_no_config]; + var updateId : u32 [confirmed]; // used to verify the synchronization of dynamic properties + var bestEffortProp : f64 [bestEffort]; + var confirmedProp : TestStruct [confirmed]; + var writableProp : WritablePropType [writable]; + + event bestEffortEvent(id: u32, arg: TestStruct) [bestEffort]; + event confirmedEvent(id: u32, arg: string) [confirmed]; + + fn constMethod(arg: TestEnum) -> u32 [const]; + fn confirmedMethod(arg: u64) -> u8 [confirmed]; + fn bestEffortMethod(arg: string) -> bool [bestEffort]; + fn localMethod() -> u16 [local]; + + // method to command the object updates to start + fn doUpdate() [confirmed]; +} diff --git a/libs/kernel/test/integration/runner.py b/libs/kernel/test/integration/runner.py index a69412e0..2150a579 100644 --- a/libs/kernel/test/integration/runner.py +++ b/libs/kernel/test/integration/runner.py @@ -4,20 +4,28 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to orchestrate multiple sen processes to run the test setup.""" +import os import subprocess import sys -import os -from time import sleep -def run_sen_command(arg): - if os.name == 'nt': # Windows - subprocess.Popen(['sen', 'run', arg], start_new_session=True, env=os.environ.copy()) + +def run_sen_command(args): + """ + Do a sen run with the given arguments. + + Args: + args: passed to sen + """ + if os.name == "nt": # Windows + subprocess.Popen(["sen", "run", args], start_new_session=True, env=os.environ.copy()) else: # Unix-like - subprocess.Popen(['./sen', 'run', arg], start_new_session=True) + subprocess.Popen(["./sen", "run", args], start_new_session=True) def main(): + """Run the test setup.""" if len(sys.argv) != 4: print("Usage: python runner.py ") sys.exit(1) @@ -31,7 +39,7 @@ def main(): run_sen_command(arg2) # Run the main instance for the smoke test - os.execv(os.path.join(os.curdir, "sen"), ['sen', 'run', arg3]) + os.execv(os.path.join(os.curdir, "sen"), ["sen", "run", arg3]) if __name__ == "__main__": diff --git a/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt b/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt index d587396b..5e057ddf 100644 --- a/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt +++ b/libs/kernel/test/integration/runtime_compatibility/CMakeLists.txt @@ -24,7 +24,6 @@ add_sen_package( sen_enable_static_analysis(runtime_1) set_target_properties(runtime_1 PROPERTIES FOLDER "test") -set_target_properties(runtime_1 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_2 @@ -42,7 +41,6 @@ add_sen_package( sen_enable_static_analysis(runtime_2) set_target_properties(runtime_2 PROPERTIES FOLDER "test") -set_target_properties(runtime_2 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Generate the needed packages add_sen_package( @@ -61,7 +59,6 @@ add_sen_package( sen_enable_static_analysis(runtime_3) set_target_properties(runtime_3 PROPERTIES FOLDER "test") -set_target_properties(runtime_3 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_4 @@ -79,7 +76,6 @@ add_sen_package( sen_enable_static_analysis(runtime_4) set_target_properties(runtime_4 PROPERTIES FOLDER "test") -set_target_properties(runtime_4 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_5 @@ -97,7 +93,6 @@ add_sen_package( sen_enable_static_analysis(runtime_5) set_target_properties(runtime_5 PROPERTIES FOLDER "test") -set_target_properties(runtime_5 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET runtime_6 @@ -115,7 +110,6 @@ add_sen_package( sen_enable_static_analysis(runtime_6) set_target_properties(runtime_6 PROPERTIES FOLDER "test") -set_target_properties(runtime_6 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # runtime compatibility tests # @@ -133,7 +127,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/runtime_1/config/runtime_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/runtime_2/config/runtime_2.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS runtime_1 runtime_2 ) @@ -151,7 +145,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/runtime_3/config/runtime_3.yaml ${CMAKE_CURRENT_SOURCE_DIR}/runtime_4/config/runtime_4.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS runtime_3 runtime_4 ) @@ -169,7 +163,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/runtime_5/config/runtime_5.yaml ${CMAKE_CURRENT_SOURCE_DIR}/runtime_6/config/runtime_6.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS runtime_5 runtime_6 ) diff --git a/libs/kernel/test/integration/tester.py b/libs/kernel/test/integration/tester.py index ff411f3d..a9b53ee7 100644 --- a/libs/kernel/test/integration/tester.py +++ b/libs/kernel/test/integration/tester.py @@ -4,20 +4,22 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== -import sys -import time -from abc import ABC, abstractmethod +"""Module to run the generic integration testes as a Sen component.""" + import sen -from datetime import datetime -class TesterBase(ABC): + +class TesterBase: + """Tester base for generic testing functions.""" + def __init__(self, name, sen_api): """ Base testing class to reproduce tests on an iterative loop inside a Python Sen component. - :param name: Name of the test to run - :param sen_api: Python Sen api used in the component. - """ + Args: + name: Name of the test to run + sen_api: Python Sen api used in the component. + """ self._name = name self.__api = sen_api self.__start_time = self.__api.time @@ -30,13 +32,12 @@ def __init__(self, name, sen_api): self.__has_failed = False def mark_as_failed(self): + """Marks the tests state as failed and stops test execution.""" self.__has_failed = True self.__api.requestKernelStop(1) def get_test_elapsed_seconds(self): - """ - Get the seconds passed since the beginning of the test. - """ + """Get the seconds passed since the beginning of the test.""" if self.__start_time.total_seconds() == 0: self.__start_time = self.__api.time return (self.__api.time - self.__start_time).total_seconds() @@ -44,9 +45,11 @@ def get_test_elapsed_seconds(self): def set_test(self, test_name, test_func, test_condition): """ Create a test to be run during the testing process. - :param test_name: Name of the test - :param test_func: Function that contains the logic of the test to execute - :param test_condition: Condition which will trigger the test. + + Args: + test_name: Name of the test + test_func: Function that contains the logic of the test to execute + test_condition: Condition which will trigger the test. """ self.__tests[test_name] = test_func self.__test_conditions[test_name] = test_condition @@ -56,10 +59,12 @@ def set_test(self, test_name, test_func, test_condition): def execute_test(self, test_name): """ Execute a given test if it hasn't been run already and its condition has been met. + Store test result inside the test dictionary and manage the assertion error in case the test fails. """ - if (not self.__have_tests_run.get(test_name) and - self.__test_conditions[test_name]()): # Test hasn't been run and its condition is met + if ( + not self.__have_tests_run.get(test_name) and self.__test_conditions[test_name]() + ): # Test hasn't been run and its condition is met try: self.__tests[test_name]() # Execute the test function self.__have_tests_run[test_name] = True # Mark as executed @@ -69,20 +74,26 @@ def execute_test(self, test_name): def run_tests(self): """ - Iterative method that runs the specified tests, checking the possible failures on assertions and marking the - component to stop in failure case. + Iterative method that runs the specified tests. + + This methods is checking the possible failures on assertions and marking the component to stop in failure case. """ for test_name in self.__tests.keys(): self.execute_test(test_name) if self.__have_tests_failed[test_name]: self.mark_as_failed() + class KernelTransportTester(TesterBase): + """Tester class to execute the kernel transport tests.""" + def set_tests(self): + """Registers the test functions.""" def test_condition(): - " Check that both tester objects are ready prior to executing the test" - global object_list, tester1, tester2 + """Check that both tester objects are ready prior to executing the test.""" + # TODO (SEN-1689): clean up global state dependence + global tester1, tester2 # noqa: PLW0603 expected_names = {"tester1", "tester2", "obj1", "obj2"} objects_present = len(object_list) == 4 and {obj.name for obj in object_list} == expected_names @@ -90,7 +101,9 @@ def test_condition(): tester2 = next((obj for obj in object_list if obj.name == "tester2"), None) testers_ready = tester1 is not None and tester2 is not None and tester1.ready and tester2.ready - print(f"[tester] checking test condition: objects_present: {objects_present} , testers_ready {testers_ready}") + print( + f"[tester] checking test condition: objects_present: {objects_present} , testers_ready {testers_ready}" + ) if objects_present and not testers_ready: print(f"tester1: {tester1.ready}") @@ -99,48 +112,36 @@ def test_condition(): return objects_present and testers_ready def abort_tests(): - global tester1, tester2 - tester1.shutdownKernel() tester2.shutdownKernel() self.mark_as_failed() def check_test_4(result): - global tester1, tester2 - assert not result, abort_tests() tester1.shutdownKernel() tester2.shutdownKernel() sen.api.requestKernelStop(0) def check_test_3(result): - global tester1 - assert not result, abort_tests() - print(f"[tester] calling tester1.checkLocalState") - tester1.checkLocalState(lambda args: check_test_4(args)) + print("[tester] calling tester1.checkLocalState") + tester1.checkLocalState(check_test_4) def check_test_2(result): - global tester2 - assert not result, abort_tests() - print(f"[tester] calling tester2.doTests") - tester2.doTests(lambda args: check_test_3(args)) + print("[tester] calling tester2.doTests") + tester2.doTests(check_test_3) def check_test_1(result): - global tester2 - assert not result, abort_tests() - print(f"[tester] calling tester2.checkLocalState") - tester2.checkLocalState(lambda args: check_test_2(args)) + print("[tester] calling tester2.checkLocalState") + tester2.checkLocalState(check_test_2) def test_body(): - """ Check setting of remote properties between two kernel instances """ - global tester1 - + """Check setting of remote properties between two kernel instances.""" # test setting remote properties in the forward direction - print(f"[tester] calling tester1.doTests") - tester1.doTests(lambda args: check_test_1(args)) + print("[tester] calling tester1.doTests") + tester1.doTests(check_test_1) self.set_test("transport_test", test_body, test_condition) @@ -151,17 +152,19 @@ def test_body(): tester1 = None tester2 = None + def run(): - global tester, object_list + """Sen run: to setup the initial component state.""" + # TODO (SEN-1689): clean up global state dependence + global tester, object_list # noqa: PLW0603 object_list = sen.api.open("SELECT * FROM session.bus") - print(f"[tester] creating obj list with interest SELECT * FROM session.bus") + print("[tester] creating obj list with interest SELECT * FROM session.bus") tester = KernelTransportTester("transport_tester", sen.api) tester.set_tests() def update(): - global tester, object_list - + """Sen update: triggers test execution.""" tester.run_tests() diff --git a/libs/kernel/test/integration/transport/CMakeLists.txt b/libs/kernel/test/integration/transport/CMakeLists.txt index f1843c73..67f9e895 100644 --- a/libs/kernel/test/integration/transport/CMakeLists.txt +++ b/libs/kernel/test/integration/transport/CMakeLists.txt @@ -25,7 +25,6 @@ add_sen_package( ) set_target_properties(transport_1 PROPERTIES FOLDER "test") -set_target_properties(transport_1 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_package( TARGET transport_2 @@ -42,7 +41,6 @@ add_sen_package( ) set_target_properties(transport_2 PROPERTIES FOLDER "test") -set_target_properties(transport_2 PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # transport tests # @@ -60,7 +58,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/transport_1/config/transport_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/transport_2/config/transport_2.yaml ${CMAKE_CURRENT_SOURCE_DIR}/../tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS transport_1 transport_2 ) diff --git a/libs/kernel/test/integration/type_clash/CMakeLists.txt b/libs/kernel/test/integration/type_clash/CMakeLists.txt index c7130501..1c90aa82 100644 --- a/libs/kernel/test/integration/type_clash/CMakeLists.txt +++ b/libs/kernel/test/integration/type_clash/CMakeLists.txt @@ -19,9 +19,7 @@ add_sen_package( NO_SCHEMA ) sen_enable_static_analysis(type_clash_participant_1) -set_target_properties( - type_clash_participant_1 PROPERTIES FOLDER "test" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) +set_target_properties(type_clash_participant_1 PROPERTIES FOLDER "test") add_sen_package( TARGET type_clash_participant_2 @@ -35,9 +33,7 @@ add_sen_package( NO_SCHEMA ) sen_enable_static_analysis(type_clash_participant_2) -set_target_properties( - type_clash_participant_2 PROPERTIES FOLDER "test" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) +set_target_properties(type_clash_participant_2 PROPERTIES FOLDER "test") add_sen_package( TARGET type_clash_participant_3 @@ -51,9 +47,7 @@ add_sen_package( NO_SCHEMA ) sen_enable_static_analysis(type_clash_participant_3) -set_target_properties( - type_clash_participant_3 PROPERTIES FOLDER "test" LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin -) +set_target_properties(type_clash_participant_3 PROPERTIES FOLDER "test") add_sen_integration_test( kernel_type_clash_test_1 @@ -63,7 +57,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/participant_1/config/participant_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/participant_2/config/participant_2.yaml ${CMAKE_CURRENT_SOURCE_DIR}/type_clash_tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS type_clash_participant_1 type_clash_participant_2 ) @@ -85,7 +79,7 @@ add_sen_integration_test( ${CMAKE_CURRENT_SOURCE_DIR}/participant_1/config/participant_1.yaml ${CMAKE_CURRENT_SOURCE_DIR}/participant_3/config/participant_3.yaml ${CMAKE_CURRENT_SOURCE_DIR}/type_clash_tester.yaml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin + WORKING_DIRECTORY $ REQ_COMPONENTS ether py REQ_DEPS type_clash_participant_1 type_clash_participant_3 ) diff --git a/libs/kernel/test/integration/type_clash/type_clash_tester.py b/libs/kernel/test/integration/type_clash/type_clash_tester.py index 7c32b850..a70150e7 100644 --- a/libs/kernel/test/integration/type_clash/type_clash_tester.py +++ b/libs/kernel/test/integration/type_clash/type_clash_tester.py @@ -4,38 +4,44 @@ # See the LICENSE.txt file for more information. # © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. # ====================================================================================================================== +"""Module to run the type clash tests as a Sen component.""" import sen from tester import TesterBase + class TypeClashTester(TesterBase): + """Tester class to execute the type clash tests.""" + def set_tests(self): + """Registers the test functions.""" + def test_condition(): return self.get_test_elapsed_seconds() > 2.5 def test_body(): - global object_list - for obj in object_list: if "obj_app_" in obj.name: - try: - obj.shutdownKernel() - except: - pass + obj.shutdownKernel() sen.api.requestKernelStop(0) self.set_test("clash_test", test_body, test_condition) + tester = None object_list = None + def run(): - global tester, object_list + """Sen run: to setup the initial component state.""" + # TODO (SEN-1689): clean up global state dependence + global tester, object_list # noqa: PLW0603 object_list = sen.api.open("SELECT * FROM session.bus") tester = TypeClashTester("type_clash_tester", sen.api) tester.set_tests() + def update(): - global tester + """Sen update: triggers test execution.""" tester.run_tests() diff --git a/test/support/CMakeLists.txt b/test/support/CMakeLists.txt new file mode 100644 index 00000000..14b9ee7a --- /dev/null +++ b/test/support/CMakeLists.txt @@ -0,0 +1,28 @@ +# === CMakeLists.txt =================================================================================================== +# Sen Infrastructure +# Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +# See the LICENSE.txt file for more information. +# © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +# ====================================================================================================================== + +set(archive_sources src/archive_test_helpers.h src/archive_test_helpers.cpp) + +# ------------------------------------------------------------------------------------------------------------- +# library +# ------------------------------------------------------------------------------------------------------------- + +add_library(archive_test_helpers STATIC ${archive_sources}) + +sen_enable_static_analysis(archive_test_helpers) + +target_include_directories(archive_test_helpers PUBLIC "$") + +target_link_libraries(archive_test_helpers PUBLIC sen::core sen::db) + +# ------------------------------------------------------------------------------------------------------------- +# IDE grouping +# ------------------------------------------------------------------------------------------------------------- + +source_group("sources" FILES ${archive_sources}) + +set_target_properties(archive_test_helpers PROPERTIES FOLDER "test/support") diff --git a/test/support/src/archive_test_helpers.cpp b/test/support/src/archive_test_helpers.cpp new file mode 100644 index 00000000..b9457bae --- /dev/null +++ b/test/support/src/archive_test_helpers.cpp @@ -0,0 +1,9 @@ +// === archive_test_helpers.cpp ======================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +// components +#include "archive_test_helpers.h" diff --git a/test/support/src/archive_test_helpers.h b/test/support/src/archive_test_helpers.h new file mode 100644 index 00000000..b3f52b37 --- /dev/null +++ b/test/support/src/archive_test_helpers.h @@ -0,0 +1,133 @@ +// === archive_test_helpers.h ========================================================================================== +// Sen Infrastructure +// Released under the Apache License v2.0 (SPDX-License-Identifier Apache-2.0). +// See the LICENSE.txt file for more information. +// © Airbus SAS, Airbus Helicopters, and Airbus Defence and Space SAU/GmbH/SAS. +// ===================================================================================================================== + +#ifndef SEN_TEST_SUPPORT_ARCHIVE_TEST_HELPERS_H +#define SEN_TEST_SUPPORT_ARCHIVE_TEST_HELPERS_H + +// sen +#include "sen/core/base/compiler_macros.h" +#include "sen/core/base/timestamp.h" +#include "sen/core/base/uuid.h" +#include "sen/core/meta/class_type.h" +#include "sen/core/meta/type.h" +#include "sen/db/output.h" + +// generated code +#include "stl/sen/db/basic_types.stl.h" + +// std +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sen::test +{ + +/// Creates a unique temporary directory and deletes it on destruction. +class TempDir +{ +public: + explicit TempDir(std::string prefix = "sen_test_") + : path_(std::filesystem::temp_directory_path() / (std::move(prefix) + getRandomPathPostFix())) + { + std::filesystem::create_directories(path_); + } + + SEN_NOCOPY_NOMOVE(TempDir) + + ~TempDir() + { + std::error_code errorCode; + std::filesystem::remove_all(path_, errorCode); + } + + /// Returns the absolute path to the generated directory. + [[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; } + +private: + /// Returns a random post fix for temporary files paths. + static std::string getRandomPathPostFix() { return sen::UuidRandomGenerator()().toString(); } + +private: + std::filesystem::path path_; +}; + +/// Creates a TimeStamp from the specified number of seconds. +[[nodiscard]] inline sen::TimeStamp makeTime(int64_t seconds) { return sen::TimeStamp(std::chrono::seconds(seconds)); } + +/// Generates output settings to write an archive using the path folder. +[[nodiscard]] inline sen::db::OutSettings makeArchiveSettings(std::string_view name, + const std::filesystem::path& folder, + bool indexKeyframes = true) +{ + sen::db::OutSettings settings; + settings.name = std::string(name); + settings.folder = folder.string(); + settings.indexKeyframes = indexKeyframes; + return settings; +} + +/// Generates output settings to write an archive using a TempDir object. +[[nodiscard]] inline sen::db::OutSettings makeArchiveSettings(std::string_view name, + const TempDir& tempDir, + bool indexKeyframes = true) +{ + return makeArchiveSettings(name, tempDir.path(), indexKeyframes); +} + +/// Constructs the full archive path by appending the archive name to the base folder. +[[nodiscard]] inline std::filesystem::path makeArchivePath(std::string_view name, const std::filesystem::path& folder) +{ + return folder / std::string(name); +} + +/// Constructs the archive path within a TempDir. +[[nodiscard]] inline std::filesystem::path makeArchivePath(std::string_view name, const TempDir& tempDir) +{ + return makeArchivePath(name, tempDir.path()); +} + +/// Creates object metadata for database registration, assigning a session and bus. +template +[[nodiscard]] inline sen::db::ObjectInfo makeObjectInfo(ObjectType* object, + std::string_view session = "test_session", + std::string_view bus = "test_bus") +{ + return sen::db::ObjectInfo {object, std::string(session), std::string(bus)}; +} + +/// Creates object metadata for database registration accepting a std::shared_ptr. +template +[[nodiscard]] inline sen::db::ObjectInfo makeObjectInfo(const std::shared_ptr& object, + std::string_view session = "test_session", + std::string_view bus = "test_bus") +{ + return makeObjectInfo(object.get(), session, bus); +} + +/// Retrieves the MemberHash id of the first property defined in the object's class. +template +[[nodiscard]] inline sen::MemberHash firstPropertyId(const ObjectType& object) +{ + return object.getClass()->getProperties(sen::ClassType::SearchMode::includeParents).front()->getId(); +} + +/// Retrieves the MemberHash id of the first event defined in the object's class. +template +[[nodiscard]] inline sen::MemberHash firstEventId(const ObjectType& object) +{ + return object.getClass()->getEvents(sen::ClassType::SearchMode::includeParents).front()->getId(); +} + +} // namespace sen::test + +#endif // SEN_TEST_SUPPORT_ARCHIVE_TEST_HELPERS_H diff --git a/test/util/chaos_monkeys/CMakeLists.txt b/test/util/chaos_monkeys/CMakeLists.txt index 3c06b5c6..1e1d2292 100644 --- a/test/util/chaos_monkeys/CMakeLists.txt +++ b/test/util/chaos_monkeys/CMakeLists.txt @@ -23,7 +23,6 @@ add_sen_package( ) set_target_properties(creator_monkeys PROPERTIES FOLDER "test/util/chaos_monkeys") -set_target_properties(creator_monkeys PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # TODO(SEN-1165): investigate flaky results for these tests # # execute creator monkeys test with one type of object with a frequency of 10hz during 10 seconds diff --git a/test/util/inheritance/CMakeLists.txt b/test/util/inheritance/CMakeLists.txt index 281b7258..9d3589ac 100644 --- a/test/util/inheritance/CMakeLists.txt +++ b/test/util/inheritance/CMakeLists.txt @@ -24,6 +24,5 @@ add_sen_package( ) set_target_properties(inheritance PROPERTIES FOLDER "examples/basic") -set_target_properties(inheritance PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) add_sen_run_smoke_test(inheritance_smoke CONFIG_FILE config/inheritance.yaml) diff --git a/test/util/my_package/CMakeLists.txt b/test/util/my_package/CMakeLists.txt index aa860c4f..e05e7987 100644 --- a/test/util/my_package/CMakeLists.txt +++ b/test/util/my_package/CMakeLists.txt @@ -26,7 +26,6 @@ add_sen_package( ) set_target_properties(my_package PROPERTIES FOLDER "examples/basic") -set_target_properties(my_package PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # smoke tests add_sen_run_smoke_test(my_package_abstract_smoke CONFIG_FILE config/my_package_abstract.yaml WILL_FAIL) diff --git a/test/util/publish_types_manually/CMakeLists.txt b/test/util/publish_types_manually/CMakeLists.txt index 951e400a..7f75d730 100644 --- a/test/util/publish_types_manually/CMakeLists.txt +++ b/test/util/publish_types_manually/CMakeLists.txt @@ -17,6 +17,4 @@ add_sen_package( ) sen_enable_static_analysis(publish_types_manually) -set_target_properties(publish_types_manually PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - add_sen_run_smoke_test(publish_types_manually CONFIG_FILE config/component.yaml) diff --git a/test/util/query_test/CMakeLists.txt b/test/util/query_test/CMakeLists.txt index d033df1d..dcded654 100644 --- a/test/util/query_test/CMakeLists.txt +++ b/test/util/query_test/CMakeLists.txt @@ -18,6 +18,4 @@ add_sen_package( sen_enable_static_analysis(query_test) -set_target_properties(query_test PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) - add_sen_run_smoke_test(query_test CONFIG_FILE config/component.yaml) diff --git a/test/util/stress_testing_utils/CMakeLists.txt b/test/util/stress_testing_utils/CMakeLists.txt index 49ebf443..b6da33ec 100644 --- a/test/util/stress_testing_utils/CMakeLists.txt +++ b/test/util/stress_testing_utils/CMakeLists.txt @@ -17,4 +17,3 @@ add_sen_package( ) target_link_libraries(stress_testing_utils PRIVATE $) -set_target_properties(stress_testing_utils PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)