diff --git a/.gitattributes b/.gitattributes index cf2aa917c..da14f678c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -46,3 +46,4 @@ # Denote all files that are truly binary and should not be modified. *.png binary *.jpg binary +tools/ctrace/test/data/**/*.raw binary diff --git a/.github/workflows/ctrace.yml b/.github/workflows/ctrace.yml index fa1be0599..374db6b6f 100644 --- a/.github/workflows/ctrace.yml +++ b/.github/workflows/ctrace.yml @@ -7,20 +7,20 @@ on: paths: - '.github/workflows/ctrace.yml' - '.github/matrix_includes_ctrace.json' - - 'cmake/**' - 'CMakeLists.txt' - - 'tools/ctrace/**' - - '!**/docs/**/*' - - '!**/*.md' + - 'tools/ctrace/CMakeLists.txt' + - 'tools/ctrace/cmake/**' + - 'tools/ctrace/src/**' + - 'tools/ctrace/test/**' pull_request: paths: - '.github/workflows/ctrace.yml' - '.github/matrix_includes_ctrace.json' - - 'cmake/**' - 'CMakeLists.txt' - - 'tools/ctrace/**' - - '!**/docs/**/*' - - '!**/*.md' + - 'tools/ctrace/CMakeLists.txt' + - 'tools/ctrace/cmake/**' + - 'tools/ctrace/src/**' + - 'tools/ctrace/test/**' release: types: [published] @@ -108,7 +108,7 @@ jobs: if: | github.event_name == 'release' && startsWith(github.ref, 'refs/tags/tools/ctrace/') - needs: [ build, unittest, coverage ] + needs: [ build, test, coverage ] runs-on: ubuntu-22.04 timeout-minutes: 25 permissions: @@ -116,11 +116,8 @@ jobs: steps: - name: Checkout devtools uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Create distribution folders - run: | - mkdir -p tools/ctrace/distribution/bin - cp LICENSE tools/ctrace/distribution/ + with: + submodules: true - name: Download ctrace linux-amd64 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -152,16 +149,8 @@ jobs: name: ctrace-windows-arm64 path: tools/ctrace/distribution/bin/windows-arm64/ - - name: Set Unix executable permissions - run: | - chmod +x bin/darwin-arm64/ctrace - chmod +x bin/linux-amd64/ctrace - chmod +x bin/linux-arm64/ctrace - working-directory: tools/ctrace/distribution - - - name: Zip distribution folder - run: zip -r ctrace.zip * - working-directory: tools/ctrace/distribution + - name: Verify and package distribution + run: tools/ctrace/scripts/package-release.sh - name: Attach zip archive to release assets uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2 @@ -172,13 +161,13 @@ jobs: overwrite: true asset_name: ctrace.zip - unittest: + test: needs: [ setup, matrix_prep ] - name: unittest (${{ matrix.runs_on }}, ${{ matrix.arch }}) + name: test (${{ matrix.runs_on }}, ${{ matrix.arch }}) runs-on: ${{ matrix.runs_on }} timeout-minutes: 25 strategy: - fail-fast: true + fail-fast: false matrix: ${{ fromJson(needs.matrix_prep.outputs.matrix) }} steps: - name: Harden Runner @@ -194,23 +183,51 @@ jobs: # https://github.com/Open-CMSIS-Pack/devtools-build-action - name: Build CtraceUnitTests + id: build_unit_tests uses: Open-CMSIS-Pack/devtools-build-action@5b24a2b5145eb406a664269b3704bb983c19242d # arm64 with: target: CtraceUnitTests build_type: Debug arch: ${{ matrix.arch }} + # https://github.com/Open-CMSIS-Pack/devtools-build-action + - name: Build CtraceIntegTests + id: build_integration_target + uses: Open-CMSIS-Pack/devtools-build-action@5b24a2b5145eb406a664269b3704bb983c19242d # arm64 + with: + target: CtraceIntegTests + build_type: Debug + arch: ${{ matrix.arch }} + - name: Run ctrace unit tests - if: (matrix.arch != 'arm64') - run: ctest -C Debug -R CtraceUnitTests + id: run_unit_tests + if: always() && (matrix.arch != 'arm64') && (steps.build_unit_tests.outcome == 'success') + run: ctest -V -C Debug -R '^CtraceUnitTests$' working-directory: ./build - - name: Archive unit tests results - if: always() && (matrix.arch != 'arm64') + - name: Run ctrace integration tests + id: run_integration_tests + if: always() && (matrix.arch != 'arm64') && (steps.build_integration_target.outcome == 'success') + run: ctest -V -C Debug -R '^(CtraceIntegTests|ctrace-)' + working-directory: ./build + + - name: Archive unit test results + # Keep the report available when test execution fails. + if: always() && (matrix.arch != 'arm64') && (steps.run_unit_tests.outcome != 'skipped') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unit_test_result_ctrace-${{ matrix.target }}-${{ matrix.arch }} - path: ./build/test_reports/ctraceunittests-*.xml + path: ./build/test_reports/ctraceunittests-report-${{ matrix.target }}-${{ matrix.arch }}.xml + retention-days: ${{ needs.setup.outputs.retention_days }} + if-no-files-found: error + + - name: Archive integration test results + # Keep the report available when test execution fails. + if: always() && (matrix.arch != 'arm64') && (steps.run_integration_tests.outcome != 'skipped') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: integ_test_result_ctrace-${{ matrix.target }}-${{ matrix.arch }} + path: ./build/test_reports/ctraceintegtests-report-${{ matrix.target }}-${{ matrix.arch }}.xml retention-days: ${{ needs.setup.outputs.retention_days }} if-no-files-found: error @@ -222,8 +239,9 @@ jobs: (github.event_name == 'release' && startsWith(github.ref, 'refs/tags/tools/ctrace/')) || ((github.event.schedule != '') && (!github.event.repository.private)) env: - lcov_base: https://github.com/linux-test-project/lcov/releases/download/v1.15/ + lcov_base: https://github.com/linux-test-project/lcov/releases/download/v1.15 lcov_installer: lcov-1.15.tar.gz + lcov_sha256: c1cda2fa33bec9aa2c2c73c87226cfe97de0831887176b45ee523c5e30f8053a runs-on: ubuntu-22.04 timeout-minutes: 25 steps: @@ -233,11 +251,6 @@ jobs: with: egress-policy: audit - - name: Install linux deps - run: | - sudo apt-get update - sudo apt-get install lcov - - name: Checkout devtools uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -252,8 +265,18 @@ jobs: build_type: Debug target: CtraceUnitTests - - name: Run CtraceUnitTests - run: ctest -V -C Debug -R CtraceUnitTests + - name: Build CtraceIntegTests + # Reconfiguring reapplies external patches and can make coverage + # metadata embedded in the previously linked test binary stale. + run: cmake --build . --target CtraceIntegTests + working-directory: ./build + + - name: Run ctrace unit tests + run: ctest -V -C Debug -R '^CtraceUnitTests$' + working-directory: ./build + + - name: Run ctrace integration tests + run: ctest -V -C Debug -R '^(CtraceIntegTests|ctrace-)' working-directory: ./build - name: Get retention days @@ -261,26 +284,38 @@ jobs: run: | echo "retention_days=$(echo '${{ (!github.event.repository.private && (github.event_name == 'push' || github.event.schedule != '')) && '7' || '1' }}')" >> $GITHUB_OUTPUT - - name: Archive unit tests results + - name: Archive unit test results if: (github.event_name == 'push') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unit_test_result_ctrace-linux-amd64 - path: ./build/test_reports/ctraceunittests-*.xml + path: ./build/test_reports/ctraceunittests-report-linux-amd64.xml + retention-days: ${{ steps.var.outputs.retention_days }} + if-no-files-found: error + + - name: Archive integration test results + if: (github.event_name == 'push') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: integ_test_result_ctrace-linux-amd64 + path: ./build/test_reports/ctraceintegtests-report-linux-amd64.xml retention-days: ${{ steps.var.outputs.retention_days }} if-no-files-found: error - name: Setup lcov1.15 run: | wget -q ${{ env.lcov_base }}/${{ env.lcov_installer }} + echo "${{ env.lcov_sha256 }} ${{ env.lcov_installer }}" | sha256sum --check --strict tar -xvf ${{ env.lcov_installer }} working-directory: ./build/tools/ctrace - name: Generate coverage report run: | lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -c --directory . --output-file full_coverage.info - lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -e full_coverage.info '*/tools/ctrace/include/*' '*/tools/ctrace/src/*' -o coverage_ctrace.info - genhtml coverage_ctrace.info --output-directory coverage_ctrace --branch-coverage + lcov-1.15/bin/lcov --rc lcov_branch_coverage=1 --rc geninfo_no_exception_branch=1 -e full_coverage.info '*/tools/ctrace/src/*' -o coverage_ctrace.info + # Preserve unfiltered branch data for review, but gate on portable source-line coverage. + lcov-1.15/bin/lcov --rc lcov_branch_coverage=0 -e full_coverage.info '*/tools/ctrace/src/*' -o coverage_ctrace_codecov.info + lcov-1.15/bin/genhtml coverage_ctrace.info --output-directory coverage_ctrace --branch-coverage working-directory: ./build/tools/ctrace - name: Upload Report to Codecov @@ -289,7 +324,7 @@ jobs: with: action: codecov/codecov-action@v4 with: | - files: ./build/tools/ctrace/coverage_ctrace.info + files: ./build/tools/ctrace/coverage_ctrace_codecov.info fail_ci_if_error: true flags: ctrace-cov token: ${{ secrets.CODECOV_TOKEN }} @@ -304,13 +339,15 @@ jobs: path: | ./build/tools/ctrace/coverage_ctrace/ ./build/tools/ctrace/coverage_ctrace.info + ./build/tools/ctrace/coverage_ctrace_codecov.info retention-days: ${{ steps.var.outputs.retention_days }} if-no-files-found: error test-results-preparation: name: "Publish Tests Results" + # A dependent reporting job is otherwise skipped after a test failure. if: ${{ always() }} - needs: [ unittest ] + needs: [ test ] runs-on: ubuntu-22.04 steps: - name: Harden Runner diff --git a/.gitmodules b/.gitmodules index c36eb1c30..c6be8207b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,6 @@ [submodule "external/json-rpc-cxx"] path = external/json-rpc-cxx url = https://github.com/jsonrpcx/json-rpc-cxx.git +[submodule "external/OpenCSD"] + path = external/OpenCSD + url = https://github.com/Linaro/OpenCSD.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 085c07dd7..c55904a82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,11 @@ apply_patch(${CMAKE_CURRENT_SOURCE_DIR}/external/cxxopts) apply_patch(${CMAKE_CURRENT_SOURCE_DIR}/external/xerces-c) apply_patch(${CMAKE_CURRENT_SOURCE_DIR}/external/yaml-cpp) +# OpenCSD is a build dependency of ctrace. +if(TARGET ctrace) + include(tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake) +endif() + # Google Test Framework set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) add_subdirectory(external/googletest) diff --git a/LICENSE.md b/LICENSE.md index 98ec77440..aef27cd9f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # License Terms -Copyright 2020-2021 Arm Limited. All rights reserved. +Copyright 2020-2026 Arm Limited. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ or testing purposes. |Google C++ Testing Framework|1.11.0|[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause)|https://github.com/google/googletest.git| Testing | |cxxopts|2.2.1|[MIT](https://opensource.org/licenses/MIT)|https://github.com/jarro2783/cxxopts.git| packgen | |yaml-cpp|0.7.0|[MIT](https://opensource.org/licenses/MIT)|https://github.com/jbeder/yaml-cpp.git| packgen | +|OpenCSD|1.8.3|[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause)|https://github.com/Linaro/OpenCSD.git| ctrace | |[GetGitRevisionDescription.cmake](./cmake/GetGitRevisionDescription.cmake)||[Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt)||Continuous integration| |[GetGitRevisionDescription.cmake.in](./cmake/GetGitRevisionDescription.cmake.in)||[Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt)||Continuous integration| |json|3.10.5|[MIT](https://opensource.org/licenses/MIT)|https://github.com/nlohmann/json| yml-schema-checker | diff --git a/external/CONTENT.md b/external/CONTENT.md index a5dc3394d..013b5809d 100644 --- a/external/CONTENT.md +++ b/external/CONTENT.md @@ -6,6 +6,7 @@ should be committed into this folder. ```txt 📂external + ┣ 📂OpenCSD ┣ 📂cxxopts ┣ 📂fmt ┣ 📂googletest @@ -14,6 +15,13 @@ should be committed into this folder. ┗ 📂yaml-cpp ``` +## OpenCSD + + +The [OpenCSD](./OpenCSD) directory contains the CoreSight trace decode library +used by ctrace and is fetched from +[here](https://github.com/Linaro/OpenCSD). + ## cxxopts diff --git a/external/OpenCSD b/external/OpenCSD new file mode 160000 index 000000000..59f153206 --- /dev/null +++ b/external/OpenCSD @@ -0,0 +1 @@ +Subproject commit 59f15320637ce6e510604e7bed550d7ad109d15d diff --git a/tools/ctrace/.clang-format b/tools/ctrace/.clang-format new file mode 100644 index 000000000..33868f084 --- /dev/null +++ b/tools/ctrace/.clang-format @@ -0,0 +1,18 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 2 +ContinuationIndentWidth: 4 +ConstructorInitializerIndentWidth: 2 +PackConstructorInitializers: Never +TabWidth: 2 +UseTab: Never +ColumnLimit: 120 +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true +PointerAlignment: Left +DerivePointerAlignment: false +SortIncludes: Never +SpaceBeforeParens: ControlStatements +AllowShortFunctionsOnASingleLine: Empty +... diff --git a/tools/ctrace/.clangd b/tools/ctrace/.clangd new file mode 100644 index 000000000..3ea8cca5e --- /dev/null +++ b/tools/ctrace/.clangd @@ -0,0 +1,5 @@ +CompileFlags: + CompilationDatabase: ../../build +Diagnostics: + UnusedIncludes: Strict + MissingIncludes: Strict diff --git a/tools/ctrace/CMakeLists.txt b/tools/ctrace/CMakeLists.txt index 6ddcbc770..375e9024a 100644 --- a/tools/ctrace/CMakeLists.txt +++ b/tools/ctrace/CMakeLists.txt @@ -9,26 +9,40 @@ get_version_from_git_tag("tools/ctrace/") project(ctrace VERSION "${PROJECT_VERSION}") dump_cmake_project_version() -set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT ctrace) +configure_file(src/cli/ProductInfo.h.in ProductInfo.h) -# ctrace library -add_library(ctracelib OBJECT src/CtraceMain.cpp include/CtraceMain.h) -target_include_directories(ctracelib PUBLIC include) +set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT ctrace) -set_property(TARGET ctracelib PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +add_subdirectory(src) -# ctrace target -add_executable(ctrace src/main.cpp) -target_link_libraries(ctrace PRIVATE ctracelib) +set(CTRACE_HEADER_FILES + src/CtraceMain.h +) +source_group("Header Files" FILES ${CTRACE_HEADER_FILES}) -set_property(TARGET ctrace PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +add_library(ctracelib OBJECT + src/CtraceMain.cpp + ${CTRACE_HEADER_FILES} +) +target_link_libraries(ctracelib PUBLIC + ctrace::control +) +target_include_directories(ctracelib PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/src" +) -if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR - "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - target_link_options(ctrace PUBLIC "-static") +add_executable(ctrace src/main.cpp) +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + target_sources(ctrace PRIVATE src/ctrace.manifest) +endif() +target_link_libraries(ctrace PRIVATE + ctracelib +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND + (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR + CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) + target_link_options(ctrace PRIVATE "-static") endif() -enable_testing() add_subdirectory(test) diff --git a/tools/ctrace/README.md b/tools/ctrace/README.md new file mode 100644 index 000000000..25452cbab --- /dev/null +++ b/tools/ctrace/README.md @@ -0,0 +1,123 @@ +# ctrace + +`ctrace` converts CMSIS Cortex-M trace captures into outputs that can be inspected or processed by other tools. It +combines a `.ctrace-run.yml` description with matching raw trace files from one trace directory. + +The current implementation decodes SWO/ITM and DWT data. Trace Bus (`*.TB.raw`) files are discovered and reported, +but deliberately skipped until that decoder is implemented. +ITM payload output supports stimulus ports `1` through `31`. Port `0` remains part of stream decoding but is +deliberately excluded from CSV and CTF event output. Trace Compass observes the same filtered CTF stream. + +![ctrace architecture](docs/architecture.svg) + +## Usage + +```text +ctrace [options] + + -t, --target Process one solution set; otherwise process all + --csv Generate CSV output + --ctf Generate CTF and Trace Compass XML output + -a, --all Generate all output formats + --type Select event types + --stream Select CoreSight Trace Bus IDs (0 to 111) + -h, --help Print command-line help + -V, --version Print the version +``` + +Values for `--type` and `--stream` are space-separated, so place the trace directory before these multi-value options. +If the directory follows them, terminate option parsing explicitly, for example +`ctrace --type itm dwt -- .trace`. With no output option, `ctrace` validates and decodes the capture without writing +output files. + +The `--type` option accepts the specification-defined selectors `itm`, `dwt`, `event`, `pmu`, `exception`, +`pcsample`, `global_ts`, `overflow`, and `error`. Decoded DWT event counters, PMU packets, and PC samples remain +disabled until their output semantics are implemented, so their selectors currently produce no rows. + +A trace directory uses solution-set-based file names: + +```text +.trace/ + Board.ctrace-run.yml + Board.SWO.raw + Board.TB.raw # discovered, currently not decoded +``` + +For `ctrace .trace --target Board --all`, the supported input produces: + +```text +.trace/ + Board.SWO.csv + Board.ctf/ + metadata + stream_0 + Board.SWO.traceanalysis.xml +``` + +The YAML reader validates fields consumed by `ctrace` and ignores unrelated or unknown fields. A malformed consumed +field is an error; an unneeded extension to the trace-run format does not break the tool. + +## Project structure + +| Path | Responsibility | +|---|---| +| `src/CtraceMain.h` | Platform-independent entry point used by the executable trampoline | +| `src/cli` | Command-line parsing and validation | +| `src/control` | Trace-directory orchestration, raw-file access, and per-file decode jobs | +| `src/tracerun` | Trace-run discovery, YAML parsing, and normalized metadata | +| `src/decode` | OpenCSD adapter, recovery, and Cortex-M semantic decoding | +| `src/model` | Backend-independent trace events and selections | +| `src/output/csv` | CSV schema and writer | +| `src/output/ctf` | CTF bundle and Trace Compass XML writers | +| `src/diagnostics` | Structured diagnostics and trace-issue reporting | +| `test/unit` | GoogleTest cases, arranged like the production modules | +| `test/integration` | GoogleTest integration suite for the application entry point and stable fixtures | +| `test/data` | Stable fixtures and expected output | + +The module boundaries, dependency direction, runtime flow, and extension points are described in the +[architecture documentation](docs/architecture.md). See also the [verified constraints](docs/constraints.md) and the +compact [remaining TODO list](docs/todo.md). + +## Build and test + +Initialize all dependencies and configure the repository from its root: + +```bash +git submodule update --init --recursive +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake --build build --target ctrace CtraceUnitTests CtraceIntegTests +``` + +Run the GoogleTest unit and integration suites plus the executable smoke tests: + +```bash +ctest --test-dir build -C Debug -R '^(CtraceUnitTests|CtraceIntegTests|ctrace-)' +``` + +Editors using `clangd` should open the devtools repository root and configure into `build`. The tool-local +`.clangd` file points clangd at that compilation database. + +## Dependencies and releases + +`ctrace` uses the devtools copies of `cxxopts`, `yaml-cpp`, GoogleTest, and the `external/OpenCSD` submodule. It does +not carry private copies below `tools/ctrace`. +Known defects in the pinned OpenCSD revision that can affect ctrace are recorded +in the [OpenCSD issue notes](docs/opencsd-issues.md). + +Publishing a GitHub Release for a tag named `tools/ctrace/`, for example `tools/ctrace/0.0.1`, runs the ctrace +workflow. It builds Windows AMD64/Arm64, Linux AMD64/Arm64, and macOS Arm64 variants and attaches `ctrace.zip` to the +release. Unit and integration tests run on Windows AMD64 and Linux AMD64; ARM64 targets are compiled but not executed, +matching the other devtools workflows. Coverage runs on Linux AMD64. The executable version is derived from the same +tool-specific tag. + +The project is licensed under Apache-2.0. Product dependencies and their licenses are recorded in the repository's +top-level `LICENSE.md`. The release archive contains the project license, third-party notices, OpenCSD copyright +notices, the application-dependency license texts, and checksums for all included files. Compiler and operating-system +runtime contents must still be inspected for each production build; the application dependency list does not claim to +cover them. Release artifacts are currently unsigned; signing, archive checksums, macOS notarization, and a formal +SBOM remain explicit release-hardening decisions. + +The code, build, test, and packaging structure is ready for release-candidate validation. The Blinky SWO and TB +captures are approved redistributable test assets, as documented in the [fixture README](test/data/README.md). +Publishing the current statically linked Linux artifacts additionally requires the runtime-license and relinking work +tracked in the [release TODO](docs/todo.md). diff --git a/tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake b/tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake new file mode 100644 index 000000000..c8893b2c8 --- /dev/null +++ b/tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake @@ -0,0 +1,62 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# Generated with AI + +set(CTRACE_OPENCSD_SOURCE_DIR "${CMAKE_SOURCE_DIR}/external/OpenCSD") +if(NOT EXISTS "${CTRACE_OPENCSD_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "OpenCSD is unavailable. Initialize the external/OpenCSD submodule." + ) +endif() + +function(ctrace_remove_opencsd_clang_cl_option_for_msvc) + if(NOT MSVC OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + return() + endif() + + set_property(TARGET opencsd_options PROPERTY INTERFACE_COMPILE_OPTIONS) + target_compile_options(opencsd PRIVATE /EHsc /W1) +endfunction() + +set(OPENCSD_BUILD_MIN_LIB_STATIC ON CACHE BOOL "" FORCE) +set(OPENCSD_FULL_PROJECT_LAYOUT OFF CACHE BOOL "" FORCE) + +# Do not let the embedded project register its own tests or replace the +# devtools install prefix. +get_property(ctrace_had_build_testing CACHE BUILD_TESTING PROPERTY TYPE SET) +if(ctrace_had_build_testing) + get_property(ctrace_saved_build_testing CACHE BUILD_TESTING PROPERTY VALUE) +endif() +set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree." FORCE) + +set(ctrace_saved_install_prefix_default "${CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT}") +set(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT FALSE) + +add_subdirectory( + "${CTRACE_OPENCSD_SOURCE_DIR}" + "${CMAKE_BINARY_DIR}/external/OpenCSD" + EXCLUDE_FROM_ALL +) + +if(ctrace_had_build_testing) + set(BUILD_TESTING "${ctrace_saved_build_testing}" CACHE BOOL "Build the testing tree." FORCE) +else() + unset(BUILD_TESTING CACHE) +endif() +set(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT "${ctrace_saved_install_prefix_default}") + +ctrace_remove_opencsd_clang_cl_option_for_msvc() +target_compile_definitions(opencsd PRIVATE ENABLE_LARGE_TRACE_SOURCES) + +add_library(ctrace-opencsd INTERFACE) +add_library(ctrace::opencsd ALIAS ctrace-opencsd) +target_compile_definitions(ctrace-opencsd INTERFACE + ENABLE_LARGE_TRACE_SOURCES +) +target_include_directories(ctrace-opencsd SYSTEM INTERFACE + "${CTRACE_OPENCSD_SOURCE_DIR}/decoder/include" +) +target_link_libraries(ctrace-opencsd INTERFACE + "$" +) diff --git a/tools/ctrace/docs/OpenCSD-NOTICE.txt b/tools/ctrace/docs/OpenCSD-NOTICE.txt new file mode 100644 index 000000000..d475ee88e --- /dev/null +++ b/tools/ctrace/docs/OpenCSD-NOTICE.txt @@ -0,0 +1,25 @@ +OpenCSD 1.8.3 +https://github.com/Linaro/OpenCSD + +The ctrace executable incorporates unmodified OpenCSD source code from the +pinned revision. The following copyright notices are retained from the +incorporated OpenCSD source files (capitalization and punctuation are +preserved): + +Copyright (c) 2015, ARM Limited. All Rights Reserved. +Copyright (c) 2015, 2019 ARM Limited. All Rights Reserved. +Copyright (c) 2015, 2019, ARM Limited. All Rights Reserved. +Copyright (c) 2015,2019,2022 ARM Limited. All Rights Reserved. +Copyright (c) 2015,2023 ARM Limited. All Rights Reserved. +Copyright (c) 2016, ARM Limited. All Rights Reserved. +Copyright (c) 2017, ARM Limited. All Rights Reserved. +Copyright (c) 2018, ARM Limited. All Rights Reserved. +Copyright (c) 2019, ARM Limited. All Rights Reserved. +Copyright (c) 2020, ARM Limited. All Rights Reserved. +Copyright (c) 2022, ARM Limited. All Rights Reserved. +Copyright (c) 2023, ARM Limited. All Rights Reserved. +Copyright (c) 2024, ARM Limited. All Rights Reserved. + +The applicable BSD-3-Clause license conditions and disclaimer are provided +in THIRD_PARTY_LICENSES/OpenCSD.txt. The OpenCSD build configuration used by +devtools is described in THIRD_PARTY_NOTICES.md. diff --git a/tools/ctrace/docs/THIRD_PARTY_NOTICES.md b/tools/ctrace/docs/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..4f8726916 --- /dev/null +++ b/tools/ctrace/docs/THIRD_PARTY_NOTICES.md @@ -0,0 +1,38 @@ +# ctrace Third-Party Notices + +The ctrace executable incorporates the following open-source components. The +release archive contains the unmodified license text for each component under +`THIRD_PARTY_LICENSES`. OpenCSD's retained source-file copyright notices are +provided in `THIRD_PARTY_LICENSES/OpenCSD-NOTICE.txt`. + +| Component | Version | License | Source | +| --------- | ------: | ------------ | -------------------------------------- | +| cxxopts | 3.0.0 | MIT | | +| yaml-cpp | 0.8.0 | MIT | | +| OpenCSD | 1.8.3 | BSD-3-Clause | | + +The source revisions used for this notice are pinned by the devtools gitlinks: + +- cxxopts: `df229cff0d5b96e146f3f11441f714e8e240cad0` +- yaml-cpp: `f7320141120f720aecc4c32be25586e7da9eb978` +- OpenCSD: `59f15320637ce6e510604e7bed550d7ad109d15d` + +GoogleTest is used only to build the test executable and is not incorporated +into the released ctrace executable. + +## Dependency build configuration + +ctrace does not add private dependency patches. It consumes the repository-wide +cxxopts and yaml-cpp targets configured by devtools. + +`tools/ctrace/cmake/dependencies/ConfigureOpenCSD.cmake` builds OpenCSD's +upstream minimal static target, enables large trace-source IDs, and adjusts its +MSVC build options. The pinned OpenCSD sources remain unchanged. + +The OpenCSD minimal static target still contains upstream decoders other than +ITM because OpenCSD 1.8.3 does not expose a supported ITM-only CMake target. +ctrace does not maintain a private source list that could silently diverge +from upstream. + +Compiler and operating-system runtime code is separate from the application +dependency list above and must be reviewed for every production artifact. diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md new file mode 100644 index 000000000..5725f0657 --- /dev/null +++ b/tools/ctrace/docs/architecture.md @@ -0,0 +1,282 @@ +# ctrace Architecture + +This document describes the internal structure of `ctrace`, the runtime data flow, and the intended extension points. +For command-line usage, build instructions, and release information, see the [project README](../README.md). The +[verified constraints](constraints.md) record preserved contracts; the compact [TODO list](todo.md) tracks remaining +work. + +![ctrace architecture](architecture.svg) + +## Scope + +`ctrace` combines a trace-run configuration with raw CoreSight trace data and converts supported trace channels into +backend-independent semantic events. Output backends consume these events to create CSV or CTF artifacts. + +The first release profile supports SWO data containing ITM and DWT packets. The command line accepts the stable type +names `itm`, `dwt`, `event`, `pmu`, `exception`, `pcsample`, `global_ts`, `overflow`, and `error`. Output semantics are +currently implemented for `itm`, `dwt`, `exception`, `global_ts`, `overflow`, and `error`. DWT event-counter and PMU +packets are retained internally but are not mapped to their selectors yet; periodic PC samples remain disabled. Trace +Bus input is discovered so that a complete trace directory can be inspected, but `*.TB.raw` files are reported and +skipped until a decoder is implemented. + +ITM stream decoding covers the architectural stimulus-port domain `0` through `31`. Public ITM payload output is +restricted to ports `1` through `31`; port `0` is decoded for stream integrity but excluded from CSV and CTF event +output. Trace Compass observes the same filtered CTF stream. + +The architecture separates protocol decoding, semantic interpretation, and output generation. This keeps output +formats independent of OpenCSD and allows another raw trace channel to reuse the event model and output backends. + +## How it works at a glance + +`ctrace` processes one solution set at a time. The common base name joins configuration and trace data; for example, +`Board.ctrace-run.yml` describes the sources and timing metadata required to decode `Board.SWO.raw`. A matching +`Board.TB.raw` is discovered as part of the same solution set but is skipped by the first release profile. + +The main in-memory path is: + +```text +command line + trace-run YAML + SWO raw file + | + v + TraceDirectoryJob / FileDecodeJob + discovery, metadata, output plan + | + v + 64 KiB non-owning byte views + | + v + OpenCSD + ITM framing, packets, recovery + | + v + OpenCsdTraceElement values + | + v + CortexMStreamDecoder / CortexMPostDecoder + timestamps, DWT pairing, quality + | + v + TraceEvent values + | + v + DecodeConsumers + / | \ + diagnostics CSV backend CTF backend +``` + +The `TraceEvent` boundary is the central design point. Before it, code handles byte offsets, OpenCSD packets, decoder +recovery, and Cortex-M state. After it, code sees backend-independent events in decode order and does not depend on +OpenCSD types. + +| Stage | Owner | Transformation | +| --- | --- | --- | +| Discover | `TraceDirectoryJob` | Trace directory and target selection to solution-set configuration and raw inputs | +| Prepare | `CtraceRunMeta`, `FileDecodeJob` | YAML representation to normalized runtime metadata and an output plan | +| Decode protocol | `DecodePipeline`, `OpenCsdItmDecoder` | Raw byte chunks to recoverable OpenCSD trace elements | +| Interpret | `CortexMStreamDecoder`, `CortexMPostDecoder` | Protocol elements to timestamped semantic events | +| Consume | `DecodeConsumers` | One ordered event stream to diagnostics and every enabled output backend | +| Complete | `TraceOutputLifecycle` | Complete active artifacts, or remove them after output or decoder failure | + +## Runtime flow + +1. `CtraceMain` parses and validates the command line. +2. `TraceRunDiscovery` finds one or all `.ctrace-run.yml` files in the selected trace directory. +3. `YmlTraceRunConfigReader` reads fields used by `ctrace`; unrelated and unknown YAML fields are ignored. +4. `CtraceRunMeta` normalizes processor, timestamp, route, ITM, and DWT source metadata. +5. `TraceDirectoryJob` associates the configuration with matching raw trace channels. +6. `FileDecodeJob` creates the requested output plan and reads supported raw files in 64 KiB blocks. +7. `DecodePipeline` receives non-owning `RawByteView` values while preserving decoder state across every file-read + boundary. +8. The OpenCSD adapter decodes ITM protocol elements and preserves decoder warnings, errors, and recovery boundaries. +9. The Cortex-M post-decoder converts protocol elements into semantic `TraceEvent` values. +10. `DecodeConsumers` forwards every event to diagnostics and the selected output backends. +11. Output lifecycle handling completes valid artifacts or removes incomplete artifacts after a failure. + +Without `--csv`, `--ctf`, or `--all`, the same pipeline runs in validation-only mode without creating output files. + +## Processing state and ownership + +One `DecodePipeline` is created for each supported raw file. It owns the OpenCSD adapter and Cortex-M stream decoder, +so protocol, timestamp, and DWT state survive arbitrary file-read boundaries. `RawFileReader` owns a single 64 KiB +buffer; each `RawByteView` borrows that buffer only for the synchronous `DecodePipeline::push` call. Calling +`DecodePipeline::finish` flushes both the OpenCSD and Cortex-M layers before the pipeline is destroyed. + +`CortexMStreamDecoder` maintains an independent post-decoder for each observed Trace Bus ID. All post-decoders emit +into the same `TraceEventSink`, preserving input order while keeping stream-specific timestamp and DWT state apart. + +There is no application-wide event queue. `DecodeConsumers` forwards each event synchronously to the output +lifecycle and issue reporter. Output backends own their files and are isolated from one another: failure of one +backend aborts its incomplete artifact but does not directly stop another active backend. A non-recoverable decoder +error aborts every still-active output for that raw file. + +The diagnostic sink lives for the complete command invocation. It therefore aggregates failures across solution sets +and determines the final process status after processing has continued wherever possible. + +## Recovery after damaged trace + +Recoverable OpenCSD packet errors establish a discontinuity at the reported raw-file offset. Decoder callbacks before +that offset are retained; callbacks at or after it belong to the failed transaction and are discarded. `ctrace` then +resets the OpenCSD decoder and feeds the following input bytes to it unchanged. OpenCSD resumes only after finding a +real ITM hardware-sync sequence; `ctrace` never inserts a synthetic sync sequence. + +Bytes consumed while no usable trace elements are produced form one explicit `data-loss` interval. At its boundary, +the Cortex-M post-decoder flushes pending events, resets incomplete DWT correlation, and marks timestamps unreliable +until the stream supplies enough timing information again. The issue remains part of the ordered `TraceEvent` stream, +so diagnostics and enabled output backends observe the same recovery boundary. + +Failure to reset OpenCSD, repeated lack of decoder progress, or an unsuccessful wait/flush operation aborts only the +current raw-file job. Other solution sets continue to be processed where possible. + +## Suggested code-reading path + +1. Start at [`CtraceMain.cpp`](../src/CtraceMain.cpp) for command-line handling and top-level error policy. +2. Follow [`TraceDirectoryJob.cpp`](../src/control/TraceDirectoryJob.cpp) to see how solution sets, YAML, SWO, and + unsupported Trace Bus input are associated. +3. Read [`FileDecodeJob.cpp`](../src/control/FileDecodeJob.cpp) for output preflight, chunked input, pipeline + construction, and finalization. +4. Continue through [`DecodePipeline.cpp`](../src/decode/DecodePipeline.cpp), + [`OpenCsdItmDecoder.cpp`](../src/decode/OpenCsdItmDecoder.cpp), and + [`CortexMStreamDecoder.cpp`](../src/decode/CortexMStreamDecoder.cpp) for the two decode representations. +5. Use [`TraceEvent.h`](../src/model/TraceEvent.h) as the semantic contract between decoding and all consumers. +6. Finish with [`DecodeConsumers.cpp`](../src/control/DecodeConsumers.cpp) and + [`TraceOutputLifecycle.cpp`](../src/output/TraceOutputLifecycle.cpp), then inspect either the CSV or CTF backend. + +This path follows one trace file through the system without requiring the build-target graph or every backend detail +up front. + +## Module boundaries + +### Entry point and orchestration + +| Module | Responsibility | +| --- | --- | +| `src/CtraceMain.h` | Platform-independent entry point used by the executable trampoline | +| `src/cli` | Command-line parsing, value normalization, and validation | +| `src/control` | Solution-set orchestration, raw-file access, output setup, and per-file decode jobs | +| `src/diagnostics` | Structured diagnostics, severity tracking, and decoder issue reporting | + +`control` is the composition layer. The raw-file reader is a private implementation detail of `FileDecodeJob`, not a +separate module or public abstraction. Control may depend on the other application modules, while lower-level modules +must not depend on control jobs or command-line details. + +### Trace-run metadata + +| Module | Responsibility | +| --- | --- | +| `src/tracerun` | File discovery, YAML parsing, schema subset validation, and normalized metadata | + +The YAML reader intentionally consumes only data required by `ctrace`. A malformed consumed field is an error, while +an unknown field is ignored. This permits compatible trace-run format extensions without weakening validation of the +data used for decoding or output generation. + +`CtraceRunMeta` is the boundary between the YAML representation and runtime processing. Decode and output modules use +normalized metadata instead of navigating YAML nodes. + +### Decode and event model + +| Module | Responsibility | +| --- | --- | +| `src/decode` | Raw-byte decoder interface, OpenCSD integration, packet recovery, timestamps, and Cortex-M semantics | +| `src/model` | Backend-independent event types, quality information, and event selection | + +OpenCSD is isolated behind adapter classes in `src/decode`. OpenCSD-specific elements do not escape into the output +modules. The post-decoder maps them to `TraceEvent` variants such as software trace, DWT data, exceptions, timestamps, +overflow, and trace issues. `TraceSelection` owns the stable public type names and maps semantic events onto that +release-facing set. + +Each event can retain its raw index, Trace Bus ID, timestamp, and quality state. This allows diagnostics and backends +to make independent decisions without reconstructing decoder state. + +### Output + +| Module | Responsibility | +| --- | --- | +| `src/output` | Backend requirements, output planning, and lifecycle management | +| `src/output/csv` | Stable CSV schema, row mapping, filtering, and file output | +| `src/output/ctf` | CTF metadata and stream encoding plus Trace Compass analysis XML | + +Output requirements are evaluated per backend. For example, missing CTF-specific metadata may disable CTF while an +independent CSV output remains valid. `--all` therefore does not make the backends share failure state unnecessarily. + +Outputs use an explicit `start`, `writeEvent`, `stop`, and `abort` lifecycle. A successful backend can finish even if +another backend fails. Decode or finalization failures trigger cleanup of incomplete artifacts. + +## Diagnostics and failure semantics + +Diagnostics carry a severity, message, context, and impact. Severity describes the issue, while impact +determines whether the current job must fail. This distinction allows a trace-run generation error to remain visible +without necessarily preventing the decoding of otherwise valid trace input. + +Decoder issue packets remain part of the event stream. `DecodeConsumers` reports every issue to stderr independently +of output filters and forwards all events to the backends. The backends apply stream and type selection internally; +selected issues become CSV error rows or CTF trace-status events. Repeated issues are not silently collapsed. + +Processing continues with other solution sets where possible. Errors are rendered as `error` even when their impact +causes a non-zero exit status. Unhandled internal ctrace failures also terminate the command after an error diagnostic. + +## External dependencies + +| Dependency | Use | +| --- | --- | +| `cxxopts` | Command-line parsing | +| `yaml-cpp` | Trace-run YAML parsing | +| `OpenCSD` 1.8.3 | ITM protocol decoding; pinned as a repository submodule | +| GoogleTest | Unit-test framework; not linked into the product executable | + +Dependencies are provided by the devtools repository. `tools/ctrace` does not maintain private library copies. +The ITM adapter currently uses OpenCSD `common/` and `interfaces/` headers because the public OpenCSD 1.8.3 boundary +does not provide equivalent access: its installed headers omit the ITM configuration and packet types required by the +decoder callbacks, and its C API exposes only the last structured error rather than all errors from one data-path +operation. Moving the adapter to the public API therefore also requires resolving these two gaps. + +The pinned OpenCSD source is built without a downstream source patch. A known unsafe empty-buffer access in an +upstream diagnostic path, its reachability, and a proposed upstream fix are recorded in the +[OpenCSD issue notes](opencsd-issues.md). + +## Extension points + +### Add a raw trace channel + +Implement a decoder that produces `TraceEvent` values, add it below `src/decode`, and select it in the control layer +for the corresponding channel. A Trace Bus implementation should replace the current warning and skip behavior while +reusing diagnostics, event selection, and output backends. + +### Add an event type + +Extend the model and its stable type names first. Then update the relevant decoder, selection behavior, and every +backend that can represent the event. Tests should cover semantic mapping separately from backend serialization. + +### Add an output backend + +Implement `TraceOutput`, define backend-specific preflight requirements, and add it to the output plan and lifecycle. +Do not introduce backend-specific state into the decode pipeline or event model. + +## Test architecture + +Unit tests under `test/unit/src` mirror the production modules. Shared file, event, and diagnostic helpers live under +`test/unit/support`. One CTest entry runs the complete GoogleTest executable and writes its XML report. + +The `CtraceIntegTests` GoogleTest target calls `CtraceMain` through the same `ctracelib` application object used by the +executable. It verifies command-line behavior, diagnostics, output cleanup, and fixture conversion. A small set of +`ctrace-` CTest smoke tests retains coverage of the platform executable and Windows manifest. Test data and expected +artifacts live under `test/data`; generated files are written only below the CMake build directory. + +CI runs the unit and integration suites on Windows AMD64 and Linux AMD64. ARM64 targets are compiled but not executed, +matching the other devtools workflows. + +## Build and release structure + +The source tree has seven static library targets: `model`, `cli`, `trace-run`, `diagnostics`, `decode`, `output`, and +`control`. The shared `ctracelib` object contains `CtraceMain`; the executable adds only the platform trampoline and +manifest where required. Dependencies form a directed, cycle-free graph with `control` as the composition root. + +The tool-specific GitHub workflow is selected by a `tools/ctrace/` release tag. It builds Windows AMD64 and +Arm64, Linux AMD64 and Arm64, and macOS Arm64 binaries. +The release archive contains the Apache-2.0 project license, application-dependency notices and license texts, +retained OpenCSD copyright notices, and per-file SHA-256 checksums. The version compiled into the executable is derived +from the same tag. The actual compiler and operating-system runtime content still requires inspection for each +production release. + +The checked-in SWO and TB captures are approved ctrace test assets and may be redistributed with devtools. Together +with the tool-specific build, test, packaging, versioning, and license integration, this forms the technical basis for +the first open-source release. diff --git a/tools/ctrace/docs/architecture.svg b/tools/ctrace/docs/architecture.svg new file mode 100644 index 000000000..28f40ea53 --- /dev/null +++ b/tools/ctrace/docs/architecture.svg @@ -0,0 +1,161 @@ + + + + ctrace software architecture + + Seven library modules and the first-release ITM and DWT data flow from command-line, trace-run YAML, and SWO raw + input through orchestration, hardware-sync recovery, the trace event model, DecodeConsumers, diagnostics, and CSV + and CTF output backends. Trace Bus input is discovered but not decoded. + + + + + + + + + + + + + + + + + ctrace architecture + Seven library boundaries · initial ITM/DWT release profile + + INPUTS + + + Command line + trace directory, target, filters + + + *.ctrace-run.yml + setup, routes, source metadata + + + *.SWO.raw + ITM and DWT byte stream + + + *.TB.raw + discovered; not decoded yet + + ORCHESTRATION + + + + cli + options · cxxopts + + + tracerun + YAML · yaml-cpp + + + control + read raw files; compose decode jobs + + + diagnostics + severity · context · impact + + + + + + + + + + DECODE PIPELINE + + + + DecodePipeline + RawByteView · chunk continuity + + + OpenCSD adapter + packets · errors · recovery + resume at real hardware sync + + + Cortex-M decode + timestamps and DWT semantics + + + TraceEvent model + events, type mapping, selection + + + OpenCSD 1.8.3 + pinned external library + + + DecodeConsumers + outputs · issue reporting + + + + + + + + + + OUTPUTS + + + + output orchestration + requirements, lifecycle, cleanup + + + CSV backend + *.SWO.csv + + + CTF backend + *.ctf + Trace Compass XML + + + + + + + runtime data flow + + service or library dependency + + unsupported input path + diff --git a/tools/ctrace/docs/constraints.md b/tools/ctrace/docs/constraints.md new file mode 100644 index 000000000..08440425d --- /dev/null +++ b/tools/ctrace/docs/constraints.md @@ -0,0 +1,45 @@ +# ctrace Constraints + +This document records contracts that implementation changes must preserve. It intentionally does not describe the +runtime flow, module inventory, supported feature profile, build, or release process; those belong in the +[architecture description](architecture.md), [README](../README.md), and [TODO list](todo.md). The CMSIS-Toolbox +[trace specification](https://github.com/Open-CMSIS-Pack/cmsis-toolbox/blob/main/docs/Experimental-Features.md#file-structure-of-ctrace-runyml) +remains authoritative for the external `*.ctrace-run.yml` format. + +## Boundaries + +- OpenCSD types remain inside the decode layer. Other modules and output backends consume semantic `TraceEvent` + values. +- An OpenCSD API migration must retain access to typed ITM configuration and packet data and must preserve every + structured decoder error from each data-path operation; falling back to only the last error or formatted log text + would change recovery behavior. +- YAML types remain inside the trace-run reader. The rest of ctrace consumes normalized configuration and metadata. +- The YAML reader validates fields consumed by ctrace; unrelated fields are outside its validation scope. Malformed + consumed fields remain errors. An ITM reference without `source` values is valid and contributes no source events. +- Backend-specific requirements and failures remain independent; requesting CTF must not disable otherwise valid CSV + output, or vice versa. + +## Decode invariants + +- Raw trace bytes are passed to the decoder unchanged; ctrace never injects synthetic synchronization. Recovery + resumes only at synchronization present in the input. +- File-read chunks are not packet boundaries. Decoder state must survive arbitrary read boundaries. +- Incomplete input and unrecoverable decoder responses remain visible errors. Discontinuities flush or clear pending + DWT state and invalidate timestamp quality before decoding continues. +- Internally, unformatted input uses Trace Bus ID `0`; its CSV `stream` field is empty. Routed IDs are restricted to + `1` through `111`. +- ITM stimulus ports are restricted to `0` through `31`. Timestamp prescalers are stream-specific, default to `1`, + and accept only `1`, `4`, `16`, or `64`. + +## Observable behavior and output safety + +- ITM port `0` is decoded for stream integrity but excluded from payload output. Decoder warnings and errors remain + observable regardless of payload filtering. +- Structured diagnostic impact determines command failure; formatted stderr text does not. +- CTF timestamps never regress, and a global timestamp does not by itself establish local timestamp quality. +- Validation-only mode creates no output. Unsupported trace channels are diagnosed and skipped. +- Cleanup of incomplete output artifacts is attempted after failure, and cleanup failures are reported. Incompatible + target types and overlapping CTF/XML paths are rejected before replacement. + +Changes to these contracts require corresponding unit or integration coverage. Update the architecture document only +when the implementation structure or data flow changes. diff --git a/tools/ctrace/docs/opencsd-issues.md b/tools/ctrace/docs/opencsd-issues.md new file mode 100644 index 000000000..4c56ad9bf --- /dev/null +++ b/tools/ctrace/docs/opencsd-issues.md @@ -0,0 +1,39 @@ + + +# Known OpenCSD issues + +## Empty ITM unsynchronized-data buffer + +The OpenCSD 1.8.3 revision pinned by devtools evaluates +`&m_packet_data[0]` in `TrcPktProcItm::flushUnsyncedBytes()`. The function can +be called with an empty `std::vector`, for example when the sync search flushes +an eight-byte unsynchronized block and then flushes again at the input-block +boundary. Indexing element zero of an empty vector has undefined behavior, +even though `outputRawPacketToMonitor()` returns immediately for a zero-length +packet and does not dereference the resulting pointer. + +ctrace reaches this OpenCSD path during the initial hardware-sync search and +when recovering after an invalid ITM packet sequence. Common release builds +may show no failure, but checked standard-library implementations, sanitizers, +or compiler optimizations can expose the undefined behavior as an assertion, +crash, or other incorrect behavior. + +devtools does not apply a downstream source patch. A possible upstream fix is +to use the vector data pointer, which is valid to pass with a zero size: + +```cpp +outputRawPacketToMonitor( + m_packet_index, + &m_curr_packet, + m_dump_unsynced_bytes, + m_packet_data.data()); +``` + +Alternatively, OpenCSD can skip the call when `m_dump_unsynced_bytes` is zero. +An upstream regression test should exercise sync recovery with an input block +ending immediately after an eight-byte unsynchronized flush. Once OpenCSD +contains and releases the fix, devtools can update the pinned submodule +revision without carrying a local modification. diff --git a/tools/ctrace/docs/todo.md b/tools/ctrace/docs/todo.md new file mode 100644 index 000000000..c75589673 --- /dev/null +++ b/tools/ctrace/docs/todo.md @@ -0,0 +1,27 @@ +# ctrace TODO + +## DWT + +- [ ] Preserve DWT reference, group, and setup-binding identities when expanding source arrays. +- [ ] Complete Armv7-M linked-comparator, range, and value-match decoding. +- [ ] Add Armv8-M and Armv8.1-M DWT decoding. +- [ ] Add `event`, `pmu`, and `pcsample` events to CSV, CTF, and Trace Compass output. + +## Multiple streams + +- [ ] Propagate processor identity into decoded events and outputs. +- [ ] Support separate trace clock domains and cross-stream synchronization in CTF. + +## Additional decoders + +- [ ] Decode formatted `*.TB.raw` input by Trace Bus ID. +- [ ] Add ETM instruction trace decoding and output. +- [ ] Add MTB instruction trace decoding and output. + +## Dependencies and release + +- [ ] Replace private OpenCSD `common/` and `interfaces/` headers with supported public APIs. +- [ ] Update OpenCSD after the [empty-buffer issue](opencsd-issues.md) is fixed upstream. +- [ ] Verify the first release archive and hosted Windows, Linux, and macOS build/test matrix. +- [ ] Complete runtime-license, source-provenance, and relinking requirements for statically linked releases. +- [ ] Decide the signing, macOS notarization, SBOM, and archive-checksum requirements for production releases. diff --git a/tools/ctrace/include/CtraceMain.h b/tools/ctrace/include/CtraceMain.h deleted file mode 100644 index 0b7bc7955..000000000 --- a/tools/ctrace/include/CtraceMain.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2026 Arm Limited. All rights reserved. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef CTRACE_MAIN_H -#define CTRACE_MAIN_H - -int CtraceMain(); - -#endif // CTRACE_MAIN_H diff --git a/tools/ctrace/scripts/package-release.sh b/tools/ctrace/scripts/package-release.sh new file mode 100755 index 000000000..8d2ffdb94 --- /dev/null +++ b/tools/ctrace/scripts/package-release.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source_dir=$(cd -- "${script_dir}/../../.." && pwd) +distribution_dir=${1:-"${source_dir}/tools/ctrace/distribution"} + +expected_binaries=( + bin/darwin-arm64/ctrace + bin/linux-amd64/ctrace + bin/linux-arm64/ctrace + bin/windows-amd64/ctrace.exe + bin/windows-arm64/ctrace.exe +) + +mkdir -p "${distribution_dir}/THIRD_PARTY_LICENSES" +cp "${source_dir}/LICENSE" "${distribution_dir}/LICENSE.txt" +cp "${source_dir}/tools/ctrace/docs/THIRD_PARTY_NOTICES.md" "${distribution_dir}/" +cp "${source_dir}/external/cxxopts/LICENSE" "${distribution_dir}/THIRD_PARTY_LICENSES/cxxopts.txt" +cp "${source_dir}/external/yaml-cpp/LICENSE" "${distribution_dir}/THIRD_PARTY_LICENSES/yaml-cpp.txt" +cp "${source_dir}/external/OpenCSD/LICENSE" "${distribution_dir}/THIRD_PARTY_LICENSES/OpenCSD.txt" +cp "${source_dir}/tools/ctrace/docs/OpenCSD-NOTICE.txt" "${distribution_dir}/THIRD_PARTY_LICENSES/" + +for binary in "${expected_binaries[@]}"; do + if [[ ! -f "${distribution_dir}/${binary}" ]]; then + echo "Missing ctrace release binary: ${binary}" >&2 + exit 1 + fi +done + +expected_inventory=$(printf '%s\n' "${expected_binaries[@]}") +actual_inventory=$(cd "${distribution_dir}" && find bin -type f | LC_ALL=C sort) +if [[ "${actual_inventory}" != "${expected_inventory}" ]]; then + echo "Unexpected ctrace release binary inventory:" >&2 + printf '%s\n' "${actual_inventory}" | sed 's/^/ /' >&2 + exit 1 +fi + +chmod +x \ + "${distribution_dir}/bin/darwin-arm64/ctrace" \ + "${distribution_dir}/bin/linux-amd64/ctrace" \ + "${distribution_dir}/bin/linux-arm64/ctrace" + +checksum() +{ + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" + else + shasum -a 256 "$1" + fi +} + +cd "${distribution_dir}" +{ + while IFS= read -r file; do + checksum "${file}" + done < <(find bin THIRD_PARTY_LICENSES -type f | LC_ALL=C sort) + checksum LICENSE.txt + checksum THIRD_PARTY_NOTICES.md +} > SHA256SUMS + +rm -f ctrace.zip +zip -r ctrace.zip bin LICENSE.txt THIRD_PARTY_LICENSES THIRD_PARTY_NOTICES.md SHA256SUMS diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt new file mode 100644 index 000000000..2c51aa3af --- /dev/null +++ b/tools/ctrace/src/CMakeLists.txt @@ -0,0 +1,189 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set(CTRACE_MODEL_HEADER_FILES + model/TraceEvent.h + model/TraceSelection.h + model/TraceStreamId.h +) +set(CTRACE_CLI_HEADER_FILES + cli/CliOptions.h + cli/CliParser.h +) +set(CTRACE_TRACE_RUN_HEADER_FILES + tracerun/CtraceRunMeta.h + tracerun/TraceRunConfig.h + tracerun/TraceRunConfigReader.h + tracerun/TraceRunDiscovery.h + tracerun/YmlTraceRunConfigReader.h +) +set(CTRACE_DIAGNOSTICS_HEADER_FILES + diagnostics/DiagnosticSink.h + diagnostics/TraceIssueReporter.h +) +set(CTRACE_DECODE_HEADER_FILES + decode/CortexMPostDecoder.h + decode/CortexMStreamDecoder.h + decode/DecodePipeline.h + decode/DwtPacketDecoder.h + decode/OpenCsdErrorController.h + decode/OpenCsdItmDecoder.h + decode/OpenCsdItmSession.h + decode/OpenCsdPacketCollector.h + decode/OpenCsdTraceElement.h + decode/SaturatingArithmetic.h +) +set(CTRACE_OUTPUT_HEADER_FILES + output/OutputRequirements.h + output/TraceOutput.h + output/TraceOutputConfig.h + output/TraceOutputLifecycle.h + output/csv/CsvFileOutput.h + output/csv/CsvRowMapper.h + output/ctf/CtfBundleOutput.h + output/ctf/CtfEncoder.h + output/ctf/CtfExceptionLaneTracker.h + output/ctf/CtfMetadataWriter.h + output/ctf/CtfSchema.h + output/ctf/CtfStreamWriter.h + output/ctf/TraceCompassXmlWriter.h +) +set(CTRACE_CONTROL_HEADER_FILES + control/DecodeConsumers.h + control/FileDecodeJob.h + control/TraceDirectoryJob.h +) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" + PREFIX "Header Files" + FILES + ${CTRACE_MODEL_HEADER_FILES} + ${CTRACE_CLI_HEADER_FILES} + ${CTRACE_TRACE_RUN_HEADER_FILES} + ${CTRACE_DIAGNOSTICS_HEADER_FILES} + ${CTRACE_DECODE_HEADER_FILES} + ${CTRACE_OUTPUT_HEADER_FILES} + ${CTRACE_CONTROL_HEADER_FILES} +) + +add_library(ctrace-model STATIC + model/TraceSelection.cpp + ${CTRACE_MODEL_HEADER_FILES} +) +add_library(ctrace::model ALIAS ctrace-model) +target_include_directories(ctrace-model PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/model" +) + +add_library(ctrace-cli STATIC + cli/CliParser.cpp + ${CTRACE_CLI_HEADER_FILES} +) +add_library(ctrace::cli ALIAS ctrace-cli) +target_include_directories(ctrace-cli + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/cli" + PRIVATE "${PROJECT_BINARY_DIR}" +) +target_link_libraries(ctrace-cli + PUBLIC ctrace::model + PRIVATE cxxopts +) + +add_library(ctrace-trace-run STATIC + tracerun/TraceRunDiscovery.cpp + tracerun/CtraceRunMeta.cpp + tracerun/YmlTraceRunConfigReader.cpp + ${CTRACE_TRACE_RUN_HEADER_FILES} +) +add_library(ctrace::trace-run ALIAS ctrace-trace-run) +target_include_directories(ctrace-trace-run PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/tracerun" +) +target_include_directories(ctrace-trace-run SYSTEM PRIVATE + "${CMAKE_SOURCE_DIR}/external/yaml-cpp/include" +) +target_link_libraries(ctrace-trace-run + PUBLIC ctrace::model + PRIVATE yaml-cpp +) + +add_library(ctrace-diagnostics STATIC + diagnostics/DiagnosticSink.cpp + diagnostics/TraceIssueReporter.cpp + ${CTRACE_DIAGNOSTICS_HEADER_FILES} +) +add_library(ctrace::diagnostics ALIAS ctrace-diagnostics) +target_include_directories(ctrace-diagnostics PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/diagnostics" +) +target_link_libraries(ctrace-diagnostics PUBLIC + ctrace::model +) + +add_library(ctrace-decode STATIC + decode/CortexMPostDecoder.cpp + decode/CortexMStreamDecoder.cpp + decode/DecodePipeline.cpp + decode/DwtPacketDecoder.cpp + decode/OpenCsdErrorController.cpp + decode/OpenCsdItmDecoder.cpp + decode/OpenCsdItmSession.cpp + decode/OpenCsdPacketCollector.cpp + ${CTRACE_DECODE_HEADER_FILES} +) +add_library(ctrace::decode ALIAS ctrace-decode) +target_include_directories(ctrace-decode PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/decode" +) +# Decode headers contain OpenCSD interface types; propagate that build-time +# dependency. ctrace installs no library headers or C++ API. +target_link_libraries(ctrace-decode PUBLIC + ctrace::model + ctrace::opencsd +) + +add_library(ctrace-output STATIC + output/csv/CsvFileOutput.cpp + output/csv/CsvRowMapper.cpp + output/ctf/CtfBundleOutput.cpp + output/ctf/CtfEncoder.cpp + output/ctf/CtfExceptionLaneTracker.cpp + output/ctf/CtfMetadataWriter.cpp + output/ctf/CtfStreamWriter.cpp + output/ctf/TraceCompassXmlWriter.cpp + output/OutputRequirements.cpp + output/TraceOutputLifecycle.cpp + ${CTRACE_OUTPUT_HEADER_FILES} +) +add_library(ctrace::output ALIAS ctrace-output) +target_include_directories(ctrace-output PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/output" +) +target_link_libraries(ctrace-output + PUBLIC + ctrace::diagnostics + ctrace::model + PRIVATE + ctrace::trace-run +) + +add_library(ctrace-control STATIC + control/DecodeConsumers.cpp + control/FileDecodeJob.cpp + control/TraceDirectoryJob.cpp + ${CTRACE_CONTROL_HEADER_FILES} +) +add_library(ctrace::control ALIAS ctrace-control) +target_include_directories(ctrace-control PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/control" +) +# FileDecodeJob.h exposes decoder types to build-tree consumers. +target_link_libraries(ctrace-control PUBLIC + ctrace::cli + ctrace::decode + ctrace::diagnostics + ctrace::model + ctrace::output + ctrace::trace-run +) diff --git a/tools/ctrace/src/CtraceMain.cpp b/tools/ctrace/src/CtraceMain.cpp index 0a8597e67..66ac93d00 100644 --- a/tools/ctrace/src/CtraceMain.cpp +++ b/tools/ctrace/src/CtraceMain.cpp @@ -2,13 +2,62 @@ * Copyright (c) 2026 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 + * Generated with AI */ #include "CtraceMain.h" -#include +#include "CliOptions.h" +#include "CliParser.h" +#include "DiagnosticSink.h" +#include "TraceDirectoryJob.h" +#include "YmlTraceRunConfigReader.h" -int CtraceMain() { - std::printf("ctrace Executable\n"); - return 0; +#include +#include +#include +#include + +/** @brief Converts a caught application failure into one structured diagnostic. */ +static void reportFailure(DiagnosticSink& diagnostics, const std::exception& error) +{ + diagnostics.report({ + DiagnosticSink::Severity::Error, + error.what(), + }); +} + +int CtraceMain(const std::vector& arguments) +{ + StderrDiagnosticSink diagnostics; + CliOptions options; + try { + if (arguments.size() <= 1U) { + std::cout << CliParser::helpString(); + return 0; + } + options = CliParser::parse(arguments); + if (options.help) { + std::cout << CliParser::helpString(); + return 0; + } + CliParser::validate(options); + if (options.version) { + std::cout << CliParser::versionString() << "\n"; + return 0; + } + } catch (const std::exception& error) { + reportFailure(diagnostics, error); + return 1; + } + + try { + YmlTraceRunConfigReader configReader; + TraceDirectoryJob job(options, diagnostics, configReader); + job.run(); + return diagnostics.failureCount() == 0U ? 0 : 1; + } catch (const std::exception& error) { + reportFailure(diagnostics, error); + return 1; + } } diff --git a/tools/ctrace/src/CtraceMain.h b/tools/ctrace/src/CtraceMain.h new file mode 100644 index 000000000..da3fb95b1 --- /dev/null +++ b/tools/ctrace/src/CtraceMain.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CTRACEMAIN_H +#define CTRACE_SRC_CTRACEMAIN_H + +#include +#include + +/** + * @brief Runs the ctrace command-line application. + * + * @param arguments Complete command line, including the executable name. + * @return Zero on success; nonzero when parsing, decoding, or output reports a + * failing diagnostic. + */ +int CtraceMain(const std::vector& arguments); + +#endif // CTRACE_SRC_CTRACEMAIN_H diff --git a/tools/ctrace/src/cli/CliOptions.h b/tools/ctrace/src/cli/CliOptions.h new file mode 100644 index 000000000..a32c2ef58 --- /dev/null +++ b/tools/ctrace/src/cli/CliOptions.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CLI_CLIOPTIONS_H +#define CTRACE_SRC_CLI_CLIOPTIONS_H + +#include "TraceSelection.h" + +#include +#include + +/** @brief Selects the trace output formats requested on the command line. */ +enum class OutputFormat { + None, + Ctf, + Csv, + All, +}; + +/** @brief Stores validated ctrace command-line options. */ +struct CliOptions { + std::optional traceDir; + std::optional targetName; + OutputFormat outputFormat = OutputFormat::None; + TraceSelection selection; + bool help = false; + bool version = false; +}; + +#endif // CTRACE_SRC_CLI_CLIOPTIONS_H diff --git a/tools/ctrace/src/cli/CliParser.cpp b/tools/ctrace/src/cli/CliParser.cpp new file mode 100644 index 000000000..1b51d509c --- /dev/null +++ b/tools/ctrace/src/cli/CliParser.cpp @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CliParser.h" + +#include "CliOptions.h" +#include "ProductInfo.h" +#include "TraceSelection.h" +#include "TraceStreamId.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Tests whether a command-line token begins with an option prefix. */ +static bool startsWithDash(const std::string& value) +{ + return !value.empty() && value.front() == '-'; +} + +/** @brief Parses a complete unsigned command-line value or reports its option. */ +static std::uint32_t parseUnsignedInteger(const std::string& value, const std::string& option) +{ + std::uint32_t parsed = 0; + const auto result = std::from_chars(value.data(), value.data() + value.size(), parsed); + if (value.empty() || result.ec != std::errc{} || result.ptr != value.data() + value.size()) { + throw std::runtime_error(option + " must be an unsigned integer, got " + value); + } + return parsed; +} + +/** @brief Consumes one repeated or space-separated multi-value option. */ +static std::optional> consumeMultiValueOption(const std::string& arg, + const std::string& option, std::size_t& index, + const std::vector& arguments) +{ + if (arg != option && arg.rfind(option + "=", 0) != 0) { + return std::nullopt; + } + + std::vector values; + const auto append = [&](const std::string& value) { + if (value.empty()) { + throw std::runtime_error("Missing value for " + option); + } + if (value.find(',') != std::string::npos) { + throw std::runtime_error(option + " values must be separated by spaces"); + } + values.push_back(value); + }; + if (arg == option) { + while (index + 1U < arguments.size() && !startsWithDash(arguments[index + 1U])) { + append(arguments[++index]); + } + } else { + append(arg.substr(option.size() + 1U)); + } + if (values.empty()) { + throw std::runtime_error("Missing value for " + option); + } + return values; +} + +/** @brief Expands multi-value options into the representation expected by cxxopts. */ +static std::vector normalizeArgsForCxxopts(const std::vector& arguments) +{ + std::vector args; + args.reserve(arguments.size()); + if (!arguments.empty()) { + args.push_back(arguments.front()); + } + + for (std::size_t index = 1U; index < arguments.size(); ++index) { + const auto& arg = arguments[index]; + if (const auto types = consumeMultiValueOption(arg, "--type", index, arguments)) { + for (const auto& type : *types) { + args.emplace_back("--type"); + args.push_back(type); + } + continue; + } + if (const auto streams = consumeMultiValueOption(arg, "--stream", index, arguments)) { + for (const auto& stream : *streams) { + args.emplace_back("--stream"); + args.push_back(stream); + } + continue; + } + args.push_back(arg); + } + return args; +} + +/** @brief Creates a stable argv view over owned argument strings. */ +static std::vector asArgv(const std::vector& args) +{ + std::vector argv; + argv.reserve(args.size()); + for (const auto& arg : args) { + argv.push_back(arg.c_str()); + } + return argv; +} + +/** @brief Resolves output switches into one normalized format selection. */ +static OutputFormat selectedOutputFormat(bool csvRequested, bool ctfRequested, bool allRequested) +{ + if (allRequested || (csvRequested && ctfRequested)) { + return OutputFormat::All; + } + if (csvRequested) { + return OutputFormat::Csv; + } + if (ctfRequested) { + return OutputFormat::Ctf; + } + return OutputFormat::None; +} + +/** @brief Registers the complete ctrace command-line grammar. */ +static void configureCliParser(cxxopts::Options& parser) +{ + using SelectionValues = std::vector; + + parser.custom_help(" [options]"); + parser.set_width(100U); + parser.allow_unrecognised_options(); + parser.add_options()("csv", "Generate only CSV output")("ctf", "Generate only CTF / Trace Compass XML output")( + "a,all", "Generate CSV and CTF / XML output")("type", "Filter output for specific packet types (default: all)", + cxxopts::value(), "sel [...]")( + "stream", "Filter output for specific streams (default: all)", cxxopts::value(), + "sel [...]")("t,target", "Specify a trace solution-set (default: all)", + cxxopts::value())("V,version", "Print version"); + parser.add_options("Hidden")("h,help", "Print help"); +} + +/** @brief Parses normalized command-line arguments into ctrace options. */ +static CliOptions parseCliArgs(const std::vector& arguments) +{ + CliOptions options; + auto normalizedArgs = normalizeArgsForCxxopts(arguments); + auto cargv = asArgv(normalizedArgs); + auto mutableArgc = static_cast(cargv.size()); + + cxxopts::Options parser("ctrace", "CMSIS trace utility"); + configureCliParser(parser); + + const auto parsed = parser.parse(mutableArgc, cargv.data()); + const auto& unknown = parsed.unmatched(); + for (const auto& argument : unknown) { + if (startsWithDash(argument)) { + throw std::runtime_error("Unknown argument: " + argument); + } + if (options.traceDir.has_value()) { + throw std::runtime_error("Unexpected positional argument: " + argument); + } + options.traceDir = argument; + } + + options.help = parsed.count("help") != 0U; + options.version = parsed.count("version") != 0U; + if (parsed.count("target") != 0U) { + options.targetName = parsed["target"].as(); + } + if (parsed.count("type") != 0U) { + for (const auto& type : parsed["type"].as>()) { + options.selection.types.push_back(type); + } + } + if (parsed.count("stream") != 0U) { + for (const auto& stream : parsed["stream"].as>()) { + const auto streamId = parseUnsignedInteger(stream, "--stream"); + if (!CoreSight::isTraceBusId(streamId)) { + throw std::runtime_error("--stream must be a CoreSight Trace Bus ID between 0 and 111, got " + stream); + } + options.selection.streams.push_back(static_cast(streamId)); + } + } + + options.outputFormat = + selectedOutputFormat(parsed.count("csv") != 0U, parsed.count("ctf") != 0U, parsed.count("all") != 0U); + return options; +} + +/** @brief Validates semantic relationships between parsed options. */ +static void validateCliOptions(const CliOptions& options) +{ + if (options.help) { + return; + } + for (const auto& type : options.selection.types) { + if (!parseTraceEventType(type).has_value()) { + throw std::runtime_error("Invalid --type value: " + type + " (accepted: " + traceEventTypeList(", ") + ")"); + } + } + if (options.version) { + return; + } + if (!options.traceDir.has_value()) { + throw std::runtime_error("Specify "); + } +} + +CliOptions CliParser::parse(const std::vector& arguments) +{ + return parseCliArgs(arguments); +} + +void CliParser::validate(const CliOptions& options) +{ + validateCliOptions(options); +} + +std::string CliParser::helpString() +{ + cxxopts::Options parser("ctrace", "CMSIS trace utility"); + configureCliParser(parser); + return parser.help({""}); +} + +std::string CliParser::versionString() +{ + return std::string("ctrace ") + CtraceVersion; +} diff --git a/tools/ctrace/src/cli/CliParser.h b/tools/ctrace/src/cli/CliParser.h new file mode 100644 index 000000000..5bce0417b --- /dev/null +++ b/tools/ctrace/src/cli/CliParser.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CLI_CLIPARSER_H +#define CTRACE_SRC_CLI_CLIPARSER_H + +#include "CliOptions.h" + +#include +#include + +/** @brief Parses and validates the ctrace command line. */ +class CliParser final { +public: + /** + * @brief Parses command-line arguments into structured options. + * @param arguments Complete command line, including the executable name. + * @return Parsed options without cross-option validation. + * @throws std::runtime_error If an option or value is malformed. + */ + static CliOptions parse(const std::vector& arguments); + /** + * @brief Validates relationships between parsed command-line options. + * @param options Options returned by parse(). + * @throws std::runtime_error If options conflict or required values are absent. + */ + static void validate(const CliOptions& options); + /** + * @brief Returns the formatted command-line help text. + * @return Self-contained usage and option documentation. + */ + static std::string helpString(); + /** + * @brief Returns the formatted ctrace version string. + * @return Version text suitable for command-line output. + */ + static std::string versionString(); + +private: + /** @brief Prevents construction of this stateless parser utility. */ + CliParser() = delete; +}; + +#endif // CTRACE_SRC_CLI_CLIPARSER_H diff --git a/tools/ctrace/src/cli/ProductInfo.h.in b/tools/ctrace/src/cli/ProductInfo.h.in new file mode 100644 index 000000000..e26924ad4 --- /dev/null +++ b/tools/ctrace/src/cli/ProductInfo.h.in @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CLI_PRODUCTINFO_H +#define CTRACE_SRC_CLI_PRODUCTINFO_H + +inline constexpr char CtraceVersion[] = "@PROJECT_VERSION_FULL@"; + +#endif // CTRACE_SRC_CLI_PRODUCTINFO_H diff --git a/tools/ctrace/src/control/DecodeConsumers.cpp b/tools/ctrace/src/control/DecodeConsumers.cpp new file mode 100644 index 000000000..7fce924a3 --- /dev/null +++ b/tools/ctrace/src/control/DecodeConsumers.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DecodeConsumers.h" + +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceOutput.h" +#include "TraceOutputLifecycle.h" +#include "TraceStreamId.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Formats an ITM enable mask as a fixed-width hexadecimal value. */ +static std::string hexMask(std::uint32_t value) +{ + std::ostringstream out; + out << "0x" << std::hex << std::setw(8) << std::setfill('0') << value; + return out.str(); +} + +DecodeConsumers::DecodeConsumers(std::vector> outputs, DiagnosticSink& diagnostics, + std::optional itmEnableMask, + std::map itmEnableMasksByTraceBusId) + : m_diagnostics(diagnostics), + m_itmEnableMask(itmEnableMask), + m_itmEnableMasksByTraceBusId(std::move(itmEnableMasksByTraceBusId)), + m_issueReporter(diagnostics), + m_outputLifecycle(std::move(outputs), diagnostics) +{ +} + +void DecodeConsumers::append(const TraceEvent& event) +{ + ++m_eventCount; + m_outputLifecycle.append(event); + reportItmConfigurationMismatch(event); + m_issueReporter.append(event); +} + +void DecodeConsumers::reportItmConfigurationMismatch(const TraceEvent& event) +{ + const auto* software = traceEventPayload(event); + if (software == nullptr || software->channel == CoreSight::kExcludedItmStimulusPort || + !CoreSight::isItmStimulusPort(software->channel)) { + return; + } + + auto enableMask = m_itmEnableMask; + const auto streamMask = m_itmEnableMasksByTraceBusId.find(event.traceBusId); + if (streamMask != m_itmEnableMasksByTraceBusId.end()) { + enableMask = streamMask->second; + } + if (!enableMask.has_value() || ((*enableMask & (1U << software->channel)) != 0U) || + !m_reportedDisabledItmChannels.emplace(event.traceBusId, software->channel).second) { + return; + } + + m_diagnostics.report({ + DiagnosticSink::Severity::Warning, + "ITM data was received on a channel not enabled by ctrace-setup.itm.enable", + { + {"stream", std::to_string(event.traceBusId)}, + {"channel", std::to_string(software->channel)}, + {"enable", hexMask(*enableMask)}, + }, + }); +} + +std::uint64_t DecodeConsumers::eventCount() const +{ + return m_eventCount; +} + +void DecodeConsumers::finishIssues() +{ + m_issueReporter.finish(); +} + +void DecodeConsumers::finishOutputs() noexcept +{ + m_outputLifecycle.finish(); +} + +void DecodeConsumers::abortOutputs() noexcept +{ + m_outputLifecycle.abort(); +} diff --git a/tools/ctrace/src/control/DecodeConsumers.h b/tools/ctrace/src/control/DecodeConsumers.h new file mode 100644 index 000000000..23090828b --- /dev/null +++ b/tools/ctrace/src/control/DecodeConsumers.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CONTROL_DECODECONSUMERS_H +#define CTRACE_SRC_CONTROL_DECODECONSUMERS_H + +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceIssueReporter.h" +#include "TraceOutput.h" +#include "TraceOutputLifecycle.h" + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Fans decoded events out to outputs, diagnostics, and configuration checks. */ +class DecodeConsumers final : public TraceEventSink { +public: + /** @brief Creates the consumers for one raw trace input. */ + DecodeConsumers(std::vector> outputs, DiagnosticSink& diagnostics, + std::optional itmEnableMask = std::nullopt, + std::map itmEnableMasksByTraceBusId = {}); + + /** @brief Forwards one decoded event to all configured consumers. */ + void append(const TraceEvent& event) override; + /** @brief Returns the number of events observed during decoding. */ + std::uint64_t eventCount() const; + /** @brief Completes deferred issue reporting. */ + void finishIssues(); + /** @brief Completes all output artifacts without throwing. */ + void finishOutputs() noexcept; + /** @brief Aborts and removes partial output artifacts without throwing. */ + void abortOutputs() noexcept; + +private: + /** @brief Reports an ITM event that contradicts the configured enable mask. */ + void reportItmConfigurationMismatch(const TraceEvent& event); + + DiagnosticSink& m_diagnostics; + std::optional m_itmEnableMask; + std::map m_itmEnableMasksByTraceBusId; + std::set> m_reportedDisabledItmChannels; + TraceIssueReporter m_issueReporter; + TraceOutputLifecycle m_outputLifecycle; + std::uint64_t m_eventCount = 0; +}; + +#endif // CTRACE_SRC_CONTROL_DECODECONSUMERS_H diff --git a/tools/ctrace/src/control/FileDecodeJob.cpp b/tools/ctrace/src/control/FileDecodeJob.cpp new file mode 100644 index 000000000..e897eda37 --- /dev/null +++ b/tools/ctrace/src/control/FileDecodeJob.cpp @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "FileDecodeJob.h" + +#include "CliOptions.h" +#include "csv/CsvFileOutput.h" +#include "ctf/CtfBundleOutput.h" +#include "CortexMStreamDecoder.h" +#include "DecodeConsumers.h" +#include "DecodePipeline.h" +#include "DiagnosticSink.h" +#include "OpenCsdItmDecoder.h" +#include "OutputRequirements.h" +#include "TraceOutput.h" +#include "TraceOutputConfig.h" +#include "TraceRunConfig.h" +#include "CtraceRunMeta.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Reads a raw trace file in reusable fixed-size chunks. */ +class RawFileReader final { +public: + /** @brief Stores one byte chunk and its end-of-file state. */ + struct ReadResult { + RawByteView bytes; + bool eof = false; + }; + + /** @brief Opens a raw trace input for binary reading. */ + explicit RawFileReader(std::filesystem::path path) + : m_path(std::move(path)), + m_stream(m_path, std::ios::binary), + m_buffer(64U * 1024U) + { + if (!m_stream) { + throw std::runtime_error("failed to open input file: " + m_path.string()); + } + } + + /** @brief Returns the next raw byte chunk. */ + ReadResult read() + { + if (m_eof || !m_stream.is_open()) { + return {{}, true}; + } + + m_stream.read(reinterpret_cast(m_buffer.data()), static_cast(m_buffer.size())); + const auto readBytes = m_stream.gcount(); + if (readBytes > 0) { + if (m_stream.eof()) { + m_eof = true; + m_stream.close(); + } + return {{m_buffer.data(), static_cast(readBytes)}, false}; + } + if (m_stream.bad()) { + throw std::runtime_error("failed to read input file: " + m_path.string()); + } + + m_eof = true; + m_stream.close(); + return {{}, true}; + } + +private: + std::filesystem::path m_path; + std::ifstream m_stream; + std::vector m_buffer; + bool m_eof = false; +}; + +/** @brief Formats event count, input size, elapsed time, and throughput. */ +static std::string decodeSummary(const DecodeResult& decode, std::chrono::steady_clock::duration elapsed) +{ + const auto seconds = std::chrono::duration(elapsed).count(); + const auto mebibytes = static_cast(decode.bytesIn) / (1024.0 * 1024.0); + const auto mebibytesPerSecond = seconds > 0.0 ? mebibytes / seconds : 0.0; + + std::ostringstream out; + out << "decoded " << decode.eventsOut << " events from " << decode.bytesIn << " bytes in " << std::fixed + << std::setprecision(3) << seconds << " s (" << std::setprecision(2) << mebibytesPerSecond << " MiB/s)"; + return out.str(); +} + +/** @brief Extracts fallback and per-stream timestamp prescalers from metadata. */ +static ItmTimestampPrescalers timestampPrescalers(const CtraceRunMeta& ctraceRunMeta) +{ + auto fallback = ctraceRunMeta.timestampPrescaler(); + if (!fallback.has_value() && !ctraceRunMeta.hasDistinctProcessorPrescalers()) { + fallback = TraceRunSchema::kDefaultTimestampPrescaler; + } + return {fallback, ctraceRunMeta.timestampPrescalersByTraceBusId()}; +} + +/** @brief Converts command-line output selection into an output request. */ +static TraceOutputRequest outputRequest(const CliOptions& options) +{ + return { + options.outputFormat == OutputFormat::Csv || options.outputFormat == OutputFormat::All, + options.outputFormat == OutputFormat::Ctf || options.outputFormat == OutputFormat::All, + options.selection, + }; +} + +/** @brief Creates the output backends enabled by a validated plan. */ +static std::vector> createConfiguredOutputs(const TraceOutputPlan& outputPlan, + DiagnosticSink& diagnostics) +{ + std::vector> outputs; + if (outputPlan.ctf.has_value()) { + outputs.push_back(std::make_unique(*outputPlan.ctf, &diagnostics)); + diagnostics.report({ + DiagnosticSink::Severity::Info, + "configured Trace Compass XML", + {{"path", outputPlan.ctf->traceCompassXmlPath.string()}}, + }); + } + if (outputPlan.csv.has_value()) { + outputs.push_back(std::make_unique(outputPlan.csv->outputPath, outputPlan.csv->selection)); + } + return outputs; +} + +FileDecodeJob::FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, + CtraceRunMeta ctraceRunMeta) + : m_options(std::move(options)), + m_rawInputPath(std::move(rawInputPath)), + m_diagnostics(diagnostics), + m_ctraceRunMeta(std::move(ctraceRunMeta)) +{ +} + +FileDecodeJob::FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, + CtraceRunMeta ctraceRunMeta, OpenCsdItmSessionFactory sessionFactory) + : m_options(std::move(options)), + m_rawInputPath(std::move(rawInputPath)), + m_diagnostics(diagnostics), + m_ctraceRunMeta(std::move(ctraceRunMeta)), + m_sessionFactory(std::move(sessionFactory)) +{ +} + +void FileDecodeJob::run() +{ + const auto prescalers = timestampPrescalers(m_ctraceRunMeta); + auto outputPlan = planTraceOutputs(outputRequest(m_options), m_rawInputPath, m_ctraceRunMeta, m_diagnostics); + if (outputPlan.hasRequestedOutputs() && !outputPlan.hasEnabledOutputs()) { + return; + } + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "applied ctrace-run meta", + { + {"path", m_ctraceRunMeta.configPath()}, + {"processors", std::to_string(m_ctraceRunMeta.processorCount())}, + {"sources", std::to_string(m_ctraceRunMeta.sources().size())}, + }, + }); + auto outputs = createConfiguredOutputs(outputPlan, m_diagnostics); + DecodeConsumers consumers(std::move(outputs), m_diagnostics, m_ctraceRunMeta.itmEnableMask(), + m_ctraceRunMeta.itmEnableMasksByTraceBusId()); + + if (prescalers.fallback.has_value()) { + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "using timestamp prescaler", + {{"value", std::to_string(*prescalers.fallback)}}, + }); + } else { + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "using Trace-Bus-ID-specific timestamp prescalers", + {{"traceBusIds", std::to_string(prescalers.byTraceBusId.size())}}, + }); + } + const auto decodeStart = std::chrono::steady_clock::now(); + DecodeResult decode; + bool decoderFatal = false; + try { + RawFileReader input(m_rawInputPath); + std::unique_ptr pipeline; + if (m_sessionFactory) { + pipeline = std::make_unique(prescalers, consumers, m_sessionFactory); + } else { + pipeline = std::make_unique(prescalers, consumers); + } + while (true) { + const auto read = input.read(); + if (read.eof) { + break; + } + pipeline->push(read.bytes); + } + decode = pipeline->finish(); + } catch (const OpenCsdFatalError& error) { + decoderFatal = true; + decode.bytesIn = error.bytesProcessed(); + decode.eventsOut = consumers.eventCount(); + } + consumers.finishIssues(); + const auto decodeEnd = std::chrono::steady_clock::now(); + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + decodeSummary(decode, decodeEnd - decodeStart), + }); + if (decoderFatal) { + consumers.abortOutputs(); + } else { + consumers.finishOutputs(); + } +} diff --git a/tools/ctrace/src/control/FileDecodeJob.h b/tools/ctrace/src/control/FileDecodeJob.h new file mode 100644 index 000000000..64ef81227 --- /dev/null +++ b/tools/ctrace/src/control/FileDecodeJob.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CONTROL_FILEDECODEJOB_H +#define CTRACE_SRC_CONTROL_FILEDECODEJOB_H + +#include "CliOptions.h" +#include "DiagnosticSink.h" +#include "OpenCsdItmDecoder.h" +#include "CtraceRunMeta.h" + +#include + +/** @brief Decodes one raw trace file and owns its configured output lifecycle. */ +class FileDecodeJob { +public: + /** + * @brief Creates a file decode job using the production OpenCSD session. + * @param options Validated command-line options. + * @param rawInputPath Raw SWO input to decode. + * @param diagnostics Sink receiving operational diagnostics. + * @param ctraceRunMeta Normalized metadata for decoding and output. + */ + FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, + CtraceRunMeta ctraceRunMeta); + /** + * @brief Creates a file decode job with an injected OpenCSD session factory. + * @param options Validated command-line options. + * @param rawInputPath Raw SWO input to decode. + * @param diagnostics Sink receiving operational diagnostics. + * @param ctraceRunMeta Normalized metadata for decoding and output. + * @param sessionFactory Factory used to create the decoder session. + */ + FileDecodeJob(CliOptions options, std::filesystem::path rawInputPath, DiagnosticSink& diagnostics, + CtraceRunMeta ctraceRunMeta, OpenCsdItmSessionFactory sessionFactory); + + /** + * @brief Runs decoding, reporting, and output completion for the input file. + * + * Recoverable trace corruption is reported and decoding resumes at hardware + * synchronization. Setup, input, and output failures are reported through the + * diagnostic sink. + */ + void run(); + +private: + CliOptions m_options; + std::filesystem::path m_rawInputPath; + DiagnosticSink& m_diagnostics; + CtraceRunMeta m_ctraceRunMeta; + OpenCsdItmSessionFactory m_sessionFactory; +}; + +#endif // CTRACE_SRC_CONTROL_FILEDECODEJOB_H diff --git a/tools/ctrace/src/control/TraceDirectoryJob.cpp b/tools/ctrace/src/control/TraceDirectoryJob.cpp new file mode 100644 index 000000000..f9b721ee5 --- /dev/null +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceDirectoryJob.h" + +#include "CliOptions.h" +#include "DiagnosticSink.h" +#include "FileDecodeJob.h" +#include "TraceRunConfig.h" +#include "TraceRunConfigReader.h" +#include "TraceRunDiscovery.h" +#include "CtraceRunMeta.h" + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Builds diagnostic context for one trace-run reference. */ +static std::vector> referenceContext(const TraceRunConfig& config, + const TraceRunReference& reference) +{ + std::vector> context{ + {"config", config.path}, + {"ctraceRef", reference.ctraceRef}, + {"type", reference.type}, + }; + if (reference.processorName.has_value()) { + context.emplace_back("pname", *reference.processorName); + } + if (reference.stream.has_value()) { + context.emplace_back("stream", std::to_string(*reference.stream)); + } + return context; +} + +/** @brief Reports informational, warning, and error annotations from trace-run references. */ +static void reportTraceRunDiagnostics(const TraceRunConfig& config, DiagnosticSink& diagnostics) +{ + for (const auto& reference : config.references) { + if (TraceRunSchema::isItmChannelZero(reference)) { + continue; + } + const auto report = [&](DiagnosticSink::Severity severity, const std::optional& message) { + if (!message.has_value() || message->empty()) { + return; + } + diagnostics.report({ + severity, + *message, + referenceContext(config, reference), + }); + }; + report(DiagnosticSink::Severity::Info, reference.info); + report(DiagnosticSink::Severity::Warning, reference.warning); + if (reference.error.has_value()) { + auto context = referenceContext(config, reference); + diagnostics.report({ + DiagnosticSink::Severity::Error, + reference.error->empty() ? "trace generation setup failed without a diagnostic message" : *reference.error, + std::move(context), + DiagnosticSink::Impact::NonFailing, + }); + } + } +} + +/** @brief Reports non-fatal inconsistencies ignored while normalizing trace-run metadata. */ +static void reportTraceRunWarnings(const CtraceRunMeta& meta, DiagnosticSink& diagnostics) +{ + for (const auto& warning : meta.warnings()) { + auto context = warning.context; + context.insert(context.begin(), {"config", meta.configPath()}); + diagnostics.report({ + DiagnosticSink::Severity::Warning, + warning.message, + std::move(context), + }); + } +} + +TraceDirectoryJob::TraceDirectoryJob(CliOptions options, DiagnosticSink& diagnostics, + const TraceRunConfigReader& configReader) + : m_options(std::move(options)), + m_diagnostics(diagnostics), + m_configReader(configReader) +{ +} + +void TraceDirectoryJob::run() +{ + std::vector configFiles; + if (m_options.traceDir.has_value()) { + configFiles = TraceRunDiscovery::selectConfigFiles(*m_options.traceDir, m_options.targetName); + } else { + throw std::runtime_error("trace directory job requires "); + } + + for (const auto& configFile : configFiles) { + const auto solutionSet = TraceRunDiscovery::solutionSetName(configFile); + try { + const auto config = m_configReader.read(configFile.string()); + m_diagnostics.report({ + DiagnosticSink::Severity::Info, + "selected trace-run configuration", + { + {"solutionSet", solutionSet}, + {"path", config.path}, + {"references", std::to_string(config.references.size())}, + {"setups", std::to_string(config.setups.size())}, + }, + }); + reportTraceRunDiagnostics(config, m_diagnostics); + const auto ctraceRunMeta = CtraceRunMeta::fromConfig(config); + reportTraceRunWarnings(ctraceRunMeta, m_diagnostics); + const auto rawInputs = TraceRunDiscovery::rawInputs(configFile); + bool processedSolutionSet = false; + for (const auto& rawInput : rawInputs) { + if (rawInput.channel != "SWO") { + m_diagnostics.report({ + DiagnosticSink::Severity::Warning, + "skipping raw trace channel that is not implemented yet", + { + {"solutionSet", solutionSet}, + {"channel", rawInput.channel}, + {"path", rawInput.path.string()}, + }, + }); + continue; + } + + FileDecodeJob fileJob(m_options, rawInput.path, m_diagnostics, ctraceRunMeta); + fileJob.run(); + processedSolutionSet = true; + } + if (!processedSolutionSet) { + m_diagnostics.report({ + DiagnosticSink::Severity::Error, + "no supported .SWO.raw input found", + { + {"solutionSet", solutionSet}, + {"traceDir", configFile.parent_path().string()}, + }, + }); + } + } catch (const std::exception& error) { + m_diagnostics.report({ + DiagnosticSink::Severity::Error, + error.what(), + { + {"solutionSet", solutionSet}, + {"config", configFile.string()}, + }, + }); + } + } +} diff --git a/tools/ctrace/src/control/TraceDirectoryJob.h b/tools/ctrace/src/control/TraceDirectoryJob.h new file mode 100644 index 000000000..1bec158ad --- /dev/null +++ b/tools/ctrace/src/control/TraceDirectoryJob.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_CONTROL_TRACEDIRECTORYJOB_H +#define CTRACE_SRC_CONTROL_TRACEDIRECTORYJOB_H + +#include "CliOptions.h" +#include "DiagnosticSink.h" +#include "TraceRunConfigReader.h" + +/** @brief Discovers and decodes the selected trace-run configurations in a directory. */ +class TraceDirectoryJob { +public: + /** + * @brief Creates a directory job with parsed options and a trace-run reader. + * @param options Validated command-line options selecting the trace directory. + * @param diagnostics Sink receiving discovery and decode diagnostics. + * @param configReader Reader used for every selected trace-run file. + */ + TraceDirectoryJob(CliOptions options, DiagnosticSink& diagnostics, const TraceRunConfigReader& configReader); + + /** + * @brief Runs discovery and all selected file decode jobs. + * + * Independent input failures are reported and do not prevent later selected + * inputs from being processed. + */ + void run(); + +private: + CliOptions m_options; + DiagnosticSink& m_diagnostics; + const TraceRunConfigReader& m_configReader; +}; + +#endif // CTRACE_SRC_CONTROL_TRACEDIRECTORYJOB_H diff --git a/tools/ctrace/src/ctrace.manifest b/tools/ctrace/src/ctrace.manifest new file mode 100644 index 000000000..853cade1e --- /dev/null +++ b/tools/ctrace/src/ctrace.manifest @@ -0,0 +1,8 @@ + + + + + true + + + diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.cpp b/tools/ctrace/src/decode/CortexMPostDecoder.cpp new file mode 100644 index 000000000..5902fa9d2 --- /dev/null +++ b/tools/ctrace/src/decode/CortexMPostDecoder.cpp @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CortexMPostDecoder.h" + +#include "DwtPacketDecoder.h" +#include "OpenCsdTraceElement.h" +#include "SaturatingArithmetic.h" +#include "TraceEvent.h" + +#include +#include +#include +#include +#include +#include + +CortexMPostDecoder::CortexMPostDecoder(TraceEventSink& eventSink) + : m_eventSink(eventSink) +{ +} + +void CortexMPostDecoder::append(OpenCsdTraceElement element) +{ + mapTimestampSegment(element); + switch (element.kind) { + case OpenCsdTraceElement::Kind::Software: + appendSoftware(element); + break; + case OpenCsdTraceElement::Kind::Hardware: + appendDwt(element); + break; + case OpenCsdTraceElement::Kind::LocalTimestamp: + appendTimestamp(element); + break; + case OpenCsdTraceElement::Kind::GlobalTimestamp: + appendGlobalTimestamp(element); + break; + case OpenCsdTraceElement::Kind::Sync: + appendSync(element); + break; + case OpenCsdTraceElement::Kind::Overflow: + appendOverflow(element); + break; + case OpenCsdTraceElement::Kind::Discontinuity: + appendDiscontinuity(element); + break; + case OpenCsdTraceElement::Kind::Error: + appendError(element); + break; + } +} + +void CortexMPostDecoder::mapTimestampSegment(OpenCsdTraceElement& element) +{ + if (element.overflow) { + startNewTimestampSegment(); + } + + switch (element.kind) { + case OpenCsdTraceElement::Kind::LocalTimestamp: + if (element.tcyc.has_value()) { + const auto mapped = SaturatingArithmetic::add(m_timestampSegmentBase, *element.tcyc); + element.tcyc = mapped; + m_mappedTimeline = mapped; + m_timelineKnown = true; + } + break; + case OpenCsdTraceElement::Kind::Overflow: + case OpenCsdTraceElement::Kind::Discontinuity: + startNewTimestampSegment(); + break; + case OpenCsdTraceElement::Kind::Error: + if (element.discontinuity) { + startNewTimestampSegment(); + } + break; + case OpenCsdTraceElement::Kind::Software: + case OpenCsdTraceElement::Kind::Hardware: + case OpenCsdTraceElement::Kind::GlobalTimestamp: + case OpenCsdTraceElement::Kind::Sync: + break; + } +} + +void CortexMPostDecoder::startNewTimestampSegment() +{ + m_timestampSegmentBase = m_timelineKnown ? m_mappedTimeline : 0U; +} + +void CortexMPostDecoder::finish() +{ + finalizePendingDiscontinuityIssues(std::nullopt); + flushPendingDataTrace(currentTraceStatus()); + flushPendingEvents(std::nullopt, currentTraceStatus()); +} + +std::uint64_t CortexMPostDecoder::eventCount() const +{ + return m_eventCount; +} + +void CortexMPostDecoder::appendSync(const OpenCsdTraceElement& element) +{ + TraceEvent event{SyncTraceEvent{}}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + queueOrEmitWhileAwaitingTimestamp(std::move(event)); +} + +void CortexMPostDecoder::appendOverflow(const OpenCsdTraceElement& element) +{ + noteOverflow(); + const auto status = currentTraceStatus(); + finalizePendingDiscontinuityIssues(std::nullopt); + flushPendingDataTrace(status); + flushPendingEvents(std::nullopt, status); + m_dwtDecoder.reset(); + + TraceEvent event{OverflowTraceEvent{ + "overflow: new timestamp segment; time across boundary may be unreliable", + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = m_timelineKnown ? std::optional(m_currentTcyc) : std::nullopt; + event.quality = TraceQuality{true, false, m_overflowCount}; + emitEvent(event); +} + +void CortexMPostDecoder::appendGlobalTimestamp(const OpenCsdTraceElement& element) +{ + flushPendingDataTrace(currentTraceStatus()); + TraceEvent event{GlobalTimestampTraceEvent{ + element.timestampValue, + element.clockChange, + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + m_pendingEvents.push_back(std::move(event)); +} + +void CortexMPostDecoder::appendDiscontinuity(const OpenCsdTraceElement& element) +{ + const auto status = markDiscontinuity(); + + queueDiscontinuityIssue( + element.sourceIndex, element.traceBusId, status, element.issueCode.value_or(TraceIssueCode::DataLoss), + element.errorMessage.empty() ? "data loss/resync boundary; timestamps across this point may not match" + : element.errorMessage, + element.rawBytesConsumed); +} + +void CortexMPostDecoder::appendError(const OpenCsdTraceElement& element) +{ + const auto status = element.discontinuity ? markDiscontinuity() : currentTraceStatus(); + + TraceEvent event{TraceIssueEvent{ + element.issueCode.value_or(TraceIssueCode::OpenCsdDecodeError), + element.issueSeverity, + element.errorMessage, + element.rawBytesConsumed, + std::nullopt, + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = m_currentTcyc; + event.quality = status; + if (element.awaitingResumeTimestamp) { + std::get(event.payload).lastValidTcyc = m_currentTcyc; + m_pendingEvents.push_back(std::move(event)); + return; + } + queueOrEmitWhileAwaitingTimestamp(std::move(event)); +} + +void CortexMPostDecoder::appendSoftware(const OpenCsdTraceElement& element) +{ + flushPendingDataTrace(currentTraceStatus()); + TraceEvent event{SoftwareTraceEvent{ + element.channel, + element.size, + element.value, + }}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = m_currentTcyc; + event.quality = currentTraceStatus(element.overflow); + m_pendingEvents.push_back(std::move(event)); +} + +void CortexMPostDecoder::appendDwt(const OpenCsdTraceElement& element) +{ + auto events = m_dwtDecoder.decode({ + element.sourceIndex, + element.traceBusId, + static_cast(element.discriminator), + element.size, + element.value, + m_currentTcyc, + currentTraceStatus(element.overflow), + }); + appendPendingEvents(std::move(events)); +} + +void CortexMPostDecoder::appendTimestamp(const OpenCsdTraceElement& element) +{ + m_currentTcyc = element.tcyc.value_or(0); + m_timestampReliable = true; + + const auto status = statusResolvedByTimestamp(element.timestampRelation); + finalizePendingDiscontinuityIssues(m_currentTcyc); + flushPendingDataTrace(status); + flushPendingEvents(m_currentTcyc, status); + + TraceEvent event{LocalTimestampTraceEvent{}}; + event.index = element.sourceIndex; + event.traceBusId = element.traceBusId; + event.tcyc = m_currentTcyc; + emitEvent(event); + + m_timestampReliable = true; + m_dataLossSinceLastTimestamp = false; +} + +void CortexMPostDecoder::flushPendingDataTrace(const TraceQuality& quality) +{ + appendPendingEvents(m_dwtDecoder.flush(quality, m_currentTcyc)); +} + +void CortexMPostDecoder::flushPendingEvents(std::optional tcyc, const TraceQuality& quality) +{ + for (auto& event : m_pendingEvents) { + const auto* issue = traceEventPayload(event); + const auto hasLastValidTcyc = issue != nullptr && issue->lastValidTcyc.has_value(); + const auto isControlEvent = isTraceEvent(event) || isTraceEvent(event); + if (tcyc.has_value() && !isControlEvent && !hasLastValidTcyc) { + event.tcyc = tcyc; + } + if (!isControlEvent && !hasLastValidTcyc) { + event.quality = quality; + } + emitEvent(event); + } + m_pendingEvents.clear(); +} + +void CortexMPostDecoder::appendPendingEvents(std::vector events) +{ + m_pendingEvents.insert(m_pendingEvents.end(), std::make_move_iterator(events.begin()), + std::make_move_iterator(events.end())); +} + +void CortexMPostDecoder::queueDiscontinuityIssue(std::uint64_t sourceIndex, std::uint8_t traceBusId, + const TraceQuality& quality, TraceIssueCode issueCode, + const std::string& message, + std::optional rawBytesConsumed) +{ + TraceEvent event{TraceIssueEvent{ + issueCode, + TraceIssueSeverity::Error, + message, + rawBytesConsumed, + m_currentTcyc, + }}; + event.index = sourceIndex; + event.traceBusId = traceBusId; + event.tcyc = m_currentTcyc; + event.quality = quality; + m_pendingEvents.push_back(std::move(event)); +} + +void CortexMPostDecoder::finalizePendingDiscontinuityIssues(std::optional firstResumedTcyc) +{ + for (auto& event : m_pendingEvents) { + auto* issue = traceEventPayload(event); + if (issue == nullptr || !issue->lastValidTcyc.has_value()) { + continue; + } + issue->message += "; timestamp " + std::to_string(*issue->lastValidTcyc) + " .. " + + (firstResumedTcyc.has_value() ? std::to_string(*firstResumedTcyc) : "unknown") + "."; + } +} + +void CortexMPostDecoder::queueOrEmitWhileAwaitingTimestamp(TraceEvent event) +{ + if (!m_pendingEvents.empty()) { + m_pendingEvents.push_back(std::move(event)); + return; + } + emitEvent(event); +} + +TraceQuality CortexMPostDecoder::markDiscontinuity() +{ + noteOverflow(); + const auto status = currentTraceStatus(); + finalizePendingDiscontinuityIssues(std::nullopt); + flushPendingDataTrace(status); + flushPendingEvents(std::nullopt, status); + m_dwtDecoder.reset(); + m_pendingEvents.clear(); + m_timestampReliable = false; + m_dataLossSinceLastTimestamp = true; + return status; +} + +void CortexMPostDecoder::emitEvent(const TraceEvent& event) +{ + m_eventSink.append(event); + ++m_eventCount; +} + +TraceQuality CortexMPostDecoder::currentTraceStatus(bool packetOverflow) const +{ + return { + m_dataLossSinceLastTimestamp || !m_timestampReliable || packetOverflow, + m_timestampReliable && !m_dataLossSinceLastTimestamp && !packetOverflow, + m_overflowCount, + }; +} + +TraceQuality CortexMPostDecoder::statusResolvedByTimestamp(LocalTimestampRelation relation) const +{ + const auto dataIntact = !m_dataLossSinceLastTimestamp; + const auto payloadDelayed = relation == LocalTimestampRelation::PayloadDelayed || + relation == LocalTimestampRelation::TimestampAndPayloadDelayed; + return {!dataIntact, dataIntact && !payloadDelayed, m_overflowCount}; +} + +void CortexMPostDecoder::noteOverflow() +{ + ++m_overflowCount; + m_dataLossSinceLastTimestamp = true; + m_timestampReliable = false; +} diff --git a/tools/ctrace/src/decode/CortexMPostDecoder.h b/tools/ctrace/src/decode/CortexMPostDecoder.h new file mode 100644 index 000000000..56cdc01e4 --- /dev/null +++ b/tools/ctrace/src/decode/CortexMPostDecoder.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_CORTEXMPOSTDECODER_H +#define CTRACE_SRC_DECODE_CORTEXMPOSTDECODER_H + +#include "OpenCsdTraceElement.h" +#include "DwtPacketDecoder.h" +#include "TraceEvent.h" + +#include +#include +#include +#include + +/** @brief Converts OpenCSD elements from one Cortex-M stream into semantic events. */ +class CortexMPostDecoder final : public OpenCsdTraceElementSink { +public: + /** @brief Creates a post-decoder that emits to the supplied event sink. */ + explicit CortexMPostDecoder(TraceEventSink& eventSink); + + /** @brief Appends one OpenCSD trace element. */ + void append(OpenCsdTraceElement element) override; + /** @brief Flushes pending state at the end of input. */ + void finish(); + + /** @brief Returns the number of semantic events emitted. */ + std::uint64_t eventCount() const; + +private: + /** @brief Emits a synchronization event and starts a reliable trace segment. */ + void appendSync(const OpenCsdTraceElement& element); + /** @brief Records a hardware overflow and invalidates timestamp quality. */ + void appendOverflow(const OpenCsdTraceElement& element); + /** @brief Emits a reconstructed global timestamp event. */ + void appendGlobalTimestamp(const OpenCsdTraceElement& element); + /** @brief Queues a recoverable stream discontinuity for ordered emission. */ + void appendDiscontinuity(const OpenCsdTraceElement& element); + /** @brief Converts a decoder issue element into a semantic error event. */ + void appendError(const OpenCsdTraceElement& element); + /** @brief Emits an ITM software event with current trace quality. */ + void appendSoftware(const OpenCsdTraceElement& element); + /** @brief Passes a DWT payload through semantic packet reconstruction. */ + void appendDwt(const OpenCsdTraceElement& element); + /** @brief Applies one local timestamp to pending and subsequent events. */ + void appendTimestamp(const OpenCsdTraceElement& element); + + /** @brief Queues an issue whose final interval ends at the next reliable timestamp. */ + void queueDiscontinuityIssue(std::uint64_t sourceIndex, std::uint8_t traceBusId, const TraceQuality& quality, + TraceIssueCode issueCode, const std::string& message, + std::optional rawBytesConsumed = std::nullopt); + /** @brief Finalizes queued discontinuity intervals at the first resumed timestamp. */ + void finalizePendingDiscontinuityIssues(std::optional firstResumedTcyc); + /** @brief Queues an event until time is known or emits it immediately. */ + void queueOrEmitWhileAwaitingTimestamp(TraceEvent event); + /** @brief Marks the current stream state as discontinuous and unreliable. */ + TraceQuality markDiscontinuity(); + /** @brief Flushes incomplete DWT reconstruction at a stream boundary. */ + void flushPendingDataTrace(const TraceQuality& quality); + /** @brief Emits all events waiting for a resolved timestamp. */ + void flushPendingEvents(std::optional tcyc, const TraceQuality& quality); + /** @brief Appends reconstructed DWT events to the pending sequence. */ + void appendPendingEvents(std::vector events); + /** @brief Sends one finalized event to the downstream sink. */ + void emitEvent(const TraceEvent& event); + /** @brief Maps a decoder-local timestamp onto the monotonic output timeline. */ + void mapTimestampSegment(OpenCsdTraceElement& element); + /** @brief Starts a new timestamp mapping segment after discontinuity. */ + void startNewTimestampSegment(); + + /** @brief Returns quality derived from current overflow and timestamp state. */ + TraceQuality currentTraceStatus(bool packetOverflow = false) const; + /** @brief Returns quality after applying a local timestamp relation. */ + TraceQuality statusResolvedByTimestamp(LocalTimestampRelation relation) const; + /** @brief Increments the saturated overflow counter. */ + void noteOverflow(); + + TraceEventSink& m_eventSink; + std::uint64_t m_eventCount = 0; + std::vector m_pendingEvents; + DwtPacketDecoder m_dwtDecoder; + bool m_timestampReliable = false; + bool m_dataLossSinceLastTimestamp = false; + std::uint64_t m_overflowCount = 0; + std::uint64_t m_currentTcyc = 0; + bool m_timelineKnown = false; + std::uint64_t m_mappedTimeline = 0; + std::uint64_t m_timestampSegmentBase = 0; +}; + +#endif // CTRACE_SRC_DECODE_CORTEXMPOSTDECODER_H diff --git a/tools/ctrace/src/decode/CortexMStreamDecoder.cpp b/tools/ctrace/src/decode/CortexMStreamDecoder.cpp new file mode 100644 index 000000000..07db9204e --- /dev/null +++ b/tools/ctrace/src/decode/CortexMStreamDecoder.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CortexMStreamDecoder.h" + +#include "CortexMPostDecoder.h" +#include "OpenCsdTraceElement.h" +#include "SaturatingArithmetic.h" +#include "TraceEvent.h" + +#include +#include +#include +#include +#include + +CortexMStreamDecoder::CortexMStreamDecoder(ItmTimestampPrescalers prescalers, TraceEventSink& eventSink) + : m_prescalers(std::move(prescalers)), + m_eventSink(eventSink) +{ +} + +CortexMStreamDecoder::~CortexMStreamDecoder() = default; + +void CortexMStreamDecoder::append(OpenCsdTraceElement element) +{ + if (element.kind == OpenCsdTraceElement::Kind::LocalTimestamp && element.tcyc.has_value()) { + element.tcyc = SaturatingArithmetic::multiply(*element.tcyc, prescaler(element.traceBusId)); + } + decoder(element.traceBusId).append(std::move(element)); +} + +void CortexMStreamDecoder::finish() +{ + for (auto& [stream, decoder] : m_decoders) { + (void)stream; + decoder->finish(); + } +} + +std::uint64_t CortexMStreamDecoder::eventCount() const +{ + std::uint64_t count = 0U; + for (const auto& [stream, decoder] : m_decoders) { + (void)stream; + count += decoder->eventCount(); + } + return count; +} + +std::uint32_t CortexMStreamDecoder::prescaler(std::uint8_t traceBusId) const +{ + const auto found = m_prescalers.byTraceBusId.find(traceBusId); + if (found != m_prescalers.byTraceBusId.end()) { + return found->second; + } + if (m_prescalers.fallback.has_value()) { + return *m_prescalers.fallback; + } + if (traceBusId == 0U) { + throw std::runtime_error("ITM timestamps with Trace Bus ID 0 cannot be assigned to " + "different processor prescalers; a formatted source ID is required"); + } + throw std::runtime_error("CoreSight Trace Bus ID " + std::to_string(traceBusId) + + " has no unambiguous processor timestamp prescaler"); +} + +CortexMPostDecoder& CortexMStreamDecoder::decoder(std::uint8_t traceBusId) +{ + auto& result = m_decoders[traceBusId]; + if (!result) { + result = std::make_unique(m_eventSink); + } + return *result; +} diff --git a/tools/ctrace/src/decode/CortexMStreamDecoder.h b/tools/ctrace/src/decode/CortexMStreamDecoder.h new file mode 100644 index 000000000..2a61ec5c7 --- /dev/null +++ b/tools/ctrace/src/decode/CortexMStreamDecoder.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_CORTEXMSTREAMDECODER_H +#define CTRACE_SRC_DECODE_CORTEXMSTREAMDECODER_H + +#include "OpenCsdTraceElement.h" +#include "TraceEvent.h" + +#include +#include +#include +#include + +class CortexMPostDecoder; + +/** @brief Stores fallback and stream-specific ITM timestamp prescalers. */ +struct ItmTimestampPrescalers { + std::optional fallback; + std::map byTraceBusId; +}; + +/** @brief Routes OpenCSD elements to per-stream Cortex-M post-decoders. */ +class CortexMStreamDecoder final : public OpenCsdTraceElementSink { +public: + /** @brief Creates a stream router with timestamp scaling configuration. */ + CortexMStreamDecoder(ItmTimestampPrescalers prescalers, TraceEventSink& eventSink); + /** @brief Destroys all per-stream post-decoders. */ + ~CortexMStreamDecoder(); + + /** @brief Disables copying because stream decoders own unique state. */ + CortexMStreamDecoder(const CortexMStreamDecoder&) = delete; + /** @brief Disables copy assignment because stream decoders own unique state. */ + CortexMStreamDecoder& operator=(const CortexMStreamDecoder&) = delete; + + /** @brief Routes one element to its Trace Bus ID stream. */ + void append(OpenCsdTraceElement element) override; + /** @brief Flushes all active stream decoders. */ + void finish(); + /** @brief Returns the combined event count of all streams. */ + std::uint64_t eventCount() const; + +private: + /** @brief Resolves the configured timestamp prescaler for one Trace Bus ID. */ + std::uint32_t prescaler(std::uint8_t traceBusId) const; + /** @brief Returns or lazily creates the post-decoder for one Trace Bus ID. */ + CortexMPostDecoder& decoder(std::uint8_t traceBusId); + + ItmTimestampPrescalers m_prescalers; + TraceEventSink& m_eventSink; + std::map> m_decoders; +}; + +#endif // CTRACE_SRC_DECODE_CORTEXMSTREAMDECODER_H diff --git a/tools/ctrace/src/decode/DecodePipeline.cpp b/tools/ctrace/src/decode/DecodePipeline.cpp new file mode 100644 index 000000000..5ddb80f1e --- /dev/null +++ b/tools/ctrace/src/decode/DecodePipeline.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DecodePipeline.h" + +#include "CortexMStreamDecoder.h" +#include "OpenCsdItmDecoder.h" +#include "TraceEvent.h" + +#include +#include +#include +#include + +DecodePipeline::DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink) + : m_streamDecoder(std::move(timestampPrescalers), eventSink), + m_decoder(m_streamDecoder) +{ +} + +DecodePipeline::DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink, + const OpenCsdItmSessionFactory& sessionFactory) + : m_streamDecoder(std::move(timestampPrescalers), eventSink), + m_decoder(m_streamDecoder, sessionFactory) +{ +} + +void DecodePipeline::push(RawByteView bytes) +{ + if (bytes.empty()) { + return; + } + if (bytes.size > std::numeric_limits::max()) { + throw std::runtime_error("raw decode chunk is too large"); + } + m_decoder.push(bytes.data, static_cast(bytes.size)); +} + +DecodeResult DecodePipeline::finish() +{ + const auto result = m_decoder.finish(); + m_streamDecoder.finish(); + return { + result.bytesIn, + m_streamDecoder.eventCount(), + }; +} diff --git a/tools/ctrace/src/decode/DecodePipeline.h b/tools/ctrace/src/decode/DecodePipeline.h new file mode 100644 index 000000000..d0ee3680d --- /dev/null +++ b/tools/ctrace/src/decode/DecodePipeline.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_DECODEPIPELINE_H +#define CTRACE_SRC_DECODE_DECODEPIPELINE_H + +#include "CortexMStreamDecoder.h" +#include "TraceEvent.h" +#include "OpenCsdItmDecoder.h" + +#include +#include + +/** @brief Provides a non-owning view of one raw trace byte chunk. */ +struct RawByteView { + const std::uint8_t* data = nullptr; + std::size_t size = 0; + + /** @brief Reports whether the view contains no bytes. */ + constexpr bool empty() const + { + return size == 0U; + } +}; + +/** @brief Summarizes raw bytes consumed and semantic events produced. */ +struct DecodeResult { + std::uint64_t bytesIn = 0; + std::uint64_t eventsOut = 0; +}; + +/** @brief Connects raw OpenCSD decoding with Cortex-M semantic post-decoding. */ +class DecodePipeline final { +public: + /** + * @brief Creates a pipeline with stream-specific timestamp prescalers. + * @param timestampPrescalers Default and per-stream timestamp prescalers. + * @param eventSink Sink receiving decoded events synchronously. + */ + DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink); + /** + * @brief Creates a pipeline with an injected OpenCSD session factory. + * @param timestampPrescalers Default and per-stream timestamp prescalers. + * @param eventSink Sink receiving decoded events synchronously. + * @param sessionFactory Factory used to create the OpenCSD session. + */ + DecodePipeline(ItmTimestampPrescalers timestampPrescalers, TraceEventSink& eventSink, + const OpenCsdItmSessionFactory& sessionFactory); + + /** + * @brief Pushes the next contiguous chunk of raw trace bytes. + * @param bytes Non-owning byte view that remains valid for this call. + * @throws OpenCsdFatalError If the external decoder cannot continue safely. + */ + void push(RawByteView bytes); + /** + * @brief Finalizes decoding and returns aggregate counters. + * @return Total raw bytes consumed and semantic events emitted. + * @throws OpenCsdFatalError If decoder finalization fails. + */ + DecodeResult finish(); + +private: + CortexMStreamDecoder m_streamDecoder; + OpenCsdItmDecoder m_decoder; +}; + +#endif // CTRACE_SRC_DECODE_DECODEPIPELINE_H diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.cpp b/tools/ctrace/src/decode/DwtPacketDecoder.cpp new file mode 100644 index 000000000..c61b2a6c0 --- /dev/null +++ b/tools/ctrace/src/decode/DwtPacketDecoder.cpp @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DwtPacketDecoder.h" + +#include "TraceEvent.h" + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Identifies the hardware source encoded by a DWT packet discriminator. */ +enum class DwtPacketSource : std::uint8_t { + EventCounter = 0U, + ExceptionTrace = 1U, + PeriodicPcSample = 2U, + PmuOverflow = 3U, +}; + +/** @brief Identifies address and value variants of DWT data-trace packets. */ +enum class DwtDataPacketType : std::uint8_t { + Address = 1U, + Value = 2U, +}; + +/** @brief Maps encoded DWT exception actions to their wire values. */ +enum class DwtExceptionActionCode : std::uint32_t { + Entered = 1U, + Exited = 2U, + Returned = 3U, +}; + +// DWT hardware source IDs 8..23 encode packet type, comparator, and subtype. +constexpr std::uint8_t kFirstDataTraceSource = 8U; +constexpr std::uint8_t kLastDataTraceSource = 23U; +constexpr std::uint8_t kDataTraceSubtypeMask = 0x1U; +constexpr std::uint8_t kDataTraceComparatorMask = 0x3U; +constexpr std::uint8_t kDataTraceComparatorShift = 1U; +constexpr std::uint8_t kDataTracePacketTypeMask = 0x3U; +constexpr std::uint8_t kDataTracePacketTypeShift = 3U; + +constexpr std::uint32_t kExceptionNumberMask = 0x1ffU; +constexpr std::uint32_t kExceptionActionMask = 0x3U; +constexpr std::uint32_t kExceptionActionShift = 12U; + +constexpr std::uint8_t kArmv7MFullPcBytes = 4U; +constexpr std::uint8_t kArmv7MAddressOffsetBytes = 2U; + +std::vector DwtPacketDecoder::decode(const DwtPayloadPacket& payload) +{ + std::vector output; + const auto discriminator = payload.discriminator; + + const auto source = static_cast(discriminator); + if (source == DwtPacketSource::EventCounter || source == DwtPacketSource::PmuOverflow) { + output = flush(payload.quality, payload.tcyc); + TraceEvent packet = source == DwtPacketSource::EventCounter + ? TraceEvent(DwtEventTraceEvent{discriminator, payload.size, payload.value}) + : TraceEvent(PmuTraceEvent{discriminator, payload.size, payload.value}); + packet.index = payload.index; + packet.traceBusId = payload.traceBusId; + packet.tcyc = payload.tcyc; + packet.quality = payload.quality; + output.push_back(std::move(packet)); + return output; + } + + if (source == DwtPacketSource::ExceptionTrace) { + output = flush(payload.quality, payload.tcyc); + const auto exceptionNumber = payload.value & kExceptionNumberMask; + const auto action = exceptionAction((payload.value >> kExceptionActionShift) & kExceptionActionMask); + if (action == ExceptionAction::Unknown) { + TraceEvent error{TraceIssueEvent{ + TraceIssueCode::InvalidExceptionAction, + TraceIssueSeverity::Error, + "invalid exception action 0x0 for exception " + std::to_string(exceptionNumber), + std::nullopt, + std::nullopt, + }}; + error.index = payload.index; + error.traceBusId = payload.traceBusId; + error.tcyc = payload.tcyc; + error.quality = payload.quality; + output.push_back(std::move(error)); + return output; + } + TraceEvent packet{ExceptionTraceEvent{exceptionNumber, action}}; + packet.index = payload.index; + packet.traceBusId = payload.traceBusId; + packet.tcyc = payload.tcyc; + packet.quality = payload.quality; + output.push_back(std::move(packet)); + return output; + } + + if (source == DwtPacketSource::PeriodicPcSample) { + // PC samples need a dedicated output event. Until that event is + // defined, flush preceding data trace but do not expose the sample as + // an address event. + output = flush(payload.quality, payload.tcyc); + return output; + } + + if (discriminator >= kFirstDataTraceSource && discriminator <= kLastDataTraceSource) { + decodeDataTrace(payload, output); + return output; + } + + output = flush(payload.quality, payload.tcyc); + return output; +} + +std::vector DwtPacketDecoder::flush(const TraceQuality& quality, std::uint64_t tcyc) +{ + std::vector output; + std::vector comparators; + for (std::uint32_t comparator = 0; comparator < m_pendingDataTrace.size(); ++comparator) { + if (m_pendingDataTrace[comparator].has_value()) { + comparators.push_back(comparator); + } + } + std::sort(comparators.begin(), comparators.end(), [this](const auto left, const auto right) { + const auto leftIndex = m_pendingDataTrace[left]->index; + const auto rightIndex = m_pendingDataTrace[right]->index; + return leftIndex == rightIndex ? left < right : leftIndex < rightIndex; + }); + for (const auto comparator : comparators) { + flushPending(comparator, quality, tcyc, output); + } + return output; +} + +void DwtPacketDecoder::reset() +{ + for (auto& pending : m_pendingDataTrace) { + pending.reset(); + } +} + +void DwtPacketDecoder::decodeDataTrace(const DwtPayloadPacket& payload, std::vector& output) +{ + const auto discriminator = payload.discriminator; + const auto comparator = + static_cast((discriminator >> kDataTraceComparatorShift) & kDataTraceComparatorMask); + const auto packetType = + static_cast((discriminator >> kDataTracePacketTypeShift) & kDataTracePacketTypeMask); + const auto secondarySubtype = (discriminator & kDataTraceSubtypeMask) != 0U; + + PendingDataTrace event; + event.index = payload.index; + event.traceBusId = payload.traceBusId; + event.quality = payload.quality; + + if (packetType == DwtDataPacketType::Address) { + const auto expectedSize = secondarySubtype ? kArmv7MAddressOffsetBytes : kArmv7MFullPcBytes; + if (payload.size != expectedSize) { + auto flushed = flush(payload.quality, payload.tcyc); + output.insert(output.end(), std::make_move_iterator(flushed.begin()), std::make_move_iterator(flushed.end())); + TraceEvent error{TraceIssueEvent{ + TraceIssueCode::UnsupportedDwtAddressPayload, + TraceIssueSeverity::Error, + "unsupported DWT " + std::string(secondarySubtype ? "address" : "PC or match") + " payload size " + + std::to_string(payload.size) + + "; the current ctrace-run format does not provide the " + "architecture and reconstruction data needed to decode it safely", + std::nullopt, + std::nullopt, + }}; + error.index = payload.index; + error.traceBusId = payload.traceBusId; + error.tcyc = payload.tcyc; + error.quality = payload.quality; + output.push_back(std::move(error)); + return; + } + if (secondarySubtype) { + event.addressLo16 = payload.value; + event.hasAddressLo16 = true; + } else { + event.pc = payload.value; + event.hasPc = true; + } + sendDataTraceEvent(comparator, event, payload.quality, payload.tcyc, output); + return; + } + // Discriminators 8..23 encode either an address or a value packet. The + // address case returned above, so the remaining packet is a value. + event.value = payload.value; + event.size = payload.size; + event.isRead = !secondarySubtype; + event.hasValue = true; + sendDataTraceEvent(comparator, event, payload.quality, payload.tcyc, output); +} + +void DwtPacketDecoder::sendDataTraceEvent(std::uint32_t comparator, const PendingDataTrace& event, + const TraceQuality& quality, std::uint64_t tcyc, + std::vector& output) +{ + auto& pending = m_pendingDataTrace[comparator]; + if (!pending.has_value()) { + pending = event; + return; + } + + // The individual short-circuit permutations are an implementation detail; + // repeated and complementary fragments are covered as complete behaviors. + const auto repeatsFragmentKind = (pending->hasPc && event.hasPc) || + (pending->hasAddressLo16 && event.hasAddressLo16) || + (pending->hasValue && event.hasValue); + if (!repeatsFragmentKind) { + pending->index = event.index; + pending->traceBusId = event.traceBusId; + pending->pc = event.hasPc ? event.pc : pending->pc; + pending->addressLo16 = event.hasAddressLo16 ? event.addressLo16 : pending->addressLo16; + pending->value = event.hasValue ? event.value : pending->value; + pending->size = event.hasValue ? event.size : pending->size; + pending->isRead = event.hasValue ? event.isRead : pending->isRead; + pending->hasPc = pending->hasPc || event.hasPc; + pending->hasAddressLo16 = pending->hasAddressLo16 || event.hasAddressLo16; + pending->hasValue = pending->hasValue || event.hasValue; + pending->quality.overflow = pending->quality.overflow || event.quality.overflow; + pending->quality.timestampReliable = pending->quality.timestampReliable && event.quality.timestampReliable; + pending->quality.overflowCount = std::max(pending->quality.overflowCount, event.quality.overflowCount); + if (pending->hasValue) { + flushPending(comparator, qualityForPendingFlush(*pending, quality), tcyc, output); + } + return; + } + + flushPending(comparator, qualityForPendingFlush(*pending, quality), tcyc, output); + pending = event; +} + +void DwtPacketDecoder::flushPending(std::uint32_t comparator, const TraceQuality& quality, std::uint64_t tcyc, + std::vector& output) +{ + auto& pending = m_pendingDataTrace[comparator]; + + const auto makePacket = [&]() -> TraceEvent { + if (pending->hasValue) { + return TraceEvent(DwtDataTraceEvent{ + comparator, + pending->size, + pending->value, + pending->isRead ? AccessType::Read : AccessType::Write, + pending->hasAddressLo16 ? std::optional(pending->addressLo16) : std::nullopt, + pending->hasPc ? std::optional(pending->pc) : std::nullopt, + }); + } + + DwtAddressTraceLocation location = DwtOffsetTraceLocation{pending->addressLo16}; + if (pending->hasPc && pending->hasAddressLo16) { + location = DwtPcAndOffsetTraceLocation{pending->pc, pending->addressLo16}; + } else if (pending->hasPc) { + location = DwtPcTraceLocation{pending->pc}; + } + return TraceEvent(DwtAddressTraceEvent{comparator, location}); + }; + + TraceEvent packet = makePacket(); + packet.index = pending->index; + packet.traceBusId = pending->traceBusId; + packet.tcyc = tcyc; + packet.quality = quality; + output.push_back(std::move(packet)); + pending.reset(); +} + +TraceQuality DwtPacketDecoder::qualityForPendingFlush(const PendingDataTrace& pending, + const TraceQuality& current) const +{ + TraceQuality quality = current; + quality.overflow = quality.overflow || pending.quality.overflow; + quality.timestampReliable = + quality.timestampReliable && !pending.quality.overflow && pending.quality.timestampReliable; + quality.overflowCount = std::max(quality.overflowCount, pending.quality.overflowCount); + return quality; +} + +ExceptionAction DwtPacketDecoder::exceptionAction(std::uint32_t value) +{ + switch (static_cast(value)) { + case DwtExceptionActionCode::Entered: + return ExceptionAction::Entered; + case DwtExceptionActionCode::Exited: + return ExceptionAction::Exited; + case DwtExceptionActionCode::Returned: + return ExceptionAction::Returned; + default: + return ExceptionAction::Unknown; + } +} diff --git a/tools/ctrace/src/decode/DwtPacketDecoder.h b/tools/ctrace/src/decode/DwtPacketDecoder.h new file mode 100644 index 000000000..1ec2fb9d1 --- /dev/null +++ b/tools/ctrace/src/decode/DwtPacketDecoder.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_DWTPACKETDECODER_H +#define CTRACE_SRC_DECODE_DWTPACKETDECODER_H + +#include "TraceEvent.h" + +#include +#include +#include +#include +#include + +/** @brief Stores a decoded DWT hardware payload and its trace metadata. */ +struct DwtPayloadPacket { + std::uint64_t index = 0; + std::uint8_t traceBusId = 0U; + std::uint8_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; + std::uint64_t tcyc = 0; + TraceQuality quality; +}; + +/** @brief Reconstructs semantic DWT events from hardware payload packets. */ +class DwtPacketDecoder { +public: + /** @brief Decodes one hardware payload and returns completed semantic events. */ + std::vector decode(const DwtPayloadPacket& payload); + /** @brief Flushes incomplete data-trace fragments at a boundary. */ + std::vector flush(const TraceQuality& quality, std::uint64_t tcyc); + /** @brief Discards all pending data-trace reconstruction state. */ + void reset(); + +private: + static constexpr std::size_t kDataTraceComparatorCount = 4U; + + /** @brief Accumulates the fragments of one pending DWT data-trace event. */ + struct PendingDataTrace { + std::uint64_t index = 0; + std::uint8_t traceBusId = 0U; + std::uint32_t pc = 0; + std::uint32_t addressLo16 = 0; + std::uint32_t value = 0; + std::uint8_t size = 4; + bool isRead = false; + bool hasPc = false; + bool hasAddressLo16 = false; + bool hasValue = false; + TraceQuality quality; + }; + + /** @brief Accumulates one DWT data-trace packet and emits completed events. */ + void decodeDataTrace(const DwtPayloadPacket& payload, std::vector& output); + /** @brief Converts one complete pending comparator state into an event. */ + void sendDataTraceEvent(std::uint32_t comparator, const PendingDataTrace& event, const TraceQuality& quality, + std::uint64_t tcyc, std::vector& output); + /** @brief Flushes one comparator state and clears its pending fragments. */ + void flushPending(std::uint32_t comparator, const TraceQuality& quality, std::uint64_t tcyc, + std::vector& output); + /** @brief Combines quality captured by a fragment with the current boundary. */ + TraceQuality qualityForPendingFlush(const PendingDataTrace& pending, const TraceQuality& current) const; + + /** @brief Maps an encoded DWT exception action to the semantic action. */ + static ExceptionAction exceptionAction(std::uint32_t value); + std::array, kDataTraceComparatorCount> m_pendingDataTrace; +}; + +#endif // CTRACE_SRC_DECODE_DWTPACKETDECODER_H diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.cpp b/tools/ctrace/src/decode/OpenCsdErrorController.cpp new file mode 100644 index 000000000..4ff158a88 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdErrorController.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdErrorController.h" + +#include "TraceEvent.h" +#include "common/ocsd_error.h" +#include "common/ocsd_error_logger.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include + +/** @brief Removes line terminators and trailing whitespace from an OpenCSD message. */ +static std::string trimTrailingWhitespace(std::string value) +{ + while (!value.empty() && + (value.back() == '\n' || value.back() == '\r' || value.back() == ' ' || value.back() == '\t')) { + value.pop_back(); + } + return value; +} + +/** @brief Uses OpenCSD itself to format one native error enum. */ +static std::string openCsdErrorText(const OpenCsdErrorRecord& error) +{ + const auto nativeError = + error.hasIndex ? ocsdError(error.severity, error.code, static_cast(error.index), error.message) + : ocsdError(error.severity, error.code, error.message); + return trimTrailingWhitespace(ocsdError::getErrorString(nativeError)); +} + +OpenCsdErrorController::OpenCsdErrorController() +{ + // Without an owned output logger, initialization only sets the verbosity and cannot fail. + static_cast(initErrorLogger(OCSD_ERR_SEV_INFO)); +} + +void OpenCsdErrorController::beginDataPathCall() +{ + m_callErrors.clear(); +} + +OpenCsdErrorController::Decision OpenCsdErrorController::decide(ocsd_datapath_resp_t response) const +{ + Decision decision; + decision.response = response; + decision.errors = m_callErrors; + + const auto recoverable = std::find_if(m_callErrors.begin(), m_callErrors.end(), [](const auto& error) { + return error.severity == OCSD_ERR_SEV_ERROR && isRecoverableStreamError(error.code); + }); + const auto nonRecoverableError = std::find_if(m_callErrors.rbegin(), m_callErrors.rend(), [](const auto& error) { + return error.severity == OCSD_ERR_SEV_ERROR && !isRecoverableStreamError(error.code); + }); + if (OCSD_DATA_RESP_IS_FATAL(response) && nonRecoverableError != m_callErrors.rend()) { + decision.error = *nonRecoverableError; + } else if (recoverable != m_callErrors.end()) { + decision.error = *recoverable; + } else if (!m_callErrors.empty()) { + decision.error = m_callErrors.back(); + } + + if (OCSD_DATA_RESP_IS_FATAL(response)) { + decision.action = response == OCSD_RESP_FATAL_INVALID_DATA && nonRecoverableError == m_callErrors.rend() && + decision.error.has_value() && decision.error->severity == OCSD_ERR_SEV_ERROR && + isRecoverableStreamError(decision.error->code) + ? Action::RecoverStream + : Action::Abort; + return decision; + } + if (recoverable != m_callErrors.end()) { + decision.action = Action::RecoverStream; + return decision; + } + if (OCSD_DATA_RESP_IS_WAIT(response)) { + decision.action = Action::Wait; + return decision; + } + decision.action = Action::Continue; + return decision; +} + +bool OpenCsdErrorController::isRecoverableStreamError(ocsd_err_t code) +{ + return code == OCSD_ERR_BAD_PACKET_SEQ || code == OCSD_ERR_INVALID_PCKT_HDR; +} + +bool OpenCsdErrorController::responseReportsError(ocsd_datapath_resp_t response) +{ + return response == OCSD_RESP_ERR_CONT || response == OCSD_RESP_ERR_WAIT || OCSD_DATA_RESP_IS_FATAL(response); +} + +std::uint64_t OpenCsdErrorController::errorOffset(const Decision& decision, std::uint64_t fallback) +{ + return decision.error.has_value() && decision.error->hasIndex ? decision.error->index : fallback; +} + +TraceIssueCode OpenCsdErrorController::issueCode(const Decision& decision) +{ + if (!decision.error.has_value()) { + return TraceIssueCode::OpenCsdDecodeError; + } + switch (decision.error->code) { + case OCSD_ERR_BAD_PACKET_SEQ: + return TraceIssueCode::OpenCsdBadPacketSequence; + case OCSD_ERR_INVALID_PCKT_HDR: + return TraceIssueCode::OpenCsdInvalidPacketHeader; + default: + return TraceIssueCode::OpenCsdDecodeError; + } +} + +std::string OpenCsdErrorController::describeApiError(ocsd_err_t code, const std::string& message) +{ + return openCsdErrorText({OCSD_ERR_SEV_ERROR, code, 0U, false, message}); +} + +std::string OpenCsdErrorController::describeSummary(const Decision& decision) +{ + std::string summary; + if (decision.error.has_value()) { + switch (decision.error->code) { + case OCSD_ERR_BAD_PACKET_SEQ: + summary = "OpenCSD detected an invalid ITM packet sequence"; + break; + case OCSD_ERR_INVALID_PCKT_HDR: + summary = "OpenCSD detected an invalid ITM packet header"; + break; + case OCSD_ERR_NOT_INIT: + summary = "OpenCSD decoder is not initialized"; + break; + case OCSD_ERR_MEM: + summary = "OpenCSD decoder ran out of memory"; + break; + case OCSD_ERR_INVALID_PARAM_VAL: + case OCSD_ERR_INVALID_PARAM_TYPE: + summary = "OpenCSD rejected a decoder parameter"; + break; + case OCSD_ERR_FILE_ERROR: + case OCSD_ERR_RDR_FILE_NOT_FOUND: + summary = "OpenCSD could not read required input data"; + break; + case OCSD_ERR_DATA_DECODE_FATAL: + summary = "OpenCSD could not decode the trace data"; + break; + default: + summary = "OpenCSD decoder error"; + break; + } + } else { + switch (decision.response) { + case OCSD_RESP_FATAL_NOT_INIT: + summary = "OpenCSD decoder is not initialized"; + break; + case OCSD_RESP_FATAL_INVALID_OP: + summary = "OpenCSD rejected a decoder operation"; + break; + case OCSD_RESP_FATAL_INVALID_PARAM: + summary = "OpenCSD rejected a decoder parameter"; + break; + case OCSD_RESP_FATAL_INVALID_DATA: + summary = "OpenCSD rejected invalid trace data"; + break; + case OCSD_RESP_FATAL_SYS_ERR: + summary = "OpenCSD reported a system error"; + break; + default: + summary = "OpenCSD decoder error"; + break; + } + } + const auto offset = errorOffset(decision, 0U); + if ((decision.error.has_value() && decision.error->hasIndex) || offset != 0U) { + summary += " at raw offset " + std::to_string(offset); + } + return summary + "."; +} + +void OpenCsdErrorController::LogError(ocsd_hndl_err_log_t handle, const ocsdError* error) +{ + if (error == nullptr) { + return; + } + m_callErrors.push_back(makeRecord(*error)); + ocsdDefaultErrorLogger::LogError(handle, error); +} + +OpenCsdErrorRecord OpenCsdErrorController::makeRecord(const ocsdError& error) +{ + OpenCsdErrorRecord record; + record.severity = error.getErrorSeverity(); + record.code = error.getErrorCode(); + record.hasIndex = error.getErrorIndex() != OCSD_BAD_TRC_INDEX; + record.index = record.hasIndex ? static_cast(error.getErrorIndex()) : 0U; + record.message = trimTrailingWhitespace(error.getMessage()); + return record; +} diff --git a/tools/ctrace/src/decode/OpenCsdErrorController.h b/tools/ctrace/src/decode/OpenCsdErrorController.h new file mode 100644 index 000000000..8cf43cd70 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdErrorController.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDERRORCONTROLLER_H +#define CTRACE_SRC_DECODE_OPENCSDERRORCONTROLLER_H + +#include "TraceEvent.h" +#include "common/ocsd_error_logger.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include + +/** @brief Stores the stable subset of one OpenCSD error callback. */ +struct OpenCsdErrorRecord { + ocsd_err_severity_t severity = OCSD_ERR_SEV_ERROR; + ocsd_err_t code = OCSD_OK; + std::uint64_t index = 0; + bool hasIndex = false; + std::string message; +}; + +/** @brief Captures OpenCSD errors and decides whether decoding can recover. */ +class OpenCsdErrorController final : public ocsdDefaultErrorLogger { +public: + /** @brief Creates and configures the OpenCSD error logger. */ + OpenCsdErrorController(); + + /** @brief Defines the decoder action selected after an OpenCSD call. */ + enum class Action { + Continue, + Wait, + RecoverStream, + Abort, + }; + + /** @brief Combines an OpenCSD response with captured errors and the selected action. */ + struct Decision { + Action action = Action::Continue; + ocsd_datapath_resp_t response = OCSD_RESP_CONT; + std::optional error; + std::vector errors; + }; + + /** @brief Clears errors before invoking one OpenCSD data-path operation. */ + void beginDataPathCall(); + /** @brief Classifies the response and errors from the current data-path operation. */ + Decision decide(ocsd_datapath_resp_t response) const; + + /** @brief Tests whether an OpenCSD error permits stream resynchronization. */ + static bool isRecoverableStreamError(ocsd_err_t code); + /** @brief Tests whether an OpenCSD response reports an error condition. */ + static bool responseReportsError(ocsd_datapath_resp_t response); + /** @brief Returns the captured raw offset or a supplied fallback. */ + static std::uint64_t errorOffset(const Decision& decision, std::uint64_t fallback); + /** @brief Returns the ctrace issue state for a decision. */ + static TraceIssueCode issueCode(const Decision& decision); + /** @brief Formats an OpenCSD API setup failure. */ + static std::string describeApiError(ocsd_err_t code, const std::string& message); + /** @brief Formats the user-facing summary for a decoder decision. */ + static std::string describeSummary(const Decision& decision); + + /** @brief Captures an OpenCSD callback while preserving default logging behavior. */ + void LogError(ocsd_hndl_err_log_t handle, const ocsdError* error) override; + +private: + /** @brief Copies stable fields from an OpenCSD error callback. */ + static OpenCsdErrorRecord makeRecord(const ocsdError& error); + + std::vector m_callErrors; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDERRORCONTROLLER_H diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp new file mode 100644 index 000000000..a4a663820 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.cpp @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdItmDecoder.h" + +#include "TraceEvent.h" +#include "OpenCsdErrorController.h" +#include "OpenCsdPacketCollector.h" +#include "OpenCsdItmSession.h" +#include "OpenCsdTraceElement.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include +#include + +static_assert(sizeof(ocsd_trc_index_t) == sizeof(std::uint64_t), "ctrace requires 64-bit OpenCSD trace indices"); + +/** @brief Implements OpenCSD feeding, bounded retry, and hardware-sync recovery. */ +class OpenCsdItmDecoderImpl { +public: + /** @brief Creates a decoder implementation around one session factory. */ + OpenCsdItmDecoderImpl(OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory) + : m_collector(elementSink) + { + try { + m_session = sessionFactory(m_collector, m_errorController); + if (m_session == nullptr) { + failInitialization("OpenCSD ITM session factory returned no session"); + } + } catch (const OpenCsdItmSessionError& error) { + failInitialization(error.what()); + } + } + + /** @brief Pushes the next raw trace byte chunk. */ + void push(const std::uint8_t* data, std::uint32_t size) + { + if (m_finished) { + throw std::runtime_error("OpenCSD ITM decoder already finished"); + } + std::uint32_t offset = 0; + while (offset < size) { + const auto span = std::min(kMaxTraceDataInBytes, size - offset); + processBlock(data + offset, span); + offset += span; + } + m_result.bytesIn = static_cast(m_traceIndex); + } + + /** @brief Completes the OpenCSD stream and returns the consumed byte count. */ + OpenCsdItmDecodeResult finish() + { + if (m_finished) { + return m_result; + } + completeConsumedDataLoss(m_traceIndex); + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = m_session->endOfTrace(); + m_collector.rethrowOutputError(); + const auto decision = m_errorController.decide(response); + if (decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(decision, 0U, m_traceIndex, 0U, "OpenCSD aborted end-of-trace processing: "); + } + if (decision.action == OpenCsdErrorController::Action::RecoverStream) { + const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, m_traceIndex); + m_collector.commitTransactionBefore(sourceOffset); + appendReportedErrors(decision, m_traceIndex, true); + } else { + m_collector.commitTransaction(); + appendReportedErrors(decision, m_traceIndex, false); + if (decision.action == OpenCsdErrorController::Action::Wait) { + flushAfterWait(); + } + } + m_finished = true; + m_result.bytesIn = static_cast(m_traceIndex); + return m_result; + } + +private: + static constexpr std::uint32_t kMaxTraceDataInBytes = 4U * 1024U; + + void appendReportedErrors(const OpenCsdErrorController::Decision& decision, std::uint64_t baseOffset, + bool discontinuity, bool force = false) + { + bool emittedError = false; + bool nextDiscontinuity = discontinuity; + for (const auto& error : decision.errors) { + if (error.severity != OCSD_ERR_SEV_ERROR && error.severity != OCSD_ERR_SEV_WARN) { + continue; + } + auto item = decision; + item.error = error; + const auto sourceOffset = OpenCsdErrorController::errorOffset(item, baseOffset); + const auto isError = error.severity == OCSD_ERR_SEV_ERROR; + m_collector.appendDecodeError(static_cast(sourceOffset), + OpenCsdErrorController::describeSummary(item), + OpenCsdErrorController::issueCode(item), nextDiscontinuity && isError, + isError ? TraceIssueSeverity::Error : TraceIssueSeverity::Warning); + if (isError) { + emittedError = true; + nextDiscontinuity = false; + } + } + if (!emittedError && (force || OpenCsdErrorController::responseReportsError(decision.response))) { + m_collector.appendDecodeError( + static_cast(OpenCsdErrorController::errorOffset(decision, baseOffset)), + OpenCsdErrorController::describeSummary(decision), OpenCsdErrorController::issueCode(decision), discontinuity, + TraceIssueSeverity::Error); + } + } + + [[noreturn]] void abortDecode(const OpenCsdErrorController::Decision& decision, std::uint32_t size, + std::uint64_t baseOffset, std::uint32_t bytesConsumed, const std::string& prefix) + { + m_collector.rollbackTransaction(); + completeConsumedDataLoss(OpenCsdErrorController::errorOffset(decision, baseOffset)); + appendReportedErrors(decision, baseOffset, true); + const auto processed = baseOffset + std::min(bytesConsumed, size); + throw OpenCsdFatalError(prefix + OpenCsdErrorController::describeSummary(decision), processed); + } + + void completeConsumedDataLoss(std::uint64_t resumeOffset) + { + if (!m_consumedDataLossStart.has_value()) { + return; + } + const auto startOffset = *m_consumedDataLossStart; + const auto endOffset = std::max(startOffset, resumeOffset); + const auto consumed = endOffset - startOffset; + m_consumedDataLossStart.reset(); + const auto boundaryAlreadyMarked = m_consumedDataLossBoundaryMarked; + m_consumedDataLossBoundaryMarked = false; + if (consumed == 0U) { + return; + } + const auto message = + "OpenCSD consumed " + std::to_string(consumed) + + " raw bytes while waiting for usable ITM trace packets; data loss until a later sync/recovery point"; + if (boundaryAlreadyMarked) { + m_collector.prependDataLossError(static_cast(startOffset), message, consumed); + } else { + m_collector.prependDiscontinuity(static_cast(startOffset), message, TraceIssueCode::DataLoss, + consumed); + } + } + + void processBlock(const std::uint8_t* data, std::uint32_t size) + { + std::uint32_t processed = 0; + bool retriedWithoutProgress = false; + while (processed < size) { + const auto callIndex = static_cast(m_traceIndex); + const auto callSize = size - processed; + std::uint32_t processedThisPass = 0; + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = m_session->pushData(m_traceIndex, callSize, data + processed, processedThisPass); + m_collector.rethrowOutputError(); + const auto decision = m_errorController.decide(response); + if (decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(decision, callSize, callIndex, processedThisPass, "OpenCSD aborted decode: "); + } + + const auto consumed = std::min(processedThisPass, callSize); + if (consumed == 0U) { + if (retriedWithoutProgress) { + m_collector.rollbackTransaction(); + completeConsumedDataLoss(m_traceIndex); + m_collector.appendDecodeError(m_traceIndex, "OpenCSD made no progress after a retry; decode aborted", + TraceIssueCode::OpenCsdNoProgress, false); + throw OpenCsdFatalError("OpenCSD made no progress after a retry", static_cast(m_traceIndex)); + } + retriedWithoutProgress = true; + } else { + retriedWithoutProgress = false; + } + + if (decision.action == OpenCsdErrorController::Action::RecoverStream) { + const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, callIndex); + completeConsumedDataLoss( + std::min(sourceOffset, m_collector.transactionFirstSourceOffset().value_or(sourceOffset))); + // Callbacks before the bad packet remain valid; callbacks at or + // after its offset belong to the failed decode transaction. + m_collector.commitTransactionBefore(sourceOffset); + appendReportedErrors(decision, callIndex, true); + processed += consumed; + m_traceIndex += consumed; + m_dataLossActive = true; + m_consumedDataLossStart = static_cast(m_traceIndex); + m_consumedDataLossBoundaryMarked = true; + resetDecoder(); + continue; + } + if (decision.action == OpenCsdErrorController::Action::Wait) { + if (m_collector.transactionElementCount() == 0U) { + m_collector.rollbackTransaction(); + } else { + completeConsumedDataLoss(m_collector.transactionFirstSourceOffset().value_or(callIndex)); + m_collector.commitTransaction(); + } + appendReportedErrors(decision, callIndex, false); + processed += consumed; + m_traceIndex += consumed; + flushAfterWait(); + continue; + } + if (consumed == 0U) { + m_collector.rollbackTransaction(); + m_collector.appendDecodeError( + m_traceIndex, + "OpenCSD made no progress while raw data was present; decoder reset and searching " + "for next real ITM async sync", + TraceIssueCode::OpenCsdNoProgress); + m_dataLossActive = true; + m_consumedDataLossStart = static_cast(m_traceIndex); + m_consumedDataLossBoundaryMarked = true; + resetDecoder(); + continue; + } + if (m_collector.transactionElementCount() == 0U) { + m_collector.rollbackTransaction(); + appendReportedErrors(decision, callIndex, false); + if (!m_dataLossActive) { + m_consumedDataLossStart = static_cast(m_traceIndex); + m_consumedDataLossBoundaryMarked = false; + m_dataLossActive = true; + } + processed += consumed; + m_traceIndex += consumed; + continue; + } + completeConsumedDataLoss(m_collector.transactionFirstSourceOffset().value_or(callIndex)); + m_collector.commitTransaction(); + appendReportedErrors(decision, callIndex, false); + m_dataLossActive = false; + processed += consumed; + m_traceIndex += consumed; + } + } + + void flushAfterWait() + { + // Bound backpressure handling so a broken decoder cannot stall a file forever. + static constexpr std::uint32_t kMaxFlushCalls = 1024U; + for (std::uint32_t call = 0; call < kMaxFlushCalls; ++call) { + m_collector.beginTransaction(); + m_errorController.beginDataPathCall(); + const auto response = m_session->flush(); + m_collector.rethrowOutputError(); + const auto decision = m_errorController.decide(response); + if (decision.action == OpenCsdErrorController::Action::Abort) { + abortDecode(decision, 0U, m_traceIndex, 0U, "OpenCSD aborted while flushing a WAIT response: "); + } + if (decision.action == OpenCsdErrorController::Action::RecoverStream) { + const auto sourceOffset = OpenCsdErrorController::errorOffset(decision, m_traceIndex); + m_collector.commitTransactionBefore(sourceOffset); + appendReportedErrors(decision, m_traceIndex, true); + resetDecoder(); + return; + } + if (m_collector.transactionElementCount() == 0U) { + m_collector.rollbackTransaction(); + } else { + m_collector.commitTransaction(); + } + appendReportedErrors(decision, m_traceIndex, false); + if (decision.action == OpenCsdErrorController::Action::Continue) { + return; + } + } + const auto limit = std::to_string(kMaxFlushCalls); + m_collector.appendDecodeError(m_traceIndex, + "OpenCSD WAIT did not clear after " + limit + " FLUSH operations; decode aborted", + TraceIssueCode::OpenCsdWaitTimeout); + throw OpenCsdFatalError("OpenCSD WAIT did not clear after " + limit + " FLUSH operations", + static_cast(m_traceIndex)); + } + + void resetDecoder() + { + m_errorController.beginDataPathCall(); + const auto response = m_session->reset(); + m_collector.rethrowOutputError(); + const auto decision = m_errorController.decide(response); + if (decision.action != OpenCsdErrorController::Action::Continue) { + appendReportedErrors(decision, m_traceIndex, true, true); + throw OpenCsdFatalError("OpenCSD decoder reset failed: " + OpenCsdErrorController::describeSummary(decision), + static_cast(m_traceIndex)); + } + appendReportedErrors(decision, m_traceIndex, false); + } + + [[noreturn]] void failInitialization(const std::string& message) + { + m_collector.appendDecodeError(m_traceIndex, message, TraceIssueCode::OpenCsdInitializationError, false); + throw OpenCsdFatalError(message, static_cast(m_traceIndex)); + } + + OpenCsdPacketCollector m_collector; + OpenCsdErrorController m_errorController; + std::unique_ptr m_session; + ocsd_trc_index_t m_traceIndex = 0; + bool m_dataLossActive = false; + std::optional m_consumedDataLossStart; + bool m_consumedDataLossBoundaryMarked = false; + OpenCsdItmDecodeResult m_result; + bool m_finished = false; +}; + +OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink) + : OpenCsdItmDecoder(elementSink, [](OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) { + return std::make_unique(collector, errorController); + }) +{ +} + +OpenCsdItmDecoder::OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink, + const OpenCsdItmSessionFactory& sessionFactory) + : m_impl(std::make_unique(elementSink, sessionFactory)) +{ +} + +OpenCsdItmDecoder::~OpenCsdItmDecoder() = default; + +void OpenCsdItmDecoder::push(const std::uint8_t* data, std::uint32_t size) +{ + m_impl->push(data, size); +} + +OpenCsdItmDecodeResult OpenCsdItmDecoder::finish() +{ + return m_impl->finish(); +} diff --git a/tools/ctrace/src/decode/OpenCsdItmDecoder.h b/tools/ctrace/src/decode/OpenCsdItmDecoder.h new file mode 100644 index 000000000..4ae654251 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmDecoder.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDITMDECODER_H +#define CTRACE_SRC_DECODE_OPENCSDITMDECODER_H + +#include "OpenCsdTraceElement.h" + +#include +#include +#include +#include +#include + +/** @brief Summarizes raw input consumed by an OpenCSD ITM decoder. */ +struct OpenCsdItmDecodeResult { + std::uint64_t bytesIn = 0; +}; + +class OpenCsdErrorController; +class OpenCsdItmSessionInterface; +class OpenCsdPacketCollector; + +/** @brief Creates an OpenCSD session connected to the supplied callbacks. */ +using OpenCsdItmSessionFactory = + std::function(OpenCsdPacketCollector&, OpenCsdErrorController&)>; + +/** @brief Reports an unrecoverable OpenCSD failure and its processed byte count. */ +class OpenCsdFatalError final : public std::runtime_error { +public: + /** + * @brief Creates a fatal decoder error. + * @param message Human-readable failure description. + * @param bytesProcessed Raw bytes consumed before the failure. + */ + OpenCsdFatalError(const std::string& message, std::uint64_t bytesProcessed) + : std::runtime_error(message), + m_bytesProcessed(bytesProcessed) + { + } + + /** + * @brief Returns the number of raw bytes processed before failure. + * @return Raw bytes consumed before the decoder became unrecoverable. + */ + std::uint64_t bytesProcessed() const noexcept + { + return m_bytesProcessed; + } + +private: + std::uint64_t m_bytesProcessed = 0; +}; + +class OpenCsdItmDecoderImpl; + +/** @brief Feeds raw ITM bytes to OpenCSD and recovers at hardware synchronization. */ +class OpenCsdItmDecoder { +public: + /** + * @brief Creates a decoder using the production OpenCSD session. + * @param elementSink Sink receiving decoded and recovery elements. + */ + OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink); + /** + * @brief Creates a decoder with an injected OpenCSD session factory. + * @param elementSink Sink receiving decoded and recovery elements. + * @param sessionFactory Factory used to construct the external session. + */ + OpenCsdItmDecoder(OpenCsdTraceElementSink& elementSink, const OpenCsdItmSessionFactory& sessionFactory); + /** @brief Destroys the decoder implementation and external session. */ + ~OpenCsdItmDecoder(); + + /** @brief Disables copying because a decoder owns one external session. */ + OpenCsdItmDecoder(const OpenCsdItmDecoder&) = delete; + /** @brief Disables copy assignment because a decoder owns one external session. */ + OpenCsdItmDecoder& operator=(const OpenCsdItmDecoder&) = delete; + + /** + * @brief Pushes the next raw byte chunk into the decoder. + * @param data Beginning of a contiguous byte chunk; may be null when size is zero. + * @param size Number of bytes available at data. + * @throws OpenCsdFatalError If OpenCSD cannot make progress or recover. + */ + void push(const std::uint8_t* data, std::uint32_t size); + /** + * @brief Completes decoding and returns the consumed byte count. + * @return Aggregate raw-input counters for the completed stream. + * @throws OpenCsdFatalError If OpenCSD cannot finalize the stream. + */ + OpenCsdItmDecodeResult finish(); + +private: + std::unique_ptr m_impl; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDITMDECODER_H diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.cpp b/tools/ctrace/src/decode/OpenCsdItmSession.cpp new file mode 100644 index 000000000..775ffb252 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmSession.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdItmSession.h" + +#include "OpenCsdErrorController.h" +#include "OpenCsdPacketCollector.h" +#include "common/ocsd_dcd_mngr_i.h" +#include "common/ocsd_lib_dcd_register.h" +#include "common/trc_component.h" +#include "interfaces/trc_data_raw_in_i.h" +#include "opencsd/itm/trc_pkt_types_itm.h" +#include "opencsd/ocsd_if_types.h" + +#include + +constexpr std::uint32_t kItmTcrSwoEnable = 1U << 4U; +constexpr ocsd_itm_cfg kItmConfig{kItmTcrSwoEnable}; + +OpenCsdItmSession::OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController) + : OpenCsdItmSession(collector, errorController, &OcsdLibDcdRegister::getDecoderRegister) +{ +} + +OpenCsdItmSession::OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, + DecoderRegistryProvider registryProvider) + : m_config(&kItmConfig) +{ + // Keep OpenCSD timestamps in raw ITM ticks. They are scaled after decode, + // where the originating CoreSight stream and processor are known. + createDecoder(collector, errorController, registryProvider); +} + +OpenCsdItmSession::~OpenCsdItmSession() noexcept = default; + +void OpenCsdSessionValidation::requireObject(const void* object, const char* message) +{ + if (object == nullptr) { + throw OpenCsdItmSessionError(message); + } +} + +void OpenCsdSessionValidation::requireSuccess(ocsd_err_t error, const char* message) +{ + if (error != OCSD_OK) { + throw OpenCsdItmSessionError(OpenCsdErrorController::describeApiError(error, message)); + } +} + +void OpenCsdItmSession::DecoderDeleter::operator()(TraceComponent* component) const noexcept +{ + if (manager != nullptr) { + manager->destroyDecoder(component); + } +} + +ocsd_datapath_resp_t OpenCsdItmSession::pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, + std::uint32_t& processed) +{ + return m_input->TraceDataIn(OCSD_OP_DATA, index, size, data, &processed); +} + +ocsd_datapath_resp_t OpenCsdItmSession::flush() +{ + return m_input->TraceDataIn(OCSD_OP_FLUSH, 0, 0, nullptr, nullptr); +} + +ocsd_datapath_resp_t OpenCsdItmSession::reset() +{ + return m_input->TraceDataIn(OCSD_OP_RESET, 0, 0, nullptr, nullptr); +} + +ocsd_datapath_resp_t OpenCsdItmSession::endOfTrace() +{ + return m_input->TraceDataIn(OCSD_OP_EOT, 0, 0, nullptr, nullptr); +} + +void OpenCsdItmSession::createDecoder(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, + DecoderRegistryProvider registryProvider) +{ + if (registryProvider == nullptr) { + throw OpenCsdItmSessionError("OpenCSD decoder registry provider is not configured"); + } + auto* registry = registryProvider(); + if (registry == nullptr) { + throw OpenCsdItmSessionError("OpenCSD decoder registry is not initialized"); + } + + auto error = registry->getDecoderMngrByName(OCSD_BUILTIN_DCD_ITM, &m_manager); + OpenCsdSessionValidation::requireSuccess(error, "failed to get OpenCSD ITM decoder manager"); + OpenCsdSessionValidation::requireObject(m_manager, "OpenCSD ITM decoder manager is not initialized"); + + TraceComponent* component = nullptr; + error = m_manager->createDecoder(OCSD_CREATE_FLG_FULL_DECODER, 0, &m_config, &component); + m_component.get_deleter().manager = m_manager; + m_component.reset(component); + OpenCsdSessionValidation::requireSuccess(error, "failed to create OpenCSD ITM decoder"); + OpenCsdSessionValidation::requireObject(m_component.get(), "OpenCSD ITM decoder component is not initialized"); + + error = m_manager->attachErrorLogger(m_component.get(), &errorController); + OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD packet-decoder error logger"); + if (m_component->getAssocComponent() != nullptr) { + error = m_manager->attachErrorLogger(m_component->getAssocComponent(), &errorController); + OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD packet-processor error logger"); + } + + error = m_manager->attachOutputSink(m_component.get(), &collector); + OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD ITM output sink"); + + error = m_manager->getDataInputI(m_component.get(), &m_input); + OpenCsdSessionValidation::requireSuccess(error, "failed to get OpenCSD ITM input interface"); + OpenCsdSessionValidation::requireObject(m_input, "OpenCSD ITM input interface is not initialized"); + + error = m_manager->attachPktMonitor(m_component.get(), &collector); + OpenCsdSessionValidation::requireSuccess(error, "failed to attach OpenCSD ITM packet monitor"); +} diff --git a/tools/ctrace/src/decode/OpenCsdItmSession.h b/tools/ctrace/src/decode/OpenCsdItmSession.h new file mode 100644 index 000000000..b6268f636 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdItmSession.h @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDITMSESSION_H +#define CTRACE_SRC_DECODE_OPENCSDITMSESSION_H + +#include "opencsd/itm/trc_cmp_cfg_itm.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include + +class IDecoderMngr; +class ITrcDataIn; +class OpenCsdErrorController; +class OpenCsdPacketCollector; +class OcsdLibDcdRegister; +class TraceComponent; + +/** @brief Abstracts one OpenCSD ITM session for production use and tests. */ +class OpenCsdItmSessionInterface { +public: + /** @brief Destroys an OpenCSD session through its interface. */ + virtual ~OpenCsdItmSessionInterface() = default; + + /** + * @brief Pushes raw bytes into OpenCSD and reports how many were consumed. + * @param index Absolute raw-stream offset of the first byte. + * @param size Number of bytes available at data. + * @param data Contiguous raw trace bytes. + * @param processed Receives the number of bytes consumed by OpenCSD. + * @return OpenCSD data-path response controlling further input. + */ + virtual ocsd_datapath_resp_t pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, + std::uint32_t& processed) = 0; + /** @brief Flushes pending OpenCSD decoder work. */ + virtual ocsd_datapath_resp_t flush() = 0; + /** @brief Resets OpenCSD decoder state for stream recovery. */ + virtual ocsd_datapath_resp_t reset() = 0; + /** @brief Signals the end of the current trace stream. */ + virtual ocsd_datapath_resp_t endOfTrace() = 0; +}; + +/** @brief Reports an OpenCSD session creation or API failure. */ +class OpenCsdItmSessionError final : public std::runtime_error { +public: + /** @brief Inherits standard runtime-error construction. */ + using std::runtime_error::runtime_error; +}; + +/** @brief Validates pointers and results returned by OpenCSD session setup APIs. */ +class OpenCsdSessionValidation final { +public: + /** + * @brief Rejects a null OpenCSD API object with a session error. + * @param object Required external API object. + * @param message Failure text used when object is null. + * @throws OpenCsdItmSessionError If object is null. + */ + static void requireObject(const void* object, const char* message); + /** + * @brief Rejects an unsuccessful OpenCSD API result with a session error. + * @param error OpenCSD result to validate. + * @param message Failure text used for an error result. + * @throws OpenCsdItmSessionError If error does not report success. + */ + static void requireSuccess(ocsd_err_t error, const char* message); + +private: + /** @brief Prevents construction of this stateless validation utility. */ + OpenCsdSessionValidation() = delete; +}; + +/** + * @brief Owns one fully wired OpenCSD ITM callback decoder. + * + * Feed and recovery policy stays in OpenCsdItmDecoder; this class only manages + * the external decoder session. + */ +class OpenCsdItmSession final : public OpenCsdItmSessionInterface { +public: + /** @brief Supplies the OpenCSD decoder registry to a session. */ + using DecoderRegistryProvider = OcsdLibDcdRegister* (*)(); + + /** + * @brief Creates and connects an OpenCSD ITM decoder session. + * @param collector Callback target for decoded packets and elements. + * @param errorController Callback target for OpenCSD errors. + * @throws OpenCsdItmSessionError If external session setup fails. + */ + OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController); + /** + * @brief Creates a session with an injectable decoder-registry provider. + * @param collector Callback target for decoded packets and elements. + * @param errorController Callback target for OpenCSD errors. + * @param registryProvider Provider used to retrieve the decoder registry. + * @throws OpenCsdItmSessionError If external session setup fails. + */ + OpenCsdItmSession(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, + DecoderRegistryProvider registryProvider); + /** @brief Disconnects and destroys the OpenCSD session without throwing. */ + ~OpenCsdItmSession() noexcept; + + /** @brief Disables copying because a session owns external decoder state. */ + OpenCsdItmSession(const OpenCsdItmSession&) = delete; + /** @brief Disables copy assignment because a session owns external decoder state. */ + OpenCsdItmSession& operator=(const OpenCsdItmSession&) = delete; + + /** @brief Pushes raw bytes into the external decoder. */ + ocsd_datapath_resp_t pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t* data, + std::uint32_t& processed) override; + /** @brief Flushes the external decoder. */ + ocsd_datapath_resp_t flush() override; + /** @brief Resets the external decoder. */ + ocsd_datapath_resp_t reset() override; + /** @brief Signals end of trace to the external decoder. */ + ocsd_datapath_resp_t endOfTrace() override; + +private: + /** @brief Destroys an OpenCSD decoder component through its owning manager. */ + struct DecoderDeleter { + IDecoderMngr* manager = nullptr; + /** @brief Destroys a component through the manager that created it. */ + void operator()(TraceComponent* component) const noexcept; + }; + + /** @brief Creates the ITM decoder and attaches callbacks and input interfaces. */ + void createDecoder(OpenCsdPacketCollector& collector, OpenCsdErrorController& errorController, + DecoderRegistryProvider registryProvider); + + ITMConfig m_config; + IDecoderMngr* m_manager = nullptr; + std::unique_ptr m_component{nullptr, DecoderDeleter{}}; + ITrcDataIn* m_input = nullptr; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDITMSESSION_H diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp new file mode 100644 index 000000000..d17f3a052 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.cpp @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdPacketCollector.h" + +#include "TraceEvent.h" +#include "TraceStreamId.h" +#include "OpenCsdTraceElement.h" +#include "common/trc_gen_elem.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/itm/trc_pkt_types_itm.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/trc_gen_elem_types.h" + +#include +#include +#include +#include +#include +#include +#include + +OpenCsdPacketCollector::OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink) + : m_elementSink(elementSink) +{ +} + +void OpenCsdPacketCollector::beginTransaction() +{ + m_transactionActive = true; + m_transactionElements.clear(); +} + +void OpenCsdPacketCollector::commitTransaction() +{ + for (auto& element : m_transactionElements) { + appendCommitted(std::move(element)); + } + m_transactionElements.clear(); + m_transactionActive = false; +} + +void OpenCsdPacketCollector::commitTransactionBefore(std::uint64_t sourceOffset) +{ + for (auto& element : m_transactionElements) { + const auto elementOffset = element.sourceIndex; + if (elementOffset < sourceOffset && element.kind != OpenCsdTraceElement::Kind::Error) { + appendCommitted(std::move(element)); + } + } + m_transactionElements.clear(); + m_transactionActive = false; +} + +void OpenCsdPacketCollector::rollbackTransaction() +{ + m_transactionElements.clear(); + m_transactionActive = false; +} + +void OpenCsdPacketCollector::rethrowOutputError() +{ + if (!m_outputError) { + return; + } + auto error = m_outputError; + m_outputError = nullptr; + std::rethrow_exception(error); +} + +std::size_t OpenCsdPacketCollector::transactionElementCount() const +{ + return m_transactionElements.size(); +} + +std::optional OpenCsdPacketCollector::transactionFirstSourceOffset() const +{ + std::optional firstOffset; + for (const auto& element : m_transactionElements) { + const auto offset = element.sourceIndex; + if (!firstOffset.has_value() || offset < *firstOffset) { + firstOffset = offset; + } + } + return firstOffset; +} + +void OpenCsdPacketCollector::appendDecodeError(ocsd_trc_index_t index, const std::string& message, + TraceIssueCode issueCode, bool discontinuity, + TraceIssueSeverity severity) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.discontinuity = discontinuity; + element.issueCode = issueCode; + element.issueSeverity = severity; + element.errorMessage = message; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::prependDiscontinuity(ocsd_trc_index_t index, const std::string& message, + TraceIssueCode issueCode, + std::optional rawBytesConsumed) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Discontinuity; + element.sourceIndex = static_cast(index); + element.discontinuity = true; + element.issueCode = issueCode; + element.errorMessage = message; + element.rawBytesConsumed = rawBytesConsumed; + if (m_transactionActive) { + m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); + return; + } + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::prependDataLossError(ocsd_trc_index_t index, const std::string& message, + std::uint64_t rawBytesConsumed) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.issueCode = TraceIssueCode::DataLoss; + element.errorMessage = message; + element.rawBytesConsumed = rawBytesConsumed; + element.awaitingResumeTimestamp = true; + if (m_transactionActive) { + m_transactionElements.insert(m_transactionElements.begin(), std::move(element)); + return; + } + appendElement(std::move(element)); +} + +ocsd_datapath_resp_t OpenCsdPacketCollector::TraceElemIn(const ocsd_trc_index_t index_sop, + const std::uint8_t trc_chan_id, const OcsdTraceElement& elem) +{ + try { + if (elem.getType() != OCSD_GEN_TRC_ELEM_ITMTRACE) { + return OCSD_RESP_CONT; + } + + const auto& info = elem.swt_itm; + const auto traceBusId = CoreSight::isTraceBusId(trc_chan_id) ? trc_chan_id : CoreSight::kUnformattedTraceBusId; + switch (info.pkt_type) { + case SWIT_PAYLOAD: + appendSoftware(index_sop, traceBusId, elem); + break; + case DWT_PAYLOAD: + appendDwt(index_sop, traceBusId, elem); + break; + case TS_SYNC: + case TS_DELAY: + case TS_PKT_DELAY: + case TS_PKT_TS_DELAY: + appendTimestamp(index_sop, traceBusId, elem); + break; + case TS_GLOBAL: + appendGlobalTimestamp(index_sop, traceBusId, elem); + break; + } + } catch (...) { + if (!m_outputError) { + m_outputError = std::current_exception(); + } + return OCSD_RESP_FATAL_SYS_ERR; + } + return OCSD_RESP_CONT; +} + +void OpenCsdPacketCollector::RawPacketDataMon(const ocsd_datapath_op_t op, const ocsd_trc_index_t index_sop, + const ItmTrcPacket* pkt, const std::uint32_t, const std::uint8_t*) +{ + try { + if (pkt == nullptr) { + return; + } + + // OpenCSD publishes the incomplete packet through the raw monitor as DATA + // while processing EOT; its following EOT monitor notification has no packet. + if (pkt->getPktType() == ITM_PKT_INCOMPLETE_EOT) { + appendDecodeError(index_sop, "incomplete ITM packet at end of input", TraceIssueCode::OpenCsdIncompleteTail, true, + TraceIssueSeverity::Error); + return; + } + if (op != OCSD_OP_DATA) { + return; + } + + switch (pkt->getPktType()) { + case ITM_PKT_ASYNC: + appendSync(index_sop); + break; + case ITM_PKT_OVERFLOW: + appendOverflow(index_sop); + break; + case ITM_PKT_TS_GLOBAL_1: + case ITM_PKT_TS_GLOBAL_2: + // The ITM decoder combines GTS1/GTS2 and publishes the complete + // 64-bit value as a generic TS_GLOBAL element. Raw fragments are + // intentionally not forwarded as independent timestamps. + break; + case ITM_PKT_BAD_SEQUENCE: + case ITM_PKT_RESERVED: + appendError(index_sop, *pkt); + break; + default: + break; + } + } catch (...) { + if (!m_outputError) { + m_outputError = std::current_exception(); + } + } +} + +void OpenCsdPacketCollector::appendSync(ocsd_trc_index_t index) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Sync; + element.sourceIndex = static_cast(index); + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendOverflow(ocsd_trc_index_t index) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Overflow; + element.sourceIndex = static_cast(index); + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendGlobalTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, + const OcsdTraceElement& elem) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::GlobalTimestamp; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.timestampValue = elem.timestamp; + element.clockChange = elem.cpu_freq_change != 0U; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt) +{ + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Error; + element.sourceIndex = static_cast(index); + element.issueCode = TraceIssueCode::OpenCsdDecodeError; + element.errorMessage = pkt.getPktType() == ITM_PKT_RESERVED ? "Reserved ITM packet" : "Bad ITM packet sequence"; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendSoftware(ocsd_trc_index_t index, std::uint8_t traceBusId, + const OcsdTraceElement& elem) +{ + const auto& info = elem.swt_itm; + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Software; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.channel = info.payload_src_id; + element.size = info.payload_size; + element.value = info.value; + element.overflow = info.overflow; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendDwt(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem) +{ + const auto& info = elem.swt_itm; + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::Hardware; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.discriminator = info.payload_src_id; + element.size = info.payload_size; + element.value = info.value; + element.overflow = info.overflow; + appendElement(std::move(element)); +} + +void OpenCsdPacketCollector::appendTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, + const OcsdTraceElement& elem) +{ + const auto& info = elem.swt_itm; + OpenCsdTraceElement element; + element.kind = OpenCsdTraceElement::Kind::LocalTimestamp; + element.sourceIndex = static_cast(index); + element.traceBusId = traceBusId; + element.timestampRelation = timestampRelation(info.pkt_type); + element.tcyc = elem.timestamp; + element.overflow = info.overflow; + appendElement(std::move(element)); +} + +LocalTimestampRelation OpenCsdPacketCollector::timestampRelation(swt_itm_type type) +{ + if (type == TS_DELAY) { + return LocalTimestampRelation::TimestampDelayed; + } + if (type == TS_PKT_DELAY) { + return LocalTimestampRelation::PayloadDelayed; + } + if (type == TS_PKT_TS_DELAY) { + return LocalTimestampRelation::TimestampAndPayloadDelayed; + } + return LocalTimestampRelation::Synchronous; +} + +void OpenCsdPacketCollector::appendElement(OpenCsdTraceElement element) +{ + if (m_transactionActive) { + m_transactionElements.push_back(std::move(element)); + return; + } + appendCommitted(std::move(element)); +} + +void OpenCsdPacketCollector::appendCommitted(OpenCsdTraceElement element) +{ + m_elementSink.append(std::move(element)); +} diff --git a/tools/ctrace/src/decode/OpenCsdPacketCollector.h b/tools/ctrace/src/decode/OpenCsdPacketCollector.h new file mode 100644 index 000000000..d4e1b63e3 --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdPacketCollector.h @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H +#define CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H + +#include "TraceEvent.h" +#include "OpenCsdTraceElement.h" +#include "common/trc_gen_elem.h" +#include "interfaces/trc_gen_elem_in_i.h" +#include "interfaces/trc_pkt_raw_in_i.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/trc_gen_elem_types.h" + +#include +#include +#include +#include +#include + +/** @brief Collects OpenCSD callbacks into transactional ctrace elements. */ +class OpenCsdPacketCollector : public ITrcGenElemIn, public IPktRawDataMon { +public: + /** + * @brief Creates a collector that emits committed elements to a sink. + * @param elementSink Sink receiving elements after transaction commit. + */ + explicit OpenCsdPacketCollector(OpenCsdTraceElementSink& elementSink); + + /** + * @brief Starts buffering elements for one recoverable decoder operation. + * + * Buffered elements become visible only after a commit, allowing the decoder + * to discard callbacks produced by an invalid packet sequence. + */ + void beginTransaction(); + /** @brief Commits all buffered elements. */ + void commitTransaction(); + /** + * @brief Commits buffered elements before a raw source offset. + * @param sourceOffset First raw offset that remains buffered. + */ + void commitTransactionBefore(std::uint64_t sourceOffset); + /** @brief Discards all buffered elements. */ + void rollbackTransaction(); + /** @brief Rethrows an exception captured from the downstream sink. */ + void rethrowOutputError(); + /** @brief Returns the number of currently buffered elements. */ + std::size_t transactionElementCount() const; + /** @brief Returns the first buffered raw offset, if present. */ + std::optional transactionFirstSourceOffset() const; + /** + * @brief Appends a decoder issue element. + * @param index Raw source offset associated with the issue. + * @param message Human-readable diagnostic text. + * @param issueCode Decoder issue state. + * @param discontinuity Whether the issue breaks semantic continuity. + * @param severity Output severity assigned to the issue. + */ + void appendDecodeError(ocsd_trc_index_t index, const std::string& message, + TraceIssueCode issueCode = TraceIssueCode::OpenCsdDecodeError, bool discontinuity = true, + TraceIssueSeverity severity = TraceIssueSeverity::Error); + /** + * @brief Prepends a discontinuity before buffered resumed events. + * @param index Raw source offset at which decoding resumes. + * @param message Human-readable recovery description. + * @param issueCode Decoder issue state. + * @param rawBytesConsumed Number of discarded bytes, when known. + */ + void prependDiscontinuity(ocsd_trc_index_t index, const std::string& message, TraceIssueCode issueCode, + std::optional rawBytesConsumed = std::nullopt); + /** + * @brief Prepends a data-loss error before buffered resumed events. + * @param index Raw source offset at which decoding resumes. + * @param message Human-readable data-loss description. + * @param rawBytesConsumed Number of discarded raw bytes. + */ + void prependDataLossError(ocsd_trc_index_t index, const std::string& message, std::uint64_t rawBytesConsumed); + /** @brief Receives one generic element callback from OpenCSD. */ + ocsd_datapath_resp_t TraceElemIn(ocsd_trc_index_t index_sop, std::uint8_t trc_chan_id, + const OcsdTraceElement& elem) override; + /** @brief Receives one raw ITM packet callback from OpenCSD. */ + void RawPacketDataMon(ocsd_datapath_op_t op, ocsd_trc_index_t index_sop, const ItmTrcPacket* pkt, std::uint32_t size, + const std::uint8_t* data) override; + +private: + /** @brief Appends a hardware synchronization element. */ + void appendSync(ocsd_trc_index_t index); + /** @brief Appends a hardware overflow element. */ + void appendOverflow(ocsd_trc_index_t index); + /** @brief Converts an OpenCSD global timestamp callback. */ + void appendGlobalTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + /** @brief Converts an OpenCSD error packet callback. */ + void appendError(ocsd_trc_index_t index, const ItmTrcPacket& pkt); + /** @brief Converts an ITM software packet callback. */ + void appendSoftware(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + /** @brief Converts a DWT hardware packet callback. */ + void appendDwt(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + /** @brief Converts an OpenCSD local timestamp callback. */ + void appendTimestamp(ocsd_trc_index_t index, std::uint8_t traceBusId, const OcsdTraceElement& elem); + /** @brief Maps the OpenCSD timestamp type to its semantic relation. */ + static LocalTimestampRelation timestampRelation(swt_itm_type type); + /** @brief Buffers or commits one element according to transaction state. */ + void appendElement(OpenCsdTraceElement element); + /** @brief Emits one committed element while deferring sink exceptions. */ + void appendCommitted(OpenCsdTraceElement element); + + OpenCsdTraceElementSink& m_elementSink; + bool m_transactionActive = false; + std::vector m_transactionElements; + std::exception_ptr m_outputError; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDPACKETCOLLECTOR_H diff --git a/tools/ctrace/src/decode/OpenCsdTraceElement.h b/tools/ctrace/src/decode/OpenCsdTraceElement.h new file mode 100644 index 000000000..93abd419e --- /dev/null +++ b/tools/ctrace/src/decode/OpenCsdTraceElement.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_OPENCSDTRACEELEMENT_H +#define CTRACE_SRC_DECODE_OPENCSDTRACEELEMENT_H + +#include "TraceEvent.h" + +#include +#include +#include + +/** @brief Describes ordering between a local timestamp and adjacent payloads. */ +enum class LocalTimestampRelation { + Synchronous, + TimestampDelayed, + PayloadDelayed, + TimestampAndPayloadDelayed, +}; + +/** @brief Stores one normalized element received from OpenCSD callbacks. */ +struct OpenCsdTraceElement { + /** @brief Identifies the normalized element payload. */ + enum class Kind { + Software, + Hardware, + LocalTimestamp, + GlobalTimestamp, + Sync, + Overflow, + Discontinuity, + Error, + }; + + Kind kind = Kind::Error; + std::uint64_t sourceIndex = 0; + + // CoreSight Trace Bus ID reported by OpenCSD. ID 0 identifies input for + // which no formatted source ID exists, such as the current SWO path. + std::uint8_t traceBusId = 0U; + std::uint32_t channel = 0; + std::uint32_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; + LocalTimestampRelation timestampRelation = LocalTimestampRelation::Synchronous; + std::uint64_t timestampValue = 0; + std::optional tcyc; + std::optional rawBytesConsumed; + bool overflow = false; + bool discontinuity = false; + bool awaitingResumeTimestamp = false; + + bool clockChange = false; + + std::optional issueCode; + TraceIssueSeverity issueSeverity = TraceIssueSeverity::Error; + std::string errorMessage; +}; + +/** @brief Receives normalized OpenCSD trace elements. */ +class OpenCsdTraceElementSink { +public: + /** @brief Destroys an element sink through its interface. */ + virtual ~OpenCsdTraceElementSink() = default; + + /** @brief Appends one normalized OpenCSD element. */ + virtual void append(OpenCsdTraceElement element) = 0; +}; + +#endif // CTRACE_SRC_DECODE_OPENCSDTRACEELEMENT_H diff --git a/tools/ctrace/src/decode/SaturatingArithmetic.h b/tools/ctrace/src/decode/SaturatingArithmetic.h new file mode 100644 index 000000000..2eff5b97f --- /dev/null +++ b/tools/ctrace/src/decode/SaturatingArithmetic.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DECODE_SATURATINGARITHMETIC_H +#define CTRACE_SRC_DECODE_SATURATINGARITHMETIC_H + +#include +#include +#include + +/** @brief Provides checked saturating arithmetic for trace timestamps. */ +class SaturatingArithmetic final { +public: + /** @brief Adds two values and clamps overflow to the largest representable value. */ + static constexpr std::uint64_t add(std::uint64_t lhs, std::uint64_t rhs) + { + const auto maximum = std::numeric_limits::max(); + return rhs > maximum - lhs ? maximum : lhs + rhs; + } + + /** @brief Multiplies by a nonzero factor and clamps overflow to the largest representable value. */ + static constexpr std::uint64_t multiply(std::uint64_t value, std::uint32_t factor) + { + if (factor == 0U) { + throw std::invalid_argument("ITM timestamp prescaler must not be zero"); + } + const auto maximum = std::numeric_limits::max(); + return value > maximum / factor ? maximum : value * factor; + } + +private: + /** @brief Prevents construction of this stateless arithmetic utility. */ + SaturatingArithmetic() = delete; +}; + +#endif // CTRACE_SRC_DECODE_SATURATINGARITHMETIC_H diff --git a/tools/ctrace/src/diagnostics/DiagnosticSink.cpp b/tools/ctrace/src/diagnostics/DiagnosticSink.cpp new file mode 100644 index 000000000..2fe2491f8 --- /dev/null +++ b/tools/ctrace/src/diagnostics/DiagnosticSink.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "DiagnosticSink.h" + +#include +#include +#include +#include +#include +#include + +/** @brief Renders a structured diagnostic in the stable command-line format. */ +static std::string formatDiagnosticEvent(const DiagnosticSink::Event& event) +{ + std::ostringstream out; + out << "[" << toString(event.severity) << "] " << event.message; + for (std::size_t index = 0; index < event.context.size(); ++index) { + const auto& item = event.context[index]; + out << (index == 0U ? ": " : ", ") << item.first << "=" << item.second; + } + out << "\n"; + return out.str(); +} + +void DiagnosticSink::report(const Event& event) +{ + if (event.impact == Impact::Failing) { + ++m_failureCount; + } + write(event); +} + +std::uint64_t DiagnosticSink::failureCount() const noexcept +{ + return m_failureCount; +} + +std::string_view toString(DiagnosticSink::Severity severity) +{ + switch (severity) { + case DiagnosticSink::Severity::Info: + return "info"; + case DiagnosticSink::Severity::Warning: + return "warning"; + case DiagnosticSink::Severity::Error: + return "error"; + } + return "unknown"; +} + +void StderrDiagnosticSink::write(const Event& event) +{ + std::cerr << formatDiagnosticEvent(event); +} diff --git a/tools/ctrace/src/diagnostics/DiagnosticSink.h b/tools/ctrace/src/diagnostics/DiagnosticSink.h new file mode 100644 index 000000000..06c0d5f3a --- /dev/null +++ b/tools/ctrace/src/diagnostics/DiagnosticSink.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DIAGNOSTICS_DIAGNOSTICSINK_H +#define CTRACE_SRC_DIAGNOSTICS_DIAGNOSTICSINK_H + +#include +#include +#include +#include +#include +#include + +/** @brief Classifies, counts, and emits structured ctrace diagnostics. */ +class DiagnosticSink { +public: + /** @brief Defines the displayed diagnostic severity. */ + enum class Severity { + Info, + Warning, + Error, + }; + + /** @brief Defines whether a diagnostic affects process success. */ + enum class Impact { + NonFailing, + Failing, + }; + + /** @brief Stores one structured diagnostic event. */ + struct Event { + /** + * @brief Constructs a fully classified diagnostic event. + * @param eventSeverity Display severity. + * @param eventMessage Human-readable display text. + * @param eventContext Structured key-value context. + * @param eventImpact Optional explicit process-success impact. + */ + Event(Severity eventSeverity, std::string eventMessage, + std::vector> eventContext = {}, + std::optional eventImpact = std::nullopt) + : severity(eventSeverity), + message(std::move(eventMessage)), + context(std::move(eventContext)), + impact(eventImpact.value_or(eventSeverity == Severity::Error ? Impact::Failing : Impact::NonFailing)) + { + } + + Severity severity = Severity::Info; + std::string message; + std::vector> context; + Impact impact = Impact::NonFailing; + }; + + /** @brief Destroys a diagnostic sink through its interface. */ + virtual ~DiagnosticSink() = default; + + /** + * @brief Records a diagnostic and forwards it to the concrete writer. + * @param event Fully classified diagnostic event. + * + * Failing impact is counted independently of display severity. + */ + void report(const Event& event); + /** + * @brief Returns the number of diagnostics with failing impact. + * @return Monotonic failure count accumulated by report(). + */ + std::uint64_t failureCount() const noexcept; + +protected: + /** @brief Writes one diagnostic in the representation selected by the sink. */ + virtual void write(const Event& event) = 0; + +private: + std::uint64_t m_failureCount = 0; +}; + +/** @brief Renders structured diagnostics to standard error. */ +class StderrDiagnosticSink final : public DiagnosticSink { +protected: + /** @brief Renders one diagnostic event to standard error. */ + void write(const Event& event) override; +}; + +/** @brief Returns the display name of a diagnostic severity. */ +std::string_view toString(DiagnosticSink::Severity severity); + +#endif // CTRACE_SRC_DIAGNOSTICS_DIAGNOSTICSINK_H diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp new file mode 100644 index 000000000..851f23a8e --- /dev/null +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceIssueReporter.h" + +#include "DiagnosticSink.h" +#include "TraceEvent.h" + +#include +#include + +/** @brief Appends a raw input offset to a diagnostic when available. */ +static std::string atRawOffset(const std::string& message, const TraceEvent& event) +{ + return message + " at raw offset " + std::to_string(event.index); +} + +/** @brief Creates the concise user-facing representation of a trace issue. */ +static std::string displayErrorMessage(const TraceEvent& event, const TraceIssueEvent& issue) +{ + switch (issue.code) { + case TraceIssueCode::DataLoss: + if (issue.rawBytesConsumed.has_value()) { + return std::to_string(*issue.rawBytesConsumed) + " raw bytes from raw offset " + std::to_string(event.index) + + " could not be decoded before the next hardware ITM sync"; + } + return "trace data at raw offset " + std::to_string(event.index) + + " could not be decoded before the next hardware ITM sync"; + case TraceIssueCode::OpenCsdBadPacketSequence: + return atRawOffset("invalid ITM packet sequence", event); + case TraceIssueCode::OpenCsdInvalidPacketHeader: + return atRawOffset("invalid ITM packet header", event); + case TraceIssueCode::OpenCsdIncompleteTail: + return "incomplete ITM packet starting at raw offset " + std::to_string(event.index) + " at end of input"; + case TraceIssueCode::OpenCsdNoProgress: + return atRawOffset("OpenCSD made no decode progress", event); + case TraceIssueCode::OpenCsdWaitTimeout: + return "OpenCSD remained blocked while flushing pending data"; + case TraceIssueCode::OpenCsdInitializationError: + return "OpenCSD initialization failed"; + case TraceIssueCode::DecodeError: + case TraceIssueCode::InvalidExceptionAction: + case TraceIssueCode::UnsupportedDwtAddressPayload: + case TraceIssueCode::OpenCsdDecodeError: + return atRawOffset("trace decode error", event); + } + return atRawOffset("trace decode error", event); +} + +TraceIssueReporter::TraceIssueReporter(DiagnosticSink& diagnostics) + : m_diagnostics(diagnostics) +{ +} + +void TraceIssueReporter::append(const TraceEvent& event) +{ + if (isTraceEvent(event)) { + reportOverflow(event); + return; + } + if (const auto* issue = traceEventPayload(event)) { + reportError(event, *issue); + } +} + +void TraceIssueReporter::finish() +{ + if (m_finished) { + return; + } + m_finished = true; + if (m_overflowPackets == 0U) { + return; + } + + const auto additionalOverflows = m_overflowPackets - 1U; + const auto firstOverflow = m_firstOverflowTimestamp.has_value() + ? "cycle timestamp " + std::to_string(*m_firstOverflowTimestamp) + : std::string("an unknown cycle timestamp"); + auto summary = "first overflow occurred at " + firstOverflow; + if (additionalOverflows > 0U) { + summary += "; " + std::to_string(additionalOverflows) + " more occurred"; + } + report(DiagnosticSink::Severity::Warning, summary); +} + +void TraceIssueReporter::reportOverflow(const TraceEvent& event) +{ + if (m_overflowPackets == 0U) { + m_firstOverflowTimestamp = event.tcyc; + } + ++m_overflowPackets; +} + +void TraceIssueReporter::reportError(const TraceEvent& event, const TraceIssueEvent& issue) +{ + report(issue.severity == TraceIssueSeverity::Warning ? DiagnosticSink::Severity::Warning + : DiagnosticSink::Severity::Error, + displayErrorMessage(event, issue)); +} + +void TraceIssueReporter::report(DiagnosticSink::Severity severity, std::string message) +{ + m_diagnostics.report({ + severity, + std::move(message), + }); +} diff --git a/tools/ctrace/src/diagnostics/TraceIssueReporter.h b/tools/ctrace/src/diagnostics/TraceIssueReporter.h new file mode 100644 index 000000000..e5741e6f8 --- /dev/null +++ b/tools/ctrace/src/diagnostics/TraceIssueReporter.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_DIAGNOSTICS_TRACEISSUEREPORTER_H +#define CTRACE_SRC_DIAGNOSTICS_TRACEISSUEREPORTER_H + +#include "DiagnosticSink.h" +#include "TraceEvent.h" + +#include +#include +#include + +/** @brief Converts trace issue and overflow events into structured diagnostics. */ +class TraceIssueReporter final : public TraceEventSink { +public: + /** @brief Creates a reporter that writes to the supplied diagnostic sink. */ + explicit TraceIssueReporter(DiagnosticSink& diagnostics); + + /** @brief Observes one event and reports issue-bearing payloads. */ + void append(const TraceEvent& event) override; + /** @brief Emits deferred summary diagnostics once decoding is complete. */ + void finish(); + +private: + /** @brief Accumulates overflow state for the final summary. */ + void reportOverflow(const TraceEvent& event); + /** @brief Reports one semantic decoder issue. */ + void reportError(const TraceEvent& event, const TraceIssueEvent& issue); + /** @brief Submits one normalized trace diagnostic to the sink. */ + void report(DiagnosticSink::Severity severity, std::string message); + + DiagnosticSink& m_diagnostics; + std::optional m_firstOverflowTimestamp; + std::uint64_t m_overflowPackets = 0; + bool m_finished = false; +}; + +#endif // CTRACE_SRC_DIAGNOSTICS_TRACEISSUEREPORTER_H diff --git a/tools/ctrace/src/main.cpp b/tools/ctrace/src/main.cpp index 4abde435d..0fc9e5e5a 100644 --- a/tools/ctrace/src/main.cpp +++ b/tools/ctrace/src/main.cpp @@ -2,10 +2,21 @@ * Copyright (c) 2026 Arm Limited. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 + * Generated with AI */ #include "CtraceMain.h" -int main() { - return CtraceMain(); +#include +#include + +/** + * @brief Converts the process command line and delegates to CtraceMain(). + * @param argc Number of command-line arguments. + * @param argv Command-line argument array. + * @return Exit status produced by CtraceMain(). + */ +int main(int argc, char* argv[]) +{ + return CtraceMain(std::vector(argv, argv + argc)); } diff --git a/tools/ctrace/src/model/TraceEvent.h b/tools/ctrace/src/model/TraceEvent.h new file mode 100644 index 000000000..49c414d3a --- /dev/null +++ b/tools/ctrace/src/model/TraceEvent.h @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_MODEL_TRACEEVENT_H +#define CTRACE_SRC_MODEL_TRACEEVENT_H + +#include +#include +#include +#include +#include + +/** @brief Identifies whether a data-trace event represents a read or write. */ +enum class AccessType { + Read, + Write, +}; + +/** @brief Describes an exception entry, exit, or return transition. */ +enum class ExceptionAction { + Entered, + Exited, + Returned, + Unknown, +}; + +/** @brief Classifies decoder issues retained in the trace event stream. */ +enum class TraceIssueSeverity { + Warning, + Error, +}; + +/** @brief Identifies one decoder issue without relying on textual state. */ +enum class TraceIssueCode { + DecodeError, + DataLoss, + InvalidExceptionAction, + UnsupportedDwtAddressPayload, + OpenCsdDecodeError, + OpenCsdBadPacketSequence, + OpenCsdInvalidPacketHeader, + OpenCsdIncompleteTail, + OpenCsdNoProgress, + OpenCsdWaitTimeout, + OpenCsdInitializationError, +}; + +/** @brief Contains one decoded ITM software stimulus event. */ +struct SoftwareTraceEvent { + std::uint32_t channel = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; +}; + +/** @brief Contains a reconstructed DWT data access event. */ +struct DwtDataTraceEvent { + std::uint32_t comparator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; + AccessType access = AccessType::Read; + std::optional addressLo16 = std::nullopt; + std::optional pc = std::nullopt; +}; + +/** @brief Identifies a DWT address event by program counter. */ +struct DwtPcTraceLocation { + std::uint32_t pc; +}; + +/** @brief Identifies a DWT address event by its low address bits. */ +struct DwtOffsetTraceLocation { + std::uint32_t addressLo16; +}; + +/** @brief Identifies a DWT address event by program counter and address offset. */ +struct DwtPcAndOffsetTraceLocation { + std::uint32_t pc; + std::uint32_t addressLo16; +}; + +/** @brief Stores one of the supported DWT address location representations. */ +using DwtAddressTraceLocation = std::variant; + +/** @brief Contains a reconstructed DWT address event. */ +struct DwtAddressTraceEvent { + std::uint32_t comparator; + DwtAddressTraceLocation location; +}; + +/** @brief Returns the program counter carried by a DWT address event, if present. */ +inline std::optional dwtAddressPc(const DwtAddressTraceEvent& event) +{ + if (const auto* pc = std::get_if(&event.location)) { + return pc->pc; + } + if (const auto* combined = std::get_if(&event.location)) { + return combined->pc; + } + return std::nullopt; +} + +/** @brief Returns the address offset carried by a DWT address event, if present. */ +inline std::optional dwtAddressOffset(const DwtAddressTraceEvent& event) +{ + if (const auto* offset = std::get_if(&event.location)) { + return offset->addressLo16; + } + if (const auto* combined = std::get_if(&event.location)) { + return combined->addressLo16; + } + return std::nullopt; +} + +/** @brief Contains a decoded exception transition. */ +struct ExceptionTraceEvent { + std::uint32_t number = 0; + ExceptionAction action = ExceptionAction::Unknown; +}; + +/** @brief Contains a decoded DWT event-counter packet. */ +struct DwtEventTraceEvent { + std::uint32_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; +}; + +/** @brief Contains a decoded PMU overflow packet. */ +struct PmuTraceEvent { + std::uint32_t discriminator = 0; + std::uint8_t size = 0; + std::uint32_t value = 0; +}; + +/** @brief Marks a decoded local timestamp packet. */ +struct LocalTimestampTraceEvent {}; + +/** @brief Contains a decoded global timestamp packet. */ +struct GlobalTimestampTraceEvent { + std::uint64_t value = 0; + bool clockChange = false; +}; + +/** @brief Records a trace overflow and its diagnostic description. */ +struct OverflowTraceEvent { + std::string message; +}; + +/** @brief Marks a hardware synchronization packet. */ +struct SyncTraceEvent {}; + +/** @brief Retains a decoder warning or error in output order. */ +struct TraceIssueEvent { + TraceIssueCode code = TraceIssueCode::DecodeError; + TraceIssueSeverity severity = TraceIssueSeverity::Error; + std::string message; + std::optional rawBytesConsumed = std::nullopt; + std::optional lastValidTcyc = std::nullopt; +}; + +/** @brief Stores the semantic payload of a decoded trace event. */ +using TraceEventPayload = std::variant; + +/** @brief Describes timestamp and data-loss quality at an event. */ +struct TraceQuality { + bool overflow = false; + bool timestampReliable = false; + std::uint64_t overflowCount = 0; +}; + +/** @brief Wraps a semantic payload with stream, position, time, and quality metadata. */ +struct TraceEvent { + /** @brief Constructs an event from one supported semantic payload. */ + template + explicit TraceEvent(Payload eventPayload) + : payload(std::move(eventPayload)) + { + } + + std::uint64_t index = 0; + // CoreSight Trace Bus ID. ID 0 identifies unformatted single-source input. + std::uint8_t traceBusId = 0U; + std::optional tcyc; + + // Quality is assigned atomically by the post-decoder. Wire/control events + // without reconstructed DWT status leave it absent. + std::optional quality; + + TraceEventPayload payload; +}; + +/** @brief Returns a const payload pointer when an event contains the requested type. */ +template const Payload* traceEventPayload(const TraceEvent& event) +{ + return std::get_if(&event.payload); +} + +/** @brief Returns a mutable payload pointer when an event contains the requested type. */ +template Payload* traceEventPayload(TraceEvent& event) +{ + return std::get_if(&event.payload); +} + +/** @brief Tests whether an event contains the requested payload type. */ +template bool isTraceEvent(const TraceEvent& event) +{ + return std::holds_alternative(event.payload); +} + +/** @brief Receives decoded semantic trace events. */ +class TraceEventSink { +public: + /** @brief Destroys a trace event sink through its interface. */ + virtual ~TraceEventSink() = default; + /** @brief Appends one decoded event to the sink. */ + virtual void append(const TraceEvent& event) = 0; +}; + +#endif // CTRACE_SRC_MODEL_TRACEEVENT_H diff --git a/tools/ctrace/src/model/TraceSelection.cpp b/tools/ctrace/src/model/TraceSelection.cpp new file mode 100644 index 000000000..bcacde28f --- /dev/null +++ b/tools/ctrace/src/model/TraceSelection.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceSelection.h" + +#include "TraceEvent.h" +#include "TraceStreamId.h" + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Maps an ITM payload to its selectable event type. */ +static std::optional typeFor(const SoftwareTraceEvent&) +{ + return TraceEventType::Itm; +} + +/** @brief Maps a DWT data payload to its selectable event type. */ +static std::optional typeFor(const DwtDataTraceEvent&) +{ + return TraceEventType::Dwt; +} + +/** @brief Maps a DWT address payload to its selectable event type. */ +static std::optional typeFor(const DwtAddressTraceEvent&) +{ + return TraceEventType::Dwt; +} + +/** @brief Maps an exception payload to its selectable event type. */ +static std::optional typeFor(const ExceptionTraceEvent&) +{ + return TraceEventType::Exception; +} + +/** @brief Keeps DWT counter payloads disabled until their output semantics are implemented. */ +static std::optional typeFor(const DwtEventTraceEvent&) +{ + return std::nullopt; +} + +/** @brief Keeps PMU payloads disabled until their output semantics are implemented. */ +static std::optional typeFor(const PmuTraceEvent&) +{ + return std::nullopt; +} + +/** @brief Excludes local timestamp control packets from type selection. */ +static std::optional typeFor(const LocalTimestampTraceEvent&) +{ + return std::nullopt; +} + +/** @brief Exposes global timestamp packets through their public output selector. */ +static std::optional typeFor(const GlobalTimestampTraceEvent&) +{ + return TraceEventType::GlobalTimestamp; +} + +/** @brief Exposes overflow packets through their public output selector. */ +static std::optional typeFor(const OverflowTraceEvent&) +{ + return TraceEventType::Overflow; +} + +/** @brief Excludes synchronization control packets from type selection. */ +static std::optional typeFor(const SyncTraceEvent&) +{ + return std::nullopt; +} + +/** @brief Exposes retained diagnostic events through the error selector. */ +static std::optional typeFor(const TraceIssueEvent&) +{ + return TraceEventType::Error; +} + +std::string_view traceEventTypeName(TraceEventType type) +{ + const auto index = static_cast(type); + return index < kTraceEventTypeNames.size() ? kTraceEventTypeNames[index] : std::string_view("unknown"); +} + +std::string traceEventTypeList(const std::string_view& separator) +{ + std::string result; + for (const auto name : kTraceEventTypeNames) { + if (!result.empty()) { + result += separator; + } + result += name; + } + return result; +} + +std::optional parseTraceEventType(const std::string_view& value) +{ + for (std::size_t index = 0; index < kTraceEventTypeNames.size(); ++index) { + if (kTraceEventTypeNames[index] == value) { + return static_cast(index); + } + } + return std::nullopt; +} + +std::optional traceEventType(const TraceEvent& event) +{ + return std::visit([](const auto& payload) { return typeFor(payload); }, event.payload); +} + +bool TraceSelection::includesType(const std::string_view& type) const +{ + return types.empty() || std::find(types.begin(), types.end(), type) != types.end(); +} + +bool TraceSelection::includesStream(std::uint8_t traceBusId) const +{ + return streams.empty() || std::find(streams.begin(), streams.end(), traceBusId) != streams.end(); +} + +bool traceEventSelectedForOutput(const TraceEvent& event, const TraceSelection& selection) +{ + const auto type = traceEventType(event); + const auto* software = traceEventPayload(event); + if (software != nullptr && software->channel == CoreSight::kExcludedItmStimulusPort) { + return false; + } + if (!selection.includesStream(event.traceBusId)) { + return false; + } + return type.has_value() && selection.includesType(traceEventTypeName(*type)); +} diff --git a/tools/ctrace/src/model/TraceSelection.h b/tools/ctrace/src/model/TraceSelection.h new file mode 100644 index 000000000..22cbdd993 --- /dev/null +++ b/tools/ctrace/src/model/TraceSelection.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_MODEL_TRACESELECTION_H +#define CTRACE_SRC_MODEL_TRACESELECTION_H + +#include +#include +#include +#include +#include +#include +#include + +struct TraceEvent; + +/** @brief Identifies event families exposed by the public output filters. */ +enum class TraceEventType : std::size_t { + Itm, + Dwt, + Event, + Pmu, + Exception, + PcSample, + GlobalTimestamp, + Overflow, + Error, + Count, +}; + +inline constexpr std::array(TraceEventType::Count)> kTraceEventTypeNames{{ + "itm", + "dwt", + "event", + "pmu", + "exception", + "pcsample", + "global_ts", + "overflow", + "error", +}}; + +/** @brief Returns the stable command-line name of an event type. */ +std::string_view traceEventTypeName(TraceEventType type); +/** @brief Returns all stable event type names joined by a separator. */ +std::string traceEventTypeList(const std::string_view& separator); +/** @brief Parses a stable event type name. */ +std::optional parseTraceEventType(const std::string_view& value); +/** @brief Returns the selectable type represented by an event, if any. */ +std::optional traceEventType(const TraceEvent& event); + +/** @brief Stores optional event-type and Trace Bus ID output filters. */ +struct TraceSelection { + std::vector types; + std::vector streams; + + /** @brief Tests whether an event type is included. */ + bool includesType(const std::string_view& type) const; + /** @brief Tests whether a Trace Bus ID is included. */ + bool includesStream(std::uint8_t traceBusId) const; +}; + +/** @brief Tests whether an event passes a complete output selection. */ +bool traceEventSelectedForOutput(const TraceEvent& event, const TraceSelection& selection); + +#endif // CTRACE_SRC_MODEL_TRACESELECTION_H diff --git a/tools/ctrace/src/model/TraceStreamId.h b/tools/ctrace/src/model/TraceStreamId.h new file mode 100644 index 000000000..29c91dcb2 --- /dev/null +++ b/tools/ctrace/src/model/TraceStreamId.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_MODEL_TRACESTREAMID_H +#define CTRACE_SRC_MODEL_TRACESTREAMID_H + +#include + +namespace CoreSight { + +inline constexpr std::uint8_t kMinAtbTraceId = 0x01U; +inline constexpr std::uint8_t kMaxAtbTraceId = 0x6fU; +inline constexpr std::uint8_t kUnformattedTraceBusId = 0x00U; +inline constexpr std::uint8_t kExcludedItmStimulusPort = 0U; +inline constexpr std::uint8_t kMaxItmStimulusPort = 31U; + +/** @brief Tests whether a value is an architectural ITM stimulus port. */ +constexpr bool isItmStimulusPort(std::uint32_t value) +{ + return value <= kMaxItmStimulusPort; +} + +/** @brief Tests whether a value is a valid formatted CoreSight ATB trace ID. */ +constexpr bool isAtbTraceId(std::uint32_t value) +{ + return value >= kMinAtbTraceId && value <= kMaxAtbTraceId; +} + +/** @brief Tests whether a value is a supported formatted or unformatted Trace Bus ID. */ +constexpr bool isTraceBusId(std::uint32_t value) +{ + return value <= kMaxAtbTraceId; +} + +} // namespace CoreSight + +#endif // CTRACE_SRC_MODEL_TRACESTREAMID_H diff --git a/tools/ctrace/src/output/OutputRequirements.cpp b/tools/ctrace/src/output/OutputRequirements.cpp new file mode 100644 index 000000000..0379e3ef1 --- /dev/null +++ b/tools/ctrace/src/output/OutputRequirements.cpp @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OutputRequirements.h" + +#include "CtraceRunMeta.h" +#include "ctf/CtfSchema.h" +#include "DiagnosticSink.h" +#include "TraceSelection.h" +#include "TraceOutputConfig.h" +#include "TraceRunConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Stores the derived CSV, CTF, and Trace Compass target paths. */ +struct OutputPaths { + std::filesystem::path csv; + std::filesystem::path ctf; + std::filesystem::path traceCompassXml; +}; + +/** @brief Derives all output targets from one raw input path. */ +static OutputPaths outputPaths(const std::filesystem::path& rawInputPath) +{ + const auto captureName = rawInputPath.filename().stem(); + const auto solutionSetName = captureName.stem(); + if (captureName.empty() || solutionSetName.empty()) { + throw std::runtime_error("cannot derive trace artifact names from " + rawInputPath.string()); + } + const auto outputDirectory = rawInputPath.parent_path(); + auto csvPath = outputDirectory / captureName; + csvPath += ".csv"; + auto ctfPath = outputDirectory / solutionSetName; + ctfPath += ".ctf"; + auto traceCompassXmlPath = outputDirectory / captureName; + traceCompassXmlPath += ".traceanalysis.xml"; + return { + std::move(csvPath), + std::move(ctfPath), + std::move(traceCompassXmlPath), + }; +} + +/** @brief Tests whether one configured source route is selected for output. */ +static bool routeMatchesSelection(const CtraceRunSourceMeta& source, const TraceSelection& selection) +{ + return selection.includesType(source.type) && selection.includesStream(source.traceBusId); +} + +static std::vector> +routeContext(const std::string_view& backend, const CtraceRunMeta& ctraceRunMeta, const CtraceRunSourceMeta& source) +{ + std::vector> context{ + {"backend", std::string(backend)}, + {"channel", std::string(source.type == "itm" ? "ITM" : "DWT") + std::to_string(source.source)}, + {"stream", std::to_string(source.traceBusId)}, + }; + if (!ctraceRunMeta.configPath().empty()) { + context.emplace_back("config", ctraceRunMeta.configPath()); + } + return context; +} + +/** @brief Reports one output preflight failure with optional context. */ +static void reportRequirementError(DiagnosticSink& diagnostics, std::string message, + std::vector> context) +{ + diagnostics.report({ + DiagnosticSink::Severity::Error, + std::move(message), + std::move(context), + }); +} + +/** @brief Validates that selected CTF routes have unambiguous stream identities. */ +static bool validateCtfRouteIdentity(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + bool valid = true; + std::map, const CtraceRunSourceMeta*> routes; + std::set> reported; + for (const auto& source : ctraceRunMeta.sources()) { + if (!routeMatchesSelection(source, selection)) { + continue; + } + const auto key = std::make_pair(source.type, source.source); + const auto [found, inserted] = routes.emplace(key, &source); + if (inserted) { + continue; + } + const auto& first = *found->second; + const auto sameMetadata = first.label == source.label && first.symbolAddress == source.symbolAddress && + first.valueType == source.valueType && first.valueSize == source.valueSize && + first.symbolTypeError == source.symbolTypeError && + first.symbolSizeError == source.symbolSizeError; + const auto indistinguishableProcessors = + first.traceBusId == source.traceBusId && first.processorName != source.processorName; + if ((sameMetadata && !indistinguishableProcessors) || !reported.insert(key).second) { + continue; + } + + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("type", source.type); + context.emplace_back("firstProcessor", first.processorName.value_or("")); + context.emplace_back("otherProcessor", source.processorName.value_or("")); + context.emplace_back("firstStream", std::to_string(first.traceBusId)); + reportRequirementError( + diagnostics, + "CTF metadata cannot describe conflicting active type/source routes from different processors or Trace Bus IDs", + std::move(context)); + } + return valid; +} + +/** @brief Stores an unambiguous clock or the diagnostics preventing selection. */ +struct SelectedClockResolution { + std::optional clockHz; + bool hasRoutes{false}; + bool valid{true}; +}; + +/** @brief Resolves a common clock from all selected stream routes. */ +static SelectedClockResolution resolveSelectedCtfClock(const CtraceRunMeta& ctraceRunMeta, + const TraceSelection& selection, DiagnosticSink& diagnostics) +{ + SelectedClockResolution result; + for (const auto& [traceBusId, timestamp] : ctraceRunMeta.timestampsByTraceBusId()) { + if (!selection.includesStream(traceBusId)) { + continue; + } + result.hasRoutes = true; + if (timestamp.clockError.has_value()) { + result.valid = false; + reportRequirementError(diagnostics, + "CTF output cannot use the configured timestamps.clock", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"pname", timestamp.processorName.value_or("")}, + {"error", *timestamp.clockError}, + }); + continue; + } + if (!timestamp.clockHz.has_value()) { + result.valid = false; + reportRequirementError(diagnostics, + "CTF output requires timestamps.clock for the processor assigned to this Trace Bus ID", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"pname", timestamp.processorName.value_or("")}, + }); + continue; + } + if (*timestamp.clockHz == 0U) { + result.valid = false; + reportRequirementError(diagnostics, + "CTF output requires timestamps.clock to be greater than zero", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"pname", timestamp.processorName.value_or("")}, + }); + continue; + } + if (result.clockHz.has_value() && *result.clockHz != *timestamp.clockHz) { + result.valid = false; + reportRequirementError(diagnostics, + "CTF output cannot combine selected Trace Bus IDs with different timestamps.clock values", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"stream", std::to_string(traceBusId)}, + {"clock", std::to_string(*timestamp.clockHz)}, + }); + continue; + } + result.clockHz = timestamp.clockHz; + } + return result; +} + +/** @brief Resolves the fallback CTF clock when no selected route supplies one. */ +static std::optional resolveDefaultCtfClock(const CtraceRunMeta& ctraceRunMeta, + const TraceSelection& selection, DiagnosticSink& diagnostics) +{ + for (const auto& error : ctraceRunMeta.timestampClockErrors()) { + reportRequirementError(diagnostics, + "CTF output cannot use the configured timestamps.clock", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + {"error", error}, + }); + } + if (!ctraceRunMeta.timestampClockErrors().empty()) { + return std::nullopt; + } + if (!ctraceRunMeta.timestampClockHz().has_value()) { + if (!selection.streams.empty() && !ctraceRunMeta.timestampsByTraceBusId().empty()) { + reportRequirementError(diagnostics, + "CTF output cannot assign unformatted or unknown Trace Bus IDs to processors with " + "different timestamps.clock values", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + }); + return std::nullopt; + } + reportRequirementError(diagnostics, + "CTF output requires timestamps.clock from an active ctrace-setup; no default is assumed", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + }); + return std::nullopt; + } + if (*ctraceRunMeta.timestampClockHz() == 0U) { + reportRequirementError(diagnostics, + "CTF output requires timestamps.clock to be greater than zero", + { + {"backend", "ctf"}, + {"config", ctraceRunMeta.configPath()}, + }); + return std::nullopt; + } + return ctraceRunMeta.timestampClockHz(); +} + +/** @brief Resolves and validates the clock used by a CTF output. */ +static std::optional resolveCtfClock(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + const auto selected = resolveSelectedCtfClock(ctraceRunMeta, selection, diagnostics); + if (!selected.valid) { + return std::nullopt; + } + if (selected.hasRoutes) { + return selected.clockHz; + } + return resolveDefaultCtfClock(ctraceRunMeta, selection, diagnostics); +} + +/** @brief Validates value type and size metadata for selected DWT routes. */ +static bool validateCtfDwtMetadata(const CtraceRunMeta& ctraceRunMeta, const TraceSelection& selection, + DiagnosticSink& diagnostics) +{ + bool valid = true; + for (const auto& source : ctraceRunMeta.sources()) { + if (source.type != "dwt" || !routeMatchesSelection(source, selection)) { + continue; + } + bool sourceValid = true; + if (source.symbolTypeError.has_value()) { + valid = false; + sourceValid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("error", *source.symbolTypeError); + reportRequirementError(diagnostics, + "CTF output cannot use the configured ctrace-run data.symbol-type", std::move(context)); + } + if (source.symbolSizeError.has_value()) { + valid = false; + sourceValid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("error", *source.symbolSizeError); + reportRequirementError(diagnostics, + "CTF output cannot use the configured ctrace-run data.symbol-size", std::move(context)); + } + if (!sourceValid) { + continue; + } + const auto validType = TraceRunSchema::isDwtDataType(source.valueType); + const auto* valueVariant = CtfSchema::valueVariantForTraceRunType(source.valueType, source.valueSize); + if (!validType) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("dataType", source.valueType); + reportRequirementError(diagnostics, + "CTF output cannot use ctrace-run data.symbol-type '" + source.valueType + "'; " + + std::string(CtfSchema::ValueTypeRequirements), + std::move(context)); + } + if (!TraceRunSchema::isDwtDataSize(source.valueSize) || (validType && valueVariant == nullptr)) { + valid = false; + auto context = routeContext("ctf", ctraceRunMeta, source); + context.emplace_back("dataType", source.valueType); + context.emplace_back("dataSize", std::to_string(source.valueSize)); + reportRequirementError(diagnostics, + "CTF output cannot use ctrace-run data.symbol-size " + std::to_string(source.valueSize) + + " with data.symbol-type '" + source.valueType + "'; " + + std::string(CtfSchema::ValueTypeRequirements), + std::move(context)); + } + } + return valid; +} + +/** @brief Converts selected trace-run sources into normalized CTF routes. */ +static std::vector resolveCtfSources(const CtraceRunMeta& ctraceRunMeta, + const TraceSelection& selection) +{ + std::set> resolvedKeys; + std::vector sources; + for (const auto& route : ctraceRunMeta.sources()) { + if ((route.type != "itm" && route.type != "dwt") || (route.type == "itm" && route.source == 0U) || + !routeMatchesSelection(route, selection) || + !resolvedKeys.emplace(route.type, route.source, route.traceBusId).second) { + continue; + } + + sources.push_back({ + route.type, + route.source, + route.traceBusId, + route.label, + route.symbolAddress, + route.valueType, + static_cast(route.valueSize), + }); + } + return sources; +} + +bool TraceOutputPlan::hasRequestedOutputs() const +{ + return csvRequested || ctfRequested; +} + +bool TraceOutputPlan::hasEnabledOutputs() const +{ + return csv.has_value() || ctf.has_value(); +} + +TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::filesystem::path& rawInputPath, + const CtraceRunMeta& ctraceRunMeta, DiagnosticSink& diagnostics) +{ + TraceOutputPlan plan; + plan.csvRequested = request.csv; + plan.ctfRequested = request.ctf; + if (!plan.hasRequestedOutputs()) { + return plan; + } + + const auto paths = outputPaths(rawInputPath); + if (plan.csvRequested) { + plan.csv = CsvOutputConfig{ + paths.csv, + request.selection, + }; + } + if (plan.ctfRequested) { + auto clock = resolveCtfClock(ctraceRunMeta, request.selection, diagnostics); + const auto validRoutes = validateCtfRouteIdentity(ctraceRunMeta, request.selection, diagnostics); + const auto validTypes = validateCtfDwtMetadata(ctraceRunMeta, request.selection, diagnostics); + auto sources = + clock.has_value() && validRoutes && validTypes + ? std::optional>(resolveCtfSources(ctraceRunMeta, request.selection)) + : std::nullopt; + if (clock.has_value() && validRoutes && validTypes && sources.has_value()) { + plan.ctf = CtfOutputConfig{ + paths.ctf, paths.traceCompassXml, *clock, request.selection, std::move(*sources), + }; + } + } + return plan; +} diff --git a/tools/ctrace/src/output/OutputRequirements.h b/tools/ctrace/src/output/OutputRequirements.h new file mode 100644 index 000000000..ca52247a2 --- /dev/null +++ b/tools/ctrace/src/output/OutputRequirements.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_OUTPUTREQUIREMENTS_H +#define CTRACE_SRC_OUTPUT_OUTPUTREQUIREMENTS_H + +#include "TraceOutputConfig.h" + +#include +#include + +class CtraceRunMeta; +class DiagnosticSink; + +/** @brief Stores independently validated CSV and CTF output configurations. */ +struct TraceOutputPlan { + bool csvRequested = false; + bool ctfRequested = false; + std::optional csv; + std::optional ctf; + + /** @brief Reports whether any output format was requested. */ + bool hasRequestedOutputs() const; + /** @brief Reports whether at least one requested output remains enabled. */ + bool hasEnabledOutputs() const; +}; + +/** + * @brief Validates output requirements and returns the formats that can be generated. + * @param request User-requested output formats, targets, and filters. + * @param rawInputPath Raw input used to derive default target names. + * @param ctraceRunMeta Metadata required by output formats. + * @param diagnostics Sink receiving independently reported format failures. + * @return Plan retaining every requested format whose requirements are satisfied. + */ +TraceOutputPlan planTraceOutputs(const TraceOutputRequest& request, const std::filesystem::path& rawInputPath, + const CtraceRunMeta& ctraceRunMeta, DiagnosticSink& diagnostics); + +#endif // CTRACE_SRC_OUTPUT_OUTPUTREQUIREMENTS_H diff --git a/tools/ctrace/src/output/TraceOutput.h b/tools/ctrace/src/output/TraceOutput.h new file mode 100644 index 000000000..294979180 --- /dev/null +++ b/tools/ctrace/src/output/TraceOutput.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_TRACEOUTPUT_H +#define CTRACE_SRC_OUTPUT_TRACEOUTPUT_H + +#include "TraceEvent.h" + +#include +#include + +/** + * @brief Defines the lifecycle and event interface of a trace output backend. + * + * A caller invokes start(), writes events synchronously in decode order, and + * then invokes stop(). If any operation fails, abort() removes incomplete output. + */ +class TraceOutput { +public: + /** @brief Destroys an output backend through its interface. */ + virtual ~TraceOutput() = default; + + /** + * @brief Returns the stable backend name used in diagnostics. + * @return Non-owning backend identifier with static lifetime. + */ + virtual std::string_view backendName() const noexcept + { + return "trace"; + } + /** + * @brief Returns the primary output target path used in diagnostics. + * @return Displayable path, or an empty string when no target exists. + */ + virtual std::string targetPath() const + { + return {}; + } + + /** @brief Prepares a new final output target before the first event. */ + virtual void start() {} + /** @brief Flushes and completes the active output target after the last event. */ + virtual void stop() {} + /** @brief Discards an incomplete active output without committing partial data. */ + virtual void abort() = 0; + /** + * @brief Writes one event synchronously in decode order. + * @param event Decoded event whose lifetime extends through this call. + */ + virtual void writeEvent(const TraceEvent& event) = 0; +}; + +#endif // CTRACE_SRC_OUTPUT_TRACEOUTPUT_H diff --git a/tools/ctrace/src/output/TraceOutputConfig.h b/tools/ctrace/src/output/TraceOutputConfig.h new file mode 100644 index 000000000..09fb8a450 --- /dev/null +++ b/tools/ctrace/src/output/TraceOutputConfig.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H +#define CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H + +#include "TraceSelection.h" + +#include +#include +#include +#include +#include +#include + +/** @brief Stores the output formats and filters requested by the user. */ +struct TraceOutputRequest { + bool csv = false; + bool ctf = false; + TraceSelection selection; +}; + +/** @brief Stores normalized source metadata required by trace outputs. */ +struct ResolvedTraceSource { + std::string type; + std::uint32_t source = 0; + std::uint8_t traceBusId = 0U; + std::optional label; + std::optional symbolAddress; + std::string valueType = "unsigned int"; + std::uint8_t valueSize = 4U; +}; + +/** @brief Configures one CSV output artifact. */ +struct CsvOutputConfig { + std::filesystem::path outputPath; + TraceSelection selection; +}; + +/** @brief Configures one CTF bundle and its Trace Compass analysis file. */ +struct CtfOutputConfig { + /** @brief Creates a complete CTF output configuration. */ + CtfOutputConfig(std::filesystem::path outputDirectory, std::filesystem::path traceCompassXmlPath, + std::uint64_t clockHz, TraceSelection selection, std::vector sources) + : outputDirectory(std::move(outputDirectory)), + traceCompassXmlPath(std::move(traceCompassXmlPath)), + coreClockHz(clockHz), + selection(std::move(selection)), + sources(std::move(sources)) + { + } + + std::filesystem::path outputDirectory; + std::filesystem::path traceCompassXmlPath; + std::uint64_t coreClockHz = 0; + TraceSelection selection; + std::vector sources; +}; + +#endif // CTRACE_SRC_OUTPUT_TRACEOUTPUTCONFIG_H diff --git a/tools/ctrace/src/output/TraceOutputLifecycle.cpp b/tools/ctrace/src/output/TraceOutputLifecycle.cpp new file mode 100644 index 000000000..8073fe7d5 --- /dev/null +++ b/tools/ctrace/src/output/TraceOutputLifecycle.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceOutputLifecycle.h" + +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceOutput.h" + +#include +#include +#include +#include +#include +#include + +/** @brief Extracts a stable message from a captured backend exception. */ +static std::string exceptionMessage(const std::exception_ptr& error) +{ + try { + std::rethrow_exception(error); + } catch (const std::exception& ex) { + return ex.what(); + } catch (...) { + // Preserve noexcept error reporting even for non-standard exceptions. + (void)0; + } + return "unknown exception"; +} + +TraceOutputLifecycle::TraceOutputLifecycle(std::vector> outputs, + DiagnosticSink& diagnostics) + : m_outputs(std::move(outputs)), + m_diagnostics(diagnostics), + m_states(m_outputs.size(), State::Inactive) +{ + for (std::size_t index = 0; index < m_outputs.size(); ++index) { + try { + m_outputs[index]->start(); + m_states[index] = State::Active; + } catch (...) { + fail(index, "start", std::current_exception()); + abortNoexcept(index); + } + } +} + +TraceOutputLifecycle::~TraceOutputLifecycle() noexcept +{ + abortActiveNoexcept(); +} + +void TraceOutputLifecycle::append(const TraceEvent& event) +{ + for (std::size_t index = 0; index < m_outputs.size(); ++index) { + if (m_states[index] != State::Active) { + continue; + } + try { + m_outputs[index]->writeEvent(event); + } catch (...) { + fail(index, "write", std::current_exception()); + abortNoexcept(index); + } + } +} + +void TraceOutputLifecycle::abort() noexcept +{ + abortActiveNoexcept(); +} + +void TraceOutputLifecycle::finish() noexcept +{ + if (m_finished) { + return; + } + + for (std::size_t index = m_outputs.size(); index > 0U; --index) { + const auto outputIndex = index - 1U; + if (m_states[outputIndex] != State::Active) { + continue; + } + try { + m_outputs[outputIndex]->stop(); + m_states[outputIndex] = State::Completed; + } catch (...) { + fail(outputIndex, "stop", std::current_exception()); + abortNoexcept(outputIndex); + } + } + m_finished = true; +} + +void TraceOutputLifecycle::fail(std::size_t index, const char* phase, const std::exception_ptr& error) noexcept +{ + m_states[index] = State::Failed; + try { + const auto message = exceptionMessage(error); + const auto backend = std::string(m_outputs[index]->backendName()); + const auto target = m_outputs[index]->targetPath(); + const auto displayMessage = + backend + " output" + (target.empty() ? std::string() : " '" + target + "'") + " failed during " + phase + + ": " + message; + m_diagnostics.report({ + DiagnosticSink::Severity::Error, + displayMessage, + { + {"outputIndex", std::to_string(index)}, + {"backend", backend}, + {"path", target}, + {"phase", phase}, + {"error", message}, + }, + }); + } catch (...) { + // Diagnostics must never escape this noexcept failure path. + (void)0; + } +} + +void TraceOutputLifecycle::abortNoexcept(std::size_t index) noexcept +{ + try { + m_outputs[index]->abort(); + } catch (...) { + fail(index, "abort", std::current_exception()); + } + m_states[index] = State::Failed; +} + +void TraceOutputLifecycle::abortActiveNoexcept() noexcept +{ + for (std::size_t index = 0; index < m_outputs.size(); ++index) { + if (m_states[index] == State::Active) { + abortNoexcept(index); + } + } + m_finished = true; +} diff --git a/tools/ctrace/src/output/TraceOutputLifecycle.h b/tools/ctrace/src/output/TraceOutputLifecycle.h new file mode 100644 index 000000000..6182da18e --- /dev/null +++ b/tools/ctrace/src/output/TraceOutputLifecycle.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_TRACEOUTPUTLIFECYCLE_H +#define CTRACE_SRC_OUTPUT_TRACEOUTPUTLIFECYCLE_H + +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceOutput.h" + +#include +#include +#include +#include + +/** @brief Coordinates start, event delivery, completion, and cleanup for outputs. */ +class TraceOutputLifecycle final : public TraceEventSink { +public: + /** @brief Creates and starts the supplied output backends. */ + explicit TraceOutputLifecycle(std::vector> outputs, DiagnosticSink& diagnostics); + /** @brief Aborts any outputs that remain active. */ + ~TraceOutputLifecycle() noexcept; + + /** @brief Writes one event to every active output. */ + void append(const TraceEvent& event) override; + /** @brief Completes all active outputs without propagating failures. */ + void finish() noexcept; + /** @brief Aborts all active outputs without propagating failures. */ + void abort() noexcept; + + /** @brief Disables copying because the lifecycle owns output backends. */ + TraceOutputLifecycle(const TraceOutputLifecycle&) = delete; + /** @brief Disables copy assignment because the lifecycle owns output backends. */ + TraceOutputLifecycle& operator=(const TraceOutputLifecycle&) = delete; + +private: + /** @brief Tracks the lifecycle state of one output backend. */ + enum class State { + Inactive, + Active, + Completed, + Failed, + }; + + /** @brief Marks one backend failed and reports the captured exception. */ + void fail(std::size_t index, const char* phase, const std::exception_ptr& error) noexcept; + /** @brief Aborts one backend and suppresses cleanup exceptions. */ + void abortNoexcept(std::size_t index) noexcept; + /** @brief Aborts every backend that is still active. */ + void abortActiveNoexcept() noexcept; + + std::vector> m_outputs; + DiagnosticSink& m_diagnostics; + std::vector m_states; + bool m_finished = false; +}; + +#endif // CTRACE_SRC_OUTPUT_TRACEOUTPUTLIFECYCLE_H diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.cpp b/tools/ctrace/src/output/csv/CsvFileOutput.cpp new file mode 100644 index 000000000..e2a409bda --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvFileOutput.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CsvFileOutput.h" + +#include "CsvRowMapper.h" +#include "TraceEvent.h" +#include "TraceSelection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Adapts a binary output file to the CSV stream interface. */ +class CsvFileStream final : public CsvFileOutput::Stream { +public: + /** @brief Opens a new binary output file, replacing an existing file. */ + explicit CsvFileStream(const std::filesystem::path& path) + : m_stream(path, std::ios::out | std::ios::trunc | std::ios::binary) + { + } + + /** @brief Returns the underlying output file stream. */ + std::ostream& output() override { return m_stream; } + + /** @brief Closes the underlying output file stream. */ + void close() override { m_stream.close(); } + +private: + std::ofstream m_stream; +}; + +/** @brief Safely removes an existing non-directory CSV target. */ +static void removeExistingCsv(const std::filesystem::path& path) +{ + const auto normalized = path.lexically_normal(); + if (path.empty() || normalized == normalized.root_path() || normalized.filename().empty() || + normalized.filename() == "." || normalized.filename() == "..") { + throw std::invalid_argument("CSV output path must identify a file"); + } + std::error_code error; + const auto status = std::filesystem::symlink_status(path, error); + if (error && error != std::errc::no_such_file_or_directory) { + throw std::runtime_error("Failed to inspect existing CSV output " + path.string() + ": " + error.message()); + } + if (!error && std::filesystem::is_directory(status)) { + throw std::runtime_error("Refusing to replace CSV output because the target is a directory: " + path.string()); + } + error.clear(); + std::filesystem::remove(path, error); + if (error) { + throw std::runtime_error("Failed to remove existing CSV output " + path.string() + ": " + error.message()); + } +} + +/** @brief Creates the parent directory of a CSV target when needed. */ +static void createParentDirectory(const std::filesystem::path& path) +{ + const auto parent = path.parent_path(); + if (parent.empty()) { + return; + } + std::error_code error; + std::filesystem::create_directories(parent, error); + if (error) { + throw std::runtime_error("Failed to create CSV output directory " + parent.string() + ": " + error.message()); + } +} + +CsvFileOutput::CsvFileOutput(std::filesystem::path outputFile, TraceSelection selection) + : CsvFileOutput(std::move(outputFile), std::move(selection), [](const std::filesystem::path& path) { + return std::make_unique(path); + }) +{ +} + +CsvFileOutput::CsvFileOutput(std::filesystem::path outputFile, TraceSelection selection, StreamFactory streamFactory) + : m_outputFile(std::move(outputFile)), + m_selection(std::move(selection)), + m_streamFactory(std::move(streamFactory)) +{ + if (!m_streamFactory) { + throw std::invalid_argument("CSV stream factory must be configured"); + } +} + +CsvFileOutput::~CsvFileOutput() +{ + try { + CsvFileOutput::abort(); + } catch (...) { + (void)0; + } +} + +std::string_view CsvFileOutput::backendName() const noexcept +{ + return "csv"; +} + +std::string CsvFileOutput::targetPath() const +{ + return m_outputFile.string(); +} + +void CsvFileOutput::start() +{ + abort(); + const auto& outputPath = m_outputFile; + removeExistingCsv(outputPath); + createParentDirectory(outputPath); + m_active = true; + m_stream = m_streamFactory(outputPath); + if (m_stream == nullptr || !m_stream->output()) { + abort(); + throw std::runtime_error("Failed to open CSV output " + outputPath.string()); + } + + m_stream->output() << CsvRowMapper::header() << "\n"; + if (!m_stream->output()) { + abort(); + throw std::runtime_error("Failed to write CSV output " + m_outputFile.string()); + } +} + +void CsvFileOutput::stop() +{ + if (m_stream != nullptr) { + m_stream->close(); + } + const auto failed = m_stream != nullptr && !m_stream->output(); + m_stream.reset(); + if (failed) { + abort(); + throw std::runtime_error("Failed to write CSV output " + m_outputFile.string()); + } + m_active = false; +} + +void CsvFileOutput::abort() +{ + m_stream.reset(); + if (m_active) { + removeExistingCsv(m_outputFile); + m_active = false; + } +} + +void CsvFileOutput::writeEvent(const TraceEvent& event) +{ + if (m_stream == nullptr) { + return; + } + if (!traceEventSelectedForOutput(event, m_selection)) { + return; + } + + m_stream->output() << CsvRowMapper::row(event) << "\n"; +} diff --git a/tools/ctrace/src/output/csv/CsvFileOutput.h b/tools/ctrace/src/output/csv/CsvFileOutput.h new file mode 100644 index 000000000..570714510 --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvFileOutput.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CSV_CSVFILEOUTPUT_H +#define CTRACE_SRC_OUTPUT_CSV_CSVFILEOUTPUT_H + +#include "TraceEvent.h" +#include "TraceSelection.h" +#include "TraceOutput.h" + +#include +#include +#include +#include +#include +#include + +/** @brief Writes selected trace events directly to a CSV file. */ +class CsvFileOutput final : public TraceOutput { +public: + /** @brief Owns one CSV stream and provides its explicit close operation. */ + class Stream { + public: + /** @brief Allows destruction through the stream interface. */ + virtual ~Stream() = default; + + /** @brief Returns the stream receiving CSV rows. */ + virtual std::ostream& output() = 0; + /** @brief Flushes and closes the stream. */ + virtual void close() = 0; + }; + + /** + * @brief Creates an output stream for a validated CSV target path. + * @param outputFile Final CSV path. + * @return Owned, writable stream abstraction. + */ + using StreamFactory = std::function(const std::filesystem::path&)>; + + /** + * @brief Creates a CSV output for one target file. + * @param outputFile Final CSV path. + * @param selection Optional event and stream filters. + */ + explicit CsvFileOutput(std::filesystem::path outputFile, TraceSelection selection = {}); + /** + * @brief Creates a CSV output using an injected stream factory. + * @param outputFile Final CSV path. + * @param selection Optional event and stream filters. + * @param streamFactory Factory used to open the target stream. + */ + CsvFileOutput(std::filesystem::path outputFile, TraceSelection selection, StreamFactory streamFactory); + /** @brief Closes an active stream without throwing. */ + ~CsvFileOutput() override; + + /** @brief Creates the target file and writes its header. */ + void start() override; + /** @brief Flushes and closes the completed CSV file. */ + void stop() override; + /** @brief Closes and removes an incomplete CSV file. */ + void abort() override; + /** + * @brief Writes one selected event as a CSV row. + * @param event Event evaluated against the configured selection. + */ + void writeEvent(const TraceEvent& event) override; + /** @brief Returns the CSV backend name. */ + std::string_view backendName() const noexcept override; + /** @brief Returns the CSV target file path. */ + std::string targetPath() const override; + +private: + std::filesystem::path m_outputFile; + TraceSelection m_selection; + StreamFactory m_streamFactory; + std::unique_ptr m_stream; + bool m_active = false; +}; + +#endif // CTRACE_SRC_OUTPUT_CSV_CSVFILEOUTPUT_H diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.cpp b/tools/ctrace/src/output/csv/CsvRowMapper.cpp new file mode 100644 index 000000000..65381c9c7 --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvRowMapper.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CsvRowMapper.h" + +#include "TraceEvent.h" +#include "TraceSelection.h" + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Identifies columns in the stable ctrace CSV schema. */ +enum class CsvColumn : std::size_t { + Cycles, + Stream, + Type, + Source, + Value, + Pc, + Offset, + Note, + Count, +}; + +constexpr std::array(CsvColumn::Count)> kCsvColumnNames{{ + "cycles", + "stream", + "type", + "source", + "value", + "pc", + "offset", + "note", +}}; + +using CsvRow = std::array(CsvColumn::Count)>; + +template static std::string joinColumns(const Columns& columns) +{ + std::ostringstream out; + for (std::size_t index = 0; index < columns.size(); ++index) { + if (index != 0U) { + out << ","; + } + out << columns[index]; + } + return out.str(); +} + +/** @brief Converts a CSV column identifier to its array index. */ +static std::size_t column(CsvColumn value) +{ + return static_cast(value); +} + +/** @brief Applies RFC-style quoting to one CSV field when required. */ +static std::string escapeCsvField(const std::string& value) +{ + if (value.find_first_of("\",\r\n") == std::string::npos) { + return value; + } + std::string escaped = "\""; + for (const auto ch : value) { + if (ch == '"') { + escaped += "\"\""; + } else { + escaped += ch; + } + } + escaped += "\""; + return escaped; +} + +/** @brief Joins escaped fields into one CSV row. */ +static std::string renderCsvRow(const CsvRow& fields) +{ + std::ostringstream out; + for (std::size_t index = 0; index < fields.size(); ++index) { + if (index != 0U) { + out << ","; + } + out << escapeCsvField(fields[index]); + } + return out.str(); +} + +/** @brief Formats an integer as a zero-padded hexadecimal CSV field. */ +static std::string hexValue(std::uint64_t value, std::uint32_t widthBytes) +{ + static constexpr char digits[] = "0123456789abcdef"; + const auto width = std::max(2U, widthBytes * 2U); + std::string out(width, '0'); + for (std::uint32_t index = 0; index < width; ++index) { + out[width - 1U - index] = digits[value & 0x0fU]; + value >>= 4U; + } + return "0x" + out; +} + +/** @brief Maps a semantic exception action to its CSV value. */ +static std::string_view exceptionActionCsvValue(ExceptionAction action) +{ + switch (action) { + case ExceptionAction::Entered: + return "0x1"; + case ExceptionAction::Exited: + return "0x2"; + case ExceptionAction::Returned: + return "0x3"; + case ExceptionAction::Unknown: + return {}; + } + return "0x0"; +} + +/** @brief Maps one semantic trace event to all CSV columns. */ +static CsvRow eventToCsvRow(const TraceEvent& event) +{ + CsvRow row{}; + if (event.tcyc.has_value()) { + row[column(CsvColumn::Cycles)] = std::to_string(*event.tcyc); + } + if (event.traceBusId != 0U) { + row[column(CsvColumn::Stream)] = std::to_string(event.traceBusId); + } + if (const auto type = traceEventType(event)) { + row[column(CsvColumn::Type)] = traceEventTypeName(*type); + } + + if (const auto* software = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(software->channel); + row[column(CsvColumn::Value)] = hexValue(software->value, software->size); + } else if (const auto* data = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(data->comparator); + row[column(CsvColumn::Value)] = hexValue(data->value, data->size); + if (data->pc.has_value()) { + row[column(CsvColumn::Pc)] = hexValue(*data->pc, 4); + } + if (data->addressLo16.has_value()) { + row[column(CsvColumn::Offset)] = hexValue(*data->addressLo16, 2); + } + } else if (const auto* address = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(address->comparator); + if (const auto pc = dwtAddressPc(*address)) { + row[column(CsvColumn::Pc)] = hexValue(*pc, 4); + } + if (const auto offset = dwtAddressOffset(*address)) { + row[column(CsvColumn::Offset)] = hexValue(*offset, 2); + } + } else if (const auto* exception = traceEventPayload(event)) { + row[column(CsvColumn::Source)] = std::to_string(exception->number); + row[column(CsvColumn::Value)] = exceptionActionCsvValue(exception->action); + } else if (const auto* timestamp = traceEventPayload(event)) { + row[column(CsvColumn::Cycles)] = std::to_string(timestamp->value); + } else if (const auto* overflow = traceEventPayload(event)) { + row[column(CsvColumn::Note)] = overflow->message.empty() + ? "overflow: new timestamp segment; time across boundary may be unreliable" + : overflow->message; + } else if (const auto* issue = traceEventPayload(event)) { + row[column(CsvColumn::Note)] = issue->message; + } + + return row; +} + + +std::string CsvRowMapper::header() +{ + return joinColumns(kCsvColumnNames); +} + +std::string CsvRowMapper::row(const TraceEvent& event) +{ + return renderCsvRow(eventToCsvRow(event)); +} diff --git a/tools/ctrace/src/output/csv/CsvRowMapper.h b/tools/ctrace/src/output/csv/CsvRowMapper.h new file mode 100644 index 000000000..e0bed1b7b --- /dev/null +++ b/tools/ctrace/src/output/csv/CsvRowMapper.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CSV_CSVROWMAPPER_H +#define CTRACE_SRC_OUTPUT_CSV_CSVROWMAPPER_H + +#include + +struct TraceEvent; + +/** @brief Maps semantic trace events to the stable CSV representation. */ +class CsvRowMapper final { +public: + /** @brief Returns the stable CSV header row. */ + static std::string header(); + /** @brief Returns one CSV row for a semantic trace event. */ + static std::string row(const TraceEvent& event); + +private: + /** @brief Prevents construction of this stateless mapping utility. */ + CsvRowMapper() = delete; +}; + +#endif // CTRACE_SRC_OUTPUT_CSV_CSVROWMAPPER_H diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp new file mode 100644 index 000000000..46f8e2548 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfBundleOutput.h" + +#include "CtfEncoder.h" +#include "TraceCompassXmlWriter.h" +#include "TraceEvent.h" +#include "TraceOutputConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Rejects empty and root-like output targets. */ +static void requireOutputTarget(const std::filesystem::path& path, const char* description) +{ + const auto normalized = path.lexically_normal(); + if (path.empty() || normalized == normalized.root_path() || normalized.filename().empty() || + normalized.filename() == "." || normalized.filename() == "..") { + throw std::invalid_argument(std::string(description) + " must identify a specific output path"); + } +} + +/** @brief Returns a lexically normalized absolute path for safety comparisons. */ +static std::filesystem::path normalizedAbsolutePath(const std::filesystem::path& path) +{ + std::error_code error; + const auto absolute = std::filesystem::absolute(path, error); + return (error ? path : absolute).lexically_normal(); +} + +/** @brief Folds one ASCII character for conservative portable path comparison. */ +static char foldedPathCharacter(char character) +{ + return character >= 'A' && character <= 'Z' ? static_cast(character + ('a' - 'A')) : character; +} + +/** @brief Compares path components without relying on target-platform case rules. */ +static bool pathComponentEquals(const std::filesystem::path& lhs, const std::filesystem::path& rhs) +{ + const auto lhsText = lhs.generic_u8string(); + const auto rhsText = rhs.generic_u8string(); + return lhsText.size() == rhsText.size() && + std::equal(lhsText.begin(), lhsText.end(), rhsText.begin(), [](char lhsCharacter, char rhsCharacter) { + return foldedPathCharacter(lhsCharacter) == foldedPathCharacter(rhsCharacter); + }); +} + +/** @brief Tests whether a candidate path contains another target path. */ +static bool isAncestorPath(const std::filesystem::path& candidate, const std::filesystem::path& path) +{ + auto candidateComponent = candidate.begin(); + auto pathComponent = path.begin(); + for (; candidateComponent != candidate.end(); ++candidateComponent, ++pathComponent) { + if (pathComponent == path.end() || !pathComponentEquals(*candidateComponent, *pathComponent)) { + return false; + } + } + return true; +} + +/** @brief Rejects CTF and Trace Compass targets that overlap unsafely. */ +static void validateOutputTargets(const std::filesystem::path& ctfDirectory, + const std::filesystem::path& traceCompassXml) +{ + requireOutputTarget(ctfDirectory, "CTF output directory"); + requireOutputTarget(traceCompassXml, "Trace Compass XML output"); + const auto normalizedCtf = normalizedAbsolutePath(ctfDirectory); + const auto normalizedXml = normalizedAbsolutePath(traceCompassXml); + if (isAncestorPath(normalizedCtf, normalizedXml) || isAncestorPath(normalizedXml, normalizedCtf)) { + throw std::invalid_argument("CTF output directory and Trace Compass XML output must be separate, non-nested paths"); + } +} + +/** @brief Removes an existing CTF directory without following unsafe targets. */ +static void removeOutputDirectory(const std::filesystem::path& path) +{ + std::error_code error; + std::filesystem::remove_all(path, error); + if (error) { + throw std::runtime_error("Failed to remove existing CTF output " + path.string() + ": " + error.message()); + } +} + +/** @brief Verifies that existing output targets have replaceable filesystem types. */ +static void validateExistingOutputTypes(const std::filesystem::path& ctfDirectory, + const std::filesystem::path& traceCompassXml) +{ + std::error_code ctfError; + const auto ctfStatus = std::filesystem::symlink_status(ctfDirectory, ctfError); + if (ctfError && ctfError != std::errc::no_such_file_or_directory) { + throw std::runtime_error("Failed to inspect existing CTF output " + ctfDirectory.string() + ": " + + ctfError.message()); + } + if (!ctfError && std::filesystem::exists(ctfStatus) && !std::filesystem::is_directory(ctfStatus) && + !std::filesystem::is_symlink(ctfStatus)) { + throw std::runtime_error("Refusing to replace CTF output because the target is not a directory: " + + ctfDirectory.string()); + } + + std::error_code xmlError; + const auto xmlStatus = std::filesystem::symlink_status(traceCompassXml, xmlError); + if (xmlError && xmlError != std::errc::no_such_file_or_directory) { + throw std::runtime_error("Failed to inspect existing Trace Compass XML " + traceCompassXml.string() + ": " + + xmlError.message()); + } + if (!xmlError && std::filesystem::is_directory(xmlStatus)) { + throw std::runtime_error("Refusing to replace Trace Compass XML because the target is a directory: " + + traceCompassXml.string()); + } +} + +/** @brief Removes an existing regular output file. */ +static void removeOutputFile(const std::filesystem::path& path) +{ + std::error_code error; + std::filesystem::remove(path, error); + if (error) { + throw std::runtime_error("Failed to remove existing Trace Compass XML " + path.string() + ": " + error.message()); + } +} + +/** @brief Creates a prepared CTF output directory. */ +static void createOutputDirectory(const std::filesystem::path& path) +{ + std::error_code error; + std::filesystem::create_directories(path, error); + if (error) { + throw std::runtime_error("Failed to create CTF output directory " + path.string() + ": " + error.message()); + } +} + +/** @brief Removes all partial artifacts after an aborted bundle. */ +static void removeIncompleteOutputs(const std::filesystem::path& ctfDirectory, + const std::filesystem::path& traceCompassXml) +{ + std::ostringstream errors; + std::error_code ctfError; + std::filesystem::remove_all(ctfDirectory, ctfError); + if (ctfError) { + errors << "CTF directory " << ctfDirectory.string() << ": " << ctfError.message(); + } + + std::error_code xmlError; + std::filesystem::remove(traceCompassXml, xmlError); + if (xmlError) { + if (errors.tellp() > 0) { + errors << "; "; + } + errors << "Trace Compass XML " << traceCompassXml.string() << ": " << xmlError.message(); + } + if (errors.tellp() > 0) { + throw std::runtime_error("Failed to remove incomplete CTF output: " + errors.str()); + } +} + +CtfBundleOutput::CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnostics) + : m_ctfOutputDirectory(std::move(config.outputDirectory)), + m_traceCompassXmlPath(std::move(config.traceCompassXmlPath)), + m_encoder(CtfEncoderConfig{ + config.coreClockHz, + std::move(config.selection), + std::move(config.sources), + diagnostics, + }) +{ + validateOutputTargets(m_ctfOutputDirectory, m_traceCompassXmlPath); +} + +CtfBundleOutput::~CtfBundleOutput() +{ + try { + CtfBundleOutput::abort(); + } catch (...) { + (void)0; + } +} + +std::string_view CtfBundleOutput::backendName() const noexcept +{ + return "ctf"; +} + +std::string CtfBundleOutput::targetPath() const +{ + return m_ctfOutputDirectory.string(); +} + +void CtfBundleOutput::start() +{ + abort(); + validateExistingOutputTypes(m_ctfOutputDirectory, m_traceCompassXmlPath); + removeOutputDirectory(m_ctfOutputDirectory); + removeOutputFile(m_traceCompassXmlPath); + createOutputDirectory(m_ctfOutputDirectory); + m_active = true; + try { + m_encoder.start(m_ctfOutputDirectory); + TraceCompassXmlWriter::writeFile(m_traceCompassXmlPath); + } catch (...) { + abort(); + throw; + } +} + +void CtfBundleOutput::stop() +{ + if (!m_active) { + return; + } + try { + m_encoder.stop(); + m_active = false; + } catch (...) { + abort(); + throw; + } +} + +void CtfBundleOutput::abort() +{ + m_encoder.abort(); + if (m_active) { + removeIncompleteOutputs(m_ctfOutputDirectory, m_traceCompassXmlPath); + m_active = false; + } +} + +void CtfBundleOutput::writeEvent(const TraceEvent& event) +{ + m_encoder.writeEvent(event); +} diff --git a/tools/ctrace/src/output/ctf/CtfBundleOutput.h b/tools/ctrace/src/output/ctf/CtfBundleOutput.h new file mode 100644 index 000000000..e5e058ef9 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfBundleOutput.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFBUNDLEOUTPUT_H +#define CTRACE_SRC_OUTPUT_CTF_CTFBUNDLEOUTPUT_H + +#include "CtfEncoder.h" +#include "TraceEvent.h" +#include "TraceOutput.h" +#include "TraceOutputConfig.h" + +#include + +class DiagnosticSink; + +/** @brief Owns a CTF directory and its companion Trace Compass XML file. */ +class CtfBundleOutput final : public TraceOutput { +public: + /** + * @brief Creates a CTF bundle output from validated configuration. + * @param config CTF paths, clock metadata, and event filters. + * @param diagnostics Optional sink for non-fatal output diagnostics. + */ + explicit CtfBundleOutput(CtfOutputConfig config, DiagnosticSink* diagnostics = nullptr); + /** @brief Aborts an active bundle before destruction. */ + ~CtfBundleOutput() override; + + /** @brief Prepares empty CTF and XML targets. */ + void start() override; + /** @brief Completes metadata, stream, and XML output. */ + void stop() override; + /** @brief Removes incomplete CTF and XML targets. */ + void abort() override; + /** + * @brief Encodes one selected semantic event. + * @param event Event evaluated and encoded by the CTF backend. + */ + void writeEvent(const TraceEvent& event) override; + /** @brief Returns the CTF backend name. */ + std::string_view backendName() const noexcept override; + /** @brief Returns the CTF output directory path. */ + std::string targetPath() const override; + +private: + std::filesystem::path m_ctfOutputDirectory; + std::filesystem::path m_traceCompassXmlPath; + CtfEncoder m_encoder; + bool m_active = false; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFBUNDLEOUTPUT_H diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.cpp b/tools/ctrace/src/output/ctf/CtfEncoder.cpp new file mode 100644 index 000000000..6d570e96f --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfEncoder.cpp @@ -0,0 +1,442 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfEncoder.h" + +#include "CtfExceptionLaneTracker.h" +#include "CtfMetadataWriter.h" +#include "CtfSchema.h" +#include "CtfStreamWriter.h" +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceOutputConfig.h" +#include "TraceSelection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Saturates an internal overflow count to the CTF field width. */ +static std::uint32_t ctfOverflowCount(std::uint64_t count) +{ + return static_cast(std::min(count, std::numeric_limits::max())); +} +/** @brief Resolves the configured CTF value representation for one DWT comparator. */ +static const CtfSchema::ValueVariant& dwtValueVariant(const ResolvedTraceSource* source, std::uint32_t comparator) +{ + static const ResolvedTraceSource defaults; + const auto& resolved = source != nullptr ? *source : defaults; + const auto* variant = CtfSchema::valueVariantForTraceRunType(resolved.valueType, resolved.valueSize); + if (variant == nullptr) { + throw std::runtime_error("CTF DWT value for comparator " + std::to_string(comparator) + + " has invalid ctrace-run data.symbol-type/data.symbol-size metadata"); + } + return *variant; +} + +/** @brief Tests whether two routes describe equivalent CTF source metadata. */ +static bool equivalentSourceMetadata(const ResolvedTraceSource& left, const ResolvedTraceSource& right) +{ + // Trace Bus ID identifies the route, not the metadata attached to it. + return left.type == right.type && left.source == right.source && left.label == right.label && + left.symbolAddress == right.symbolAddress && left.valueType == right.valueType && + left.valueSize == right.valueSize; +} + +/** @brief Finds an unambiguous configured source for one event route. */ +static const ResolvedTraceSource* resolvedTraceSource(const CtfEncoderConfig& config, const char* type, + std::uint8_t traceBusId, std::uint32_t source) +{ + const auto exact = + std::find_if(config.sources.begin(), config.sources.end(), [&](const ResolvedTraceSource& candidate) { + return candidate.type == type && candidate.source == source && candidate.traceBusId == traceBusId; + }); + if (exact != config.sources.end() || traceBusId != 0U) { + return exact == config.sources.end() ? nullptr : &*exact; + } + + const ResolvedTraceSource* unique = nullptr; + for (const auto& candidate : config.sources) { + if (candidate.type != type || candidate.source != source) { + continue; + } + if (unique != nullptr && !equivalentSourceMetadata(*unique, candidate)) { + throw std::runtime_error("CTF cannot resolve conflicting metadata for unformatted " + std::string(type) + + " source " + std::to_string(source)); + } + if (unique == nullptr) { + unique = &candidate; + } + } + return unique; +} + +/** @brief Sign-extends a sample from its configured source width. */ +static std::uint32_t signExtendSample(std::uint32_t value, std::uint8_t sourceBytes) +{ + if (sourceBytes >= 4U) { + return value; + } + const auto bits = sourceBytes * 8U; + const auto signBit = 1U << (bits - 1U); + const auto mask = (signBit << 1U) - 1U; + const auto v = value & mask; + return (v ^ signBit) - signBit; +} + +/** @brief Writes one sample using its selected CTF variant encoding. */ +static void writeVariantValue(CtfStreamWriter::Record& record, std::uint32_t data, std::uint8_t sourceSize, + const CtfSchema::ValueVariant& info) +{ + if (info.floatingPoint) { + record.writeU32(data); + return; + } + const auto value = info.signedInteger ? signExtendSample(data, sourceSize) : data; + if (info.byteSize == 1U) { + record.writeU8(static_cast(value & 0xffU)); + } else if (info.byteSize == 2U) { + record.writeU16(static_cast(value & 0xffffU)); + } else { + record.writeU32(value); + } +} + +CtfEncoder::CtfEncoder(CtfEncoderConfig config) + : m_config(std::move(config)) +{ + if (m_config.coreClockHz == 0U) { + throw std::invalid_argument("CTF output requires a non-zero timestamps.clock"); + } +} + +CtfEncoder::~CtfEncoder() +{ + abort(); +} + +void CtfEncoder::start(const std::filesystem::path& outputDirectory) +{ + abort(); + m_outputDirectory = outputDirectory; + try { + m_streamStates.clear(); + m_reportedDwtSizeMismatches.clear(); + m_exceptionLanes.clear(); + m_stream.open(m_outputDirectory / "stream_0", CtfSchema::SwoStreamId); + m_recording = true; + auto initialTraceBusIds = + std::set(m_config.selection.streams.begin(), m_config.selection.streams.end()); + if (initialTraceBusIds.empty()) { + initialTraceBusIds.insert(0U); + } + for (const auto traceBusId : initialTraceBusIds) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), traceBusId, + m_config.selection.types.empty()); + (void)exceptionLane(traceBusId); + } + } catch (...) { + abort(); + throw; + } +} + +void CtfEncoder::stop() +{ + if (!m_recording) { + return; + } + m_recording = false; + m_stream.close(); + writeMetadataFile(); +} + +void CtfEncoder::abort() noexcept +{ + m_recording = false; + m_stream.abort(); + m_outputDirectory.clear(); +} + +void CtfEncoder::writeEvent(const TraceEvent& event) +{ + if (!m_recording) { + return; + } + if (!m_config.selection.includesStream(event.traceBusId)) { + return; + } + if (!isTraceEvent(event) && event.tcyc.has_value()) { + auto& eventTimestamp = m_streamStates[event.traceBusId].eventTimestamp; + eventTimestamp = std::max(eventTimestamp, *event.tcyc); + if (event.quality.has_value() && event.quality->timestampReliable) { + m_streamStates[event.traceBusId].localTimestampObserved = true; + } + } + + const auto selected = traceEventSelectedForOutput(event, m_config.selection); + if (const auto* software = traceEventPayload(event)) { + if (selected) { + writeSoftwareEvent(event, *software); + } + } else if (const auto* exception = traceEventPayload(event)) { + if (exception->action != ExceptionAction::Unknown) { + writeExceptionEvent(event.traceBusId, *exception); + } + } else if (const auto* data = traceEventPayload(event)) { + if (selected) { + writeDwtValueEvent(event, *data); + } + } else if (const auto* address = traceEventPayload(event)) { + if (selected) { + writeDwtAddrEvent(event, *address); + } + } else if (isTraceEvent(event)) { + auto& streamState = m_streamStates[event.traceBusId]; + if (event.quality.has_value()) { + streamState.overflowCount = std::max(streamState.overflowCount, event.quality->overflowCount); + } else { + ++streamState.overflowCount; + } + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), event.traceBusId, selected); + } else if (isTraceEvent(event)) { + m_streamStates[event.traceBusId].localTimestampObserved = true; + } else if (isTraceEvent(event)) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::Resync), event.traceBusId, + m_config.selection.types.empty()); + } else if (const auto* timestamp = traceEventPayload(event)) { + if (selected) { + writeGlobalTimestampEvent(event, *timestamp); + } + } else if (const auto* issue = traceEventPayload(event)) { + if (issue->code == TraceIssueCode::DataLoss) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); + } else { + if (event.quality.has_value() && event.quality->overflow) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), event.traceBusId, selected); + } + if (selected) { + writeTraceStatusEvent(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), event.traceBusId, true); + } + } + } +} + +std::uint64_t CtfEncoder::allocateEventTimestamp(std::uint8_t traceBusId) +{ + // CtfStreamWriter applies the final monotonic clamp across the multiplexed + // CTF stream. This value remains local to the CoreSight Trace Bus ID. + return m_streamStates[traceBusId].eventTimestamp; +} + +void CtfEncoder::writeSoftwareEvent(const TraceEvent& event, const SoftwareTraceEvent& software) +{ + const auto* variant = CtfSchema::valueVariantForTraceRunType("unsigned int", software.size); + if (variant == nullptr) { + throw std::runtime_error("CTF ITM value has an invalid SWO payload size"); + } + const auto quality = computeSampleQuality(event); + const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto payloadSize = 1U + 1U + variant->byteSize + 1U + 4U; + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::Itm), eventTimestamp, event.traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(software.channel & 0xffU)); + record.writeU8(CtfSchema::value(variant->tag)); + writeVariantValue(record, software.value, software.size, *variant); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); +} + +void CtfEncoder::writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data) +{ + const auto* source = resolvedTraceSource(m_config, "dwt", event.traceBusId, data.comparator); + reportDwtSizeMismatch(event, data, source); + const auto& variant = dwtValueVariant(source, data.comparator); + const auto hasPc = data.pc.has_value() ? 1U : 0U; + const auto hasAddress = data.addressLo16.has_value() ? 1U : 0U; + const auto payloadSize = 1U + 1U + 1U + variant.byteSize + 1U + hasPc * 4U + 1U + hasAddress * 2U + 1U + 4U; + const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto quality = computeSampleQuality(event); + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtValue), eventTimestamp, event.traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(data.comparator & 0xffU)); + record.writeU8(CtfSchema::value(data.access == AccessType::Read + ? CtfSchema::DwtAccess::Read + : CtfSchema::DwtAccess::Write)); + record.writeU8(CtfSchema::value(variant.tag)); + writeVariantValue(record, data.value, data.size, variant); + record.writeU8(static_cast(hasPc)); + if (hasPc != 0U) { + record.writeU32(*data.pc); + } + record.writeU8(static_cast(hasAddress)); + if (hasAddress != 0U) { + record.writeU16(static_cast(*data.addressLo16 & 0xffffU)); + } + record.writeU8(quality.first); + record.writeU32(quality.second); + }); +} + +void CtfEncoder::reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, + const ResolvedTraceSource* source) +{ + const auto configuredSize = source != nullptr ? source->valueSize : ResolvedTraceSource{}.valueSize; + if (configuredSize == data.size || m_config.diagnostics == nullptr || + !m_reportedDwtSizeMismatches.insert({event.traceBusId, data.comparator}).second) { + return; + } + + std::vector> context{ + {"backend", "ctf"}, + {"channel", "DWT" + std::to_string(data.comparator)}, + {"configuredSize", std::to_string(configuredSize)}, + {"swoSize", std::to_string(data.size)}, + }; + context.emplace_back("stream", std::to_string(event.traceBusId)); + m_config.diagnostics->report({ + DiagnosticSink::Severity::Warning, + "configured ctrace-run data.symbol-size does not match the decoded SWO payload size", + std::move(context), + }); +} + +void CtfEncoder::writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTraceEvent& address) +{ + constexpr auto payloadSize = 1U + 1U + 1U + 4U + 2U + 1U + 4U; + const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + const auto quality = computeSampleQuality(event); + const auto pc = dwtAddressPc(address); + const auto addressOffset = dwtAddressOffset(address); + const auto hasPc = pc.has_value() ? 1U : 0U; + const auto hasAddress = addressOffset.has_value() ? 1U : 0U; + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::DwtAddress), eventTimestamp, event.traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(static_cast(address.comparator & 0xffU)); + record.writeU8(static_cast(hasPc)); + record.writeU8(static_cast(hasAddress)); + record.writeU32(pc.value_or(0U)); + record.writeU16(static_cast(addressOffset.value_or(0U) & 0xffffU)); + record.writeU8(quality.first); + record.writeU32(quality.second); + }); +} + +void CtfEncoder::writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp) +{ + constexpr auto payloadSize = 8U + 1U; + const auto eventTimestamp = allocateEventTimestamp(event.traceBusId); + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), eventTimestamp, event.traceBusId, + payloadSize, [&](CtfStreamWriter::Record& record) { + record.writeU64(timestamp.value); + record.writeU8(timestamp.clockChange ? 1U : 0U); + }); +} + +void CtfEncoder::writeTraceStatusEvent(std::uint8_t reason, std::uint8_t traceBusId, bool emitEvent) +{ + if (emitEvent) { + constexpr auto payloadSize = 1U + 4U; + const auto eventTimestamp = allocateEventTimestamp(traceBusId); + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::TraceStatus), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU8(reason); + record.writeU32(ctfOverflowCount(m_streamStates[traceBusId].overflowCount)); + }); + } + + if (reason == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow) || + reason == CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss)) { + const auto lane = m_exceptionLanes.find(traceBusId); + if (lane != m_exceptionLanes.end()) { + lane->second.resetForDiscontinuity( + [this, traceBusId](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + emitExceptionRecord(traceBusId, number, action); + }); + } + } +} + +void CtfEncoder::writeExceptionEvent(std::uint8_t traceBusId, const ExceptionTraceEvent& exception) +{ + exceptionLane(traceBusId) + .consume(exception, [this, traceBusId](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + emitExceptionRecord(traceBusId, number, action); + }); +} + +void CtfEncoder::emitExceptionRecord(std::uint8_t traceBusId, std::uint32_t number, + CtfExceptionLaneTracker::RecordAction action) +{ + TraceEvent selectionEvent{ExceptionTraceEvent{ + number, + action == CtfExceptionLaneTracker::RecordAction::Enter ? ExceptionAction::Entered : ExceptionAction::Exited, + }}; + selectionEvent.traceBusId = traceBusId; + if (!traceEventSelectedForOutput(selectionEvent, m_config.selection)) { + return; + } + constexpr auto payloadSize = 2U + 1U + 2U; + const auto eventTimestamp = allocateEventTimestamp(traceBusId); + const auto encodedAction = + CtfSchema::value(action == CtfExceptionLaneTracker::RecordAction::Enter ? CtfSchema::ExceptionAction::Entered + : CtfSchema::ExceptionAction::Exited); + m_stream.writeRecord(CtfSchema::value(CtfSchema::EventId::Exception), eventTimestamp, traceBusId, payloadSize, + [&](CtfStreamWriter::Record& record) { + record.writeU16(static_cast(number & 0xffffU)); + record.writeU8(encodedAction); + record.writeU16(static_cast(number & 0xffffU)); + }); +} + +CtfExceptionLaneTracker& CtfEncoder::exceptionLane(std::uint8_t traceBusId) +{ + const auto [lane, inserted] = m_exceptionLanes.try_emplace(traceBusId); + if (inserted) { + lane->second.startThreadMode( + [this, traceBusId](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + emitExceptionRecord(traceBusId, number, action); + }); + } + return lane->second; +} + +std::pair CtfEncoder::computeSampleQuality(const TraceEvent& event) +{ + auto& streamState = m_streamStates[event.traceBusId]; + const auto previousOverflowCount = streamState.overflowCount; + const auto overflowCount = event.quality.has_value() ? event.quality->overflowCount : streamState.overflowCount; + const auto timestampReliable = event.quality.has_value() ? event.quality->timestampReliable : true; + const auto overflow = event.quality.has_value() ? event.quality->overflow : overflowCount > previousOverflowCount; + const auto flags = + static_cast((overflow ? CtfSchema::SampleFlagOverflow : 0U) | + (timestampReliable ? CtfSchema::SampleFlagTimestampReliable : 0U) | + (streamState.localTimestampObserved ? 0U : CtfSchema::SampleFlagBeforeFirstTimestamp)); + streamState.overflowCount = std::max(streamState.overflowCount, overflowCount); + return {flags, ctfOverflowCount(overflowCount)}; +} + +void CtfEncoder::writeMetadataFile() +{ + std::set observedExceptionNumbers; + for (const auto& [traceBusId, lane] : m_exceptionLanes) { + (void)traceBusId; + observedExceptionNumbers.insert(lane.observedExceptionNumbers().begin(), lane.observedExceptionNumbers().end()); + } + CtfMetadataWriter::write(m_outputDirectory, m_stream.uuidString(), m_config.coreClockHz, m_config.sources, + { + observedExceptionNumbers.begin(), + observedExceptionNumbers.end(), + }); +} diff --git a/tools/ctrace/src/output/ctf/CtfEncoder.h b/tools/ctrace/src/output/ctf/CtfEncoder.h new file mode 100644 index 000000000..d7659eb39 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfEncoder.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFENCODER_H +#define CTRACE_SRC_OUTPUT_CTF_CTFENCODER_H + +#include "CtfExceptionLaneTracker.h" +#include "CtfStreamWriter.h" +#include "TraceSelection.h" +#include "TraceEvent.h" +#include "TraceOutputConfig.h" + +#include +#include +#include +#include +#include +#include + +class DiagnosticSink; + +/** @brief Stores the clock, selection, sources, and diagnostics for CTF encoding. */ +struct CtfEncoderConfig { + std::uint64_t coreClockHz = 0; + TraceSelection selection; + std::vector sources; + DiagnosticSink* diagnostics = nullptr; +}; + +/** @brief Encodes semantic trace events into one CTF stream and metadata set. */ +class CtfEncoder final { +public: + /** @brief Creates an encoder from validated CTF configuration. */ + explicit CtfEncoder(CtfEncoderConfig config); + /** @brief Aborts active output before destruction. */ + ~CtfEncoder(); + + /** @brief Disables copying because the encoder owns stream state. */ + CtfEncoder(const CtfEncoder&) = delete; + /** @brief Disables copy assignment because the encoder owns stream state. */ + CtfEncoder& operator=(const CtfEncoder&) = delete; + + /** @brief Starts writing into a prepared CTF directory. */ + void start(const std::filesystem::path& outputDirectory); + /** @brief Completes stream data and writes final metadata. */ + void stop(); + /** @brief Aborts stream output without throwing. */ + void abort() noexcept; + /** @brief Encodes one selected semantic event. */ + void writeEvent(const TraceEvent& event); + +private: + /** @brief Tracks timestamp and trace-quality state for one output stream. */ + struct StreamState { + std::uint64_t eventTimestamp = 0; + std::uint64_t overflowCount = 0; + bool localTimestampObserved = false; + }; + + /** @brief Allocates the next monotonic event timestamp for one stream. */ + std::uint64_t allocateEventTimestamp(std::uint8_t traceBusId); + /** @brief Writes metadata that matches the completed binary stream. */ + void writeMetadataFile(); + /** @brief Emits or applies a trace-status transition. */ + void writeTraceStatusEvent(std::uint8_t reason, std::uint8_t traceBusId, bool emitEvent = true); + /** @brief Encodes one ITM software event. */ + void writeSoftwareEvent(const TraceEvent& event, const SoftwareTraceEvent& software); + /** @brief Encodes one DWT data value event. */ + void writeDwtValueEvent(const TraceEvent& event, const DwtDataTraceEvent& data); + /** @brief Reports configured and decoded DWT width mismatches once per route. */ + void reportDwtSizeMismatch(const TraceEvent& event, const DwtDataTraceEvent& data, const ResolvedTraceSource* source); + /** @brief Encodes one DWT address event. */ + void writeDwtAddrEvent(const TraceEvent& event, const DwtAddressTraceEvent& address); + /** @brief Encodes one reconstructed global timestamp event. */ + void writeGlobalTimestampEvent(const TraceEvent& event, const GlobalTimestampTraceEvent& timestamp); + /** @brief Applies one exception transition to its CTF lane state. */ + void writeExceptionEvent(std::uint8_t traceBusId, const ExceptionTraceEvent& exception); + /** @brief Emits one concrete exception lane record. */ + void emitExceptionRecord(std::uint8_t traceBusId, std::uint32_t number, CtfExceptionLaneTracker::RecordAction action); + /** @brief Returns the exception tracker for one stream. */ + CtfExceptionLaneTracker& exceptionLane(std::uint8_t traceBusId); + /** @brief Computes CTF sample flags and saturated overflow count. */ + std::pair computeSampleQuality(const TraceEvent& event); + + CtfEncoderConfig m_config; + std::filesystem::path m_outputDirectory; + CtfStreamWriter m_stream; + bool m_recording = false; + std::map m_streamStates; + std::set> m_reportedDwtSizeMismatches; + std::map m_exceptionLanes; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFENCODER_H diff --git a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp new file mode 100644 index 000000000..4b9ee03ef --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfExceptionLaneTracker.h" + +#include "TraceEvent.h" + +#include +#include +#include + +void CtfExceptionLaneTracker::startThreadMode(const RecordEmitter& emit) +{ + setActiveContext(kThreadModeNumber, emit); +} + +void CtfExceptionLaneTracker::resetForDiscontinuity(const RecordEmitter& emit) +{ + m_contextStack.clear(); + closeActiveContext(emit); +} + +void CtfExceptionLaneTracker::consume(const ExceptionTraceEvent& event, const RecordEmitter& emit) +{ + const auto number = event.number & 0x1ffU; + switch (event.action) { + case ExceptionAction::Entered: + enterContext(number); + break; + case ExceptionAction::Exited: + exitContext(number); + break; + case ExceptionAction::Returned: + returnToContext(number); + break; + case ExceptionAction::Unknown: + return; + } + updateActiveContext(emit); +} + +const std::vector& CtfExceptionLaneTracker::observedExceptionNumbers() const +{ + return m_observedExceptionNumbers; +} + +void CtfExceptionLaneTracker::setActiveContext(std::uint32_t number, const RecordEmitter& emit) +{ + if (m_activeContextNumber.has_value() && *m_activeContextNumber == number) { + return; + } + if (m_activeContextNumber.has_value()) { + emitRecord(*m_activeContextNumber, RecordAction::Exit, emit); + } + emitRecord(number, RecordAction::Enter, emit); + m_activeContextNumber = number; +} + +void CtfExceptionLaneTracker::closeActiveContext(const RecordEmitter& emit) +{ + if (!m_activeContextNumber.has_value()) { + return; + } + emitRecord(*m_activeContextNumber, RecordAction::Exit, emit); + m_activeContextNumber.reset(); +} + +void CtfExceptionLaneTracker::updateActiveContext(const RecordEmitter& emit) +{ + setActiveContext(m_contextStack.empty() ? kThreadModeNumber : m_contextStack.back().number, emit); +} + +void CtfExceptionLaneTracker::emitRecord(std::uint32_t number, RecordAction action, const RecordEmitter& emit) +{ + if (std::find(m_observedExceptionNumbers.begin(), m_observedExceptionNumbers.end(), number) == + m_observedExceptionNumbers.end()) { + m_observedExceptionNumbers.push_back(number); + } + emit(number, action); +} + +void CtfExceptionLaneTracker::enterContext(std::uint32_t number) +{ + if (!m_contextStack.empty() && m_contextStack.back().state == ContextState::Running) { + m_contextStack.back().state = ContextState::Preempted; + } + m_contextStack.push_back({number, ContextState::Running}); +} + +void CtfExceptionLaneTracker::exitContext(std::uint32_t number) +{ + if (m_contextStack.empty() || m_contextStack.back().number != number || + m_contextStack.back().state != ContextState::Running) { + return; + } + m_contextStack.pop_back(); +} + +void CtfExceptionLaneTracker::returnToContext(std::uint32_t number) +{ + while (!m_contextStack.empty() && m_contextStack.back().number != number) { + m_contextStack.pop_back(); + } + if (!m_contextStack.empty()) { + m_contextStack.back().state = ContextState::Running; + } else if (number != kThreadModeNumber) { + m_contextStack.push_back({number, ContextState::Running}); + } +} diff --git a/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h new file mode 100644 index 000000000..67aa813d6 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfExceptionLaneTracker.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFEXCEPTIONLANETRACKER_H +#define CTRACE_SRC_OUTPUT_CTF_CTFEXCEPTIONLANETRACKER_H + +#include "TraceEvent.h" + +#include +#include +#include +#include + +/** @brief Tracks active Cortex-M exception contexts for Trace Compass lanes. */ +class CtfExceptionLaneTracker { +public: + /** @brief Selects whether an emitted lane record enters or exits a context. */ + enum class RecordAction { + Enter, + Exit, + }; + + /** @brief Emits one exception-lane record. */ + using RecordEmitter = std::function; + + /** @brief Starts the initial thread-mode context. */ + void startThreadMode(const RecordEmitter& emit); + /** @brief Closes the active lane and clears state at a discontinuity. */ + void resetForDiscontinuity(const RecordEmitter& emit); + /** @brief Applies one exception transition and emits resulting lane records. */ + void consume(const ExceptionTraceEvent& event, const RecordEmitter& emit); + /** @brief Returns exception numbers observed by this tracker. */ + const std::vector& observedExceptionNumbers() const; + +private: + static constexpr std::uint32_t kThreadModeNumber = 0; + + /** @brief Identifies whether an exception context is active or preempted. */ + enum class ContextState : std::uint8_t { + Running, + Preempted, + }; + + /** @brief Stores one exception context on the nesting stack. */ + struct ContextFrame { + std::uint32_t number = 0; + ContextState state = ContextState::Running; + }; + + /** @brief Switches the emitted active lane to one context. */ + void setActiveContext(std::uint32_t number, const RecordEmitter& emit); + /** @brief Closes the currently emitted active lane. */ + void closeActiveContext(const RecordEmitter& emit); + /** @brief Reconciles emitted lane state with the context stack. */ + void updateActiveContext(const RecordEmitter& emit); + /** @brief Emits and records one lane transition. */ + void emitRecord(std::uint32_t number, RecordAction action, const RecordEmitter& emit); + /** @brief Pushes or reactivates an entered exception context. */ + void enterContext(std::uint32_t number); + /** @brief Removes an exited exception context. */ + void exitContext(std::uint32_t number); + /** @brief Returns the stack to a previously active context. */ + void returnToContext(std::uint32_t number); + + std::vector m_contextStack; + std::optional m_activeContextNumber; + std::vector m_observedExceptionNumbers; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFEXCEPTIONLANETRACKER_H diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp new file mode 100644 index 000000000..a869ccc29 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.cpp @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfMetadataWriter.h" + +#include "CtfSchema.h" +#include "TraceOutputConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Escapes one string for use in TSDL metadata. */ +static std::string tsdlString(const std::string& value) +{ + static constexpr char hexDigits[] = "0123456789ABCDEF"; + std::string out = "\""; + for (const auto raw : value) { + const auto ch = static_cast(raw); + if (ch == '\\' || ch == '"') { + out.push_back('\\'); + out.push_back(static_cast(ch)); + } else if (ch == '\n') { + out += "\\n"; + } else if (ch == '\r') { + out += "\\r"; + } else if (ch == '\t') { + out += "\\t"; + } else if (ch < 0x20U || ch == 0x7fU) { + out += "\\x"; + out.push_back(hexDigits[(ch >> 4U) & 0x0fU]); + out.push_back(hexDigits[ch & 0x0fU]); + } else { + out.push_back(static_cast(ch)); + } + } + out.push_back('"'); + return out; +} + +/** @brief Formats an integer as an unsigned hexadecimal TSDL value. */ +static std::string hexValue(std::uint64_t value) +{ + std::ostringstream out; + out << std::hex << std::uppercase << value; + return out.str(); +} + +/** @brief Allocates a stable unique label for one metadata enumeration value. */ +static std::string uniqueEnumLabel(const std::string& preferred, const std::string& fallback, + std::set& used) +{ + auto candidate = preferred.empty() ? fallback : preferred; + if (used.insert(candidate).second) { + return candidate; + } + if (used.insert(fallback).second) { + return fallback; + } + for (std::uint32_t suffix = 1;; ++suffix) { + const auto label = fallback + "_" + std::to_string(suffix); + if (used.insert(label).second) { + return label; + } + } +} + +/** @brief Returns a mapped metadata label or an empty string. */ +static std::string mapValueOrEmpty(const std::map& values, std::uint32_t key) +{ + const auto found = values.find(key); + return found == values.end() ? "" : found->second; +} + +static std::vector +exceptionNumbersWithDefaults(const std::vector& observedExceptionNumbers) +{ + std::vector exceptions = {0, 1, 2, 3, 4, 5, 6, 7, 11, 12, 14, 15}; + for (const auto number : observedExceptionNumbers) { + if (std::find(exceptions.begin(), exceptions.end(), number) == exceptions.end()) { + exceptions.push_back(number); + } + } + std::sort(exceptions.begin(), exceptions.end()); + return exceptions; +} + +/** @brief Returns the architectural name for a known Cortex-M exception. */ +static std::string exceptionName(std::uint32_t number) +{ + switch (number) { + case 0: + return "ThreadMode"; + case 1: + return "Reset"; + case 2: + return "NMI"; + case 3: + return "HardFault"; + case 4: + return "MemManage"; + case 5: + return "BusFault"; + case 6: + return "UsageFault"; + case 7: + return "SecureFault"; + case 11: + return "SVCall"; + case 12: + return "DebugMonitor"; + case 14: + return "PendSV"; + case 15: + return "SysTick"; + default: + if (number >= 16U) { + return "External IRQ " + std::to_string(number - 16U); + } + return "Reserved " + std::to_string(number); + } +} + +/** @brief Returns the TSDL scalar type used by one value variant. */ +static std::string_view tsdlValueType(const CtfSchema::ValueVariant& variant) +{ + if (variant.floatingPoint) { + return "ieee_float32_t"; + } + if (variant.signedInteger) { + return variant.byteSize == 1U ? "int8_t" : variant.byteSize == 2U ? "int16_t" : "int32_t"; + } + return variant.byteSize == 1U ? "uint8_t" : variant.byteSize == 2U ? "uint16_t" : "uint32_t"; +} + +/** @brief Generates the variant-specific TSDL fields for sample values. */ +static std::string ctfValueFields(const std::string_view& prefix) +{ + std::ostringstream out; + out << " enum : uint8_t { "; + for (std::size_t index = 0; index < CtfSchema::ValueVariants.size(); ++index) { + const auto& variant = CtfSchema::ValueVariants[index]; + if (index > 0U) { + out << ", "; + } + out << variant.name << " = " << static_cast(CtfSchema::value(variant.tag)); + } + out << " } m_cmsis" << prefix << "_value_type;\n" + << " variant {\n"; + for (const auto& variant : CtfSchema::ValueVariants) { + out << " " << tsdlValueType(variant) << " " << variant.name << ";\n"; + } + out << " } m_cmsis" << prefix << "_value;\n"; + return out.str(); +} + +/** @brief Stores source labels and exception lanes emitted into CTF metadata. */ +struct MetadataSymbols { + std::map dwtValueTypes; + std::map itmNames; + std::map dwtNames; + std::map dwtAddressStarts; + std::map dwtAddressEnds; +}; + +/** @brief Collects deduplicated source and exception symbols for metadata. */ +static MetadataSymbols collectMetadataSymbols(const std::vector& sources) +{ + MetadataSymbols symbols; + for (const auto& source : sources) { + const auto id = source.source; + if (source.type == "itm") { + if (source.label.has_value()) { + symbols.itmNames[id] = *source.label; + } + continue; + } + if (source.type != "dwt") { + continue; + } + symbols.dwtValueTypes[id] = source.valueType; + if (source.label.has_value()) { + symbols.dwtNames[id] = *source.label; + } + if (source.symbolAddress.has_value()) { + symbols.dwtAddressStarts[id] = *source.symbolAddress; + const auto extent = static_cast(source.valueSize - 1U); + if (*source.symbolAddress <= std::numeric_limits::max() - extent) { + symbols.dwtAddressEnds[id] = *source.symbolAddress + extent; + } + } + } + return symbols; +} + +/** @brief Writes the TSDL trace, environment, and clock declarations. */ +static void writeTraceEnvironment(std::ostream& out, const std::string& uuidString, std::uint64_t coreClockHz, + const MetadataSymbols& symbols) +{ + out << R"(/* CTF 1.8 */ +trace { + major = 1; + minor = 8; + uuid = ")" + << uuidString << R"("; + byte_order = le; + packet.header := struct { + integer { size = 32; align = 8; signed = false; } magic; + integer { size = 8; align = 8; signed = false; } uuid[16]; + integer { size = 32; align = 8; signed = false; } stream_id; + }; +}; + +env { + cmsis_ctf_profile = "cmsis.ctf"; + cmsis_ctf_profile_version = 1; +)"; + for (const auto& entry : symbols.dwtValueTypes) { + out << " cmsis_dwt" << entry.first << "_value_type = " << tsdlString(entry.second) << ";\n"; + } + for (const auto& entry : symbols.dwtAddressStarts) { + out << " cmsis_dwt" << entry.first << "_address_start = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; + } + for (const auto& entry : symbols.dwtAddressEnds) { + out << " cmsis_dwt" << entry.first << "_address_end = " << tsdlString("0x" + hexValue(entry.second)) << ";\n"; + } + out << R"(}; + +clock { + name = swo_clock; + precision = 0; + offset_s = 0; + offset = 0; + absolute = false; + freq = )" + << coreClockHz << R"(; +}; +)"; +} + +/** @brief Writes reusable TSDL type and enumeration declarations. */ +static void writeTypeDefinitions(std::ostream& out, const MetadataSymbols& symbols, + const std::vector& observedExceptionNumbers) +{ + out << R"( +typealias integer { size = 8; align = 8; signed = false; } := uint8_t; +typealias integer { size = 16; align = 8; signed = false; byte_order = le; } := uint16_t; +typealias integer { size = 32; align = 8; signed = false; byte_order = le; } := uint32_t; +typealias integer { size = 8; align = 8; signed = true; } := int8_t; +typealias integer { size = 16; align = 8; signed = true; byte_order = le; } := int16_t; +typealias integer { size = 32; align = 8; signed = true; byte_order = le; } := int32_t; +typealias floating_point { exp_dig = 8; mant_dig = 24; align = 8; byte_order = le; } := ieee_float32_t; +typealias integer { size = 64; align = 8; signed = false; byte_order = le; } := uint64_t; +typealias integer { size = 64; align = 8; signed = false; map = clock.swo_clock.value; } := swo_clock_t; + +typealias enum : uint8_t { + "read" = )" + << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Read)) << R"(, + "write" = )" + << static_cast(CtfSchema::value(CtfSchema::DwtAccess::Write)) << R"( +} := cmsis_dwt_access_t; +typealias enum : uint8_t { + "trace_start" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart)) << R"(, + "resync" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::Resync)) << R"(, + "overflow" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::Overflow)) << R"(, + "decode_error" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)) << R"(, + "data_loss" = )" + << static_cast(CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss)) << R"( +} := cmsis_trace_status_reason_t; +typealias enum : uint8_t { + "entered" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Entered)) << R"(, + "exited" = )" + << static_cast(CtfSchema::value(CtfSchema::ExceptionAction::Exited)) << R"( +} := cmsis_exception_action_t; +typealias enum : uint8_t { +)"; + std::set itmLabels; + for (std::uint32_t channel = 1U; channel < 32U; ++channel) { + const auto fallback = "ITM" + std::to_string(channel); + out << " " << tsdlString(uniqueEnumLabel(mapValueOrEmpty(symbols.itmNames, channel), fallback, itmLabels)) + << " = " << channel << ",\n"; + } + out << R"(} := cmsis_itm_channel_t; +typealias enum : uint8_t { +)"; + std::set dwtLabels; + for (std::uint32_t comparator = 0U; comparator < 4U; ++comparator) { + const auto label = mapValueOrEmpty(symbols.dwtNames, comparator); + const auto fallback = "DWT" + std::to_string(comparator); + out << " " << tsdlString(uniqueEnumLabel(label, fallback, dwtLabels)) << " = " << comparator << ",\n"; + } + out << R"( +} := cmsis_dwt_comparator_t; +typealias enum : uint16_t { +)"; + for (const auto number : exceptionNumbersWithDefaults(observedExceptionNumbers)) { + out << " " << tsdlString(exceptionName(number)) << " = " << number << ",\n"; + } + out << R"(} := cmsis_exception_number_t; +)"; +} + +/** @brief Writes the packet and event context definition for the SWO stream. */ +static void writeStreamDefinition(std::ostream& out) +{ + out << R"( +stream { + id = )" + << CtfSchema::SwoStreamId << R"(; + event.header := struct { + uint32_t id; + swo_clock_t timestamp; + }; + event.context := struct { + uint8_t cmsis_trace_bus_id; + }; + packet.context := struct { + uint32_t packet_size; + uint32_t content_size; + swo_clock_t timestamp_begin; + swo_clock_t timestamp_end; + uint32_t events_discarded; + uint32_t packet_seq_num; + }; +}; +)"; +} + +/** @brief Writes the ITM software event declaration. */ +static void writeItmEvent(std::ostream& out) +{ + out << R"( +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::Itm) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::Itm) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_itm_channel_t cmsis_itm_channel; +)" << ctfValueFields("itm") + << R"( uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; +)"; +} + +/** @brief Writes the DWT value event declaration. */ +static void writeDwtValueEvent(std::ostream& out) +{ + out << R"( +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::DwtValue) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::DwtValue) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + cmsis_dwt_access_t cmsis_dwt_access; +)" << ctfValueFields("dwt") + << R"( uint8_t cmsis_has_pc; + uint32_t cmsis_pc[cmsis_has_pc]; + uint8_t cmsis_has_address_lo16; + uint16_t cmsis_address_lo16[cmsis_has_address_lo16]; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; +)"; +} + +/** @brief Writes the DWT address event declaration. */ +static void writeDwtAddressEvent(std::ostream& out) +{ + out << R"( +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::DwtAddress) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::DwtAddress) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_dwt_comparator_t cmsis_dwt_comparator; + uint8_t cmsis_has_pc; + uint8_t cmsis_has_address_lo16; + uint32_t cmsis_pc; + uint16_t cmsis_address_lo16; + uint8_t cmsis_sample_flags; + uint32_t cmsis_overflow_count; + }; +}; +)"; +} + +/** @brief Writes status, exception, and global timestamp declarations. */ +static void writeStatusEvents(std::ostream& out) +{ + out << R"( +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::TraceStatus) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::TraceStatus) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_trace_status_reason_t cmsis_trace_status_reason; + uint32_t cmsis_overflow_count; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::Exception) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::Exception) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + cmsis_exception_number_t cmsis_exception_number; + cmsis_exception_action_t cmsis_exception_action; + uint16_t cmsis_exception_number_value; + }; +}; + +event { + id = )" + << CtfSchema::value(CtfSchema::EventId::GlobalTimestamp) << R"(; + name = ")" + << CtfSchema::eventName(CtfSchema::EventId::GlobalTimestamp) << R"("; + stream_id = )" + << CtfSchema::SwoStreamId << R"(; + fields := struct { + uint64_t cmsis_global_timestamp; + uint8_t cmsis_clock_change; + }; +}; +)"; +} + +void CtfMetadataWriter::write(const std::filesystem::path& outputDir, const std::string& uuidString, + std::uint64_t coreClockHz, const std::vector& sources, + const std::vector& observedExceptionNumbers) +{ + const auto metadataPath = outputDir / "metadata"; + std::ofstream out(metadataPath, std::ios::out | std::ios::trunc); + if (!out) { + throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); + } + + const auto symbols = collectMetadataSymbols(sources); + writeTraceEnvironment(out, uuidString, coreClockHz, symbols); + writeTypeDefinitions(out, symbols, observedExceptionNumbers); + writeStreamDefinition(out); + writeItmEvent(out); + writeDwtValueEvent(out); + writeDwtAddressEvent(out); + writeStatusEvents(out); + out.close(); + if (!out) { + throw std::runtime_error("Failed to write CTF metadata " + metadataPath.string()); + } +} diff --git a/tools/ctrace/src/output/ctf/CtfMetadataWriter.h b/tools/ctrace/src/output/ctf/CtfMetadataWriter.h new file mode 100644 index 000000000..a6232f1de --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfMetadataWriter.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFMETADATAWRITER_H +#define CTRACE_SRC_OUTPUT_CTF_CTFMETADATAWRITER_H + +#include "TraceOutputConfig.h" + +#include +#include +#include +#include + +/** @brief Writes the CTF metadata description for a completed trace bundle. */ +class CtfMetadataWriter final { +public: + /** @brief Writes metadata for clock, event schemas, sources, and exception lanes. */ + static void write(const std::filesystem::path& outputDir, const std::string& uuidString, std::uint64_t coreClockHz, + const std::vector& sources, + const std::vector& observedExceptionNumbers); + +private: + /** @brief Prevents construction of this stateless metadata utility. */ + CtfMetadataWriter() = delete; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFMETADATAWRITER_H diff --git a/tools/ctrace/src/output/ctf/CtfSchema.h b/tools/ctrace/src/output/ctf/CtfSchema.h new file mode 100644 index 000000000..d5d436fa9 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfSchema.h @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFSCHEMA_H +#define CTRACE_SRC_OUTPUT_CTF_CTFSCHEMA_H + +#include +#include +#include +#include + +namespace CtfSchema { + +inline constexpr std::uint32_t Magic = 0xC1FC1FC1U; +inline constexpr std::uint32_t SwoStreamId = 0U; + +/** @brief Identifies the event records emitted by ctrace CTF output. */ +enum class EventId : std::uint32_t { + Itm = 0U, + DwtValue = 1U, + DwtAddress = 2U, + TraceStatus = 3U, + Exception = 4U, + GlobalTimestamp = 5U, +}; + +/** @brief Classifies CTF trace-status records. */ +enum class TraceStatusReason : std::uint8_t { + TraceStart = 0U, + Resync = 1U, + Overflow = 2U, + DecodeError = 3U, + DataLoss = 4U, +}; + +/** @brief Encodes DWT read and write access in CTF records. */ +enum class DwtAccess : std::uint8_t { + Read = 0U, + Write = 1U, +}; + +/** @brief Encodes exception entry and exit actions in CTF records. */ +enum class ExceptionAction : std::uint8_t { + Entered = 0U, + Exited = 1U, +}; + +/** @brief Identifies the supported CTF sample value encodings. */ +enum class ValueTag : std::uint8_t { + Signed8 = 0U, + Unsigned8 = 1U, + Signed16 = 2U, + Unsigned16 = 3U, + Signed32 = 4U, + Unsigned32 = 5U, + Float32 = 6U, +}; + +/** @brief Describes one supported CTF sample value encoding. */ +struct ValueVariant { + ValueTag tag; + std::string_view name; + std::string_view traceCompassType; + std::uint8_t byteSize; + bool floatingPoint; + bool signedInteger; +}; + +inline constexpr std::array ValueVariants{{ + {ValueTag::Signed8, "i8", "long", 1U, false, true}, + {ValueTag::Unsigned8, "u8", "long", 1U, false, false}, + {ValueTag::Signed16, "i16", "long", 2U, false, true}, + {ValueTag::Unsigned16, "u16", "long", 2U, false, false}, + {ValueTag::Signed32, "i32", "long", 4U, false, true}, + {ValueTag::Unsigned32, "u32", "long", 4U, false, false}, + {ValueTag::Float32, "f32", "double", 4U, true, false}, +}}; + +inline constexpr std::string_view ValueTypeRequirements = + "supported data.symbol-type values are 'unsigned int', 'signed int', and 'float'; " + "data.symbol-size must be 1, 2, or 4, and float requires size 4"; + +/** @brief Returns the schema descriptor for a value tag. */ +constexpr const ValueVariant& valueVariant(ValueTag tag) +{ + return ValueVariants[static_cast(tag)]; +} + +/** @brief Resolves trace-run type metadata to a supported CTF value encoding. */ +constexpr const ValueVariant* valueVariantForTraceRunType(const std::string_view& typeName, std::uint64_t byteSize) +{ + if (typeName == "float") { + return byteSize == 4U ? &valueVariant(ValueTag::Float32) : nullptr; + } + + const auto signedInteger = typeName == "signed int"; + if (!signedInteger && typeName != "unsigned int") { + return nullptr; + } + if (byteSize == 1U) { + return &valueVariant(signedInteger ? ValueTag::Signed8 : ValueTag::Unsigned8); + } + if (byteSize == 2U) { + return &valueVariant(signedInteger ? ValueTag::Signed16 : ValueTag::Unsigned16); + } + if (byteSize == 4U) { + return &valueVariant(signedInteger ? ValueTag::Signed32 : ValueTag::Unsigned32); + } + return nullptr; +} + +inline constexpr std::uint8_t SampleFlagOverflow = 1U << 0U; +inline constexpr std::uint8_t SampleFlagTimestampReliable = 1U << 1U; +inline constexpr std::uint8_t SampleFlagBeforeFirstTimestamp = 1U << 2U; + +/** @brief Returns the integer representation of an event ID. */ +constexpr std::uint32_t value(EventId id) +{ + return static_cast(id); +} + +/** @brief Returns the integer representation of a trace-status reason. */ +constexpr std::uint8_t value(TraceStatusReason reason) +{ + return static_cast(reason); +} + +/** @brief Returns the integer representation of a value tag. */ +constexpr std::uint8_t value(ValueTag tag) +{ + return static_cast(tag); +} + +/** @brief Returns the integer representation of a DWT access type. */ +constexpr std::uint8_t value(DwtAccess access) +{ + return static_cast(access); +} + +/** @brief Returns the integer representation of an exception action. */ +constexpr std::uint8_t value(ExceptionAction action) +{ + return static_cast(action); +} + +/** @brief Returns the stable schema name of a CTF event ID. */ +constexpr std::string_view eventName(EventId id) +{ + switch (id) { + case EventId::Itm: + return "ITM"; + case EventId::DwtValue: + return "DWT_VALUE"; + case EventId::TraceStatus: + return "TRACE_STATUS"; + case EventId::DwtAddress: + return "DWT_ADDR"; + case EventId::Exception: + return "EXCEPTION"; + case EventId::GlobalTimestamp: + return "GLOBAL_TIMESTAMP"; + } + return "UNKNOWN"; +} + +} // namespace CtfSchema + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFSCHEMA_H diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp new file mode 100644 index 000000000..adc26a6c7 --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfStreamWriter.h" + +#include "CtfSchema.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr std::size_t kPacketSizeBytes = 65536U; +constexpr std::size_t kPacketHeaderSize = 24U; +constexpr std::size_t kPacketContextSize = 32U; +constexpr std::size_t kPacketOverhead = kPacketHeaderSize + kPacketContextSize; +constexpr std::size_t kEventPrefixSize = 13U; +/** @brief Formats a binary UUID in canonical textual form. */ +static std::string formatUuid(const std::array& uuid) +{ + std::ostringstream out; + out << std::hex << std::setfill('0'); + for (std::size_t index = 0; index < uuid.size(); ++index) { + if (index == 4U || index == 6U || index == 8U || index == 10U) { + out << '-'; + } + out << std::setw(2) << static_cast(uuid[index]); + } + return out.str(); +} + +CtfStreamWriter::Record::Record(std::vector& buffer, std::size_t offset, std::size_t endOffset) + : m_buffer(buffer), + m_offset(offset), + m_endOffset(endOffset) +{ +} + +void CtfStreamWriter::Record::requireSpace(std::size_t size) const +{ + if (size > m_endOffset - m_offset) { + throw std::logic_error("CTF record payload exceeds its declared size"); + } +} + +void CtfStreamWriter::Record::writeU8(std::uint8_t value) +{ + requireSpace(1U); + m_buffer[m_offset++] = value; +} + +void CtfStreamWriter::Record::writeU16(std::uint16_t value) +{ + requireSpace(2U); + writeU8(static_cast(value & 0xffU)); + writeU8(static_cast((value >> 8U) & 0xffU)); +} + +void CtfStreamWriter::Record::writeU32(std::uint32_t value) +{ + requireSpace(4U); + for (unsigned shift = 0U; shift < 32U; shift += 8U) { + writeU8(static_cast((value >> shift) & 0xffU)); + } +} + +void CtfStreamWriter::Record::writeU64(std::uint64_t value) +{ + requireSpace(8U); + for (unsigned shift = 0U; shift < 64U; shift += 8U) { + writeU8(static_cast((value >> shift) & 0xffU)); + } +} + +CtfStreamWriter::~CtfStreamWriter() +{ + abort(); +} + +void CtfStreamWriter::open(const std::filesystem::path& filePath, std::uint32_t streamId) +{ + abort(); + m_filePath = filePath; + m_streamId = streamId; + m_packetSequence = 0U; + m_lastTimestamp.reset(); + m_uuid.fill(0U); + + std::random_device random; + for (auto& byte : m_uuid) { + byte = static_cast(random()); + } + m_uuid[6] = static_cast((m_uuid[6] & 0x0fU) | 0x40U); + m_uuid[8] = static_cast((m_uuid[8] & 0x3fU) | 0x80U); + m_uuidString = formatUuid(m_uuid); + + m_packetBuffer.assign(kPacketSizeBytes, 0U); + beginPacket(); + m_file.open(m_filePath, std::ios::binary | std::ios::out | std::ios::trunc); + if (!m_file) { + abort(); + throw std::runtime_error("Failed to open CTF stream " + filePath.string()); + } + m_open = true; +} + +void CtfStreamWriter::close() +{ + if (!m_open) { + return; + } + flushPacket(); + m_file.close(); + m_open = false; + m_packetBuffer.clear(); + if (!m_file) { + throw std::runtime_error("Failed to write CTF stream in " + m_filePath.parent_path().string()); + } +} + +void CtfStreamWriter::abort() noexcept +{ + if (m_file.is_open()) { + m_file.close(); + } + m_open = false; + m_packetBuffer.clear(); + m_filePath.clear(); +} + +void CtfStreamWriter::writeRecord(std::uint32_t eventId, std::uint64_t timestamp, std::uint8_t traceBusId, + std::size_t payloadSize, const RecordCallback& writePayload) +{ + if (!m_open) { + return; + } + const auto totalSize = kEventPrefixSize + payloadSize; + if (totalSize > kPacketSizeBytes - kPacketOverhead) { + throw std::invalid_argument("CTF record does not fit into a packet"); + } + if (m_contentOffset + totalSize > kPacketSizeBytes) { + flushPacket(); + } + + timestamp = monotonicTimestamp(timestamp); + const auto recordEnd = m_contentOffset + totalSize; + Record record(m_packetBuffer, m_contentOffset, recordEnd); + record.writeU32(eventId); + record.writeU64(timestamp); + record.writeU8(traceBusId); + writePayload(record); + if (record.m_offset != recordEnd) { + throw std::logic_error("CTF record payload is shorter than its declared size"); + } + + m_contentOffset = recordEnd; + if (m_eventCount == 0U) { + m_timestampBegin = timestamp; + m_timestampEnd = timestamp; + } else { + m_timestampBegin = std::min(m_timestampBegin, timestamp); + m_timestampEnd = std::max(m_timestampEnd, timestamp); + } + ++m_eventCount; +} + +const std::string& CtfStreamWriter::uuidString() const noexcept +{ + return m_uuidString; +} + +void CtfStreamWriter::beginPacket() +{ + std::fill(m_packetBuffer.begin(), m_packetBuffer.end(), std::uint8_t{0}); + m_contentOffset = kPacketOverhead; + m_eventCount = 0U; + m_timestampBegin = 0U; + m_timestampEnd = 0U; +} + +void CtfStreamWriter::flushPacket() +{ + if (m_eventCount == 0U) { + return; + } + + Record header(m_packetBuffer, 0U, kPacketOverhead); + header.writeU32(CtfSchema::Magic); + for (const auto byte : m_uuid) { + header.writeU8(byte); + } + header.writeU32(m_streamId); + header.writeU32(static_cast(kPacketSizeBytes * 8U)); + header.writeU32(static_cast(m_contentOffset * 8U)); + header.writeU64(m_timestampBegin); + header.writeU64(m_timestampEnd); + header.writeU32(0U); + header.writeU32(m_packetSequence); + + m_file.write(reinterpret_cast(m_packetBuffer.data()), static_cast(m_packetBuffer.size())); + ++m_packetSequence; + beginPacket(); +} + +std::uint64_t CtfStreamWriter::monotonicTimestamp(std::uint64_t timestamp) +{ + if (m_lastTimestamp.has_value() && timestamp < *m_lastTimestamp) { + timestamp = *m_lastTimestamp; + } + m_lastTimestamp = timestamp; + return timestamp; +} diff --git a/tools/ctrace/src/output/ctf/CtfStreamWriter.h b/tools/ctrace/src/output/ctf/CtfStreamWriter.h new file mode 100644 index 000000000..8cae8baea --- /dev/null +++ b/tools/ctrace/src/output/ctf/CtfStreamWriter.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_CTFSTREAMWRITER_H +#define CTRACE_SRC_OUTPUT_CTF_CTFSTREAMWRITER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Writes packetized binary CTF stream records. */ +class CtfStreamWriter final { +public: + /** @brief Provides bounded little-endian writes into one reserved record payload. */ + class Record final { + public: + /** @brief Writes an unsigned 8-bit field. */ + void writeU8(std::uint8_t value); + /** @brief Writes an unsigned 16-bit field. */ + void writeU16(std::uint16_t value); + /** @brief Writes an unsigned 32-bit field. */ + void writeU32(std::uint32_t value); + /** @brief Writes an unsigned 64-bit field. */ + void writeU64(std::uint64_t value); + + private: + friend class CtfStreamWriter; + + /** @brief Creates a bounded view over one reserved payload range. */ + Record(std::vector& buffer, std::size_t offset, std::size_t endOffset); + + /** @brief Rejects writes that exceed the reserved payload range. */ + void requireSpace(std::size_t size) const; + + std::vector& m_buffer; + std::size_t m_offset; + std::size_t m_endOffset; + }; + + /** @brief Writes a caller-defined record payload. */ + using RecordCallback = std::function; + + /** @brief Creates an inactive CTF stream writer. */ + CtfStreamWriter() = default; + /** @brief Aborts an open stream before destruction. */ + ~CtfStreamWriter(); + + /** @brief Disables copying because the writer owns an output stream. */ + CtfStreamWriter(const CtfStreamWriter&) = delete; + /** @brief Disables copy assignment because the writer owns an output stream. */ + CtfStreamWriter& operator=(const CtfStreamWriter&) = delete; + + /** @brief Opens a new CTF stream file with the supplied stream ID. */ + void open(const std::filesystem::path& filePath, std::uint32_t streamId); + /** @brief Flushes the final packet and closes the stream. */ + void close(); + /** @brief Closes and removes an incomplete stream without throwing. */ + void abort() noexcept; + + /** @brief Appends one timestamped CTF event record. */ + void writeRecord(std::uint32_t eventId, std::uint64_t timestamp, std::uint8_t traceBusId, std::size_t payloadSize, + const RecordCallback& writePayload); + + /** @brief Returns the UUID shared by the stream and metadata. */ + const std::string& uuidString() const noexcept; + +private: + /** @brief Initializes a new packet buffer and writes its fixed context. */ + void beginPacket(); + /** @brief Finalizes and writes the current packet when it contains events. */ + void flushPacket(); + /** @brief Clamps a timestamp to the last emitted stream timestamp. */ + std::uint64_t monotonicTimestamp(std::uint64_t timestamp); + + std::filesystem::path m_filePath; + std::ofstream m_file; + std::vector m_packetBuffer; + std::size_t m_contentOffset = 0; + std::uint32_t m_eventCount = 0; + std::uint64_t m_timestampBegin = 0; + std::uint64_t m_timestampEnd = 0; + std::optional m_lastTimestamp; + std::uint32_t m_streamId = 0; + std::uint32_t m_packetSequence = 0; + std::array m_uuid{}; + std::string m_uuidString; + bool m_open = false; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_CTFSTREAMWRITER_H diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp new file mode 100644 index 000000000..2de1a1ce3 --- /dev/null +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceCompassXmlWriter.h" + +#include "CtfSchema.h" + +#include +#include +#include +#include +#include +#include +#include + +constexpr const char* kTraceCompassAnalysisVersionPlaceholder = "__SWO_ANALYSIS_VERSION__"; + +/** @brief Reads the analysis version encoded in generated Trace Compass XML. */ +static std::uint32_t traceCompassAnalysisVersion(const std::string& xml) +{ + constexpr std::uint32_t kFnvOffsetBasis = 2166136261U; + constexpr std::uint32_t kFnvPrime = 16777619U; + constexpr std::uint32_t kTraceCompassPositiveMask = 0x7fffffffU; + + std::uint32_t hash = kFnvOffsetBasis; + for (const auto ch : xml) { + hash ^= static_cast(ch); + hash *= kFnvPrime; + } + const auto version = hash & kTraceCompassPositiveMask; + return version == 0U ? 1U : version; +} + +/** @brief Inserts the current analysis version into generated XML. */ +static std::string withTraceCompassAnalysisVersion(std::string xml) +{ + const auto version = std::to_string(traceCompassAnalysisVersion(xml)); + const auto placeholder = std::string(kTraceCompassAnalysisVersionPlaceholder); + const auto position = xml.find(placeholder); + xml.replace(position, placeholder.size(), version); + return xml; +} + +/** @brief Generates Trace Compass value handlers for one CTF event route. */ +static std::string valueHandlers(CtfSchema::EventId eventId, const char* prefix, const char* routeField, + const char* valueAttribute) +{ + std::ostringstream handlers; + for (const auto& arm : CtfSchema::ValueVariants) { + handlers << R"( + + + + + + + + + + + + + +)"; + } + return handlers.str(); +} + +/** @brief Generates the Trace Compass state-provider definition. */ +static std::string stateProviderXml() +{ + std::ostringstream xml; + xml << R"( + +)"; + return xml.str(); +} + +/** @brief Generates the Trace Compass time-graph view definitions. */ +static std::string viewsXml() +{ + std::ostringstream xml; + xml << R"( + + + + + + + + + +)"; + return xml.str(); +} + +/** @brief Assembles the complete versioned Trace Compass analysis XML. */ +static std::string traceCompassXml() +{ + std::ostringstream xml; + xml << R"( + +)"; + xml << stateProviderXml(); + xml << viewsXml(); + xml << R"( +)"; + return withTraceCompassAnalysisVersion(xml.str()); +} + +void TraceCompassXmlWriter::writeFile(const std::filesystem::path& filePath) +{ + if (!filePath.parent_path().empty()) { + std::filesystem::create_directories(filePath.parent_path()); + } + std::ofstream out(filePath, std::ios::out | std::ios::trunc); + if (!out) { + throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); + } + out << traceCompassXml(); + out.close(); + if (!out) { + throw std::runtime_error("Failed to write Trace Compass XML " + filePath.string()); + } +} diff --git a/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h new file mode 100644 index 000000000..f168d91a2 --- /dev/null +++ b/tools/ctrace/src/output/ctf/TraceCompassXmlWriter.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H +#define CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H + +#include + +/** @brief Writes the Trace Compass analysis definition accompanying CTF output. */ +class TraceCompassXmlWriter final { +public: + /** @brief Writes the complete analysis definition to a file. */ + static void writeFile(const std::filesystem::path& path); + +private: + /** @brief Prevents construction of this stateless XML utility. */ + TraceCompassXmlWriter() = delete; +}; + +#endif // CTRACE_SRC_OUTPUT_CTF_TRACECOMPASSXMLWRITER_H diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.cpp b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp new file mode 100644 index 000000000..bd729c456 --- /dev/null +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.cpp @@ -0,0 +1,626 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtraceRunMeta.h" + +#include "TraceRunConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Accumulates normalized metadata for one processor. */ +struct ProcessorMeta { + std::optional name; + bool timestampsEnabled = false; + std::optional timestampClockHz; + std::optional timestampClockError; + std::optional timestampPrescaler; + std::optional itmEnableMask; +}; + +/** @brief Identifies one processor and its optional Trace Bus ID. */ +struct ProcessorIdentity { + bool multipleProcessors = false; + std::optional singleProcessorName; + bool constrainedBySetups = false; + std::set setupNames; + + /** @brief Tests whether a reference is consistent with the active setups. */ + bool accepts(const TraceRunReference& reference) const + { + if (!constrainedBySetups || setupNames.empty()) { + return true; + } + const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (!name.has_value()) { + return setupNames.size() == 1U; + } + return setupNames.find(*name) != setupNames.end(); + } + + /** @brief Resolves an optional processor name to its canonical binding name. */ + std::optional canonicalName(const std::optional& name) const + { + if (!multipleProcessors) { + return singleProcessorName; + } + return TraceRunSchema::normalizedProcessorName(name); + } +}; + +using ReferenceProblem = TraceRunSchema::ReferenceProblem; + +/** @brief Formats a trace-run validation error with source location. */ +static std::string configError(const TraceRunConfig& config, std::size_t line, const std::string& message) +{ + auto location = config.path; + if (line > 0U) { + location += "(" + std::to_string(line) + ")"; + } + return location + ": " + message; +} + +/** @brief Builds warning context for one trace-run reference. */ +static std::vector> warningContext(const TraceRunReference& reference) +{ + std::vector> context{ + {"ctraceRef", reference.ctraceRef}, + {"type", reference.type}, + }; + if (reference.line > 0U) { + context.emplace_back("line", std::to_string(reference.line)); + } + if (reference.processorName.has_value()) { + context.emplace_back("pname", *reference.processorName); + } + if (reference.stream.has_value()) { + context.emplace_back("stream", std::to_string(*reference.stream)); + } + return context; +} + +/** @brief Retains one ignored root-node inconsistency for non-fatal reporting. */ +static void addRootInconsistency(std::vector& warnings, std::string message, + std::vector> context = {}) +{ + warnings.push_back({std::move(message), std::move(context)}); +} + +/** @brief Formats the structural validation problem of one reference. */ +static std::string referenceProblemMessage(const TraceRunConfig& config, const TraceRunReference& reference, + ReferenceProblem problem) +{ + if (problem == ReferenceProblem::DuplicateSource) { + return configError(config, reference.line, + reference.line > 0U ? "duplicate value in 'source' array" : "duplicate value in source array"); + } + if (problem == ReferenceProblem::InvalidStream) { + return configError(config, reference.line, + reference.line > 0U ? "'stream' must be a CoreSight ATB trace ID between 1 and 111" + : "stream must be a CoreSight ATB trace ID between 1 and 111"); + } + return configError(config, reference.line, + reference.line > 0U ? "ITM 'source' must be between 0 and 31" + : "ITM source must be between 0 and 31"); +} + +/** @brief Tests whether a setup contributes metadata to any consumed route. */ +static bool consumesSetup(const TraceRunConfig& config, const TraceRunSetup& setup) +{ + if (setup.timestamps.has_value() || setup.itm.has_value()) { + return true; + } + for (const auto& reference : config.references) { + if (!TraceRunSchema::isUsableReference(reference) || reference.type != "dwt" || + !TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { + continue; + } + const auto index = reference.dataSetupIndex; + if (index.has_value() && *index < setup.data.size()) { + return true; + } + } + return false; +} + +/** @brief Tests whether a reference can bind a processor to one stream. */ +static bool isUsableStreamBinding(const TraceRunReference& reference) +{ + return TraceRunSchema::contributesStreamBinding(reference) && + TraceRunSchema::referenceProblem(reference) == ReferenceProblem::None; +} + +/** @brief Resolves the unambiguous processor identity of a trace-run file. */ +static ProcessorIdentity processorIdentity(const TraceRunConfig& config, std::vector& warnings) +{ + // Active setups define the authoritative processor set when present. + std::set setupNames; + std::set> uniqueSetupNames; + bool unnamedSetup = false; + std::size_t setupCount = 0U; + const TraceRunSetup* singleActiveSetup = nullptr; + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup)) { + continue; + } + ++setupCount; + singleActiveSetup = &setup; + const auto name = TraceRunSchema::normalizedProcessorName(setup.processorName); + if (!uniqueSetupNames.insert(name).second) { + throw std::runtime_error(configError(config, setup.line, + (setup.line > 0U ? "duplicate active 'ctrace-setup' for pname '" + : "duplicate active ctrace-setup for pname '") + + name.value_or("") + "'")); + } + if (name.has_value()) { + setupNames.insert(*name); + } else { + unnamedSetup = true; + } + } + + // Only usable stream bindings may contribute fallback processor identities. + std::set referenceNames; + bool unnamedReference = false; + for (const auto& reference : config.references) { + if (!isUsableStreamBinding(reference)) { + continue; + } + const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (name.has_value()) { + referenceNames.insert(*name); + } else { + unnamedReference = true; + } + } + + // Reconcile references according to the number and naming of active setups. + if (setupCount > 1U) { + if (unnamedSetup) { + throw std::runtime_error(config.path + + ": pname is required for every ctrace-setup in a multi-processor configuration"); + } + for (const auto& reference : config.references) { + if (!isUsableStreamBinding(reference)) { + continue; + } + const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (!name.has_value()) { + addRootInconsistency(warnings, + "ignoring ctrace-ref without pname because multiple ctrace-setup processors are active", + warningContext(reference)); + } else if (setupNames.find(*name) == setupNames.end()) { + addRootInconsistency(warnings, "ignoring ctrace-ref pname '" + *name + + "' because it has no matching ctrace-setup", + warningContext(reference)); + } + } + return {true, std::nullopt, true, setupNames}; + } + + if (setupCount == 1U) { + const auto setupName = TraceRunSchema::normalizedProcessorName(singleActiveSetup->processorName); + if (setupName.has_value()) { + for (const auto& reference : config.references) { + if (!isUsableStreamBinding(reference)) { + continue; + } + const auto name = TraceRunSchema::normalizedProcessorName(reference.processorName); + if (name.has_value() && *name != *setupName) { + addRootInconsistency(warnings, "ignoring ctrace-ref pname '" + *name + + "' because it does not match ctrace-setup pname '" + *setupName + "'", + warningContext(reference)); + } + } + return {false, setupName, true, setupNames}; + } + if (referenceNames.size() > 1U) { + addRootInconsistency(warnings, + "ignoring conflicting ctrace-ref pnames because the single ctrace-setup has no pname", + {{"ctraceRefPnames", std::to_string(referenceNames.size())}}); + } + return { + false, + referenceNames.size() == 1U ? std::optional(*referenceNames.begin()) : std::nullopt, + true, + {}, + }; + } + + if (referenceNames.size() > 1U) { + if (unnamedReference) { + throw std::runtime_error(config.path + + ": pname is required for every ctrace-ref in a multi-processor configuration"); + } + return {true, std::nullopt}; + } + return { + false, + referenceNames.empty() ? std::nullopt : std::optional(*referenceNames.begin()), + false, + {}, + }; +} + +/** @brief Resolves the data setup referenced by one DWT route. */ +static const TraceRunDataSetup* referencedDataSetup(const TraceRunConfig& config, const TraceRunReference& reference) +{ + const auto index = reference.dataSetupIndex; + if (!index.has_value()) { + return nullptr; + } + for (const auto& setup : config.setups) { + if (!TraceRunSchema::processorNamesMayBind(setup.processorName, reference.processorName)) { + continue; + } + const auto* candidate = *index < setup.data.size() ? &setup.data[*index] : nullptr; + if (candidate != nullptr) { + return candidate; + } + } + return nullptr; +} + +/** @brief Converts one validated reference into normalized source metadata. */ +static CtraceRunSourceMeta sourceMeta(const TraceRunConfig& config, const TraceRunReference& reference, + std::uint32_t source, const ProcessorIdentity& processorIdentity) +{ + const auto* dataSetup = reference.type == "dwt" ? referencedDataSetup(config, reference) : nullptr; + return { + reference.type, + processorIdentity.canonicalName(reference.processorName), + static_cast(reference.stream.value_or(0U)), + source, + reference.label, + reference.type == "dwt" ? reference.symbolAddress : std::nullopt, + dataSetup != nullptr ? dataSetup->symbolType.value_or(std::string(TraceRunSchema::kDefaultDwtDataType)) + : std::string(TraceRunSchema::kDefaultDwtDataType), + dataSetup != nullptr ? dataSetup->symbolSize.value_or(TraceRunSchema::kDefaultDwtDataSize) + : TraceRunSchema::kDefaultDwtDataSize, + dataSetup != nullptr ? dataSetup->symbolTypeError : std::nullopt, + dataSetup != nullptr ? dataSetup->symbolSizeError : std::nullopt, + }; +} + +/** @brief Returns or creates accumulated metadata for one processor. */ +static ProcessorMeta& processorMeta(std::vector& processors, const std::optional& name) +{ + const auto found = std::find_if(processors.begin(), processors.end(), + [&](const ProcessorMeta& processor) { return processor.name == name; }); + if (found != processors.end()) { + return *found; + } + processors.emplace_back(); + processors.back().name = name; + return processors.back(); +} + +template +/** @brief Returns a setting only when all relevant processors agree. */ +static std::optional commonProcessorSetting(const std::vector& processors, + const std::optional ProcessorMeta::* member) +{ + std::optional common; + for (const auto& processor : processors) { + if (!processor.timestampsEnabled) { + continue; + } + const auto& candidate = processor.*member; + if (!candidate.has_value()) { + return std::nullopt; + } + if (common.has_value() && common != candidate) { + return std::nullopt; + } + common = candidate; + } + return common; +} + +/** @brief Finds accumulated metadata for an optional processor identity. */ +static const ProcessorMeta* findProcessor(const std::vector& processors, + const std::optional& name) +{ + const auto found = std::find_if(processors.begin(), processors.end(), + [&](const ProcessorMeta& processor) { return processor.name == name; }); + return found != processors.end() ? &*found : nullptr; +} + +/** @brief Stores the result of binding one processor to a trace stream. */ +struct ResolvedStreamBinding { + std::size_t line = 0U; + std::uint8_t traceBusId = 0U; + std::optional processorName; + std::string ctraceRef; + const ProcessorMeta* processor = nullptr; +}; + +/** @brief Builds warning context for one resolved stream binding. */ +static std::vector> warningContext(const ResolvedStreamBinding& binding) +{ + std::vector> context{ + {"ctraceRef", binding.ctraceRef}, + {"stream", std::to_string(binding.traceBusId)}, + }; + if (binding.line > 0U) { + context.emplace_back("line", std::to_string(binding.line)); + } + if (binding.processorName.has_value()) { + context.emplace_back("pname", *binding.processorName); + } + return context; +} + +/** @brief Resolves validated processor-to-stream bindings for all references. */ +static std::vector resolveStreamBindings(const TraceRunConfig& config, + const ProcessorIdentity& processorIdentity, + const std::vector& processors) +{ + std::vector bindings; + for (const auto& reference : config.references) { + if (!isUsableStreamBinding(reference) || !processorIdentity.accepts(reference)) { + continue; + } + const auto processorName = processorIdentity.canonicalName(reference.processorName); + bindings.push_back({ + reference.line, + static_cast(reference.stream.value_or(0U)), + processorName, + reference.ctraceRef, + findProcessor(processors, processorName), + }); + } + return bindings; +} + +static std::map +buildTimestampsByTraceBusId(const std::vector& bindings, + std::vector& warnings) +{ + std::map result; + for (const auto& binding : bindings) { + CtraceRunTimestampMeta candidate; + candidate.processorName = binding.processorName; + if (binding.processor != nullptr && binding.processor->timestampsEnabled) { + candidate.clockHz = binding.processor->timestampClockHz; + candidate.clockError = binding.processor->timestampClockError; + } + + const auto [found, inserted] = result.emplace(binding.traceBusId, candidate); + if (inserted || (found->second.processorName == candidate.processorName && + found->second.clockHz == candidate.clockHz && found->second.clockError == candidate.clockError)) { + continue; + } + addRootInconsistency(warnings, + "ignoring conflicting ctrace-setup timestamps.clock assignment for CoreSight Trace Bus ID " + + std::to_string(binding.traceBusId), + warningContext(binding)); + } + return result; +} + +static std::map +buildTimestampPrescalersByTraceBusId(const std::vector& bindings, + std::vector& warnings) +{ + std::map result; + for (const auto& binding : bindings) { + const auto prescaler = binding.processor != nullptr && binding.processor->timestampPrescaler.has_value() + ? *binding.processor->timestampPrescaler + : TraceRunSchema::kDefaultTimestampPrescaler; + const auto [found, inserted] = result.emplace(binding.traceBusId, prescaler); + if (!inserted && found->second != prescaler) { + addRootInconsistency( + warnings, + "ignoring conflicting ctrace-setup timestamps.itm-prescaler assignment for CoreSight Trace Bus ID " + + std::to_string(binding.traceBusId), + warningContext(binding)); + } + } + return result; +} + +/** @brief Returns the common ITM enable mask across relevant processors. */ +static std::optional commonItmEnableMask(const std::vector& processors) +{ + std::optional common; + for (const auto& processor : processors) { + if (!processor.itmEnableMask.has_value()) { + return std::nullopt; + } + if (common.has_value() && common != processor.itmEnableMask) { + return std::nullopt; + } + common = processor.itmEnableMask; + } + return common; +} + +static std::map +buildItmEnableMasksByTraceBusId(const std::vector& bindings, + std::vector& warnings) +{ + std::map> candidates; + for (const auto& binding : bindings) { + const auto enableMask = binding.processor != nullptr ? binding.processor->itmEnableMask : std::nullopt; + const auto [found, inserted] = candidates.emplace(binding.traceBusId, enableMask); + if (!inserted && found->second != enableMask) { + addRootInconsistency(warnings, + "ignoring conflicting ctrace-setup itm.enable assignment for CoreSight Trace Bus ID " + + std::to_string(binding.traceBusId), + warningContext(binding)); + } + } + + std::map result; + for (const auto& [traceBusId, enableMask] : candidates) { + if (enableMask.has_value()) { + result.emplace(traceBusId, *enableMask); + } + } + return result; +} + +/** @brief Tests whether processors require different timestamp prescalers. */ +static bool containsDistinctProcessorPrescalers(const std::vector& processors) +{ + std::optional first; + for (const auto& processor : processors) { + if (!processor.timestampsEnabled || !processor.timestampPrescaler.has_value()) { + continue; + } + if (first.has_value() && first != processor.timestampPrescaler) { + return true; + } + first = processor.timestampPrescaler; + } + return false; +} + +CtraceRunMeta CtraceRunMeta::fromConfig(const TraceRunConfig& config) +{ + CtraceRunMeta ctraceRunMeta; + ctraceRunMeta.m_configPath = config.path; + + for (const auto& reference : config.references) { + if (!TraceRunSchema::hasConsumedRouteShape(reference) && !TraceRunSchema::contributesStreamBinding(reference)) { + continue; + } + const auto problem = TraceRunSchema::referenceProblem(reference); + if (problem != ReferenceProblem::None && !reference.error.has_value()) { + throw std::runtime_error(referenceProblemMessage(config, reference, problem)); + } + } + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup) || !setup.timestamps.has_value() || + !setup.timestamps->timestampPrescaler.has_value() || + TraceRunSchema::isTimestampPrescaler(*setup.timestamps->timestampPrescaler)) { + continue; + } + throw std::runtime_error(configError(config, setup.timestamps->line, + setup.timestamps->line > 0U + ? "'timestamps.itm-prescaler' must be one of 1, 4, 16, or 64" + : "ctrace-setup timestamps.itm-prescaler must be one of 1, 4, 16, or 64")); + } + + const auto identity = processorIdentity(config, ctraceRunMeta.m_warnings); + std::vector processors; + + for (const auto& setup : config.setups) { + if (!consumesSetup(config, setup)) { + continue; + } + const auto processorName = identity.canonicalName(setup.processorName); + auto& processor = processorMeta(processors, processorName); + if (setup.timestamps.has_value()) { + processor.timestampsEnabled = true; + processor.timestampClockHz = setup.timestamps->clockHz; + processor.timestampClockError = setup.timestamps->clockError; + processor.timestampPrescaler = + setup.timestamps->timestampPrescaler.value_or(TraceRunSchema::kDefaultTimestampPrescaler); + if (setup.timestamps->clockError.has_value()) { + ctraceRunMeta.m_timestampClockErrors.push_back(*setup.timestamps->clockError); + } + } + if (setup.itm.has_value()) { + processor.itmEnableMask = setup.itm->enableMask; + } + } + + for (const auto& reference : config.references) { + if (isUsableStreamBinding(reference) && identity.accepts(reference)) { + (void)processorMeta(processors, identity.canonicalName(reference.processorName)); + } + if (!TraceRunSchema::isUsableReference(reference) || !identity.accepts(reference)) { + continue; + } + for (const auto source : reference.sources) { + ctraceRunMeta.m_sources.push_back(sourceMeta(config, reference, source, identity)); + } + } + const auto streamBindings = resolveStreamBindings(config, identity, processors); + ctraceRunMeta.m_timestampClockHz = commonProcessorSetting(processors, &ProcessorMeta::timestampClockHz); + ctraceRunMeta.m_timestampsByTraceBusId = + buildTimestampsByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); + ctraceRunMeta.m_timestampPrescaler = commonProcessorSetting(processors, &ProcessorMeta::timestampPrescaler); + ctraceRunMeta.m_timestampPrescalersByTraceBusId = + buildTimestampPrescalersByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); + ctraceRunMeta.m_itmEnableMask = commonItmEnableMask(processors); + ctraceRunMeta.m_itmEnableMasksByTraceBusId = + buildItmEnableMasksByTraceBusId(streamBindings, ctraceRunMeta.m_warnings); + ctraceRunMeta.m_processorCount = processors.size(); + ctraceRunMeta.m_distinctProcessorPrescalers = containsDistinctProcessorPrescalers(processors); + + return ctraceRunMeta; +} + +const std::string& CtraceRunMeta::configPath() const +{ + return m_configPath; +} + +const std::optional& CtraceRunMeta::timestampClockHz() const +{ + return m_timestampClockHz; +} + +const std::map& CtraceRunMeta::timestampsByTraceBusId() const +{ + return m_timestampsByTraceBusId; +} + +const std::optional& CtraceRunMeta::timestampPrescaler() const +{ + return m_timestampPrescaler; +} + +const std::map& CtraceRunMeta::timestampPrescalersByTraceBusId() const +{ + return m_timestampPrescalersByTraceBusId; +} + +const std::optional& CtraceRunMeta::itmEnableMask() const +{ + return m_itmEnableMask; +} + +const std::map& CtraceRunMeta::itmEnableMasksByTraceBusId() const +{ + return m_itmEnableMasksByTraceBusId; +} + +const std::vector& CtraceRunMeta::timestampClockErrors() const +{ + return m_timestampClockErrors; +} + +bool CtraceRunMeta::hasDistinctProcessorPrescalers() const +{ + return m_distinctProcessorPrescalers; +} + +std::size_t CtraceRunMeta::processorCount() const +{ + return m_processorCount; +} + +const std::vector& CtraceRunMeta::sources() const +{ + return m_sources; +} + +const std::vector& CtraceRunMeta::warnings() const +{ + return m_warnings; +} diff --git a/tools/ctrace/src/tracerun/CtraceRunMeta.h b/tools/ctrace/src/tracerun/CtraceRunMeta.h new file mode 100644 index 000000000..2bfb93dec --- /dev/null +++ b/tools/ctrace/src/tracerun/CtraceRunMeta.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_TRACERUN_CTRACERUNMETA_H +#define CTRACE_SRC_TRACERUN_CTRACERUNMETA_H + +#include +#include +#include +#include +#include +#include +#include + +struct TraceRunConfig; + +/** @brief Stores normalized metadata for one trace source route. */ +struct CtraceRunSourceMeta { + std::string type; + std::optional processorName; + std::uint8_t traceBusId = 0U; + std::uint32_t source = 0; + std::optional label; + std::optional symbolAddress; + std::string valueType = "unsigned int"; + std::uint64_t valueSize = 4U; + std::optional symbolTypeError; + std::optional symbolSizeError; +}; + +/** @brief Stores normalized timestamp metadata for one processor stream. */ +struct CtraceRunTimestampMeta { + std::optional processorName; + std::optional clockHz; + std::optional clockError; +}; + +/** @brief Describes one non-fatal inconsistency between ctrace-setup and ctrace-refs. */ +struct CtraceRunWarning { + std::string message; + std::vector> context; +}; + +/** @brief Provides validated trace-run metadata consumed by decoding and output. */ +class CtraceRunMeta { +public: + /** + * @brief Normalizes a parsed trace-run configuration. + * @param config Parsed trace-run configuration. + * @return Metadata indexed for decoder and output consumption. + * + * Ambiguous processor-wide values remain absent while per-stream values and + * validation errors are retained for diagnostics and output planning. + */ + static CtraceRunMeta fromConfig(const TraceRunConfig& config); + + /** @brief Returns the source trace-run configuration path. */ + const std::string& configPath() const; + /** @brief Returns the unambiguous timestamp clock, if available. */ + const std::optional& timestampClockHz() const; + /** @brief Returns timestamp metadata indexed by Trace Bus ID. */ + const std::map& timestampsByTraceBusId() const; + /** @brief Returns the unambiguous ITM timestamp prescaler, if available. */ + const std::optional& timestampPrescaler() const; + /** @brief Returns timestamp prescalers indexed by Trace Bus ID. */ + const std::map& timestampPrescalersByTraceBusId() const; + /** @brief Returns the unambiguous ITM stimulus enable mask, if available. */ + const std::optional& itmEnableMask() const; + /** @brief Returns ITM stimulus enable masks indexed by Trace Bus ID. */ + const std::map& itmEnableMasksByTraceBusId() const; + /** @brief Returns clock validation errors retained for output planning. */ + const std::vector& timestampClockErrors() const; + /** @brief Reports whether processor-specific timestamp prescalers differ. */ + bool hasDistinctProcessorPrescalers() const; + /** @brief Returns the number of processors represented by the configuration. */ + std::size_t processorCount() const; + /** @brief Returns all normalized source routes. */ + const std::vector& sources() const; + /** @brief Returns non-fatal inconsistencies ignored during normalization. */ + const std::vector& warnings() const; + +private: + std::string m_configPath; + std::optional m_timestampClockHz; + std::map m_timestampsByTraceBusId; + std::optional m_timestampPrescaler; + std::map m_timestampPrescalersByTraceBusId; + std::optional m_itmEnableMask; + std::map m_itmEnableMasksByTraceBusId; + std::vector m_timestampClockErrors; + std::size_t m_processorCount = 0; + bool m_distinctProcessorPrescalers = false; + std::vector m_sources; + std::vector m_warnings; +}; + +#endif // CTRACE_SRC_TRACERUN_CTRACERUNMETA_H diff --git a/tools/ctrace/src/tracerun/TraceRunConfig.h b/tools/ctrace/src/tracerun/TraceRunConfig.h new file mode 100644 index 000000000..ef977aaa6 --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunConfig.h @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_TRACERUN_TRACERUNCONFIG_H +#define CTRACE_SRC_TRACERUN_TRACERUNCONFIG_H + +#include "TraceStreamId.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace TraceRunSchema { + +inline constexpr std::array kTimestampPrescalers{{ + 1U, + 4U, + 16U, + 64U, +}}; +inline constexpr std::uint32_t kDefaultTimestampPrescaler = 1U; +inline constexpr std::string_view kDefaultDwtDataType = "unsigned int"; +inline constexpr std::uint8_t kDefaultDwtDataSize = 4U; + +/** @brief Tests whether a fixed array contains a value. */ +template +constexpr bool contains(const std::array& values, const Value& candidate) +{ + for (const auto& value : values) { + if (value == candidate) { + return true; + } + } + return false; +} + +/** @brief Tests whether an ITM timestamp prescaler is supported. */ +constexpr bool isTimestampPrescaler(std::uint32_t prescaler) +{ + return contains(kTimestampPrescalers, prescaler); +} + +/** @brief Tests whether a DWT data type is supported. */ +constexpr bool isDwtDataType(const std::string_view& type) +{ + return type == "unsigned int" || type == "signed int" || type == "float"; +} + +/** @brief Tests whether a DWT data size is supported. */ +constexpr bool isDwtDataSize(std::uint64_t size) +{ + return size == 1U || size == 2U || size == 4U; +} + +/** @brief Tests whether a trace source type is decoded in the first release. */ +constexpr bool supportsSource(const std::string_view& type) +{ + return type == "dwt" || type == "itm"; +} + +/** @brief Tests whether ctrace consumes metadata for a reference type. */ +constexpr bool consumesReferenceMetadata(const std::string_view& type) +{ + return type == "dwt" || type == "itm" || type == "event" || type == "pmu" || type == "pcsample"; +} + +/** @brief Tests whether an ITM stimulus port number is valid. */ +constexpr bool isItmSource(std::uint32_t source) +{ + return CoreSight::isItmStimulusPort(source); +} + +/** @brief Converts empty processor names to an absent value. */ +inline std::optional normalizedProcessorName(std::optional name) +{ + if (!name.has_value() || name->empty()) { + return std::nullopt; + } + return name; +} + +/** @brief Tests whether two optional processor names can describe the same route. */ +inline bool processorNamesMayBind(const std::optional& left, const std::optional& right) +{ + const auto normalizedLeft = normalizedProcessorName(left); + const auto normalizedRight = normalizedProcessorName(right); + return !normalizedLeft.has_value() || !normalizedRight.has_value() || normalizedLeft == normalizedRight; +} + +} // namespace TraceRunSchema + +/** @brief Stores one parsed ctrace reference and its resolved routing metadata. */ +struct TraceRunReference { + std::string ctraceRef; + std::string type; + std::optional processorName; + std::optional info; + std::optional warning; + std::optional error; + std::optional symbolAddress; + std::optional label; + // The reader retains the complete YAML value. CtraceRunMeta validates and + // narrows it to the CoreSight ATB trace-ID domain. + std::optional stream; + std::vector sources; + // Index of the referenced ctrace-setup.data entry, derived from the + // specified ctrace-ref path form [/]data#. + std::optional dataSetupIndex; + std::size_t line = 0U; +}; + +namespace TraceRunSchema { + +/** @brief Classifies structural problems in a parsed trace reference. */ +enum class ReferenceProblem { + None, + DuplicateSource, + InvalidStream, + InvalidItmSource, +}; + +/** @brief Tests whether a reference selects ITM stimulus port zero, which is excluded from output. */ +inline bool isItmChannelZero(const TraceRunReference& reference) +{ + return reference.type == "itm" && reference.sources.size() == 1U && + reference.sources.front() == CoreSight::kExcludedItmStimulusPort; +} + +/** @brief Tests whether a reference has the fields needed for a decoded route. */ +inline bool hasConsumedRouteShape(const TraceRunReference& reference) +{ + if (!supportsSource(reference.type) || reference.sources.empty()) { + return false; + } + if (reference.type == "itm") { + return !isItmChannelZero(reference); + } + return true; +} + +/** @brief Tests whether a reference represents timestamp configuration. */ +inline bool isTimestampReference(const TraceRunReference& reference) +{ + constexpr std::string_view name = "timestamps"; + const auto separator = reference.ctraceRef.rfind('/'); + const auto leaf = separator == std::string::npos ? std::string_view(reference.ctraceRef) + : std::string_view(reference.ctraceRef).substr(separator + 1U); + return leaf == name; +} + +/** @brief Tests whether a reference configures the processor ITM, including its ATB stream ID. */ +inline bool isProcessorItmReference(const TraceRunReference& reference) +{ + constexpr std::string_view name = "itm"; + const auto separator = reference.ctraceRef.rfind('/'); + const auto leaf = separator == std::string::npos ? std::string_view(reference.ctraceRef) + : std::string_view(reference.ctraceRef).substr(separator + 1U); + return reference.type == "itm" && leaf == name; +} + +/** @brief Tests whether a reference participates in processor-to-stream binding. */ +inline bool contributesStreamBinding(const TraceRunReference& reference) +{ + return (reference.type == "dwt" || reference.type == "itm") && + (!reference.sources.empty() || isTimestampReference(reference) || isProcessorItmReference(reference)); +} + +/** @brief Returns the first structural problem detected in a reference. */ +inline ReferenceProblem referenceProblem(const TraceRunReference& reference) +{ + for (std::size_t left = 0U; left < reference.sources.size(); ++left) { + for (std::size_t right = left + 1U; right < reference.sources.size(); ++right) { + if (reference.sources[left] == reference.sources[right]) { + return ReferenceProblem::DuplicateSource; + } + } + } + if (reference.stream.has_value() && !CoreSight::isAtbTraceId(*reference.stream)) { + return ReferenceProblem::InvalidStream; + } + if (reference.type == "itm") { + for (const auto source : reference.sources) { + if (!isItmSource(source)) { + return ReferenceProblem::InvalidItmSource; + } + } + } + return ReferenceProblem::None; +} + +/** @brief Tests whether a reference is structurally valid and usable by ctrace. */ +inline bool isUsableReference(const TraceRunReference& reference) +{ + return hasConsumedRouteShape(reference) && referenceProblem(reference) == ReferenceProblem::None; +} + +} // namespace TraceRunSchema + +/** @brief Stores timestamp-related fields copied from one trace setup. */ +struct TraceRunTimestampSetup { + std::optional clockHz = std::nullopt; + std::optional timestampPrescaler = std::nullopt; + std::optional clockError = std::nullopt; + std::size_t line = 0U; +}; + +/** @brief Stores data-type fields copied from one DWT data setup. */ +struct TraceRunDataSetup { + std::optional symbolType = std::nullopt; + std::optional symbolSize = std::nullopt; + std::optional symbolTypeError = std::nullopt; + std::optional symbolSizeError = std::nullopt; +}; + +/** @brief Stores ITM stimulus-port configuration copied from one trace setup. */ +struct TraceRunItmSetup { + std::uint32_t enableMask = 0U; +}; + +/** @brief Stores the ctrace setup metadata consumed by the decoder. */ +struct TraceRunSetup { + std::optional processorName; + std::optional timestamps; + std::optional itm; + std::vector data; + std::size_t line = 0U; +}; + +/** @brief Stores a parsed `*.ctrace-run.yml` input. */ +struct TraceRunConfig { + std::string path; + std::vector references; + std::vector setups; +}; + +#endif // CTRACE_SRC_TRACERUN_TRACERUNCONFIG_H diff --git a/tools/ctrace/src/tracerun/TraceRunConfigReader.h b/tools/ctrace/src/tracerun/TraceRunConfigReader.h new file mode 100644 index 000000000..6c60992ed --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunConfigReader.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_TRACERUN_TRACERUNCONFIGREADER_H +#define CTRACE_SRC_TRACERUN_TRACERUNCONFIGREADER_H + +#include "TraceRunConfig.h" + +#include + +/** @brief Defines the interface for reading trace-run configuration files. */ +class TraceRunConfigReader { +public: + /** @brief Destroys a trace-run reader through its interface. */ + virtual ~TraceRunConfigReader() = default; + + /** + * @brief Reads and validates a trace-run configuration file. + * @param path Configuration file path. + * @return Parsed configuration including all ctrace-relevant fields. + * @throws std::runtime_error If the file cannot be read or is invalid. + */ + virtual TraceRunConfig read(const std::string& path) const = 0; +}; + +#endif // CTRACE_SRC_TRACERUN_TRACERUNCONFIGREADER_H diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp new file mode 100644 index 000000000..30e903637 --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceRunDiscovery.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr std::string_view ConfigSuffix = ".ctrace-run.yml"; + +/** @brief Tests whether a string ends in the supplied suffix. */ +static bool endsWith(const std::string_view& value, const std::string_view& suffix) +{ + return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix; +} + +/** @brief Tests whether a file extension names a supported trace channel. */ +static bool isTraceChannel(const std::string_view& value) +{ + return value == "SWO" || value == "TB" || value == "ER"; +} + +/** @brief Tests whether a solution-set name is reserved by Windows. */ +static bool isWindowsReservedFilename(const std::string_view& value) +{ + const auto extension = value.find('.'); + const auto basename = value.substr(0U, extension); + std::string upper; + upper.reserve(basename.size()); + std::transform(basename.begin(), basename.end(), std::back_inserter(upper), [](const char character) { + return static_cast(std::toupper(static_cast(character))); + }); + + if (upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL" || upper == "CONIN$" || upper == "CONOUT$") { + return true; + } + return upper.size() == 4U && (upper.compare(0U, 3U, "COM") == 0 || upper.compare(0U, 3U, "LPT") == 0) && + upper[3] >= '1' && upper[3] <= '9'; +} + +/** @brief Tests whether a solution-set name is portable and path-safe. */ +static bool isSafeSolutionSetName(const std::string_view& value) +{ + if (value.empty() || value == "." || value == ".." || value.back() == '.' || value.back() == ' ' || + isWindowsReservedFilename(value)) { + return false; + } + + constexpr std::string_view invalidFilenameCharacters = "<>:\"/\\|?*"; + return std::none_of(value.begin(), value.end(), [invalidFilenameCharacters](const char character) { + const auto byte = static_cast(character); + return byte < 0x20U || invalidFilenameCharacters.find(character) != std::string_view::npos; + }); +} + +/** @brief Rejects absent, non-regular, or unreadable trace-run files. */ +static void requireReadableConfigFile(const std::filesystem::path& path) +{ + if (!std::filesystem::is_regular_file(path)) { + throw std::runtime_error("trace-run configuration not found: " + path.string()); + } +} + +std::vector TraceRunDiscovery::selectConfigFiles(const std::filesystem::path& traceDir, + const std::optional& target) +{ + if (!std::filesystem::is_directory(traceDir)) { + throw std::runtime_error("trace directory not found: " + traceDir.string()); + } + + if (target.has_value()) { + if (!isSafeSolutionSetName(*target)) { + throw std::runtime_error("--target must be a solution-set name, got " + *target); + } + auto path = traceDir / (*target + std::string(ConfigSuffix)); + requireReadableConfigFile(path); + return {std::move(path)}; + } + + std::vector configs; + for (const auto& entry : std::filesystem::directory_iterator(traceDir)) { + if (!entry.is_regular_file()) { + continue; + } + const auto filename = entry.path().filename().string(); + if (endsWith(filename, ConfigSuffix) && filename.size() > std::string_view(ConfigSuffix).size()) { + configs.push_back(entry.path()); + } + } + std::sort(configs.begin(), configs.end()); + if (configs.empty()) { + throw std::runtime_error("no *" + std::string(ConfigSuffix) + + " files found in trace directory: " + traceDir.string()); + } + return configs; +} + +std::string TraceRunDiscovery::solutionSetName(const std::filesystem::path& configFile) +{ + const auto filename = configFile.filename().string(); + const auto suffix = std::string_view(ConfigSuffix); + if (!endsWith(filename, suffix) || filename.size() == suffix.size()) { + throw std::runtime_error("expected " + std::string(ConfigSuffix) + ", got " + configFile.string()); + } + return filename.substr(0, filename.size() - suffix.size()); +} + +std::vector TraceRunDiscovery::rawInputs(const std::filesystem::path& configFile) +{ + const auto solutionSet = solutionSetName(configFile); + const auto prefix = solutionSet + "."; + constexpr std::string_view suffix = ".raw"; + const auto directory = configFile.has_parent_path() ? configFile.parent_path() : std::filesystem::path("."); + + std::vector inputs; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (!entry.is_regular_file()) { + continue; + } + const auto filename = entry.path().filename().string(); + if (filename.rfind(prefix, 0) != 0 || !endsWith(filename, suffix)) { + continue; + } + const auto channelSize = filename.size() - prefix.size() - suffix.size(); + if (channelSize == 0U) { + continue; + } + const auto channel = filename.substr(prefix.size(), channelSize); + if (!isTraceChannel(channel)) { + continue; + } + inputs.push_back({entry.path(), channel}); + } + std::sort(inputs.begin(), inputs.end(), + [](const TraceRunRawInput& left, const TraceRunRawInput& right) { return left.path < right.path; }); + return inputs; +} diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.h b/tools/ctrace/src/tracerun/TraceRunDiscovery.h new file mode 100644 index 000000000..25fe46c48 --- /dev/null +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_TRACERUN_TRACERUNDISCOVERY_H +#define CTRACE_SRC_TRACERUN_TRACERUNDISCOVERY_H + +#include +#include +#include +#include + +/** @brief Associates one discovered raw trace input with its channel name. */ +struct TraceRunRawInput { + std::filesystem::path path; + std::string channel; +}; + +/** @brief Discovers trace-run configurations and their raw input files. */ +class TraceRunDiscovery final { +public: + /** + * @brief Selects one target configuration or every configuration in a trace directory. + * @param traceDir Directory containing trace-run files. + * @param target Optional solution-set name to select exclusively. + * @return Deterministically ordered configuration paths. + * @throws std::runtime_error If the directory or requested target is invalid. + */ + static std::vector selectConfigFiles(const std::filesystem::path& traceDir, + const std::optional& target); + /** + * @brief Derives the solution-set name from a trace-run filename. + * @param configFile Trace-run configuration path. + * @return Filename without the ctrace-run suffix. + */ + static std::string solutionSetName(const std::filesystem::path& configFile); + /** + * @brief Discovers the supported raw inputs associated with a trace-run file. + * @param configFile Trace-run configuration path. + * @return Deterministically ordered existing raw inputs and their channel names. + */ + static std::vector rawInputs(const std::filesystem::path& configFile); + +private: + /** @brief Prevents construction of this stateless discovery utility. */ + TraceRunDiscovery() = delete; +}; + +#endif // CTRACE_SRC_TRACERUN_TRACERUNDISCOVERY_H diff --git a/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp new file mode 100644 index 000000000..4e4712168 --- /dev/null +++ b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.cpp @@ -0,0 +1,567 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "YmlTraceRunConfigReader.h" + +#include "TraceRunConfig.h" +#include "yaml-cpp/exceptions.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/parse.h" +#include "yaml-cpp/node/type.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Node = YAML::Node; + +/** @brief Converts a YAML mark to a one-based line number. */ +static std::size_t lineNumber(const Node& node) +{ + return node.Mark().line >= 0 ? static_cast(node.Mark().line + 1) : 0U; +} + +/** @brief Formats a YAML validation error with file and line. */ +static std::string errorMessage(const std::string& path, const Node& node, const std::string& message) +{ + std::ostringstream out; + out << path; + if (lineNumber(node) > 0U) { + out << '(' << lineNumber(node) << ')'; + } + out << ": " << message; + return out.str(); +} + +[[noreturn]] static void fail(const std::string& path, const Node& node, const std::string& message) +{ + throw std::runtime_error(errorMessage(path, node, message)); +} + +/** @brief Returns the child node for one scalar mapping key, if present. */ +static Node childNode(const Node& element, const std::string_view& tag) +{ + for (const auto& entry : element) { + if (!entry.first.IsScalar() || entry.first.Scalar() != tag) { + continue; + } + return entry.second; + } + return Node(YAML::NodeType::Undefined); +} + +/** @brief Returns a child mapping or sequence container, if present. */ +static Node childContainer(const Node& element, const std::string_view& tag) +{ + const auto node = childNode(element, tag); + return node && (node.IsMap() || node.IsSequence()) ? node : Node(YAML::NodeType::Undefined); +} + +/** @brief Reads one optional scalar mapping value as text. */ +static std::optional optionalAttribute(const Node& element, const std::string_view& name) +{ + const auto node = childNode(element, name); + if (!node) { + return std::nullopt; + } + if (node.IsNull()) { + return std::string{}; + } + if (!node.IsScalar()) { + return std::nullopt; + } + return node.Scalar(); +} + +/** @brief Reads and validates an optional processor name. */ +static std::optional processorNameAttribute(const std::string& path, const Node& element) +{ + const auto node = childNode(element, "pname"); + if (!node || node.IsNull()) { + return std::nullopt; + } + if (!node.IsScalar()) { + fail(path, node, "'pname' must be a scalar string"); + } + return TraceRunSchema::normalizedProcessorName(std::optional(node.Scalar())); +} + +/** @brief Reads a processor name without making unrelated YAML fatal. */ +static std::optional bestEffortProcessorName(const Node& element) +{ + const auto node = childNode(element, "pname"); + if (!node || node.IsNull() || !node.IsScalar()) { + return std::nullopt; + } + return TraceRunSchema::normalizedProcessorName(std::optional(node.Scalar())); +} + +/** @brief Parses and range-checks one required unsigned mapping value. */ +static std::uint64_t unsignedValue(const std::string& path, const Node& element, const std::string_view& name, + const std::string& value, std::uint64_t maximum) +{ + std::string_view digits(value); + int base = 10; + if (digits.size() >= 2U && digits[0] == '0' && (digits[1] == 'x' || digits[1] == 'X')) { + base = 16; + digits.remove_prefix(2); + } + if (digits.empty()) { + fail(path, element, "'" + std::string(name) + "' must be an unsigned integer"); + } + + std::uint64_t parsed = 0; + const auto result = std::from_chars(digits.data(), digits.data() + digits.size(), parsed, base); + if (result.ec != std::errc{} || result.ptr != digits.data() + digits.size() || parsed > maximum) { + fail(path, element, "'" + std::string(name) + "' must be an unsigned integer in range"); + } + return parsed; +} + +/** @brief Parses and range-checks one optional unsigned mapping value. */ +static std::optional optionalUnsignedAttribute(const std::string& path, const Node& element, + const std::string_view& name, std::uint64_t maximum) +{ + const auto value = optionalAttribute(element, name); + if (!value.has_value()) { + return std::nullopt; + } + return unsignedValue(path, element, name, *value, maximum); +} + +/** @brief Parses an optional unsigned value while deferring validation errors. */ +static std::optional deferredUnsignedAttribute(const std::string& path, const Node& element, + const std::string_view& name, std::uint64_t maximum, + std::optional& error) +{ + try { + return optionalUnsignedAttribute(path, element, name, maximum); + } catch (const std::runtime_error& ex) { + error = ex.what(); + return std::nullopt; + } +} + +/** @brief Parses an optional unsigned value only when its setup is consumed. */ +static std::optional bestEffortUnsignedAttribute(const std::string& path, const Node& element, + const std::string_view& name, std::uint64_t maximum) +{ + try { + return optionalUnsignedAttribute(path, element, name, maximum); + } catch (const std::runtime_error&) { + return std::nullopt; + } +} + +// ctrace-ref identifies the originating ctrace.yml node. The data index links +// a generated DWT route to its copied ctrace-setup.data metadata. +/** @brief Extracts a DWT data setup index from a ctrace reference path. */ +static std::optional dwtDataIndex(const std::string_view& ctraceRef) +{ + constexpr std::string_view marker = "data#"; + const auto markerPosition = ctraceRef.rfind(marker); + if (markerPosition == std::string_view::npos || (markerPosition != 0U && ctraceRef[markerPosition - 1U] != '/')) { + return std::nullopt; + } + const auto value = ctraceRef.substr(markerPosition + marker.size()); + if (value.empty()) { + return std::nullopt; + } + std::size_t index = 0U; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), index); + if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size()) { + return std::nullopt; + } + return index; +} + +/** @brief Collects data setup indices consumed by matching references. */ +static std::set referencedDataSetupIndices(const std::vector& references, + const std::optional& setupProcessorName) +{ + std::set indices; + for (const auto& reference : references) { + if (reference.type != "dwt" || !TraceRunSchema::isUsableReference(reference)) { + continue; + } + if (!TraceRunSchema::processorNamesMayBind(setupProcessorName, reference.processorName)) { + continue; + } + const auto index = reference.dataSetupIndex; + if (index.has_value()) { + indices.insert(*index); + } + } + return indices; +} + +/** @brief Requires one YAML node to be a sequence. */ +static void requireSequence(const std::string& path, const Node& element, const std::string_view& name) +{ + if (!element.IsSequence()) { + fail(path, element, "'" + std::string(name) + "' must be an array"); + } +} + +/** @brief Locates and validates the root trace-run mapping. */ +static Node traceRunRoot(const std::string& path, const Node& document) +{ + if (!document.IsMap()) { + fail(path, document, "expected a YAML map containing 'ctrace-run'"); + } + + const auto root = childNode(document, "ctrace-run"); + if (!root) { + fail(path, document, "missing top-level 'ctrace-run' node"); + } + if (!root.IsMap()) { + fail(path, root, "top-level 'ctrace-run' node must be a map"); + } + return root; +} + +/** @brief Parses scalar or sequence source identifiers from one reference. */ +static std::vector parseSources(const std::string& path, const Node& reference) +{ + const auto sourceNode = childNode(reference, "source"); + if (!sourceNode) { + return {}; + } + if (sourceNode.IsScalar() || sourceNode.IsNull()) { + const auto scalarSource = sourceNode.IsScalar() ? sourceNode.Scalar() : std::string{}; + return {static_cast( + unsignedValue(path, sourceNode, "source", scalarSource, std::numeric_limits::max()))}; + } + + requireSequence(path, sourceNode, "source"); + std::vector sources; + for (const auto& item : sourceNode) { + if (!item.IsScalar() || item.Scalar().empty()) { + fail(path, item, "each 'source' entry must be an unsigned integer"); + } + const auto source = static_cast( + unsignedValue(path, item, "source", item.Scalar(), std::numeric_limits::max())); + sources.push_back(source); + } + return sources; +} + +/** @brief Stores diagnostics copied from one parsed trace reference. */ +struct ReferenceDiagnostics { + std::optional info; + std::optional warning; + std::optional error; +}; + +/** @brief Parses optional informational diagnostics attached to a reference. */ +static ReferenceDiagnostics parseReferenceDiagnostics(const std::string& path, const Node& element) +{ + const auto message = [&](const std::string_view& name) { + const auto value = optionalAttribute(element, name); + if (childContainer(element, name)) { + fail(path, element, "'" + std::string(name) + "' must be a scalar message"); + } + return value; + }; + return { + message("info"), + message("warning"), + message("error"), + }; +} + +/** @brief Parses one relevant trace-run reference. */ +static std::optional parseReference(const std::string& path, const Node& element) +{ + if (!element.IsMap()) { + fail(path, element, "each 'ctrace-refs' entry must be a map"); + } + + const auto requiredScalar = [&](const std::string_view& name) { + const auto node = childNode(element, name); + if (!node || !node.IsScalar() || node.Scalar().empty()) { + fail(path, node ? node : element, "missing required '" + std::string(name) + "' scalar in ctrace-ref entry"); + } + return node.Scalar(); + }; + + TraceRunReference reference; + reference.type = requiredScalar("type"); + if (!TraceRunSchema::consumesReferenceMetadata(reference.type)) { + return std::nullopt; + } + reference.ctraceRef = requiredScalar("ctrace-ref"); + reference.line = lineNumber(element); + if (reference.type == "dwt") { + reference.dataSetupIndex = dwtDataIndex(reference.ctraceRef); + } + + const auto diagnostics = parseReferenceDiagnostics(path, element); + reference.info = diagnostics.info; + reference.warning = diagnostics.warning; + reference.error = diagnostics.error; + + const auto parseStream = [&]() -> std::optional { + if (childContainer(element, "stream")) { + fail(path, element, "'stream' must be a scalar unsigned integer"); + } + const auto stream = optionalUnsignedAttribute(path, element, "stream", std::numeric_limits::max()); + if (!stream.has_value()) { + return std::nullopt; + } + return static_cast(*stream); + }; + + const auto parseRoute = [&]() { + reference.processorName = processorNameAttribute(path, element); + reference.stream = parseStream(); + reference.sources = parseSources(path, element); + if (reference.type == "dwt") { + reference.symbolAddress = + bestEffortUnsignedAttribute(path, element, "symbol-address", std::numeric_limits::max()); + } + reference.label = optionalAttribute(element, "label"); + }; + + if (!TraceRunSchema::supportsSource(reference.type)) { + // These source types currently contribute diagnostics only. Preserve + // a valid processor name for log context, but do not validate fields + // that no ctrace decoder or output consumes. + reference.processorName = bestEffortProcessorName(element); + return reference; + } + + if (diagnostics.error.has_value()) { + auto diagnosticReference = reference; + diagnosticReference.processorName = bestEffortProcessorName(element); + try { + parseRoute(); + if (TraceRunSchema::isUsableReference(reference) || TraceRunSchema::isItmChannelZero(reference)) { + return reference; + } + return diagnosticReference; + } catch (const std::runtime_error&) { + return diagnosticReference; + } + } + + parseRoute(); + return reference; +} + +/** @brief Parses every consumed reference from the trace-run root. */ +static std::vector parseReferences(const std::string& path, const Node& root) +{ + const auto referencesNode = childNode(root, "ctrace-refs"); + if (!referencesNode) { + fail(path, root, "missing required 'ctrace-refs' array"); + } + requireSequence(path, referencesNode, "ctrace-refs"); + + std::vector references; + for (const auto& item : referencesNode) { + auto reference = parseReference(path, item); + if (reference.has_value()) { + references.push_back(std::move(*reference)); + } + } + return references; +} + +/** @brief Parses timestamp metadata from one consumed setup. */ +static std::optional parseTimestampSetup(const std::string& path, const Node& element) +{ + const auto timestampsNode = childNode(element, "timestamps"); + if (!timestampsNode) { + return std::nullopt; + } + if (timestampsNode.IsScalar() || timestampsNode.IsNull()) { + TraceRunTimestampSetup timestamps; + timestamps.line = lineNumber(timestampsNode); + if (timestampsNode.IsScalar() && !timestampsNode.Scalar().empty()) { + timestamps.clockError = "'timestamps' must be empty or a map"; + } + return timestamps; + } + if (!timestampsNode.IsMap()) { + TraceRunTimestampSetup timestamps; + timestamps.line = lineNumber(timestampsNode); + timestamps.clockError = "'timestamps' must be empty or a map"; + return timestamps; + } + + TraceRunTimestampSetup timestamps; + timestamps.line = lineNumber(timestampsNode); + const auto clock = childNode(timestampsNode, "clock"); + if (clock && !clock.IsScalar() && !clock.IsNull()) { + timestamps.clockError = "'timestamps.clock' must be a scalar unsigned integer"; + } else { + timestamps.clockHz = deferredUnsignedAttribute(path, timestampsNode, "clock", + std::numeric_limits::max(), timestamps.clockError); + } + if (childContainer(timestampsNode, "itm-prescaler")) { + fail(path, timestampsNode, "'timestamps.itm-prescaler' must be a scalar unsigned integer"); + } + const auto prescaler = + optionalUnsignedAttribute(path, timestampsNode, "itm-prescaler", std::numeric_limits::max()); + if (prescaler.has_value()) { + timestamps.timestampPrescaler = static_cast(*prescaler); + } + return timestamps; +} + +/** @brief Parses ITM enable-mask metadata from one consumed setup. */ +static std::optional parseItmSetup(const std::string& path, const Node& element) +{ + const auto itmNode = childNode(element, "itm"); + if (!itmNode) { + return std::nullopt; + } + if (!itmNode.IsMap()) { + fail(path, itmNode, "'itm' must be a map containing 'enable'"); + } + const auto enableNode = childNode(itmNode, "enable"); + if (!enableNode || !enableNode.IsScalar() || enableNode.Scalar().empty()) { + fail(path, enableNode ? enableNode : itmNode, "'itm.enable' is required and must be a scalar unsigned integer"); + } + return TraceRunItmSetup{static_cast( + unsignedValue(path, enableNode, "itm.enable", enableNode.Scalar(), std::numeric_limits::max()))}; +} + +/** @brief Parses only DWT data setups referenced by consumed routes. */ +static std::vector parseReferencedDataSetups(const std::string& path, const Node& element, + const std::set& referencedIndices) +{ + const auto dataNode = + referencedIndices.empty() ? Node(YAML::NodeType::Undefined) : childNode(element, "data"); + if (!dataNode || !dataNode.IsSequence()) { + return {}; + } + + std::vector dataSetups; + std::size_t index = 0U; + bool foundReferencedEntry = false; + for (const auto& item : dataNode) { + if (referencedIndices.find(index++) == referencedIndices.end()) { + dataSetups.emplace_back(); + continue; + } + foundReferencedEntry = true; + TraceRunDataSetup data; + if (!item.IsMap()) { + dataSetups.push_back(std::move(data)); + continue; + } + const auto type = childNode(item, "symbol-type"); + if (type && !type.IsScalar() && !type.IsNull()) { + data.symbolTypeError = "'data.symbol-type' must be a scalar string"; + } else if (type && !type.IsNull()) { + data.symbolType = optionalAttribute(item, "symbol-type"); + } + const auto size = childNode(item, "symbol-size"); + if (size && !size.IsScalar() && !size.IsNull()) { + data.symbolSizeError = "'data.symbol-size' must be a scalar unsigned integer"; + } else if (size && !size.IsNull()) { + data.symbolSize = deferredUnsignedAttribute(path, item, "symbol-size", std::numeric_limits::max(), + data.symbolSizeError); + } + dataSetups.push_back(std::move(data)); + } + return foundReferencedEntry ? dataSetups : std::vector{}; +} + +/** @brief Parses one trace setup and its consumed metadata groups. */ +static TraceRunSetup parseSetup(const std::string& path, const Node& element, + const std::vector& references) +{ + TraceRunSetup setup; + setup.line = lineNumber(element); + setup.processorName = processorNameAttribute(path, element); + const auto referencedDataIndices = referencedDataSetupIndices(references, setup.processorName); + setup.timestamps = parseTimestampSetup(path, element); + setup.itm = parseItmSetup(path, element); + setup.data = parseReferencedDataSetups(path, element, referencedDataIndices); + return setup; +} + +/** @brief Parses all trace setups referenced by relevant routes. */ +static std::vector parseSetups(const std::string& path, const Node& root, + const std::vector& references) +{ + const auto setupNode = childNode(root, "ctrace-setup"); + if (!setupNode) { + return {}; + } + requireSequence(path, setupNode, "ctrace-setup"); + + std::vector setups; + for (const auto& item : setupNode) { + if (!item.IsMap()) { + fail(path, item, "each 'ctrace-setup' entry must be a map"); + } + // The copied ctrace.yml setup semantics define the presence of + // 'disable' itself as sufficient to ignore the complete list entry. + // Its value therefore has no schema that ctrace needs to validate. + const auto disable = childNode(item, "disable"); + if (disable) { + continue; + } + auto setup = parseSetup(path, item, references); + if (!setup.timestamps.has_value() && !setup.itm.has_value() && setup.data.empty()) { + continue; + } + setups.push_back(std::move(setup)); + } + return setups; +} + +TraceRunConfig YmlTraceRunConfigReader::read(const std::string& path) const +{ + if (path.empty()) { + throw std::runtime_error("trace-run configuration path is empty"); + } + + std::vector documents; + try { + documents = YAML::LoadAllFromFile(path); + } catch (const YAML::Exception& error) { + std::ostringstream message; + message << "failed to parse trace-run configuration: " << path; + if (error.mark.line >= 0) { + message << '(' << (error.mark.line + 1); + if (error.mark.column >= 0) { + message << ',' << (error.mark.column + 1); + } + message << ')'; + } + message << ": " << error.msg; + throw std::runtime_error(message.str()); + } + if (documents.size() != 1U) { + const auto location = documents.size() > 1U ? documents[1] : Node(YAML::NodeType::Undefined); + fail(path, location, "expected exactly one YAML document"); + } + + const auto root = traceRunRoot(path, documents.front()); + + TraceRunConfig config; + config.path = path; + config.references = parseReferences(path, root); + config.setups = parseSetups(path, root, config.references); + return config; +} diff --git a/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.h b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.h new file mode 100644 index 000000000..bffa321cf --- /dev/null +++ b/tools/ctrace/src/tracerun/YmlTraceRunConfigReader.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_TRACERUN_YMLTRACERUNCONFIGREADER_H +#define CTRACE_SRC_TRACERUN_YMLTRACERUNCONFIGREADER_H + +#include "TraceRunConfig.h" +#include "TraceRunConfigReader.h" + +#include + +/** @brief Reads trace-run configuration files encoded as YAML. */ +class YmlTraceRunConfigReader final : public TraceRunConfigReader { +public: + /** + * @brief Reads and validates the ctrace-relevant YAML fields. + * @param path YAML trace-run configuration path. + * @return Parsed processors, trace sources, and setup metadata. + * @throws std::runtime_error If YAML syntax or required fields are invalid. + */ + TraceRunConfig read(const std::string& path) const override; +}; + +#endif // CTRACE_SRC_TRACERUN_YMLTRACERUNCONFIGREADER_H diff --git a/tools/ctrace/test/CMakeLists.txt b/tools/ctrace/test/CMakeLists.txt index 342a2184d..f6a5e6283 100644 --- a/tools/ctrace/test/CMakeLists.txt +++ b/tools/ctrace/test/CMakeLists.txt @@ -3,3 +3,4 @@ # SPDX-License-Identifier: Apache-2.0 add_subdirectory(unit) +add_subdirectory(integration) diff --git a/tools/ctrace/test/data/.gitattributes b/tools/ctrace/test/data/.gitattributes new file mode 100644 index 000000000..63719c3ba --- /dev/null +++ b/tools/ctrace/test/data/.gitattributes @@ -0,0 +1,5 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +*.csv text eol=lf diff --git a/tools/ctrace/test/data/Arm-reset/Arm.SWO.raw b/tools/ctrace/test/data/Arm-reset/Arm.SWO.raw new file mode 100644 index 000000000..eb997caf2 Binary files /dev/null and b/tools/ctrace/test/data/Arm-reset/Arm.SWO.raw differ diff --git a/tools/ctrace/test/data/Arm-reset/Arm.ctrace-run.yml b/tools/ctrace/test/data/Arm-reset/Arm.ctrace-run.yml new file mode 100644 index 000000000..efeea0951 --- /dev/null +++ b/tools/ctrace/test/data/Arm-reset/Arm.ctrace-run.yml @@ -0,0 +1,28 @@ +ctrace-run: + ctrace-setup: + - timestamps: + clock: 280000000 + itm-prescaler: 1 + ctrace-refs: + - ctrace-ref: timestamps + type: itm + - ctrace-ref: data#0 + type: dwt + source: 0 + symbol-address: 0x2404AEA4 + label: Sine + - ctrace-ref: data#1 + type: dwt + source: 1 + symbol-address: 0x2404AEA0 + label: Rectangle + - ctrace-ref: data#2 + type: dwt + source: 2 + symbol-address: 0x2404AE9C + label: Random + - ctrace-ref: data#3 + type: dwt + source: 3 + symbol-address: 0x24000E88 + label: Current diff --git a/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.SWO.csv b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.SWO.csv new file mode 100644 index 000000000..f62d11e33 --- /dev/null +++ b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.SWO.csv @@ -0,0 +1,532 @@ +cycles,stream,type,source,value,pc,offset,note +74999,,dwt,0,0x000000c2,0x0800b8aa,, +149998,,dwt,0,0x000000c3,0x0800b8aa,, +224997,,dwt,0,0x000000c4,0x0800b8aa,, +299996,,dwt,0,0x000000c5,0x0800b8aa,, +374995,,dwt,0,0x000000c6,0x0800b8aa,, +449994,,dwt,0,0x000000c7,0x0800b8aa,, +524993,,dwt,0,0x000000c8,0x0800b8aa,, +599992,,dwt,0,0x000000c9,0x0800b8aa,, +674996,,dwt,0,0x000000ca,0x0800b8aa,, +749990,,dwt,0,0x000000cb,0x0800b8aa,, +824989,,dwt,0,0x000000cc,0x0800b8aa,, +899988,,dwt,0,0x000000cd,0x0800b8aa,, +974987,,dwt,0,0x000000ce,0x0800b8aa,, +1049986,,dwt,0,0x000000cf,0x0800b8aa,, +1124985,,dwt,0,0x000000d0,0x0800b8aa,, +1199984,,dwt,0,0x000000d1,0x0800b8aa,, +1274983,,dwt,0,0x000000d2,0x0800b8aa,, +1349982,,dwt,0,0x000000d3,0x0800b8aa,, +1424981,,dwt,0,0x000000d4,0x0800b8aa,, +1499980,,dwt,0,0x000000d5,0x0800b8aa,, +1574979,,dwt,0,0x000000d6,0x0800b8aa,, +1649978,,dwt,0,0x000000d7,0x0800b8aa,, +1724977,,dwt,0,0x000000d8,0x0800b8aa,, +1799976,,dwt,0,0x000000d9,0x0800b8aa,, +1874975,,dwt,0,0x000000da,0x0800b8aa,, +1949974,,dwt,0,0x000000db,0x0800b8aa,, +2024973,,dwt,0,0x000000dc,0x0800b8aa,, +2099972,,dwt,0,0x000000dd,0x0800b8aa,, +2174971,,dwt,0,0x000000de,0x0800b8aa,, +2249972,,dwt,0,0x000000df,0x0800b8aa,, +2324969,,dwt,0,0x000000e0,0x0800b8aa,, +2399968,,dwt,0,0x000000e1,0x0800b8aa,, +2474967,,dwt,0,0x000000e2,0x0800b8aa,, +2549966,,dwt,0,0x000000e3,0x0800b8aa,, +2624965,,dwt,0,0x000000e4,0x0800b8aa,, +2699964,,dwt,0,0x000000e5,0x0800b8aa,, +2774963,,dwt,0,0x000000e6,0x0800b8aa,, +2849962,,dwt,0,0x000000e7,0x0800b8aa,, +2924961,,dwt,0,0x000000e8,0x0800b8aa,, +2999960,,dwt,0,0x000000e9,0x0800b8aa,, +3074959,,dwt,0,0x000000ea,0x0800b8aa,, +3149958,,dwt,0,0x000000eb,0x0800b8aa,, +3224957,,dwt,0,0x000000ec,0x0800b8aa,, +3299956,,dwt,0,0x000000ed,0x0800b8aa,, +3374955,,dwt,0,0x000000ee,0x0800b8aa,, +3449954,,dwt,0,0x000000ef,0x0800b8aa,, +3524953,,dwt,0,0x000000f0,0x0800b8aa,, +3599952,,dwt,0,0x000000f1,0x0800b8aa,, +3674951,,dwt,0,0x000000f2,0x0800b8aa,, +3749950,,dwt,0,0x000000f3,0x0800b8aa,, +3824951,,dwt,0,0x000000f4,0x0800b8aa,, +3899948,,dwt,0,0x000000f5,0x0800b8aa,, +3974947,,dwt,0,0x000000f6,0x0800b8aa,, +4049946,,dwt,0,0x000000f7,0x0800b8aa,, +4124945,,dwt,0,0x000000f8,0x0800b8aa,, +4199944,,dwt,0,0x000000f9,0x0800b8aa,, +4274943,,dwt,0,0x000000fa,0x0800b8aa,, +4349942,,dwt,0,0x000000fb,0x0800b8aa,, +4424941,,dwt,0,0x000000fc,0x0800b8aa,, +4499940,,dwt,0,0x000000fd,0x0800b8aa,, +4574939,,dwt,0,0x000000fe,0x0800b8aa,, +4649938,,dwt,0,0x000000ff,0x0800b8aa,, +4724937,,dwt,0,0x00000100,0x0800b8aa,, +4799936,,dwt,0,0x00000101,0x0800b8aa,, +4874935,,dwt,0,0x00000102,0x0800b8aa,, +4949934,,dwt,0,0x00000103,0x0800b8aa,, +5024933,,dwt,0,0x00000104,0x0800b8aa,, +5099932,,dwt,0,0x00000105,0x0800b8aa,, +5174931,,dwt,0,0x00000106,0x0800b8aa,, +5249930,,dwt,0,0x00000107,0x0800b8aa,, +5324929,,dwt,0,0x00000108,0x0800b8aa,, +5399930,,dwt,0,0x00000109,0x0800b8aa,, +5474927,,dwt,0,0x0000010a,0x0800b8aa,, +5549926,,dwt,0,0x0000010b,0x0800b8aa,, +5624925,,dwt,0,0x0000010c,0x0800b8aa,, +5699924,,dwt,0,0x0000010d,0x0800b8aa,, +5774923,,dwt,0,0x0000010e,0x0800b8aa,, +5849922,,dwt,0,0x0000010f,0x0800b8aa,, +5924921,,dwt,0,0x00000110,0x0800b8aa,, +5999920,,dwt,0,0x00000111,0x0800b8aa,, +6074919,,dwt,0,0x00000112,0x0800b8aa,, +6149918,,dwt,0,0x00000113,0x0800b8aa,, +6224917,,dwt,0,0x00000114,0x0800b8aa,, +6299916,,dwt,0,0x00000115,0x0800b8aa,, +6374915,,dwt,0,0x00000116,0x0800b8aa,, +6449914,,dwt,0,0x00000117,0x0800b8aa,, +6524913,,dwt,0,0x00000118,0x0800b8aa,, +6599912,,dwt,0,0x00000119,0x0800b8aa,, +6674911,,dwt,0,0x0000011a,0x0800b8aa,, +6749910,,dwt,0,0x0000011b,0x0800b8aa,, +6824909,,dwt,0,0x0000011c,0x0800b8aa,, +6899908,,dwt,0,0x0000011d,0x0800b8aa,, +6974909,,dwt,0,0x0000011e,0x0800b8aa,, +7049906,,dwt,0,0x0000011f,0x0800b8aa,, +7124905,,dwt,0,0x00000120,0x0800b8aa,, +7199904,,dwt,0,0x00000121,0x0800b8aa,, +7274903,,dwt,0,0x00000122,0x0800b8aa,, +7349902,,dwt,0,0x00000123,0x0800b8aa,, +7424901,,dwt,0,0x00000124,0x0800b8aa,, +7424901,,dwt,0,0x00000915,0x0800b8aa,, +7499900,,dwt,0,0x00000916,0x0800b8aa,, +7574899,,dwt,0,0x00000917,0x0800b8aa,, +7649898,,dwt,0,0x00000918,0x0800b8aa,, +7724897,,dwt,0,0x00000919,0x0800b8aa,, +7799896,,dwt,0,0x0000091a,0x0800b8aa,, +7874895,,dwt,0,0x0000091b,0x0800b8aa,, +7949894,,dwt,0,0x0000091c,0x0800b8aa,, +8024893,,dwt,0,0x0000091d,0x0800b8aa,, +8099892,,dwt,0,0x0000091e,0x0800b8aa,, +8174891,,dwt,0,0x0000091f,0x0800b8aa,, +8249890,,dwt,0,0x00000920,0x0800b8aa,, +8324889,,dwt,0,0x00000921,0x0800b8aa,, +8399888,,dwt,0,0x00000922,0x0800b8aa,, +8474887,,dwt,0,0x00000923,0x0800b8aa,, +8549886,,dwt,0,0x00000924,0x0800b8aa,, +8624885,,dwt,0,0x00000925,0x0800b8aa,, +8699884,,dwt,0,0x00000926,0x0800b8aa,, +8774883,,dwt,0,0x00000927,0x0800b8aa,, +8849884,,dwt,0,0x00000928,0x0800b8aa,, +8924881,,dwt,0,0x00000929,0x0800b8aa,, +8999880,,dwt,0,0x0000092a,0x0800b8aa,, +9074879,,dwt,0,0x0000092b,0x0800b8aa,, +9149878,,dwt,0,0x0000092c,0x0800b8aa,, +9224877,,dwt,0,0x0000092d,0x0800b8aa,, +9299876,,dwt,0,0x0000092e,0x0800b8aa,, +9374875,,dwt,0,0x0000092f,0x0800b8aa,, +9449874,,dwt,0,0x00000930,0x0800b8aa,, +9524873,,dwt,0,0x00000931,0x0800b8aa,, +9599872,,dwt,0,0x00000932,0x0800b8aa,, +9674871,,dwt,0,0x00000933,0x0800b8aa,, +9749870,,dwt,0,0x00000934,0x0800b8aa,, +9824869,,dwt,0,0x00000935,0x0800b8aa,, +9899868,,dwt,0,0x00000936,0x0800b8aa,, +9974867,,dwt,0,0x00000937,0x0800b8aa,, +10049866,,dwt,0,0x00000938,0x0800b8aa,, +10124865,,dwt,0,0x00000939,0x0800b8aa,, +10199864,,dwt,0,0x0000093a,0x0800b8aa,, +10274863,,dwt,0,0x0000093b,0x0800b8aa,, +10349862,,dwt,0,0x0000093c,0x0800b8aa,, +10424863,,dwt,0,0x0000093d,0x0800b8aa,, +10499860,,dwt,0,0x0000093e,0x0800b8aa,, +10574859,,dwt,0,0x0000093f,0x0800b8aa,, +10649858,,dwt,0,0x00000940,0x0800b8aa,, +10724857,,dwt,0,0x00000941,0x0800b8aa,, +10799856,,dwt,0,0x00000942,0x0800b8aa,, +10874855,,dwt,0,0x00000943,0x0800b8aa,, +10949854,,dwt,0,0x00000944,0x0800b8aa,, +11024853,,dwt,0,0x00000945,0x0800b8aa,, +11099852,,dwt,0,0x00000946,0x0800b8aa,, +11174851,,dwt,0,0x00000947,0x0800b8aa,, +11249850,,dwt,0,0x00000948,0x0800b8aa,, +11324849,,dwt,0,0x00000949,0x0800b8aa,, +11399848,,dwt,0,0x0000094a,0x0800b8aa,, +11474847,,dwt,0,0x0000094b,0x0800b8aa,, +11549846,,dwt,0,0x0000094c,0x0800b8aa,, +11624845,,dwt,0,0x0000094d,0x0800b8aa,, +11699844,,dwt,0,0x0000094e,0x0800b8aa,, +11774843,,dwt,0,0x0000094f,0x0800b8aa,, +11849842,,dwt,0,0x00000950,0x0800b8aa,, +11924841,,dwt,0,0x00000951,0x0800b8aa,, +11999842,,dwt,0,0x00000952,0x0800b8aa,, +12074839,,dwt,0,0x00000953,0x0800b8aa,, +12149838,,dwt,0,0x00000954,0x0800b8aa,, +12224837,,dwt,0,0x00000955,0x0800b8aa,, +12299836,,dwt,0,0x00000956,0x0800b8aa,, +12374835,,dwt,0,0x00000957,0x0800b8aa,, +12449834,,dwt,0,0x00000958,0x0800b8aa,, +12524833,,dwt,0,0x00000959,0x0800b8aa,, +12599832,,dwt,0,0x0000095a,0x0800b8aa,, +12674831,,dwt,0,0x0000095b,0x0800b8aa,, +12749830,,dwt,0,0x0000095c,0x0800b8aa,, +12824829,,dwt,0,0x0000095d,0x0800b8aa,, +12899828,,dwt,0,0x0000095e,0x0800b8aa,, +12974827,,dwt,0,0x0000095f,0x0800b8aa,, +13049826,,dwt,0,0x00000960,0x0800b8aa,, +13124825,,dwt,0,0x00000961,0x0800b8aa,, +13199829,,dwt,0,0x00000962,0x0800b8aa,, +13274823,,dwt,0,0x00000963,0x0800b8aa,, +13349822,,dwt,0,0x00000964,0x0800b8aa,, +13424821,,dwt,0,0x00000965,0x0800b8aa,, +13499820,,dwt,0,0x00000966,0x0800b8aa,, +13574819,,dwt,0,0x00000967,0x0800b8aa,, +13649818,,dwt,0,0x00000968,0x0800b8aa,, +13724817,,dwt,0,0x00000969,0x0800b8aa,, +13799816,,dwt,0,0x0000096a,0x0800b8aa,, +13874815,,dwt,0,0x0000096b,0x0800b8aa,, +13949814,,dwt,0,0x0000096c,0x0800b8aa,, +14024813,,dwt,0,0x0000096d,0x0800b8aa,, +14099812,,dwt,0,0x0000096e,0x0800b8aa,, +14174811,,dwt,0,0x0000096f,0x0800b8aa,, +14249810,,dwt,0,0x00000970,0x0800b8aa,, +14324809,,dwt,0,0x00000971,0x0800b8aa,, +14399808,,dwt,0,0x00000972,0x0800b8aa,, +14474807,,dwt,0,0x00000973,0x0800b8aa,, +14549806,,dwt,0,0x00000974,0x0800b8aa,, +14624805,,dwt,0,0x00000975,0x0800b8aa,, +14699804,,dwt,0,0x00000976,0x0800b8aa,, +14774805,,dwt,0,0x00000977,0x0800b8aa,, +14849802,,dwt,0,0x00000978,0x0800b8aa,, +14924801,,dwt,0,0x00000979,0x0800b8aa,, +14999800,,dwt,0,0x0000097a,0x0800b8aa,, +15074799,,dwt,0,0x0000097b,0x0800b8aa,, +15149798,,dwt,0,0x0000097c,0x0800b8aa,, +15224797,,dwt,0,0x0000097d,0x0800b8aa,, +15299796,,dwt,0,0x0000097e,0x0800b8aa,, +15374795,,dwt,0,0x0000097f,0x0800b8aa,, +15449794,,dwt,0,0x00000980,0x0800b8aa,, +15524793,,dwt,0,0x00000981,0x0800b8aa,, +15599792,,dwt,0,0x00000982,0x0800b8aa,, +15674791,,dwt,0,0x00000983,0x0800b8aa,, +15749790,,dwt,0,0x00000984,0x0800b8aa,, +15824789,,dwt,0,0x00000985,0x0800b8aa,, +15899788,,dwt,0,0x00000986,0x0800b8aa,, +15974787,,dwt,0,0x00000987,0x0800b8aa,, +16049786,,dwt,0,0x00000988,0x0800b8aa,, +16124785,,dwt,0,0x00000989,0x0800b8aa,, +16199784,,dwt,0,0x0000098a,0x0800b8aa,, +16274783,,dwt,0,0x0000098b,0x0800b8aa,, +16349784,,dwt,0,0x0000098c,0x0800b8aa,, +16424781,,dwt,0,0x0000098d,0x0800b8aa,, +16499780,,dwt,0,0x0000098e,0x0800b8aa,, +16574779,,dwt,0,0x0000098f,0x0800b8aa,, +16649778,,dwt,0,0x00000990,0x0800b8aa,, +16724777,,dwt,0,0x00000991,0x0800b8aa,, +16799776,,dwt,0,0x00000992,0x0800b8aa,, +16874775,,dwt,0,0x00000993,0x0800b8aa,, +16949774,,dwt,0,0x00000994,0x0800b8aa,, +17024773,,dwt,0,0x00000995,0x0800b8aa,, +17099772,,dwt,0,0x00000996,0x0800b8aa,, +17174771,,dwt,0,0x00000997,0x0800b8aa,, +17249770,,dwt,0,0x00000998,0x0800b8aa,, +17324769,,dwt,0,0x00000999,0x0800b8aa,, +17399768,,dwt,0,0x0000099a,0x0800b8aa,, +17474767,,dwt,0,0x0000099b,0x0800b8aa,, +17549766,,dwt,0,0x0000099c,0x0800b8aa,, +17624765,,dwt,0,0x0000099d,0x0800b8aa,, +17699764,,dwt,0,0x0000099e,0x0800b8aa,, +17774763,,dwt,0,0x0000099f,0x0800b8aa,, +17849762,,dwt,0,0x000009a0,0x0800b8aa,, +17924763,,dwt,0,0x000009a1,0x0800b8aa,, +17999760,,dwt,0,0x000009a2,0x0800b8aa,, +18074759,,dwt,0,0x000009a3,0x0800b8aa,, +18149758,,dwt,0,0x000009a4,0x0800b8aa,, +18224757,,dwt,0,0x000009a5,0x0800b8aa,, +18299756,,dwt,0,0x000009a6,0x0800b8aa,, +18374755,,dwt,0,0x000009a7,0x0800b8aa,, +18449754,,dwt,0,0x000009a8,0x0800b8aa,, +18524753,,dwt,0,0x000009a9,0x0800b8aa,, +18599752,,dwt,0,0x000009aa,0x0800b8aa,, +18674751,,dwt,0,0x000009ab,0x0800b8aa,, +18749750,,dwt,0,0x000009ac,0x0800b8aa,, +18824749,,dwt,0,0x000009ad,0x0800b8aa,, +18899748,,dwt,0,0x000009ae,0x0800b8aa,, +18974747,,dwt,0,0x000009af,0x0800b8aa,, +19049746,,dwt,0,0x000009b0,0x0800b8aa,, +19124745,,dwt,0,0x000009b1,0x0800b8aa,, +19199744,,dwt,0,0x000009b2,0x0800b8aa,, +19274743,,dwt,0,0x000009b3,0x0800b8aa,, +19349742,,dwt,0,0x000009b4,0x0800b8aa,, +19424741,,dwt,0,0x000009b5,0x0800b8aa,, +19499742,,dwt,0,0x000009b6,0x0800b8aa,, +19574739,,dwt,0,0x000009b7,0x0800b8aa,, +19649738,,dwt,0,0x000009b8,0x0800b8aa,, +19724737,,dwt,0,0x000009b9,0x0800b8aa,, +19799736,,dwt,0,0x000009ba,0x0800b8aa,, +19874735,,dwt,0,0x000009bb,0x0800b8aa,, +19949734,,dwt,0,0x000009bc,0x0800b8aa,, +20024733,,dwt,0,0x000009bd,0x0800b8aa,, +20099732,,dwt,0,0x000009be,0x0800b8aa,, +20174731,,dwt,0,0x000009bf,0x0800b8aa,, +20249730,,dwt,0,0x000009c0,0x0800b8aa,, +20324729,,dwt,0,0x000009c1,0x0800b8aa,, +20399728,,dwt,0,0x000009c2,0x0800b8aa,, +20474727,,dwt,0,0x000009c3,0x0800b8aa,, +20549726,,dwt,0,0x000009c4,0x0800b8aa,, +20624725,,dwt,0,0x000009c5,0x0800b8aa,, +20699729,,dwt,0,0x000009c6,0x0800b8aa,, +20774723,,dwt,0,0x000009c7,0x0800b8aa,, +20849722,,dwt,0,0x000009c8,0x0800b8aa,, +20924721,,dwt,0,0x000009c9,0x0800b8aa,, +20999720,,dwt,0,0x000009ca,0x0800b8aa,, +21074719,,dwt,0,0x000009cb,0x0800b8aa,, +21149718,,dwt,0,0x000009cc,0x0800b8aa,, +21224717,,dwt,0,0x000009cd,0x0800b8aa,, +21299716,,dwt,0,0x000009ce,0x0800b8aa,, +21374715,,dwt,0,0x000009cf,0x0800b8aa,, +21449714,,dwt,0,0x000009d0,0x0800b8aa,, +21524713,,dwt,0,0x000009d1,0x0800b8aa,, +21599712,,dwt,0,0x000009d2,0x0800b8aa,, +21674711,,dwt,0,0x000009d3,0x0800b8aa,, +21749710,,dwt,0,0x000009d4,0x0800b8aa,, +21824709,,dwt,0,0x000009d5,0x0800b8aa,, +21899708,,dwt,0,0x000009d6,0x0800b8aa,, +21974707,,dwt,0,0x000009d7,0x0800b8aa,, +22049706,,dwt,0,0x000009d8,0x0800b8aa,, +22124705,,dwt,0,0x000009d9,0x0800b8aa,, +22199704,,dwt,0,0x000009da,0x0800b8aa,, +22274705,,dwt,0,0x000009db,0x0800b8aa,, +22349702,,dwt,0,0x000009dc,0x0800b8aa,, +22424701,,dwt,0,0x000009dd,0x0800b8aa,, +22499700,,dwt,0,0x000009de,0x0800b8aa,, +22574699,,dwt,0,0x000009df,0x0800b8aa,, +22649698,,dwt,0,0x000009e0,0x0800b8aa,, +22724697,,dwt,0,0x000009e1,0x0800b8aa,, +22799696,,dwt,0,0x000009e2,0x0800b8aa,, +22874695,,dwt,0,0x000009e3,0x0800b8aa,, +22949694,,dwt,0,0x000009e4,0x0800b8aa,, +23024693,,dwt,0,0x000009e5,0x0800b8aa,, +23099692,,dwt,0,0x000009e6,0x0800b8aa,, +23174691,,dwt,0,0x000009e7,0x0800b8aa,, +23249690,,dwt,0,0x000009e8,0x0800b8aa,, +23324689,,dwt,0,0x000009e9,0x0800b8aa,, +23399688,,dwt,0,0x000009ea,0x0800b8aa,, +23474687,,dwt,0,0x000009eb,0x0800b8aa,, +23549686,,dwt,0,0x000009ec,0x0800b8aa,, +23624685,,dwt,0,0x000009ed,0x0800b8aa,, +23699684,,dwt,0,0x000009ee,0x0800b8aa,, +23774683,,dwt,0,0x000009ef,0x0800b8aa,, +23849684,,dwt,0,0x000009f0,0x0800b8aa,, +23924681,,dwt,0,0x000009f1,0x0800b8aa,, +23999680,,dwt,0,0x000009f2,0x0800b8aa,, +24074679,,dwt,0,0x000009f3,0x0800b8aa,, +24149678,,dwt,0,0x000009f4,0x0800b8aa,, +24224677,,dwt,0,0x000009f5,0x0800b8aa,, +24299676,,dwt,0,0x000009f6,0x0800b8aa,, +24374675,,dwt,0,0x000009f7,0x0800b8aa,, +24449674,,dwt,0,0x000009f8,0x0800b8aa,, +24524673,,dwt,0,0x000009f9,0x0800b8aa,, +24599672,,dwt,0,0x000009fa,0x0800b8aa,, +24674671,,dwt,0,0x000009fb,0x0800b8aa,, +24749670,,dwt,0,0x000009fc,0x0800b8aa,, +24824669,,dwt,0,0x000009fd,0x0800b8aa,, +24899668,,dwt,0,0x000009fe,0x0800b8aa,, +24974667,,dwt,0,0x000009ff,0x0800b8aa,, +25049666,,dwt,0,0x00000a00,0x0800b8aa,, +25124665,,dwt,0,0x00000a01,0x0800b8aa,, +25199664,,dwt,0,0x00000a02,0x0800b8aa,, +25274663,,dwt,0,0x00000a03,0x0800b8aa,, +25349662,,dwt,0,0x00000a04,0x0800b8aa,, +25424663,,dwt,0,0x00000a05,0x0800b8aa,, +25499660,,dwt,0,0x00000a06,0x0800b8aa,, +25574659,,dwt,0,0x00000a07,0x0800b8aa,, +25649658,,dwt,0,0x00000a08,0x0800b8aa,, +25724657,,dwt,0,0x00000a09,0x0800b8aa,, +25799656,,dwt,0,0x00000a0a,0x0800b8aa,, +25874655,,dwt,0,0x00000a0b,0x0800b8aa,, +25949654,,dwt,0,0x00000a0c,0x0800b8aa,, +26024653,,dwt,0,0x00000a0d,0x0800b8aa,, +26099652,,dwt,0,0x00000a0e,0x0800b8aa,, +26174651,,dwt,0,0x00000a0f,0x0800b8aa,, +26249650,,dwt,0,0x00000a10,0x0800b8aa,, +26324649,,dwt,0,0x00000a11,0x0800b8aa,, +26399648,,dwt,0,0x00000a12,0x0800b8aa,, +26474647,,dwt,0,0x00000a13,0x0800b8aa,, +26549646,,dwt,0,0x00000a14,0x0800b8aa,, +26624645,,dwt,0,0x00000a15,0x0800b8aa,, +26699644,,dwt,0,0x00000a16,0x0800b8aa,, +26774643,,dwt,0,0x00000a17,0x0800b8aa,, +26849642,,dwt,0,0x00000a18,0x0800b8aa,, +26924641,,dwt,0,0x00000a19,0x0800b8aa,, +26999642,,dwt,0,0x00000a1a,0x0800b8aa,, +27074639,,dwt,0,0x00000a1b,0x0800b8aa,, +27149638,,dwt,0,0x00000a1c,0x0800b8aa,, +27224637,,dwt,0,0x00000a1d,0x0800b8aa,, +27299636,,dwt,0,0x00000a1e,0x0800b8aa,, +27374635,,dwt,0,0x00000a1f,0x0800b8aa,, +27449634,,dwt,0,0x00000a20,0x0800b8aa,, +27524633,,dwt,0,0x00000a21,0x0800b8aa,, +27599632,,dwt,0,0x00000a22,0x0800b8aa,, +27674631,,dwt,0,0x00000a23,0x0800b8aa,, +27749630,,dwt,0,0x00000a24,0x0800b8aa,, +27824629,,dwt,0,0x00000a25,0x0800b8aa,, +27899628,,dwt,0,0x00000a26,0x0800b8aa,, +27974627,,dwt,0,0x00000a27,0x0800b8aa,, +28049626,,dwt,0,0x00000a28,0x0800b8aa,, +28124625,,dwt,0,0x00000a29,0x0800b8aa,, +28199629,,dwt,0,0x00000a2a,0x0800b8aa,, +28274623,,dwt,0,0x00000a2b,0x0800b8aa,, +28349622,,dwt,0,0x00000a2c,0x0800b8aa,, +28424621,,dwt,0,0x00000a2d,0x0800b8aa,, +28499620,,dwt,0,0x00000a2e,0x0800b8aa,, +28574619,,dwt,0,0x00000a2f,0x0800b8aa,, +28649618,,dwt,0,0x00000a30,0x0800b8aa,, +28724617,,dwt,0,0x00000a31,0x0800b8aa,, +28799616,,dwt,0,0x00000a32,0x0800b8aa,, +28874615,,dwt,0,0x00000a33,0x0800b8aa,, +28949614,,dwt,0,0x00000a34,0x0800b8aa,, +29024613,,dwt,0,0x00000a35,0x0800b8aa,, +29099612,,dwt,0,0x00000a36,0x0800b8aa,, +29174611,,dwt,0,0x00000a37,0x0800b8aa,, +29249610,,dwt,0,0x00000a38,0x0800b8aa,, +29249610,,dwt,0,,0xb8aa47aa,, +29249610,,error,,,,,OpenCSD detected an invalid ITM packet sequence at raw offset 8195. +29249610,,error,,,,,OpenCSD consumed 2179 raw bytes while waiting for usable ITM trace packets; data loss until a later sync/recovery point; timestamp 29249610 .. 29324609. +29324609,,dwt,0,0x00000ebd,0x0800b8aa,, +29399608,,dwt,0,0x00000ebe,0x0800b8aa,, +29474607,,dwt,0,0x00000ebf,0x0800b8aa,, +29549606,,dwt,0,0x00000ec0,0x0800b8aa,, +29624605,,dwt,0,0x00000ec1,0x0800b8aa,, +29699604,,dwt,0,0x00000ec2,0x0800b8aa,, +29774603,,dwt,0,0x00000ec3,0x0800b8aa,, +29849602,,dwt,0,0x00000ec4,0x0800b8aa,, +29924601,,dwt,0,0x00000ec5,0x0800b8aa,, +29999600,,dwt,0,0x00000ec6,0x0800b8aa,, +30074599,,dwt,0,0x00000ec7,0x0800b8aa,, +30149598,,dwt,0,0x00000ec8,0x0800b8aa,, +30224597,,dwt,0,0x00000ec9,0x0800b8aa,, +30299598,,dwt,0,0x00000eca,0x0800b8aa,, +30374595,,dwt,0,0x00000ecb,0x0800b8aa,, +30449594,,dwt,0,0x00000ecc,0x0800b8aa,, +30524593,,dwt,0,0x00000ecd,0x0800b8aa,, +30599592,,dwt,0,0x00000ece,0x0800b8aa,, +30674591,,dwt,0,0x00000ecf,0x0800b8aa,, +30749590,,dwt,0,0x00000ed0,0x0800b8aa,, +30824589,,dwt,0,0x00000ed1,0x0800b8aa,, +30899588,,dwt,0,0x00000ed2,0x0800b8aa,, +30974587,,dwt,0,0x00000ed3,0x0800b8aa,, +31049586,,dwt,0,0x00000ed4,0x0800b8aa,, +31124585,,dwt,0,0x00000ed5,0x0800b8aa,, +31199584,,dwt,0,0x00000ed6,0x0800b8aa,, +31274583,,dwt,0,0x00000ed7,0x0800b8aa,, +31349582,,dwt,0,0x00000ed8,0x0800b8aa,, +31424581,,dwt,0,0x00000ed9,0x0800b8aa,, +31499585,,dwt,0,0x00000eda,0x0800b8aa,, +31574579,,dwt,0,0x00000edb,0x0800b8aa,, +31649578,,dwt,0,0x00000edc,0x0800b8aa,, +31724577,,dwt,0,0x00000edd,0x0800b8aa,, +31799576,,dwt,0,0x00000ede,0x0800b8aa,, +31874575,,dwt,0,0x00000edf,0x0800b8aa,, +31949574,,dwt,0,0x00000ee0,0x0800b8aa,, +32024573,,dwt,0,0x00000ee1,0x0800b8aa,, +32099572,,dwt,0,0x00000ee2,0x0800b8aa,, +32174571,,dwt,0,0x00000ee3,0x0800b8aa,, +32249570,,dwt,0,0x00000ee4,0x0800b8aa,, +32324569,,dwt,0,0x00000ee5,0x0800b8aa,, +32399568,,dwt,0,0x00000ee6,0x0800b8aa,, +32474567,,dwt,0,0x00000ee7,0x0800b8aa,, +32549566,,dwt,0,0x00000ee8,0x0800b8aa,, +32624565,,dwt,0,0x00000ee9,0x0800b8aa,, +32699564,,dwt,0,0x00000eea,0x0800b8aa,, +32774563,,dwt,0,0x00000eeb,0x0800b8aa,, +32849562,,dwt,0,0x00000eec,0x0800b8aa,, +32924561,,dwt,0,0x00000eed,0x0800b8aa,, +32999560,,dwt,0,0x00000eee,0x0800b8aa,, +33074561,,dwt,0,0x00000eef,0x0800b8aa,, +33149558,,dwt,0,0x00000ef0,0x0800b8aa,, +33224557,,dwt,0,0x00000ef1,0x0800b8aa,, +33299556,,dwt,0,0x00000ef2,0x0800b8aa,, +33374555,,dwt,0,0x00000ef3,0x0800b8aa,, +33449554,,dwt,0,0x00000ef4,0x0800b8aa,, +33524553,,dwt,0,0x00000ef5,0x0800b8aa,, +33599552,,dwt,0,0x00000ef6,0x0800b8aa,, +33674551,,dwt,0,0x00000ef7,0x0800b8aa,, +33749550,,dwt,0,0x00000ef8,0x0800b8aa,, +33824549,,dwt,0,0x00000ef9,0x0800b8aa,, +33899548,,dwt,0,0x00000efa,0x0800b8aa,, +33974547,,dwt,0,0x00000efb,0x0800b8aa,, +34049546,,dwt,0,0x00000efc,0x0800b8aa,, +34124545,,dwt,0,0x00000efd,0x0800b8aa,, +34199544,,dwt,0,0x00000efe,0x0800b8aa,, +34274543,,dwt,0,0x00000eff,0x0800b8aa,, +34349542,,dwt,0,0x00000f00,0x0800b8aa,, +34424541,,dwt,0,0x00000f01,0x0800b8aa,, +34499540,,dwt,0,0x00000f02,0x0800b8aa,, +34574539,,dwt,0,0x00000f03,0x0800b8aa,, +34649540,,dwt,0,0x00000f04,0x0800b8aa,, +34724537,,dwt,0,0x00000f05,0x0800b8aa,, +34799536,,dwt,0,0x00000f06,0x0800b8aa,, +34874535,,dwt,0,0x00000f07,0x0800b8aa,, +34949534,,dwt,0,0x00000f08,0x0800b8aa,, +35024533,,dwt,0,0x00000f09,0x0800b8aa,, +35099532,,dwt,0,0x00000f0a,0x0800b8aa,, +35174531,,dwt,0,0x00000f0b,0x0800b8aa,, +35249530,,dwt,0,0x00000f0c,0x0800b8aa,, +35324529,,dwt,0,0x00000f0d,0x0800b8aa,, +35399528,,dwt,0,0x00000f0e,0x0800b8aa,, +35474527,,dwt,0,0x00000f0f,0x0800b8aa,, +35549526,,dwt,0,0x00000f10,0x0800b8aa,, +35624525,,dwt,0,0x00000f11,0x0800b8aa,, +35699524,,dwt,0,0x00000f12,0x0800b8aa,, +35774523,,dwt,0,0x00000f13,0x0800b8aa,, +35849522,,dwt,0,0x00000f14,0x0800b8aa,, +35924521,,dwt,0,0x00000f15,0x0800b8aa,, +35999520,,dwt,0,0x00000f16,0x0800b8aa,, +36074519,,dwt,0,0x00000f17,0x0800b8aa,, +36149518,,dwt,0,0x00000f18,0x0800b8aa,, +36224519,,dwt,0,0x00000f19,0x0800b8aa,, +36299516,,dwt,0,0x00000f1a,0x0800b8aa,, +36374515,,dwt,0,0x00000f1b,0x0800b8aa,, +36449514,,dwt,0,0x00000f1c,0x0800b8aa,, +36524513,,dwt,0,0x00000f1d,0x0800b8aa,, +36599512,,dwt,0,0x00000f1e,0x0800b8aa,, +36674511,,dwt,0,0x00000f1f,0x0800b8aa,, +36749510,,dwt,0,0x00000f20,0x0800b8aa,, +36824509,,dwt,0,0x00000f21,0x0800b8aa,, +36899508,,dwt,0,0x00000f22,0x0800b8aa,, +36974507,,dwt,0,0x00000f23,0x0800b8aa,, +37049506,,dwt,0,0x00000f24,0x0800b8aa,, +37124505,,dwt,0,0x00000f25,0x0800b8aa,, +37199504,,dwt,0,0x00000f26,0x0800b8aa,, +37274503,,dwt,0,0x00000f27,0x0800b8aa,, +37349502,,dwt,0,0x00000f28,0x0800b8aa,, +37424501,,dwt,0,0x00000f29,0x0800b8aa,, +37499500,,dwt,0,0x00000f2a,0x0800b8aa,, +37574499,,dwt,0,0x00000f2b,0x0800b8aa,, +37649498,,dwt,0,0x00000f2c,0x0800b8aa,, +37724497,,dwt,0,0x00000f2d,0x0800b8aa,, +37799498,,dwt,0,0x00000f2e,0x0800b8aa,, +37874495,,dwt,0,0x00000f2f,0x0800b8aa,, +37949494,,dwt,0,0x00000f30,0x0800b8aa,, +38024493,,dwt,0,0x00000f31,0x0800b8aa,, +38099492,,dwt,0,0x00000f32,0x0800b8aa,, +38174491,,dwt,0,0x00000f33,0x0800b8aa,, +38249490,,dwt,0,0x00000f34,0x0800b8aa,, +38324489,,dwt,0,0x00000f35,0x0800b8aa,, +38399488,,dwt,0,0x00000f36,0x0800b8aa,, +38474487,,dwt,0,0x00000f37,0x0800b8aa,, +38549486,,dwt,0,0x00000f38,0x0800b8aa,, +38624485,,dwt,0,0x00000f39,0x0800b8aa,, +38699484,,dwt,0,0x00000f3a,0x0800b8aa,, +38774483,,dwt,0,0x00000f3b,0x0800b8aa,, +38849482,,dwt,0,0x00000f3c,0x0800b8aa,, +38924481,,dwt,0,0x00000f3d,0x0800b8aa,, +38999485,,dwt,0,0x00000f3e,0x0800b8aa,, +39074479,,dwt,0,0x00000f3f,0x0800b8aa,, +39149478,,dwt,0,0x00000f40,0x0800b8aa,, +39224477,,dwt,0,0x00000f41,0x0800b8aa,, +39299476,,dwt,0,0x00000f42,0x0800b8aa,, +39374475,,dwt,0,0x00000f43,0x0800b8aa,, +39449474,,dwt,0,0x00000f44,0x0800b8aa,, +39449474,,error,,,,,incomplete ITM packet at end of input diff --git a/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.SWO.raw b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.SWO.raw new file mode 100644 index 000000000..182805e47 Binary files /dev/null and b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.SWO.raw differ diff --git a/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.TB.raw b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.TB.raw new file mode 100644 index 000000000..a3ea92c8f Binary files /dev/null and b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.TB.raw differ diff --git a/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.ctrace-run.yml b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.ctrace-run.yml new file mode 100644 index 000000000..e54084523 --- /dev/null +++ b/tools/ctrace/test/data/Blinky+Arm/Blinky+Arm.ctrace-run.yml @@ -0,0 +1,59 @@ +ctrace-run: + generated-by: pyTS v0.1.0 + created-by: CMSIS-Debugger v1.4.0 + ctrace-setup: + - pname: CM7 + data: + - location: Blinky_cm7|osRtxInfo.kernel.tick + access: W + output: PC+value + symbol-file: Blinky_cm7.axf + symbol: osRtxInfo.kernel.tick + address: 0x24000024 + size: 4 + type: unsigned int + synchronization: + DWT: 16M + timestamps: + itm-prescaler: 1 + - pname: CM4 + disable: + timestamps: + itm-prescaler: 1 + data: [] + exceptions: + pcsampling: + period: 16384 + ctrace-refs: + - ctrace-ref: CM7/timestamps + type: dwt + pname: CM7 + regs: + - name: ITM_TCR + value: 0x00000003 + mask: 0x00000303 + - ctrace-ref: CM7/data#0 + type: dwt + pname: CM7 + symbol-file: Blinky_cm7.axf + symbol-address: 0x24000024 + regs: + - name: DWT_COMP0 + value: 0x24000024 + - name: DWT_MASK0 + value: 0x00000002 + - name: DWT_FUNCTION0 + value: 0x0000000f + - name: ITM_TCR + value: 0x00000009 + mask: 0x00000009 + - ctrace-ref: CM7/synchronization#0 + type: dwt + pname: CM7 + regs: + - name: DWT_CTRL + value: 0x00000400 + mask: 0x00000c00 + - name: ITM_TCR + value: 0x00000005 + mask: 0x00000005 diff --git a/tools/ctrace/test/data/README.md b/tools/ctrace/test/data/README.md new file mode 100644 index 000000000..5271d3657 --- /dev/null +++ b/tools/ctrace/test/data/README.md @@ -0,0 +1,46 @@ + + +# ctrace Test Data + +This directory contains the versioned YAML inputs and raw trace captures used +by the reader and executable-level tests. Generated CSV/CTF outputs stay in the +build tree and are not versioned, except for reference output used by an exact +comparison test. + +The Blinky fixture is stored under the generic `Blinky+Arm` target name. It was +captured from a CMSIS project with CMSIS-Debugger 1.4.0 and pyTS 0.1.0, as +recorded in the accompanying `ctrace-run` file. It contains SWO and TB input. +The integration test compares the generated SWO CSV byte-for-byte with its +reference and verifies that TB is reported as a trace channel that is not +implemented yet. + +The Blinky YAML, SWO capture, and TB capture are approved ctrace test assets and +may be redistributed as part of Open-CMSIS-Pack/devtools. The reference CSV is +derived from the SWO capture and is covered by the same approval and the +repository-wide Apache-2.0 license terms. + +The approved Blinky fixture set is identified by these SHA-256 values: + +- SWO capture: `f2de14241242697fa0948f1878850cce81575c404233c5c135aa68fc582dc72c` +- TB capture: `b0fccabe1a326ffe9fadf12d5c3a205d87628985e5e75a99da23c97d7f33d13b` +- Derived CSV: `6138cc60deee8bc16a8a889a6d9156ed76f389c4831afafc5125e4a0d00074cc` +- Trace-run YAML: `deef176a7a924a9c24a126ea460994e86afee3d216e6839da515296758797966` + +The `Arm-reset` fixture is an approved excerpt of an Arm target capture. It +starts at the hardware ITM sync immediately before an MCU-reset discontinuity +that OpenCSD reports as an invalid packet sequence. The integration test +verifies that ctrace discards the damaged interval, finds the next hardware ITM +sync, and continues decoding DWT events. The bounded excerpt keeps Debug tests +portable across CI platforms. The trace-run YAML retains only metadata needed +by the test. + +- SWO capture: `8c7ba2b90e42188517c7b793e8b7dd4030fa5455b7a38a2de15d8ca2b47995c9` +- Trace-run YAML: `455c28a490c771d8960b1c5f44785ff2deb05cd6e3a640251c0c7b78f09c5ed1` + +`trace-run` contains only the small current-schema inputs needed by executable +tests. Reader unit tests cover only the fields consumed by ctrace. A C++ +entry-point test creates a reviewable eight-byte ITM stream below the build tree +and verifies all output formats without an external fixture generator. diff --git a/tools/ctrace/test/data/trace-run/Board.ctrace-run.yml b/tools/ctrace/test/data/trace-run/Board.ctrace-run.yml new file mode 100644 index 000000000..e15f866dc --- /dev/null +++ b/tools/ctrace/test/data/trace-run/Board.ctrace-run.yml @@ -0,0 +1,20 @@ +ctrace-run: + ctrace-setup: + - timestamps: + clock: 400000000 + itm-prescaler: 1 + itm: + enable: 0xFFFFFFFE + ctrace-refs: + - ctrace-ref: opaque/printf-route + type: itm + info: configured ITM channel + stream: 1 + source: 0 + - ctrace-ref: data#0 + type: dwt + info: configured DWT data trace + warning: aligned DWT range + label: sample counter + stream: 1 + source: [0, 1] diff --git a/tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml b/tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml new file mode 100644 index 000000000..32a229b45 --- /dev/null +++ b/tools/ctrace/test/data/trace-run/Minimal.ctrace-run.yml @@ -0,0 +1,5 @@ +ctrace-run: + ctrace-setup: + - timestamps: + clock: 400000000 + ctrace-refs: [] diff --git a/tools/ctrace/test/integration/CMakeLists.txt b/tools/ctrace/test/integration/CMakeLists.txt new file mode 100644 index 000000000..ee0b30048 --- /dev/null +++ b/tools/ctrace/test/integration/CMakeLists.txt @@ -0,0 +1,89 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +add_executable(CtraceIntegTests + src/CtraceIntegTests.cpp +) + +set_property(TARGET CtraceIntegTests PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" +) +set_property(TARGET CtraceIntegTests PROPERTY + VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" +) + +target_compile_definitions(CtraceIntegTests PRIVATE + CTRACE_TEST_DATA_DIR="${PROJECT_SOURCE_DIR}/test/data" + CTRACE_TEST_OUTPUT_DIR="${CMAKE_CURRENT_BINARY_DIR}/testoutput" +) +target_link_libraries(CtraceIntegTests PRIVATE + ctracelib + gtest_main +) + +# The executable-level smoke tests below exercise the platform trampoline. +add_dependencies(CtraceIntegTests ctrace) + +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/test_reports") +add_test( + NAME CtraceIntegTests + COMMAND CtraceIntegTests + --gtest_output=xml:test_reports/ctraceintegtests-report-${SYSTEM}-${CPU_ARCH}.xml + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" +) +set_tests_properties(CtraceIntegTests PROPERTIES + LABELS "integration" + TIMEOUT 120 +) + +add_test( + NAME ctrace-version + COMMAND ctrace --version +) + +set(CTRACE_VERSION_REGEX "${PROJECT_VERSION_FULL}") +string(REPLACE "." "\\." CTRACE_VERSION_REGEX "${CTRACE_VERSION_REGEX}") +string(REPLACE "+" "\\+" CTRACE_VERSION_REGEX "${CTRACE_VERSION_REGEX}") +set_tests_properties(ctrace-version PROPERTIES + PASS_REGULAR_EXPRESSION "^ctrace ${CTRACE_VERSION_REGEX}[\r\n]+$" +) + +add_test( + NAME ctrace-no-args-shows-help + COMMAND ctrace +) +set_tests_properties(ctrace-no-args-shows-help PROPERTIES + PASS_REGULAR_EXPRESSION "ctrace \\[options\\]" +) + +add_test( + NAME ctrace-help + COMMAND ctrace --help +) +set_tests_properties(ctrace-help PROPERTIES + PASS_REGULAR_EXPRESSION "ctrace \\[options\\]" +) + +set_tests_properties( + ctrace-version + ctrace-no-args-shows-help + ctrace-help + PROPERTIES + LABELS "integration" + TIMEOUT 30 +) + +add_test( + NAME ctrace-windows-manifest + COMMAND "${CMAKE_COMMAND}" + "-DCTRACE_EXECUTABLE=$" + "-DMANIFEST_FILE=${PROJECT_SOURCE_DIR}/src/ctrace.manifest" + "-DMT_EXECUTABLE=${CMAKE_MT}" + "-DEMBEDDED_MANIFEST_FILE=${CMAKE_CURRENT_BINARY_DIR}/ctrace-embedded.manifest" + -P "${CMAKE_CURRENT_SOURCE_DIR}/src/ValidateWindowsManifest.cmake" +) +set_tests_properties(ctrace-windows-manifest PROPERTIES + LABELS "integration" + TIMEOUT 30 +) diff --git a/tools/ctrace/test/integration/README.md b/tools/ctrace/test/integration/README.md new file mode 100644 index 000000000..a9cb07e7a --- /dev/null +++ b/tools/ctrace/test/integration/README.md @@ -0,0 +1,17 @@ + + +# ctrace Integration Tests + +`CtraceIntegTests` calls the `CtraceMain` application entry point linked from the +same object library as the `ctrace` executable. It exercises file-oriented +workflows with fixtures from `test/data` and writes generated output only under +the CMake build directory. Small CTest smoke tests separately cover the platform +executable and Windows manifest. + +The complete Blinky fixture covers exact CSV output and discovery of +unsupported Trace Bus input. The versioned Arm target capture covers recovery +across a real MCU-reset trace discontinuity and verifies that DWT decoding +continues after the next hardware ITM sync. diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp new file mode 100644 index 000000000..48de3c7e9 --- /dev/null +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtraceMain.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct RunResult { + int exitCode; + std::string stderrText; +}; + +class CtraceIntegTests : public testing::Test { +protected: + void SetUp() override + { + const auto* testInfo = testing::UnitTest::GetInstance()->current_test_info(); + m_workDirectory = std::filesystem::path{CTRACE_TEST_OUTPUT_DIR} / testInfo->name(); + std::error_code error; + std::filesystem::remove_all(m_workDirectory, error); + ASSERT_FALSE(error) << error.message(); + ASSERT_TRUE(std::filesystem::create_directories(m_workDirectory)) << m_workDirectory; + } + + static const std::filesystem::path& testDataDirectory() + { + static const std::filesystem::path path{CTRACE_TEST_DATA_DIR}; + return path; + } + + const std::filesystem::path& workDirectory() const + { + return m_workDirectory; + } + + void copyFixtureFile(const std::filesystem::path& fixtureDirectory, const std::string& fileName) const + { + const auto source = fixtureDirectory / fileName; + ASSERT_TRUE(std::filesystem::is_regular_file(source)) << source; + std::error_code error; + const auto copied = std::filesystem::copy_file(source, m_workDirectory / fileName, + std::filesystem::copy_options::overwrite_existing, error); + ASSERT_TRUE(copied) << source << ": " << error.message(); + } + + RunResult run(std::vector arguments) const + { + testing::internal::CaptureStderr(); + const auto exitCode = CtraceMain(arguments); + return {exitCode, testing::internal::GetCapturedStderr()}; + } + +private: + std::filesystem::path m_workDirectory; +}; + +void writeFile(const std::filesystem::path& path, const std::string& contents = {}) +{ + std::ofstream output(path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(output) << path; + output.write(contents.data(), static_cast(contents.size())); + ASSERT_TRUE(output) << path; +} + +std::string readTextFile(const std::filesystem::path& path) +{ + std::ifstream input(path, std::ios::binary); + if (!input) { + throw std::runtime_error("failed to read test file: " + path.string()); + } + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +std::vector readBinaryFile(const std::filesystem::path& path) +{ + std::ifstream input(path, std::ios::binary); + if (!input) { + throw std::runtime_error("failed to read binary test file: " + path.string()); + } + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +void expectNonEmptyFile(const std::filesystem::path& path) +{ + ASSERT_TRUE(std::filesystem::is_regular_file(path)) << path; + EXPECT_GT(std::filesystem::file_size(path), 0U) << path; +} + +void expectContains(std::string_view text, std::string_view expected) +{ + EXPECT_NE(std::string_view::npos, text.find(expected)) << expected << "\n" << text; +} + +void expectNotContains(std::string_view text, std::string_view unexpected) +{ + EXPECT_EQ(std::string_view::npos, text.find(unexpected)) << unexpected << "\n" << text; +} + +TEST_F(CtraceIntegTests, GeneratesAllOutputs) +{ + writeFile(workDirectory() / "Minimal.ctrace-run.yml", R"yml(ctrace-run: + ctrace-setup: + - timestamps: + clock: 400000000 + ctrace-refs: [] +)yml"); + + const std::string raw{"\0\0\0\0\0\x80\x09\x41", 8U}; + writeFile(workDirectory() / "Minimal.SWO.raw", raw); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Minimal", "--all"}); + EXPECT_EQ(0, result.exitCode) << result.stderrText; + EXPECT_EQ("cycles,stream,type,source,value,pc,offset,note\n" + "0,,itm,1,0x41,,,\n", + readTextFile(workDirectory() / "Minimal.SWO.csv")); + expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "metadata"); + expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "stream_0"); + expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); +} + +TEST_F(CtraceIntegTests, RejectsInvalidOptionCombination) +{ + const auto result = run({"ctrace", "--version", "--type", "DWT"}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "[error] Invalid --type value: DWT"); +} + +TEST_F(CtraceIntegTests, ReportsMissingTraceDirectory) +{ + const auto missingDirectory = workDirectory() / "missing"; + const auto result = run({"ctrace", missingDirectory.string()}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "[error] trace directory not found:"); +} + +TEST_F(CtraceIntegTests, AppliesTraceRunConfiguration) +{ + const auto fixtureDirectory = testDataDirectory() / "trace-run"; + copyFixtureFile(fixtureDirectory, "Board.ctrace-run.yml"); + writeFile(workDirectory() / "Board.SWO.raw"); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Board"}); + EXPECT_EQ(0, result.exitCode) << result.stderrText; + expectContains(result.stderrText, "aligned DWT range"); + expectContains(result.stderrText, "configured DWT data trace"); + expectContains(result.stderrText, "applied ctrace-run meta"); + expectNotContains(result.stderrText, "configured ITM channel"); +} + +TEST_F(CtraceIntegTests, GeneratesRequestedOutputsAfterDecoderError) +{ + const auto fixtureDirectory = testDataDirectory() / "trace-run"; + copyFixtureFile(fixtureDirectory, "Minimal.ctrace-run.yml"); + writeFile(workDirectory() / "Minimal.SWO.raw", std::string{"\x01\x41", 2U}); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Minimal", "--all"}); + EXPECT_EQ(1, result.exitCode); + expectContains(result.stderrText, "[info] decoded 1 events from 2 bytes"); + + const auto csvPath = workDirectory() / "Minimal.SWO.csv"; + expectNonEmptyFile(csvPath); + expectContains(readTextFile(csvPath), ",error,"); + expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "metadata"); + expectNonEmptyFile(workDirectory() / "Minimal.ctf" / "stream_0"); + expectNonEmptyFile(workDirectory() / "Minimal.SWO.traceanalysis.xml"); +} + +TEST_F(CtraceIntegTests, ConvertsBlinkyFixtureAndSkipsUnsupportedTraceBusInput) +{ + const auto fixtureDirectory = testDataDirectory() / "Blinky+Arm"; + copyFixtureFile(fixtureDirectory, "Blinky+Arm.SWO.raw"); + copyFixtureFile(fixtureDirectory, "Blinky+Arm.TB.raw"); + copyFixtureFile(fixtureDirectory, "Blinky+Arm.ctrace-run.yml"); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Blinky+Arm", "--csv"}); + EXPECT_EQ(1, result.exitCode) << result.stderrText; + expectContains(result.stderrText, "skipping raw trace channel that is not implemented yet:"); + expectContains(result.stderrText, "channel=TB"); + EXPECT_EQ(readTextFile(fixtureDirectory / "Blinky+Arm.SWO.csv"), + readTextFile(workDirectory() / "Blinky+Arm.SWO.csv")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Blinky+Arm.TB.csv")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Blinky+Arm.TB.traceanalysis.xml")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Blinky+Arm.TB.ctf")); +} + +TEST_F(CtraceIntegTests, RecoversAtHardwareSyncAfterResetDiscontinuity) +{ + const auto fixtureDirectory = testDataDirectory() / "Arm-reset"; + copyFixtureFile(fixtureDirectory, "Arm.SWO.raw"); + copyFixtureFile(fixtureDirectory, "Arm.ctrace-run.yml"); + + const auto raw = readBinaryFile(workDirectory() / "Arm.SWO.raw"); + ASSERT_EQ(131071U, raw.size()); + constexpr std::array hardwareSync{0U, 0U, 0U, 0U, 0U, 0x80U}; + for (const auto offset : {0U, 128U}) { + ASSERT_LE(offset + hardwareSync.size(), raw.size()); + EXPECT_TRUE(std::equal(hardwareSync.begin(), hardwareSync.end(), raw.begin() + offset)) << offset; + } + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Arm", "--csv"}); + EXPECT_EQ(1, result.exitCode) << result.stderrText; + expectContains(result.stderrText, "[error] invalid ITM packet sequence at raw offset 10"); + expectContains(result.stderrText, + "[error] 116 raw bytes from raw offset 12 could not be decoded before the next hardware ITM sync"); + expectContains(result.stderrText, "[info] decoded 52374 events from 131071 bytes"); + expectNotContains(result.stderrText, "OpenCSD made no progress"); + expectNotContains(result.stderrText, "OpenCSD made no decode progress"); + expectNotContains(result.stderrText, "decode aborted"); + + const auto csv = readTextFile(workDirectory() / "Arm.SWO.csv"); + expectNotContains(csv, ",itm,"); + const auto recoveryError = csv.find("0,,error,,,,,OpenCSD detected an invalid ITM packet sequence at raw offset 10."); + const auto dataLoss = + csv.find("0,,error,,,,,OpenCSD consumed 116 raw bytes while waiting for usable ITM trace packets"); + const auto firstResumedEvent = csv.find("271773258,,dwt,0,0x00,,,"); + const auto lateResumedEvent = csv.find("425766493,,dwt,0,0x2a,,,"); + ASSERT_NE(std::string::npos, recoveryError); + ASSERT_NE(std::string::npos, dataLoss); + ASSERT_NE(std::string::npos, firstResumedEvent); + ASSERT_NE(std::string::npos, lateResumedEvent); + EXPECT_LT(recoveryError, dataLoss); + EXPECT_LT(dataLoss, firstResumedEvent); + EXPECT_LT(firstResumedEvent, lateResumedEvent); +} + +} // namespace diff --git a/tools/ctrace/test/integration/src/ValidateWindowsManifest.cmake b/tools/ctrace/test/integration/src/ValidateWindowsManifest.cmake new file mode 100644 index 000000000..a0d879e19 --- /dev/null +++ b/tools/ctrace/test/integration/src/ValidateWindowsManifest.cmake @@ -0,0 +1,57 @@ +# Copyright (c) 2026 Arm Limited. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +function(validate_manifest manifest_file description) + if(NOT EXISTS "${manifest_file}") + message(FATAL_ERROR "Missing ${description}: ${manifest_file}") + endif() + + file(READ "${manifest_file}" manifest) + if(NOT manifest MATCHES + "[ \t\r\n]*true[ \t\r\n]*") + message(FATAL_ERROR + "${description} does not enable long-path awareness: ${manifest_file}") + endif() + if(NOT manifest MATCHES + "xmlns:ws2=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\"") + message(FATAL_ERROR + "${description} uses no recognized long-path namespace: ${manifest_file}") + endif() +endfunction() + +if(NOT DEFINED MANIFEST_FILE) + message(FATAL_ERROR "Missing -DMANIFEST_FILE=...") +endif() +validate_manifest("${MANIFEST_FILE}" "Windows manifest source") + +if(WIN32) + foreach(required_variable CTRACE_EXECUTABLE MT_EXECUTABLE EMBEDDED_MANIFEST_FILE) + if(NOT DEFINED ${required_variable} OR "${${required_variable}}" STREQUAL "") + message(FATAL_ERROR "Missing -D${required_variable}=...") + endif() + endforeach() + if(NOT EXISTS "${CTRACE_EXECUTABLE}") + message(FATAL_ERROR "Missing ctrace executable: ${CTRACE_EXECUTABLE}") + endif() + if(NOT EXISTS "${MT_EXECUTABLE}") + message(FATAL_ERROR "Missing Windows manifest tool: ${MT_EXECUTABLE}") + endif() + + file(REMOVE "${EMBEDDED_MANIFEST_FILE}") + execute_process( + COMMAND "${MT_EXECUTABLE}" + -nologo + "-inputresource:${CTRACE_EXECUTABLE};#1" + "-out:${EMBEDDED_MANIFEST_FILE}" + RESULT_VARIABLE mt_result + OUTPUT_VARIABLE mt_stdout + ERROR_VARIABLE mt_stderr + ) + if(NOT mt_result EQUAL 0) + message(FATAL_ERROR + "Failed to extract the embedded ctrace manifest (${mt_result}):\n" + "${mt_stdout}${mt_stderr}") + endif() + validate_manifest("${EMBEDDED_MANIFEST_FILE}" "Embedded ctrace manifest") +endif() diff --git a/tools/ctrace/test/unit/CMakeLists.txt b/tools/ctrace/test/unit/CMakeLists.txt index 7bd28ef9b..05aff6161 100644 --- a/tools/ctrace/test/unit/CMakeLists.txt +++ b/tools/ctrace/test/unit/CMakeLists.txt @@ -2,15 +2,70 @@ # # SPDX-License-Identifier: Apache-2.0 -add_executable(CtraceUnitTests src/CtraceMainTest.cpp) +set(CTRACE_TEST_HEADER_FILES + support/CtfTestSupport.h + support/OpenCsdSessionTestSupport.h + support/OpenCsdTestSupport.h + support/TestPath.h + support/TestPlatform.h + support/TestSupport.h + support/TraceOutputTestSupport.h + support/TraceRunTestSupport.h +) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" + PREFIX "Header Files" + FILES ${CTRACE_TEST_HEADER_FILES} +) -set_property(TARGET CtraceUnitTests PROPERTY - MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -set_property(TARGET CtraceUnitTests PROPERTY - VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(CTRACE_TEST_RETARGET_SOURCE_FILE support/retarget/windows/TestPlatform.cpp) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(CTRACE_TEST_RETARGET_SOURCE_FILE support/retarget/linux/TestPlatform.cpp) +else() + set(CTRACE_TEST_RETARGET_SOURCE_FILE support/retarget/posix/TestPlatform.cpp) +endif() -target_link_libraries(CtraceUnitTests PRIVATE ctracelib gtest_main) +add_executable(CtraceUnitTests + src/cli/CliParserTests.cpp + src/control/DecodeConsumersTests.cpp + src/control/TraceDirectoryJobTests.cpp + src/decode/CortexMPostDecoderTests.cpp + src/decode/DecodePipelineTests.cpp + src/decode/DwtPacketDecoderTests.cpp + src/decode/OpenCsdErrorControllerTests.cpp + src/decode/OpenCsdItmDecoderTests.cpp + src/decode/OpenCsdPacketCollectorTests.cpp + src/diagnostics/DiagnosticsTests.cpp + src/model/TraceSelectionTests.cpp + src/output/OutputRequirementsTests.cpp + src/output/TraceOutputLifecycleTests.cpp + src/output/csv/CsvFileOutputTests.cpp + src/output/csv/CsvRowMapperTests.cpp + src/output/ctf/CtfBundleOutputTests.cpp + src/output/ctf/CtfEncoderTests.cpp + src/output/ctf/CtfMetadataWriterTests.cpp + src/output/ctf/CtfSchemaTests.cpp + src/output/ctf/CtfStreamWriterTests.cpp + src/tracerun/CtraceRunMetaTests.cpp + src/tracerun/TraceRunConfigReaderTests.cpp + src/tracerun/TraceRunDiscoveryTests.cpp + ${CTRACE_TEST_RETARGET_SOURCE_FILE} + ${CTRACE_TEST_HEADER_FILES} +) -add_test(NAME CtraceUnitTests - COMMAND CtraceUnitTests --gtest_output=xml:test_reports/ctraceunittests-report-${SYSTEM}-${CPU_ARCH}.xml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +target_link_libraries(CtraceUnitTests PRIVATE + ctracelib + gtest_main +) + +target_include_directories(CtraceUnitTests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/support" +) + +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/test_reports") +add_test( + NAME CtraceUnitTests + COMMAND CtraceUnitTests + --gtest_output=xml:test_reports/ctraceunittests-report-${SYSTEM}-${CPU_ARCH}.xml + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" +) diff --git a/tools/ctrace/test/unit/src/CtraceMainTest.cpp b/tools/ctrace/test/unit/src/CtraceMainTest.cpp deleted file mode 100644 index a06c9fda7..000000000 --- a/tools/ctrace/test/unit/src/CtraceMainTest.cpp +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2026 Arm Limited. All rights reserved. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "gtest/gtest.h" - -#include "CtraceMain.h" - -TEST(CtraceMainTest, PrintsExecutableName) { - testing::internal::CaptureStdout(); - - EXPECT_EQ(0, CtraceMain()); - - EXPECT_EQ("ctrace Executable\n", testing::internal::GetCapturedStdout()); -} diff --git a/tools/ctrace/test/unit/src/cli/CliParserTests.cpp b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp new file mode 100644 index 000000000..ae5ac7936 --- /dev/null +++ b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include +#include "CliOptions.h" +#include "CliParser.h" +#include +#include +#include +#include +#include +#include + +/** @brief Parses a compact command-line argument list. */ +static CliOptions parse(std::initializer_list arguments) +{ + return CliParser::parse(std::vector(arguments.begin(), arguments.end())); +} + +/** @brief Parses and validates a compact command-line argument list. */ +static CliOptions parseAndValidate(std::initializer_list arguments) +{ + auto options = parse(arguments); + CliParser::validate(options); + return options; +} + +/** @brief Tests whether CLI validation reports expected text. */ +static bool validationErrorContains(std::initializer_list arguments, std::string_view expected) +{ + return throwsWithMessage( + [&arguments] { + const auto options = parse(arguments); + CliParser::validate(options); + }, + expected); +} + +/** @brief Tests whether parsing the supplied command line fails. */ +static bool parseFails(std::initializer_list arguments) +{ + return throwsException([&arguments] { (void)parse(arguments); }); +} + +/** @brief Requires a command line to select the expected output format. */ +static void expectOutputFormat(std::initializer_list arguments, OutputFormat expected) +{ + EXPECT_EQ(parseAndValidate(arguments).outputFormat, expected); +} + +TEST(CtraceUnitTests, testCliParserOutputOptions) +{ + const auto all = parseAndValidate( + {"ctrace", ".trace", "--all", "--target", "Board.Debug", "--type", "dwt", "itm", "--stream", "1", "2"}); + ASSERT_TRUE(all.traceDir == std::optional(".trace")) << "CliParser trace directory mismatch"; + ASSERT_TRUE(all.targetName == std::optional("Board.Debug")) << "CliParser target mismatch"; + ASSERT_TRUE(all.outputFormat == OutputFormat::All) << "CliParser all output mismatch"; + ASSERT_TRUE(all.selection.types == std::vector({"dwt", "itm"})) << "CliParser type filter mismatch"; + ASSERT_TRUE(all.selection.streams == std::vector({1U, 2U})) << "CliParser stream filter mismatch"; + + const auto futureTypes = parseAndValidate({"ctrace", ".trace", "--type", "event", "pmu", "pcsample"}); + ASSERT_TRUE(futureTypes.selection.types == std::vector({"event", "pmu", "pcsample"})) + << "CliParser must accept specified packet types before their output semantics are implemented"; + + expectOutputFormat({"ctrace", ".trace"}, OutputFormat::None); + expectOutputFormat({"ctrace", ".trace", "--csv"}, OutputFormat::Csv); + expectOutputFormat({"ctrace", ".trace", "--ctf"}, OutputFormat::Ctf); + expectOutputFormat({"ctrace", ".trace", "-a"}, OutputFormat::All); + expectOutputFormat({"ctrace", ".trace", "--csv", "--ctf"}, OutputFormat::All); + + const auto version = parseAndValidate({"ctrace", "-V"}); + ASSERT_TRUE(version.version) << "CliParser short version alias mismatch"; + + const auto help = parseAndValidate({"ctrace", "--help"}); + ASSERT_TRUE(help.help) << "CliParser help option mismatch"; + const auto shortHelp = parseAndValidate({"ctrace", "-h"}); + ASSERT_TRUE(shortHelp.help) << "CliParser short help alias mismatch"; + const auto helpText = CliParser::helpString(); + ASSERT_TRUE(helpText.find("--help") == std::string::npos) << "CliParser help text must hide --help"; + for (const auto expected : { + "Generate only CSV output", + "Generate only CTF / Trace Compass XML output", + "Generate CSV and CTF / XML output", + "Filter output for specific packet types (default: all)", + "Filter output for specific streams (default: all)", + "Specify a trace solution-set (default: all)", + "Print version", + }) { + ASSERT_TRUE(helpText.find(expected) != std::string::npos) << "CliParser help text differs from the specification"; + } + + const auto trailingTraceDirectory = + parseAndValidate({"ctrace", "--type", "dwt", "itm", "--stream", "1", "2", "--all", "--", ".trace"}); + ASSERT_TRUE(trailingTraceDirectory.traceDir == std::optional(".trace")) + << "CliParser must accept a delimited trace directory after selector options"; + ASSERT_TRUE(trailingTraceDirectory.selection.types == std::vector({"dwt", "itm"}) && + trailingTraceDirectory.selection.streams == std::vector({1U, 2U})) + << "CliParser trailing trace directory must preserve selector values"; +} + +TEST(CtraceUnitTests, testCliParserRejectsInvalidSelections) +{ + ASSERT_TRUE(validationErrorContains({"ctrace", "-V", "--type", "DWT"}, "Invalid --type value: DWT")) + << "--version must not bypass packet type validation"; + + ASSERT_TRUE(validationErrorContains({"ctrace"}, "Specify ")) + << "CliParser should require a trace directory"; + + ASSERT_TRUE(validationErrorContains({"ctrace", ".trace", "--type", "invalid"}, "Invalid --type value")) + << "CliParser should reject invalid packet types"; + ASSERT_TRUE(validationErrorContains({"ctrace", ".trace", "--type", "dwt", "invalid", "--all"}, + "Invalid --type value: invalid")) + << "CliParser should reject an invalid selector after a valid selector"; + + ASSERT_TRUE(validationErrorContains({"ctrace", ".trace", "--type", "DWT"}, "Invalid --type value: DWT")) + << "CliParser should enforce case-sensitive packet type names"; + + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--type=dwt,event"})) + << "CliParser should accept only the specified space-separated selectors"; +} + +TEST(CtraceUnitTests, testCliParserStreamRangeAndUnknownOptions) +{ + const auto traceBusZero = parse({"ctrace", ".trace", "--stream", "0"}); + ASSERT_TRUE(traceBusZero.selection.streams == std::vector({0U})) + << "CliParser must accept Trace Bus ID 0 for unformatted input"; + + for (const auto* stream : {"112", "255"}) { + ASSERT_TRUE(validationErrorContains({"ctrace", ".trace", "--stream", stream}, "CoreSight Trace Bus ID")) + << std::string("CliParser accepted invalid Trace Bus ID ") + stream; + } + + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--unknown"})) << "CliParser should reject unknown options"; + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--stream", "invalid"})) + << "CliParser should reject non-numeric stream IDs"; + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--stream", "1x"})) + << "CliParser should reject trailing stream ID characters"; + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--stream", "999999999999999999999"})) + << "CliParser should reject overflowing stream IDs"; + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--type="})) << "CliParser should reject an empty inline selector"; + ASSERT_TRUE(parseFails({"ctrace", ".trace", "--type"})) << "CliParser should require at least one selector value"; + ASSERT_TRUE(parseFails({"ctrace", ".trace", "unexpected"})) << "CliParser should reject a second positional argument"; +} diff --git a/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp b/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp new file mode 100644 index 000000000..3da63b9b3 --- /dev/null +++ b/tools/ctrace/test/unit/src/control/DecodeConsumersTests.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include "TraceOutputTestSupport.h" +#include "DecodeConsumers.h" +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceOutput.h" + +#include + +#include +#include +#include +#include + +using TraceOutputTestSupport::TestTraceOutput; + +/** @brief Records diagnostic delivery in a shared lifecycle call sequence. */ +class OrderingDiagnosticSink final : public DiagnosticSink { +public: + /** @brief Creates a sink that appends to the supplied call sequence. */ + explicit OrderingDiagnosticSink(std::vector& calls) + : m_calls(calls) + { + } + +protected: + /** @brief Records one diagnostic delivery. */ + void write(const Event&) override + { + m_calls.push_back("diagnostic"); + } + +private: + std::vector& m_calls; +}; + +TEST(CtraceUnitTests, testDecodeConsumersForwardsWarningsAndFailsOnErrorsWithOutputs) +{ + std::vector calls; + std::vector> outputs; + outputs.push_back(std::make_unique(calls)); + OrderingDiagnosticSink diagnostics(calls); + DecodeConsumers consumers(std::move(outputs), diagnostics); + + TraceEvent warning = issuePacket(TraceIssueCode::OpenCsdDecodeError, "decoder warning", TraceIssueSeverity::Warning); + consumers.append(warning); + EXPECT_EQ((std::vector{"start", "write", "diagnostic"}), calls); + EXPECT_EQ(1U, consumers.eventCount()); + EXPECT_EQ(0U, diagnostics.failureCount()); + + TraceEvent error = warning; + auto& errorIssue = std::get(error.payload); + errorIssue.severity = TraceIssueSeverity::Error; + errorIssue.code = TraceIssueCode::OpenCsdDecodeError; + errorIssue.message = "decoder error"; + consumers.append(error); + EXPECT_EQ((std::vector{"start", "write", "diagnostic", "write", "diagnostic"}), calls); + EXPECT_EQ(2U, consumers.eventCount()); + EXPECT_EQ(1U, diagnostics.failureCount()); + + consumers.abortOutputs(); + EXPECT_EQ((std::vector{"start", "write", "diagnostic", "write", "diagnostic", "abort"}), calls); +} + +TEST(CtraceUnitTests, testDecodeConsumersWarnsForDisabledItmChannelsOnce) +{ + CollectingDiagnosticSink unknownDiagnostics; + DecodeConsumers unknownConsumers({}, unknownDiagnostics); + unknownConsumers.append(softwarePacket(3U)); + EXPECT_TRUE(unknownDiagnostics.events().empty()); + + CollectingDiagnosticSink diagnostics; + DecodeConsumers consumers({}, diagnostics, 0x00000002U, {{2U, 0x00000004U}}); + + auto enabled = softwarePacket(1U); + consumers.append(enabled); + + auto disabled = softwarePacket(2U); + consumers.append(disabled); + consumers.append(disabled); + + auto printfChannel = softwarePacket(0U); + consumers.append(printfChannel); + + auto invalidChannel = softwarePacket(32U); + consumers.append(invalidChannel); + + auto streamSpecificDisabled = softwarePacket(1U); + streamSpecificDisabled.traceBusId = 2U; + consumers.append(streamSpecificDisabled); + + auto streamSpecificEnabled = softwarePacket(2U); + streamSpecificEnabled.traceBusId = 2U; + consumers.append(streamSpecificEnabled); + + ASSERT_EQ(2U, diagnostics.events().size()); + for (const auto& event : diagnostics.events()) { + EXPECT_EQ(DiagnosticSink::Severity::Warning, event.severity); + EXPECT_EQ(DiagnosticSink::Impact::NonFailing, event.impact); + } +} diff --git a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp new file mode 100644 index 000000000..b87479925 --- /dev/null +++ b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdSessionTestSupport.h" +#include "TestPath.h" +#include "TestPlatform.h" +#include "TestSupport.h" +#include "TraceRunTestSupport.h" +#include +#include "CliOptions.h" +#include "CtraceRunMeta.h" +#include "DiagnosticSink.h" +#include "FileDecodeJob.h" +#include "TraceDirectoryJob.h" +#include "TraceRunConfig.h" +#include "TraceRunConfigReader.h" +#include "opencsd/ocsd_if_types.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Creates the default trace-run configuration used by directory tests. */ +static TraceRunConfig defaultTraceRunConfig() +{ + TraceRunConfig config; + config.setups.push_back(TraceRunTestSupport::makeTimestampSetup(std::nullopt, 100000000U)); + return config; +} + +/** @brief Supplies deterministic trace-run configurations to directory-job tests. */ +class TestTraceRunConfigReader final : public TraceRunConfigReader { +public: + /** @brief Creates a reader with fixed data and an optional failure mode. */ + explicit TestTraceRunConfigReader(TraceRunConfig config = defaultTraceRunConfig(), bool fail = false) + : m_config(std::move(config)), + m_fail(fail) + { + } + + /** @brief Records the path and returns or rejects the configured data. */ + TraceRunConfig read(const std::string& path) const override + { + m_paths.push_back(path); + if (m_fail) { + throw std::runtime_error("synthetic config failure"); + } + auto config = m_config; + config.path = path; + return config; + } + + /** @brief Returns every configuration path requested from this reader. */ + const std::vector& paths() const + { + return m_paths; + } + +private: + mutable std::vector m_paths; + TraceRunConfig m_config; + bool m_fail = false; +}; + +/** @brief Writes trace-run and raw-input pairs for selected test targets. */ +static void writeTraceInputs(const std::filesystem::path& traceDirectory, + std::initializer_list targetNames) +{ + for (const auto targetName : targetNames) { + const auto target = std::string(targetName); + writeTestFile(traceDirectory / (target + ".ctrace-run.yml"), "ctrace-run:\n"); + writeTestFile(traceDirectory / (target + ".SWO.raw")); + } +} + +TEST(CtraceUnitTests, testTraceDirectoryTargetAndOutputNames) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-job-test"); + const auto& root = temporaryPath.path(); + const auto traceDir = root / ".trace"; + writeTraceInputs(traceDir, {"Alpha", "Beta"}); + + CliOptions options; + options.traceDir = traceDir.string(); + options.targetName = "Alpha"; + options.outputFormat = OutputFormat::All; + + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader; + TraceDirectoryJob job(options, diagnostics, reader); + const auto checkpoint = diagnostics.failureCount(); + job.run(); + + ASSERT_TRUE(diagnostics.failureCount() == checkpoint) << "TraceDirectoryJob target run failed"; + ASSERT_TRUE(reader.paths().size() == 1U) << "TraceDirectoryJob should read one selected YAML file"; + ASSERT_TRUE(std::filesystem::path(reader.paths()[0]).filename() == "Alpha.ctrace-run.yml") + << "TraceDirectoryJob reader path mismatch"; + ASSERT_TRUE(std::filesystem::is_regular_file(traceDir / "Alpha.SWO.csv")) + << "TraceDirectoryJob CSV output name mismatch"; + ASSERT_TRUE(std::filesystem::is_regular_file(traceDir / "Alpha.ctf" / "metadata")) + << "TraceDirectoryJob CTF output name mismatch"; + ASSERT_TRUE(std::filesystem::is_regular_file(traceDir / "Alpha.SWO.traceanalysis.xml")) + << "TraceDirectoryJob XML output name mismatch"; + ASSERT_TRUE(!std::filesystem::exists(traceDir / "Beta.SWO.csv")) + << "TraceDirectoryJob should not process unselected target"; +} + +TEST(CtraceUnitTests, testTraceDirectoryBatchCheckAndExplicitConfig) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-check-test"); + const auto& root = temporaryPath.path(); + const auto traceDir = root / ".trace"; + writeTraceInputs(traceDir, {"Alpha", "Beta"}); + + CliOptions batchOptions; + batchOptions.traceDir = traceDir.string(); + + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader batchReader; + TraceDirectoryJob batchJob(batchOptions, diagnostics, batchReader); + const auto batchCheckpoint = diagnostics.failureCount(); + batchJob.run(); + + ASSERT_TRUE(diagnostics.failureCount() == batchCheckpoint) << "TraceDirectoryJob batch check failed"; + ASSERT_TRUE(batchReader.paths().size() == 2U) << "TraceDirectoryJob batch should read every trace-run file"; + ASSERT_TRUE(!std::filesystem::exists(traceDir / "Alpha.SWO.csv")) << "check-only batch should not create CSV output"; + ASSERT_TRUE(!std::filesystem::exists(traceDir / "Alpha.ctf")) << "check-only batch should not create CTF output"; + + writeTestFile(traceDir / "Broken.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Broken.SWO.raw", std::string{static_cast(0x01), 'A'}); + + CliOptions brokenOptions; + brokenOptions.traceDir = traceDir.string(); + brokenOptions.targetName = "Broken"; + + TestTraceRunConfigReader brokenReader; + TraceDirectoryJob brokenJob(brokenOptions, diagnostics, brokenReader); + const auto brokenCheckpoint = diagnostics.failureCount(); + brokenJob.run(); + ASSERT_TRUE(diagnostics.failureCount() > brokenCheckpoint) + << "check-only trace directory should fail on decoder error packets"; +} + +TEST(CtraceUnitTests, testTraceDirectoryReportsGenerationDiagnosticsAndMissingSwo) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-directory-diagnostics-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.TB.raw", "unsupported"); + + TraceRunConfig config; + auto reported = TraceRunTestSupport::makeReference("event", "core", 3U, {}, "core/event"); + reported.info = "producer note"; + reported.warning = "producer warning"; + reported.error = "producer error"; + TraceRunReference emptyError = reported; + emptyError.ctraceRef = "core/pmu"; + emptyError.type = "pmu"; + emptyError.info.reset(); + emptyError.warning = ""; + emptyError.error = ""; + auto channelZero = TraceRunTestSupport::makeReference("itm", std::nullopt, std::nullopt, {0U}, "core/itm"); + channelZero.error = "ignored channel zero"; + TraceRunReference noStream = reported; + noStream.ctraceRef = "core/no-stream"; + noStream.stream.reset(); + noStream.warning.reset(); + noStream.error.reset(); + auto inconsistent = TraceRunTestSupport::makeReference("itm", "other", 3U, {1U}, "other/itm"); + config.setups.push_back(TraceRunTestSupport::makeTimestampSetup("core")); + config.references = {reported, emptyError, channelZero, noStream, inconsistent}; + + CliOptions options; + options.traceDir = traceDir.string(); + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(diagnostics.containsMessage("producer note")); + EXPECT_TRUE(diagnostics.containsMessage("producer warning")); + EXPECT_TRUE(diagnostics.containsMessage("producer error")); + EXPECT_TRUE(diagnostics.containsMessage("does not match ctrace-setup pname")); + EXPECT_TRUE(diagnostics.containsMessage("skipping raw trace channel")); + EXPECT_TRUE(diagnostics.containsMessage("no supported .SWO.raw input found")); +} + +TEST(CtraceUnitTests, testTraceDirectoryReportsConfigFailureAndRequiresDirectory) +{ + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader({}, true); + EXPECT_THROW(TraceDirectoryJob(CliOptions{}, diagnostics, reader).run(), std::runtime_error); + + const TemporaryTestPath temporaryPath("ctrace-trace-directory-config-failure-test"); + const auto traceDir = temporaryPath.path() / ".trace"; + writeTestFile(traceDir / "Alpha.ctrace-run.yml", "invalid"); + CliOptions options; + options.traceDir = traceDir.string(); + TraceDirectoryJob(options, diagnostics, reader).run(); + EXPECT_TRUE(diagnostics.containsMessage("synthetic config failure")); +} + +TEST(CtraceUnitTests, testFileDecodeJobHandlesMissingInputAndDisabledCtf) +{ + const TemporaryTestPath temporaryPath("ctrace-file-decode-control-test"); + CollectingDiagnosticSink diagnostics; + CliOptions checkOnly; + FileDecodeJob missing(checkOnly, temporaryPath.path() / "missing.raw", diagnostics, CtraceRunMeta::fromConfig({})); + EXPECT_THROW(missing.run(), std::runtime_error); + + const auto rawPath = temporaryPath.path() / "empty.SWO.raw"; + writeTestFile(rawPath); + CliOptions ctf; + ctf.outputFormat = OutputFormat::Ctf; + FileDecodeJob disabled(ctf, rawPath, diagnostics, CtraceRunMeta::fromConfig({})); + EXPECT_NO_THROW(disabled.run()); + EXPECT_TRUE(diagnostics.containsMessage("CTF output requires timestamps.clock")); +} + +TEST(CtraceUnitTests, testFileDecodeJobReportsPerStreamPrescalers) +{ + const TemporaryTestPath temporaryPath("ctrace-file-decode-prescalers-test"); + const auto rawPath = temporaryPath.path() / "empty.SWO.raw"; + writeTestFile(rawPath); + + TraceRunConfig config; + config.setups = { + TraceRunTestSupport::makeTimestampSetup("first", 100U, 4U), + TraceRunTestSupport::makeTimestampSetup("second", 100U, 16U), + }; + config.references = { + TraceRunTestSupport::makeReference("itm", "first", 1U, {1U}, "first/itm"), + TraceRunTestSupport::makeReference("itm", "second", 2U, {1U}, "second/itm"), + }; + + CollectingDiagnosticSink diagnostics; + FileDecodeJob job(CliOptions{}, rawPath, diagnostics, CtraceRunMeta::fromConfig(config)); + EXPECT_NO_THROW(job.run()); + const auto diagnostic = std::find_if(diagnostics.events().begin(), diagnostics.events().end(), [](const auto& event) { + return event.message == "using Trace-Bus-ID-specific timestamp prescalers"; + }); + ASSERT_NE(diagnostic, diagnostics.events().end()); + EXPECT_EQ(diagnostic->severity, DiagnosticSink::Severity::Info); + EXPECT_EQ(diagnostic->context, (std::vector>{{"traceBusIds", "2"}})); +} + +TEST(CtraceUnitTests, testFileDecodeJobAbortsOutputsAfterFatalDecoderError) +{ + const TemporaryTestPath temporaryPath("ctrace-file-decode-fatal-test"); + const auto rawPath = temporaryPath.path() / "fatal.SWO.raw"; + writeTestFile(rawPath, "x"); + CliOptions options; + options.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink diagnostics; + const auto script = std::make_shared(); + script->pushes = {{OCSD_RESP_FATAL_SYS_ERR, 1U}}; + + FileDecodeJob job(options, rawPath, diagnostics, CtraceRunMeta::fromConfig({}), + OpenCsdSessionTestSupport::scriptedFactory(script)); + EXPECT_NO_THROW(job.run()); + EXPECT_GT(diagnostics.failureCount(), 0U); + EXPECT_FALSE(std::filesystem::exists(temporaryPath.path() / "fatal.SWO.csv")); +} + +TEST(CtraceUnitTests, testFileDecodeJobReportsRawInputReadFailures) +{ + if (!TestPlatform::supports(TestPlatformCapability::DirectoryReadFailure)) { + GTEST_SKIP(); + } + const TemporaryTestPath temporaryPath("ctrace-file-decode-read-failure-test"); + temporaryPath.createDirectory(); + CollectingDiagnosticSink diagnostics; + FileDecodeJob job(CliOptions{}, temporaryPath.path(), diagnostics, CtraceRunMeta::fromConfig({})); + EXPECT_THROW(job.run(), std::runtime_error); +} diff --git a/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp new file mode 100644 index 000000000..198dbc171 --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/CortexMPostDecoderTests.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdTestSupport.h" +#include "TestSupport.h" + +#include + +#include "CortexMPostDecoder.h" +#include "OpenCsdTraceElement.h" + +#include +#include + +using OpenCsdTestSupport::openCsdElement; +using OpenCsdTestSupport::openCsdSoftwareElement; +using OpenCsdTestSupport::openCsdTimestampElement; + +TEST(CtraceUnitTests, testCortexMPostDecoderUsesLocalTimestampRelations) +{ + const std::array, 4> cases{{ + {LocalTimestampRelation::Synchronous, true}, + {LocalTimestampRelation::TimestampDelayed, true}, + {LocalTimestampRelation::PayloadDelayed, false}, + {LocalTimestampRelation::TimestampAndPayloadDelayed, false}, + }}; + + for (const auto& [relation, expectedReliable] : cases) { + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(openCsdSoftwareElement(1U)); + + decoder.append(openCsdTimestampElement(100U, 0U, 0U, relation)); + decoder.finish(); + + ASSERT_TRUE(sink.events().size() == 2U) << "local timestamp relation event count mismatch"; + ASSERT_TRUE(sink.events().front().quality.has_value() && + sink.events().front().quality->timestampReliable == expectedReliable) + << "local timestamp relation reliability mismatch"; + } +} + +TEST(CtraceUnitTests, testCortexMPostDecoderOffsetsTimestampsAfterOverflow) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(openCsdTimestampElement(100U)); + + decoder.append(openCsdElement(OpenCsdTraceElement::Kind::Overflow)); + + decoder.append(openCsdTimestampElement(10U)); + decoder.append(openCsdTimestampElement(15U)); + + ASSERT_TRUE(sink.events().size() == 4) << "timestamp segment overflow event count mismatch"; + ASSERT_TRUE(sink.events()[0].tcyc == 100U) << "timestamp segment first tcyc mismatch"; + ASSERT_TRUE(sink.events()[2].tcyc == 110U) << "timestamp segment post-overflow tcyc mismatch"; + ASSERT_TRUE(sink.events()[3].tcyc == 115U) << "timestamp segment post-overflow increment mismatch"; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderLeavesInitialOverflowTimestampUnknown) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(openCsdElement(OpenCsdTraceElement::Kind::Overflow)); + + ASSERT_EQ(sink.events().size(), 1U); + EXPECT_FALSE(sink.events().front().tcyc.has_value()); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderUsesTimestampOverflowFlag) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(openCsdTimestampElement(100U)); + + auto timestamp = openCsdTimestampElement(9U); + timestamp.overflow = true; + decoder.append(timestamp); + + ASSERT_TRUE(sink.events().size() == 2) << "timestamp overflow flag event count mismatch"; + ASSERT_TRUE(sink.events()[1].tcyc == 109U) << "post-decoder should use packet overflow flag as segment boundary"; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderMapsDiscontinuityTimestamp) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(openCsdTimestampElement(200U)); + decoder.append(openCsdElement(OpenCsdTraceElement::Kind::Discontinuity)); + decoder.append(openCsdTimestampElement(7U)); + + ASSERT_TRUE(sink.events().size() == 3) << "timestamp discontinuity event count mismatch"; + ASSERT_TRUE(sink.events()[2].tcyc == 207U) << "post-decoder explicit discontinuity timestamp mismatch"; +} diff --git a/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp new file mode 100644 index 000000000..b15d8e3c3 --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/DecodePipelineTests.cpp @@ -0,0 +1,638 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +// Cortex-M post-decoder and end-to-end decode pipeline tests. +#include "OpenCsdTestSupport.h" +#include "TestSupport.h" + +#include + +#include "CortexMPostDecoder.h" +#include "CortexMStreamDecoder.h" +#include "DecodePipeline.h" +#include "OpenCsdTraceElement.h" +#include "TraceEvent.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using OpenCsdTestSupport::openCsdElement; +using OpenCsdTestSupport::openCsdSoftwareElement; +using OpenCsdTestSupport::openCsdTimestampElement; + +/** @brief Returns the software payload from a test event when present. */ +static const SoftwareTraceEvent* softwareEvent(const TraceEvent& event) +{ + return traceEventPayload(event); +} + +/** @brief Returns the issue payload from a test event when present. */ +static const TraceIssueEvent* issueEvent(const TraceEvent& event) +{ + return traceEventPayload(event); +} + +/** @brief Tests whether decoded events contain a software value and channel. */ +static bool hasSoftwareValue(const std::vector& events, std::uint8_t value, std::uint32_t channel = 0U) +{ + for (const auto& event : events) { + const auto* software = softwareEvent(event); + if (software != nullptr && software->channel == channel && software->size == 1U && software->value == value) { + return true; + } + } + return false; +} + +/** @brief Finds a decoded issue by code and optional raw offset. */ +static const TraceIssueEvent* findIssue(const std::vector& events, TraceIssueCode code, + std::optional index = std::nullopt) +{ + for (const auto& event : events) { + const auto* issue = issueEvent(event); + if (issue != nullptr && issue->code == code && (!index.has_value() || event.index == index.value())) { + return issue; + } + } + return nullptr; +} + +/** @brief Counts decoded issues having a stable code. */ +static std::size_t countIssues(const std::vector& events, TraceIssueCode code) +{ + std::size_t count = 0U; + for (const auto& event : events) { + const auto* issue = issueEvent(event); + if (issue != nullptr && issue->code == code) { + ++count; + } + } + return count; +} + +template static constexpr RawByteView rawBytes(const std::uint8_t (&bytes)[Size]) +{ + return {bytes, Size}; +} + +/** @brief Creates a non-owning raw byte view for a test vector. */ +static RawByteView rawBytes(const std::vector& bytes) +{ + return {bytes.data(), bytes.size()}; +} + +/** @brief Stores the result and events returned by one test decode. */ +struct DecodedTrace { + DecodeResult result; + std::vector events; +}; + +/** @brief Decodes test chunks and returns counters and collected events. */ +static DecodedTrace decodeTrace(std::initializer_list chunks, std::uint32_t timestampPrescaler = 16U) +{ + CollectingEventSink sink; + DecodePipeline pipeline(ItmTimestampPrescalers{timestampPrescaler, {}}, sink); + for (const auto chunk : chunks) { + pipeline.push(chunk); + } + return {pipeline.finish(), std::move(sink.events())}; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderSoftwareTimestampBoundary) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + decoder.append(openCsdSoftwareElement(0U, 'A', 4U, 1U)); + decoder.append(openCsdTimestampElement(120U, 5U, 1U)); + + decoder.finish(); + const auto& packets = sink.events(); + ASSERT_TRUE(packets.size() == 2) << "CortexMPostDecoder packet count mismatch"; + const auto* decodedSoftware = softwareEvent(packets[0]); + ASSERT_TRUE(decodedSoftware != nullptr) << "CortexMPostDecoder first packet type mismatch"; + ASSERT_TRUE(decodedSoftware->channel == 0) << "CortexMPostDecoder software channel mismatch"; + ASSERT_TRUE(decodedSoftware->size == 1U && decodedSoftware->value == 'A') << "CortexMPostDecoder payload mismatch"; + ASSERT_TRUE(packets[0].tcyc.has_value() && packets[0].tcyc.value() == 120) + << "CortexMPostDecoder software tcyc mismatch"; + ASSERT_TRUE(packets[0].quality.has_value() && packets[0].quality->timestampReliable) + << "CortexMPostDecoder software timestamp reliability mismatch"; + ASSERT_TRUE(isTraceEvent(packets[1])) + << "CortexMPostDecoder timestamp packet type mismatch"; + ASSERT_TRUE(packets[1].tcyc.has_value() && packets[1].tcyc.value() == 120) + << "CortexMPostDecoder timestamp tcyc mismatch"; +} + +TEST(CtraceUnitTests, testCortexMStreamDecoderAppliesPerStreamPrescalers) +{ + CollectingEventSink sink; + CortexMStreamDecoder decoder(ItmTimestampPrescalers{1U, {{1U, 4U}, {2U, 16U}}}, sink); + + const auto stream1Software = openCsdSoftwareElement(1U, 0x11U, 0U, 1U); + decoder.append(stream1Software); + + const auto stream2Software = openCsdSoftwareElement(1U, 0x22U, 0U, 2U); + decoder.append(stream2Software); + + const auto stream1Timestamp = openCsdTimestampElement(10U, 0U, 1U); + decoder.append(stream1Timestamp); + + const auto stream2Timestamp = openCsdTimestampElement(10U, 0U, 2U); + decoder.append(stream2Timestamp); + decoder.finish(); + + ASSERT_TRUE(sink.events().size() == 4U) << "per-stream timestamp event count mismatch"; + ASSERT_TRUE(sink.events()[0].traceBusId == 1U && sink.events()[0].tcyc == std::optional(40U)) + << "stream 1 timestamp prescaler mismatch"; + ASSERT_TRUE(sink.events()[2].traceBusId == 2U && sink.events()[2].tcyc == std::optional(160U)) + << "stream 2 timestamp prescaler mismatch"; +} + +TEST(CtraceUnitTests, testCortexMStreamDecoderValidatesAndSaturatesPrescalers) +{ + CollectingEventSink sink; + auto timestamp = openCsdTimestampElement(std::numeric_limits::max()); + + CortexMStreamDecoder saturating(ItmTimestampPrescalers{2U, {}}, sink); + saturating.append(timestamp); + saturating.finish(); + ASSERT_EQ(sink.events().size(), 1U); + EXPECT_EQ(sink.events().front().tcyc, std::numeric_limits::max()); + EXPECT_EQ(saturating.eventCount(), 1U); + + CortexMStreamDecoder zero(ItmTimestampPrescalers{0U, {}}, sink); + EXPECT_THROW(zero.append(timestamp), std::invalid_argument); + + CortexMStreamDecoder unresolved(ItmTimestampPrescalers{std::nullopt, {}}, sink); + EXPECT_THROW(unresolved.append(timestamp), std::runtime_error); + timestamp.traceBusId = 7U; + EXPECT_THROW(unresolved.append(timestamp), std::runtime_error); + + const auto timestampWithoutValue = openCsdElement(OpenCsdTraceElement::Kind::LocalTimestamp); + EXPECT_NO_THROW(saturating.append(timestampWithoutValue)); +} + +TEST(CtraceUnitTests, testDecodePipelineRejectsInvalidChunkSizes) +{ + CollectingEventSink sink; + DecodePipeline pipeline(ItmTimestampPrescalers{1U, {}}, sink); + EXPECT_NO_THROW(pipeline.push({nullptr, 0U})); + EXPECT_THROW(pipeline.push({nullptr, static_cast(std::numeric_limits::max()) + 1U}), + std::runtime_error); + const auto result = pipeline.finish(); + EXPECT_EQ(result.bytesIn, 0U); + EXPECT_EQ(result.eventsOut, 0U); +} + +TEST(CtraceUnitTests, testCortexMPostDecoderReportsDiscontinuityInterval) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + auto discontinuity = openCsdElement(OpenCsdTraceElement::Kind::Discontinuity); + discontinuity.issueCode = TraceIssueCode::DataLoss; + discontinuity.errorMessage = "OpenCSD consumed 2 raw bytes"; + discontinuity.rawBytesConsumed = 2U; + decoder.append(discontinuity); + + decoder.append(openCsdSoftwareElement(0U, 'B', 8U)); + decoder.append(openCsdTimestampElement(42U, 10U)); + + decoder.finish(); + const auto& packets = sink.events(); + ASSERT_TRUE(packets.size() == 3U) << "discontinuity interval packet count mismatch"; + const auto* discontinuityIssue = issueEvent(packets[0]); + ASSERT_TRUE(discontinuityIssue != nullptr) << "discontinuity interval should start with its error"; + ASSERT_TRUE(packets[0].tcyc == std::optional(0U)) << "discontinuity last valid timestamp mismatch"; + ASSERT_TRUE(discontinuityIssue->lastValidTcyc == std::optional(0U)) + << "discontinuity start field mismatch"; + ASSERT_TRUE(discontinuityIssue->message.find("timestamp 0 .. 42.") != std::string::npos) + << "discontinuity interval message mismatch"; + ASSERT_TRUE(isTraceEvent(packets[1])) << "discontinuity interval payload order mismatch"; + ASSERT_TRUE(packets[1].tcyc == std::optional(42U)) << "resumed payload timestamp mismatch"; + ASSERT_TRUE(isTraceEvent(packets[2])) << "discontinuity interval timestamp order mismatch"; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderSeparatesRecoveryCauseAndDataLoss) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + auto cause = openCsdElement(OpenCsdTraceElement::Kind::Error, 8U); + cause.discontinuity = true; + cause.issueCode = TraceIssueCode::OpenCsdBadPacketSequence; + cause.errorMessage = "OpenCSD detected an invalid ITM packet sequence at raw offset 8."; + decoder.append(cause); + + auto loss = openCsdElement(OpenCsdTraceElement::Kind::Error, 10U); + loss.awaitingResumeTimestamp = true; + loss.issueCode = TraceIssueCode::DataLoss; + loss.errorMessage = "OpenCSD consumed 2 raw bytes"; + loss.rawBytesConsumed = 2U; + decoder.append(loss); + + decoder.append(openCsdTimestampElement(42U, 12U)); + + decoder.finish(); + const auto& packets = sink.events(); + ASSERT_TRUE(packets.size() == 3U) << "recovery cause/data-loss packet count mismatch"; + const auto* causeIssue = issueEvent(packets[0]); + const auto* lossIssue = issueEvent(packets[1]); + ASSERT_TRUE(causeIssue != nullptr && causeIssue->code == TraceIssueCode::OpenCsdBadPacketSequence) + << "recovery cause should be emitted first"; + ASSERT_TRUE(causeIssue->message.find("timestamp") == std::string::npos) + << "recovery cause should not contain the data-loss timestamp interval"; + ASSERT_TRUE(lossIssue != nullptr && lossIssue->code == TraceIssueCode::DataLoss) + << "recovery data loss should be a separate error"; + ASSERT_TRUE(lossIssue->message.find("timestamp 0 .. 42.") != std::string::npos) + << "recovery data-loss interval mismatch"; + ASSERT_TRUE(packets[0].quality.has_value() && packets[0].quality->overflowCount == 1U && + packets[1].quality.has_value() && packets[1].quality->overflowCount == 1U) + << "separate data-loss error must not count a second discontinuity"; + ASSERT_TRUE(isTraceEvent(packets[2])) << "recovery timestamp packet missing"; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderOverflowFlushesDwtSegments) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + const auto firstTimestamp = openCsdTimestampElement(100U, 1U, 1U); + decoder.append(firstTimestamp); + + auto pc = openCsdElement(OpenCsdTraceElement::Kind::Hardware, 2U, 1U); + pc.discriminator = 8; + pc.size = 4; + pc.value = 0x08001234U; + decoder.append(pc); + + const auto overflow = openCsdElement(OpenCsdTraceElement::Kind::Overflow, 3U); + decoder.append(overflow); + + auto value = openCsdElement(OpenCsdTraceElement::Kind::Hardware, 4U, 1U); + value.discriminator = 16; + value.size = 4; + value.value = 0x55U; + decoder.append(value); + + const auto secondTimestamp = openCsdTimestampElement(20U, 5U, 1U); + decoder.append(secondTimestamp); + + decoder.finish(); + const auto& packets = sink.events(); + ASSERT_TRUE(packets.size() == 5) << "CortexMPostDecoder overflow segment packet count mismatch"; + ASSERT_TRUE(isTraceEvent(packets[0])) + << "overflow segment first packet should be timestamp"; + const auto* address = traceEventPayload(packets[1]); + ASSERT_TRUE(address != nullptr) << "overflow should flush pending DWT fragment as an address event"; + ASSERT_TRUE(dwtAddressPc(*address) == std::optional(0x08001234U)) << "flushed DWT PC mismatch"; + ASSERT_TRUE(packets[1].tcyc.has_value() && packets[1].tcyc.value() == 100) << "flushed DWT PC timestamp mismatch"; + ASSERT_TRUE(packets[1].quality.has_value() && packets[1].quality->overflow) + << "flushed DWT PC should carry overflow status"; + ASSERT_TRUE(!packets[1].quality->timestampReliable) << "flushed DWT PC should be timestamp-unreliable"; + ASSERT_TRUE(isTraceEvent(packets[2])) << "overflow segment should emit overflow marker"; + const auto* data = traceEventPayload(packets[3]); + ASSERT_TRUE(data != nullptr) << "post-overflow DWT value missing"; + ASSERT_TRUE(!data->pc.has_value()) << "post-overflow DWT value must not inherit prior PC"; + ASSERT_TRUE(data->value == 0x55U) << "post-overflow DWT value mismatch"; + ASSERT_TRUE(packets[3].tcyc.has_value() && packets[3].tcyc.value() == 120) + << "post-overflow DWT value timestamp mismatch"; + ASSERT_TRUE(packets[3].quality.has_value() && packets[3].quality->overflow) + << "post-overflow DWT value should carry overflow status"; + ASSERT_TRUE(!packets[3].quality->timestampReliable) << "post-overflow DWT value should be timestamp-unreliable"; + ASSERT_TRUE(isTraceEvent(packets[4])) + << "overflow segment final packet should be timestamp"; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderPreservesDecoderTimestamps) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + const auto firstTimestamp = openCsdTimestampElement(100U, (std::uint64_t{1} << 32U) + 1U); + decoder.append(firstTimestamp); + + const auto secondTimestamp = openCsdTimestampElement(10U, 2U); + decoder.append(secondTimestamp); + + decoder.finish(); + const auto& packets = sink.events(); + ASSERT_TRUE(packets.size() == 2) << "preserve timestamp packet count mismatch"; + ASSERT_TRUE(packets[0].index == firstTimestamp.sourceIndex) << "raw offsets must not truncate above 4 GiB"; + ASSERT_TRUE(packets[0].tcyc.has_value() && packets[0].tcyc.value() == 100) << "first preserved timestamp mismatch"; + ASSERT_TRUE(packets[1].tcyc.has_value() && packets[1].tcyc.value() == 10) << "second preserved timestamp mismatch"; +} + +TEST(CtraceUnitTests, testCortexMPostDecoderPreservesGlobalTimestampOrder) +{ + CollectingEventSink sink; + CortexMPostDecoder decoder(sink); + + const auto software = openCsdSoftwareElement(1U, 'A', 1U); + decoder.append(software); + + auto dwtPc = openCsdElement(OpenCsdTraceElement::Kind::Hardware, 2U); + dwtPc.discriminator = 8U; + dwtPc.size = 4U; + dwtPc.value = 0x08001234U; + decoder.append(dwtPc); + + auto globalTimestamp = openCsdElement(OpenCsdTraceElement::Kind::GlobalTimestamp, 3U); + globalTimestamp.timestampValue = 0x123456789abcdef0ULL; + decoder.append(globalTimestamp); + + auto warning = openCsdElement(OpenCsdTraceElement::Kind::Error, 4U); + warning.issueCode = TraceIssueCode::OpenCsdDecodeError; + warning.issueSeverity = TraceIssueSeverity::Warning; + warning.errorMessage = "decoder warning"; + decoder.append(warning); + + const auto localTimestamp = openCsdTimestampElement(42U, 5U); + decoder.append(localTimestamp); + decoder.finish(); + + ASSERT_TRUE(sink.events().size() == 5U) << "global timestamp order packet count mismatch"; + ASSERT_TRUE(isTraceEvent(sink.events()[0])) + << "global timestamp must not overtake preceding software data"; + ASSERT_TRUE(isTraceEvent(sink.events()[1])) + << "global timestamp must flush and follow preceding DWT data"; + const auto* timestamp = traceEventPayload(sink.events()[2]); + ASSERT_TRUE(timestamp != nullptr && timestamp->value == globalTimestamp.timestampValue) + << "global timestamp payload/order mismatch"; + ASSERT_TRUE(sink.events()[0].tcyc == std::optional(42U) && + sink.events()[1].tcyc == std::optional(42U)) + << "preceding payloads must retain the following local timestamp"; + ASSERT_TRUE(!sink.events()[2].tcyc.has_value()) + << "global timestamp must remain independent of the local timestamp domain"; + ASSERT_TRUE(!sink.events()[2].quality.has_value()) << "global timestamp must not acquire local trace quality"; + ASSERT_TRUE( + (isTraceEvent(sink.events()[3]) && sink.events()[3].tcyc == std::optional(42U))) + << "a warning must not overtake pending payload or global timestamp packets"; + ASSERT_TRUE(isTraceEvent(sink.events()[4])) + << "local timestamp must remain after the global timestamp boundary"; +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversAtRealSync) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, 0xfeU, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + const auto decoded = decodeTrace({rawBytes(trace)}); + + ASSERT_TRUE(decoded.result.bytesIn == sizeof(trace)) << "recovery byte count mismatch"; + ASSERT_TRUE(hasSoftwareValue(decoded.events, static_cast('A'))) + << "recovery should preserve packets before the damaged section"; + const auto* error = findIssue(decoded.events, TraceIssueCode::OpenCsdBadPacketSequence, 8U); + ASSERT_TRUE( + (error != nullptr && error->message == "OpenCSD detected an invalid ITM packet sequence at raw offset 8.")) + << "recovery should report the exact OpenCSD error offset"; + ASSERT_TRUE(hasSoftwareValue(decoded.events, static_cast('B'))) + << "recovery should resume after the next real ITM sync"; +} + +TEST(CtraceUnitTests, testDecodePipelineKeepsCallbacksAttachedAcrossRepeatedResets) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, 0xfeU, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), 0x00U, 0xfeU, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('C'), + }; + const auto decoded = decodeTrace({rawBytes(trace)}); + + ASSERT_TRUE(decoded.result.bytesIn == sizeof(trace)) << "repeated recovery byte count mismatch"; + ASSERT_TRUE(hasSoftwareValue(decoded.events, static_cast('A')) && + hasSoftwareValue(decoded.events, static_cast('B')) && + hasSoftwareValue(decoded.events, static_cast('C'))) + << "decoder resets must retain all OpenCSD callbacks"; + ASSERT_TRUE(countIssues(decoded.events, TraceIssueCode::OpenCsdBadPacketSequence) == 2U) + << "each damaged section must trigger one recoverable reset"; +} + +TEST(CtraceUnitTests, testDecodePipelineReconstructsGlobalTimestamp) +{ + constexpr std::uint64_t lower = 0x00f23456ULL; + constexpr std::uint64_t upper = 0x1020304c000000ULL; + std::vector trace{ + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x00U, + 0x80U, + 0x94U, + static_cast(0x80U | ((lower >> 0U) & 0x7fU)), + static_cast(0x80U | ((lower >> 7U) & 0x7fU)), + static_cast(0x80U | ((lower >> 14U) & 0x7fU)), + static_cast((lower >> 21U) & 0x1fU), + 0xb4U, + static_cast(0x80U | ((upper >> 26U) & 0x7fU)), + static_cast(0x80U | ((upper >> 33U) & 0x7fU)), + static_cast(0x80U | ((upper >> 40U) & 0x7fU)), + static_cast(0x80U | ((upper >> 47U) & 0x7fU)), + static_cast(0x80U | ((upper >> 54U) & 0x7fU)), + static_cast((upper >> 61U) & 0x07U), + }; + const auto decoded = decodeTrace({rawBytes(trace)}, 1U); + + ASSERT_TRUE(decoded.result.bytesIn == trace.size()) << "global timestamp input byte count mismatch"; + std::size_t globalTimestampCount = 0U; + for (const auto& packet : decoded.events) { + if (const auto* timestamp = traceEventPayload(packet)) { + ++globalTimestampCount; + ASSERT_TRUE(timestamp->value == (upper | lower)) << "GTS1/GTS2 reconstruction lost timestamp bits"; + ASSERT_TRUE(!timestamp->clockChange) << "unexpected global timestamp clock-change flag"; + } + } + ASSERT_TRUE(globalTimestampCount == 1U) << "GTS1/GTS2 must produce exactly one reconstructed global timestamp"; +} + +TEST(CtraceUnitTests, testDecodePipelineAppliesPrescalerAfterOpenCsd) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x09U, 0x41U, 0x20U, + }; + + const auto decoded = decodeTrace({rawBytes(trace)}); + + bool foundScaledSoftware = false; + for (const auto& event : decoded.events) { + const auto* software = softwareEvent(event); + foundScaledSoftware = foundScaledSoftware || (software != nullptr && software->channel == 1U && + event.tcyc == std::optional(32U)); + } + ASSERT_TRUE(foundScaledSoftware) << "DecodePipeline must apply the ITM prescaler after OpenCSD decoding"; +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversWhenErrorSpansChunks) +{ + const std::uint8_t firstChunk[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, + }; + const std::uint8_t secondChunk[] = { + 0xfeU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + const auto decoded = decodeTrace({rawBytes(firstChunk), rawBytes(secondChunk)}); + + ASSERT_TRUE(decoded.result.bytesIn == sizeof(firstChunk) + sizeof(secondChunk)) + << "split recovery byte count mismatch"; + ASSERT_TRUE(findIssue(decoded.events, TraceIssueCode::OpenCsdBadPacketSequence, 8U) != nullptr) + << "split bad packet should retain its header offset"; + ASSERT_TRUE(hasSoftwareValue(decoded.events, static_cast('B'))) + << "recovery should resume after a split bad packet"; +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversFromReservedHeader) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x04U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + const auto decoded = decodeTrace({rawBytes(trace)}); + + const auto* error = findIssue(decoded.events, TraceIssueCode::OpenCsdInvalidPacketHeader, 8U); + ASSERT_TRUE(error != nullptr && error->message == "OpenCSD detected an invalid ITM packet header at raw offset 8.") + << "reserved header should report its exact OpenCSD error"; + ASSERT_TRUE(hasSoftwareValue(decoded.events, static_cast('B'))) + << "recovery should resume after a reserved header"; +} + +TEST(CtraceUnitTests, testDecodePipelineFinishesWithoutLaterSync) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x00U, 0xfeU, 0xffU, 0xffU, + }; + const auto decoded = decodeTrace({rawBytes(trace)}); + + ASSERT_TRUE(decoded.result.bytesIn == sizeof(trace)) << "bad-tail byte count mismatch"; + ASSERT_TRUE(findIssue(decoded.events, TraceIssueCode::OpenCsdBadPacketSequence, 8U) != nullptr) + << "bad tail should report its exact OpenCSD error and finish"; +} + +TEST(CtraceUnitTests, testDecodePipelineReportsIncompletePacketAtEndOfInput) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), 0x03U, 0x12U, + }; + const auto decoded = decodeTrace({rawBytes(trace)}); + + ASSERT_TRUE(!decoded.events.empty()) << "incomplete trace should emit packets"; + const auto& last = decoded.events.back(); + const auto* issue = issueEvent(last); + ASSERT_TRUE(issue != nullptr) << "incomplete trace should end with an error packet"; + ASSERT_TRUE(issue->code == TraceIssueCode::OpenCsdIncompleteTail) << "incomplete trace issue code mismatch"; + ASSERT_TRUE(last.index == 8U) << "incomplete trace should report the partial packet header offset"; +} + +TEST(CtraceUnitTests, testDecodePipelineRecoversFromOverlongContinuationPackets) +{ + const std::vector> damagedPackets{ + {0x80U, 0x80U, 0x80U, 0x80U, 0x80U}, + {0x94U, 0x80U, 0x80U, 0x80U, 0x80U}, + {0xb4U, 0x80U, 0x80U, 0x80U, 0x80U, 0x80U, 0x80U}, + {0x88U, 0x80U, 0x80U, 0x80U, 0x80U}, + }; + + for (const auto& damaged : damagedPackets) { + std::vector trace{ + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('A'), + }; + trace.insert(trace.end(), damaged.begin(), damaged.end()); + trace.insert(trace.end(), {0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U}); + trace.insert(trace.end(), {0x01U, static_cast('B')}); + + const auto decoded = decodeTrace({rawBytes(trace)}); + + ASSERT_TRUE(findIssue(decoded.events, TraceIssueCode::OpenCsdBadPacketSequence) != nullptr) + << "overlong continuation packet should emit the OpenCSD error"; + ASSERT_TRUE(hasSoftwareValue(decoded.events, static_cast('B'))) + << "decode should resume after an overlong continuation packet"; + } +} + +TEST(CtraceUnitTests, testDecodePipelinePreservesDwtEventAndPmuPackets) +{ + const std::uint8_t trace[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x05U, 0x21U, 0x1dU, 0x81U, + }; + const auto decoded = decodeTrace({rawBytes(trace)}); + + bool foundEvent = false; + bool foundPmu = false; + for (const auto& packet : decoded.events) { + const auto* event = traceEventPayload(packet); + const auto* pmu = traceEventPayload(packet); + foundEvent = + foundEvent || (event != nullptr && event->discriminator == 0U && event->size == 1U && event->value == 0x21U); + foundPmu = foundPmu || (pmu != nullptr && pmu->discriminator == 3U && pmu->size == 1U && pmu->value == 0x81U); + } + ASSERT_TRUE(foundEvent) << "OpenCSD DWT event-counter packet must survive post-decoding"; + ASSERT_TRUE(foundPmu) << "OpenCSD PMU-overflow packet must survive post-decoding"; +} + +TEST(CtraceUnitTests, testDecodePipelineDoesNotInjectSync) +{ + const std::uint8_t validWithoutAsync[] = {0x01U, static_cast('A')}; + const auto decoded = decodeTrace({rawBytes(validWithoutAsync)}); + + ASSERT_TRUE(decoded.result.bytesIn == sizeof(validWithoutAsync)) << "decode byte count mismatch"; + bool foundPayload = false; + bool foundDataLoss = false; + for (const auto& packet : decoded.events) { + if (softwareEvent(packet) != nullptr) { + foundPayload = true; + } + const auto* issue = issueEvent(packet); + if (issue != nullptr && issue->code == TraceIssueCode::DataLoss) { + foundDataLoss = true; + ASSERT_TRUE(issue->rawBytesConsumed == std::optional(sizeof(validWithoutAsync))) + << "decode should count all raw bytes consumed before synchronization"; + ASSERT_TRUE(issue->message.find("OpenCSD consumed 2 raw bytes") != std::string::npos) + << "decode data-loss message should include the consumed raw-byte count"; + ASSERT_TRUE(issue->message.find("timestamp 0 .. unknown.") != std::string::npos) + << "decode should mark a missing post-recovery timestamp as unknown"; + } + } + ASSERT_TRUE(!foundPayload) << "decode should not decode sync-less data by injecting sync"; + ASSERT_TRUE(foundDataLoss) << "decode should mark sync-less data loss"; +} + +TEST(CtraceUnitTests, testDecodePipelineCountsBytesUntilRealSync) +{ + const std::uint8_t leading[] = {0x01U, static_cast('A')}; + const std::uint8_t synchronized[] = { + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x80U, 0x01U, static_cast('B'), + }; + const auto decoded = decodeTrace({rawBytes(leading), rawBytes(synchronized)}); + + bool foundDataLoss = false; + bool foundPayloadAfterDataLoss = false; + for (const auto& packet : decoded.events) { + const auto* issue = issueEvent(packet); + if (issue != nullptr && issue->code == TraceIssueCode::DataLoss) { + foundDataLoss = true; + ASSERT_TRUE(issue->rawBytesConsumed == std::optional(sizeof(leading))) + << "decode should count bytes only up to the real sync offset"; + continue; + } + const auto* software = softwareEvent(packet); + if (foundDataLoss && software != nullptr && software->channel == 0U && software->size == 1U && + software->value == static_cast('B')) { + foundPayloadAfterDataLoss = true; + } + } + + ASSERT_TRUE(foundDataLoss) << "decode should emit data loss before the recovered stream"; + ASSERT_TRUE(foundPayloadAfterDataLoss) << "decode should emit synchronized payload after the counted data loss"; +} diff --git a/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp new file mode 100644 index 000000000..347361757 --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/DwtPacketDecoderTests.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +// DWT packet decoding and explicitly deferred DWT feature tests. +#include "TestSupport.h" + +#include + +#include "csv/CsvRowMapper.h" +#include "DwtPacketDecoder.h" +#include "TraceEvent.h" +#include "TraceSelection.h" + +#include +#include + +/** @brief Creates a DWT payload packet with test defaults. */ +static DwtPayloadPacket dwtPayload(std::uint8_t discriminator, std::uint8_t size = 0U, std::uint32_t value = 0U, + std::uint64_t index = 0U, std::uint8_t traceBusId = 0U, std::uint64_t tcyc = 0U) +{ + return {index, traceBusId, discriminator, size, value, tcyc, {}}; +} + +TEST(CtraceUnitTests, testDwtPcSampleIsSuppressedUntilDedicatedEventExists) +{ + DwtPacketDecoder decoder; + auto payload = dwtPayload(2U, 4U, 0x08001234U, 19U, 3U, 949339000U); + payload.quality.timestampReliable = true; + + const auto packets = decoder.decode(payload); + ASSERT_TRUE(packets.empty()) << "DWT PC samples must remain suppressed until their output event is defined"; +} + +TEST(CtraceUnitTests, testDwtCounterPacketsArePreservedUntilOutputSemanticsExist) +{ + const auto verify = [](std::uint8_t discriminator, const char* selector) { + DwtPacketDecoder decoder; + auto payload = dwtPayload(discriminator, 1U, 0x21U, 23U, 4U, 949339100U); + payload.quality.overflow = true; + payload.quality.timestampReliable = false; + payload.quality.overflowCount = 7U; + + const auto packets = decoder.decode(payload); + ASSERT_TRUE(packets.size() == 1U) << "DWT counter packet must be preserved internally"; + const auto& packet = packets.front(); + const auto* event = traceEventPayload(packet); + const auto* pmu = traceEventPayload(packet); + ASSERT_TRUE((discriminator == 0U && event != nullptr && pmu == nullptr) || + (discriminator == 3U && event == nullptr && pmu != nullptr)) + << "DWT counter semantic event type mismatch"; + const auto actualDiscriminator = event != nullptr ? event->discriminator : pmu->discriminator; + const auto actualSize = event != nullptr ? event->size : pmu->size; + const auto actualValue = event != nullptr ? event->value : pmu->value; + ASSERT_TRUE(actualDiscriminator == discriminator) << "DWT counter discriminator mismatch"; + ASSERT_TRUE(actualSize == 1U && actualValue == 0x21U) << "DWT counter payload mismatch"; + ASSERT_TRUE(packet.index == 23U && packet.traceBusId == 4U) << "DWT counter identity mismatch"; + ASSERT_TRUE(packet.tcyc == 949339100U) << "DWT counter timestamp mismatch"; + ASSERT_TRUE(packet.quality.has_value() && packet.quality->overflow) << "DWT counter overflow status mismatch"; + ASSERT_TRUE(packet.quality->overflowCount == 7U) << "DWT counter overflow count mismatch"; + ASSERT_TRUE(!traceEventType(packet).has_value()) << "unimplemented DWT counter must not expose an output type"; + ASSERT_TRUE(!traceEventSelectedForOutput(packet, TraceSelection{})) + << "DWT counter packet must remain disabled by default"; + ASSERT_TRUE(!traceEventSelectedForOutput(packet, TraceSelection{{selector}, {}})) + << "DWT counter selector must remain disabled until output semantics exist"; + }; + + verify(0U, "event"); + verify(3U, "pmu"); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderRejectsReservedExceptionAction) +{ + DwtPacketDecoder decoder; + auto payload = dwtPayload(1U, 2U, 11U, 17U, 3U, 1234U); + payload.quality.timestampReliable = true; + + const auto packets = decoder.decode(payload); + ASSERT_TRUE(packets.size() == 1U) << "DwtPacketDecoder reserved exception action packet count mismatch"; + const auto* issue = traceEventPayload(packets[0]); + ASSERT_TRUE(issue != nullptr) << "DwtPacketDecoder reserved exception action should emit only an error"; + ASSERT_TRUE(issue->code == TraceIssueCode::InvalidExceptionAction) + << "DwtPacketDecoder reserved exception action code mismatch"; + ASSERT_TRUE(issue->message == "invalid exception action 0x0 for exception 11") + << "DwtPacketDecoder reserved exception action message mismatch"; + ASSERT_TRUE(packets[0].index == 17U && packets[0].traceBusId == 3U) + << "DwtPacketDecoder reserved exception action identity mismatch"; + ASSERT_TRUE(packets[0].tcyc.has_value() && *packets[0].tcyc == 1234U) + << "DwtPacketDecoder reserved exception action timestamp mismatch"; + ASSERT_TRUE(CsvRowMapper::row(packets[0]) == "1234,3,error,,,,,invalid exception action 0x0 for exception 11") + << "DwtPacketDecoder reserved exception error CSV mismatch"; +} + +TEST(CtraceUnitTests, testDwtPacketDecoderFlushesPendingEventsInRawOrder) +{ + DwtPacketDecoder decoder; + + const auto comparatorThree = dwtPayload(14U, 4U, 0x08003000U, 10U, 3U); + ASSERT_TRUE(decoder.decode(comparatorThree).empty()) + << "an incomplete DWT comparator-three event must remain pending"; + + const auto comparatorOne = dwtPayload(10U, 4U, 0x08001000U, 10U, 3U); + ASSERT_TRUE(decoder.decode(comparatorOne).empty()) << "an incomplete DWT comparator-one event must remain pending"; + + const auto comparatorZero = dwtPayload(8U, 4U, 0x08000000U, 20U, 4U); + ASSERT_TRUE(decoder.decode(comparatorZero).empty()) << "an incomplete DWT comparator-zero event must remain pending"; + + const auto packets = decoder.flush({}, 123U); + ASSERT_TRUE(packets.size() == 3U) << "DWT flush must emit all pending comparator events"; + ASSERT_TRUE(packets[0].index == 10U && packets[1].index == 10U && packets[2].index == 20U) + << "DWT flush must preserve raw-stream order across comparators"; + ASSERT_TRUE(packets[0].traceBusId == 3U && packets[1].traceBusId == 3U && packets[2].traceBusId == 4U) + << "DWT flush must preserve the identity of each pending event"; + const auto* first = traceEventPayload(packets[0]); + const auto* second = traceEventPayload(packets[1]); + const auto* third = traceEventPayload(packets[2]); + ASSERT_TRUE(first != nullptr && first->comparator == 1U && second != nullptr && second->comparator == 3U && + third != nullptr && third->comparator == 0U) + << "DWT raw-order regression must not rewrite comparator identities"; +} + +TEST(CtraceUnitTests, testDwtPacketDecoderPreservesRepeatedAddressFragments) +{ + const auto verify = [](std::uint8_t discriminator, std::uint32_t firstValue, std::uint32_t secondValue) { + DwtPacketDecoder decoder; + const auto size = static_cast((discriminator & 1U) == 0U ? 4U : 2U); + const auto first = dwtPayload(discriminator, size, firstValue, 10U, 3U); + ASSERT_TRUE(decoder.decode(first).empty()) << "first DWT address fragment must remain pending"; + + auto packets = decoder.decode(dwtPayload(discriminator, size, secondValue, 20U, 4U, 200U)); + ASSERT_TRUE(packets.size() == 1U) << "a repeated DWT address fragment must flush its predecessor"; + const auto* firstAddress = traceEventPayload(packets.front()); + ASSERT_TRUE(firstAddress != nullptr && packets.front().index == 10U && packets.front().traceBusId == 3U) + << "the first repeated DWT address fragment lost its identity"; + const auto firstPc = dwtAddressPc(*firstAddress); + const auto firstOffset = dwtAddressOffset(*firstAddress); + + packets = decoder.flush({}, 300U); + ASSERT_TRUE(packets.size() == 1U) << "the second DWT address fragment must remain available"; + const auto* secondAddress = traceEventPayload(packets.front()); + ASSERT_TRUE(secondAddress != nullptr && packets.front().index == 20U && packets.front().traceBusId == 4U) + << "the second repeated DWT address fragment lost its identity"; + + if (discriminator == 8U) { + ASSERT_TRUE(firstPc == std::optional(firstValue) && + dwtAddressPc(*secondAddress) == std::optional(secondValue)) + << "repeated DWT PC fragments were overwritten"; + } else { + ASSERT_TRUE(firstOffset == std::optional(firstValue) && + dwtAddressOffset(*secondAddress) == std::optional(secondValue)) + << "repeated DWT offset fragments were overwritten"; + } + }; + + verify(8U, 0x08001000U, 0x08002000U); + verify(9U, 0x1000U, 0x2000U); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderRejectsUnsupportedAddressWidths) +{ + const auto verify = [](std::uint8_t discriminator, std::uint8_t size) { + DwtPacketDecoder decoder; + auto payload = dwtPayload(discriminator, size, 0x12345678U, 17U, 3U, 99U); + payload.quality.timestampReliable = true; + + const auto packets = decoder.decode(payload); + ASSERT_TRUE(packets.size() == 1U) << "unsupported DWT address width must emit one error"; + const auto* issue = traceEventPayload(packets.front()); + ASSERT_TRUE(issue != nullptr && issue->code == TraceIssueCode::UnsupportedDwtAddressPayload && + issue->severity == TraceIssueSeverity::Error) + << "unsupported DWT address width diagnostic mismatch"; + ASSERT_TRUE(packets.front().index == 17U && packets.front().traceBusId == 3U && + packets.front().tcyc == std::optional(99U)) + << "unsupported DWT address width diagnostic lost packet identity"; + }; + + verify(8U, 1U); + verify(9U, 4U); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderMapsAllExceptionActions) +{ + const auto verify = [](std::uint32_t actionCode, ExceptionAction expected) { + DwtPacketDecoder decoder; + auto payload = dwtPayload(1U, 2U, (actionCode << 12U) | 11U, 17U, 3U, 1234U); + payload.quality.timestampReliable = true; + + const auto packets = decoder.decode(payload); + ASSERT_EQ(packets.size(), 1U); + const auto* exception = traceEventPayload(packets.front()); + ASSERT_NE(exception, nullptr); + EXPECT_EQ(exception->number, 11U); + EXPECT_EQ(exception->action, expected); + }; + + verify(1U, ExceptionAction::Entered); + verify(2U, ExceptionAction::Exited); + verify(3U, ExceptionAction::Returned); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderFlushesPendingTraceForUnknownSource) +{ + DwtPacketDecoder decoder; + const auto address = dwtPayload(8U, 4U, 0x08001234U, 10U); + EXPECT_TRUE(decoder.decode(address).empty()); + + auto unknown = dwtPayload(7U, 0U, 0U, 11U, 0U, 42U); + const auto packets = decoder.decode(unknown); + ASSERT_EQ(packets.size(), 1U); + EXPECT_NE(traceEventPayload(packets.front()), nullptr); + EXPECT_EQ(packets.front().tcyc, std::optional(42U)); + + decoder.reset(); + EXPECT_TRUE(decoder.flush({}, 43U).empty()); + + unknown.discriminator = 24U; + EXPECT_TRUE(decoder.decode(unknown).empty()); +} + +TEST(CtraceUnitTests, testDwtPacketDecoderCombinesPcOffsetAndValue) +{ + DwtPacketDecoder decoder; + + auto pc = dwtPayload(8U, 4U, 0x08001234U, 10U); + pc.quality.timestampReliable = true; + EXPECT_TRUE(decoder.decode(pc).empty()); + + auto offset = pc; + offset.index = 11U; + offset.discriminator = 9U; + offset.size = 2U; + offset.value = 0x20U; + EXPECT_TRUE(decoder.decode(offset).empty()); + + auto value = pc; + value.index = 12U; + value.discriminator = 16U; + value.size = 4U; + value.value = 0xabcdef01U; + const auto packets = decoder.decode(value); + ASSERT_EQ(packets.size(), 1U); + const auto* data = traceEventPayload(packets.front()); + ASSERT_NE(data, nullptr); + EXPECT_EQ(data->access, AccessType::Read); + EXPECT_EQ(data->pc, std::optional(0x08001234U)); + EXPECT_EQ(data->addressLo16, std::optional(0x20U)); + + DwtPacketDecoder addressDecoder; + EXPECT_TRUE(addressDecoder.decode(pc).empty()); + EXPECT_TRUE(addressDecoder.decode(offset).empty()); + const auto addresses = addressDecoder.flush({}, 100U); + ASSERT_EQ(addresses.size(), 1U); + const auto* address = traceEventPayload(addresses.front()); + ASSERT_NE(address, nullptr); + EXPECT_EQ(dwtAddressPc(*address), std::optional(0x08001234U)); + EXPECT_EQ(dwtAddressOffset(*address), std::optional(0x20U)); +} diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp new file mode 100644 index 000000000..5958d100d --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/OpenCsdErrorControllerTests.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" + +#include + +#include "OpenCsdErrorController.h" +#include "TraceEvent.h" +#include "common/ocsd_error.h" +#include "common/ocsd_msg_logger.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include + +/** @brief Collects text forwarded by the OpenCSD logger. */ +class MessageSink final : public ocsdMsgLogStrOutI { +public: + /** @brief Appends one logger message. */ + void printOutStr(const std::string& message) override + { + m_messages += message; + } + + /** @brief Returns all collected logger text. */ + const std::string& messages() const + { + return m_messages; + } + +private: + std::string m_messages; +}; + +/** @brief Creates an OpenCSD error decision for formatting tests. */ +static OpenCsdErrorController::Decision makeDecision(ocsd_datapath_resp_t response, ocsd_err_t code, + std::optional offset = std::nullopt) +{ + OpenCsdErrorController::Decision decision; + decision.response = response; + OpenCsdErrorRecord error; + error.code = code; + error.index = offset.value_or(0U); + error.hasIndex = offset.has_value(); + decision.error = std::move(error); + return decision; +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerClassifiesResponses) +{ + OpenCsdErrorController controller; + const auto source = controller.RegisterErrorSource("ITM packet processor"); + + controller.beginDataPathCall(); + const ocsdError recoverable(OCSD_ERR_SEV_ERROR, OCSD_ERR_BAD_PACKET_SEQ, 123U, 1U, "invalid async sequence"); + controller.LogError(source, &recoverable); + const auto recovery = controller.decide(OCSD_RESP_FATAL_INVALID_DATA); + ASSERT_TRUE(recovery.action == OpenCsdErrorController::Action::RecoverStream) + << "bad packet sequence should be recoverable"; + ASSERT_TRUE(recovery.error.has_value() && recovery.error->hasIndex && recovery.error->index == 123U) + << "OpenCSD error offset should be preserved"; + ASSERT_TRUE(recovery.errors.size() == 1U && recovery.errors[0].code == OCSD_ERR_BAD_PACKET_SEQ) + << "OpenCSD decision should retain every error reported for the datapath call"; + ASSERT_TRUE(OpenCsdErrorController::describeSummary(recovery) == + "OpenCSD detected an invalid ITM packet sequence at raw offset 123.") + << "OpenCSD error module should provide a concise user-facing summary"; + ASSERT_TRUE(OpenCsdErrorController::issueCode(recovery) == TraceIssueCode::OpenCsdBadPacketSequence) + << "OpenCSD error module should own issue-code selection"; + const auto apiError = OpenCsdErrorController::describeApiError(OCSD_ERR_MEM, "decoder setup failed"); + ASSERT_TRUE(apiError.find("OCSD_ERR_MEM") != std::string::npos && + apiError.find("decoder setup failed") != std::string::npos) + << "OpenCSD error module should own API error formatting"; + + controller.beginDataPathCall(); + ASSERT_TRUE(controller.decide(OCSD_RESP_FATAL_INVALID_DATA).action == OpenCsdErrorController::Action::Abort) + << "fatal response without a classified stream error should abort"; + + controller.beginDataPathCall(); + const ocsdError invalidHeader(OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 456U, 1U, "reserved packet header"); + controller.LogError(source, &invalidHeader); + ASSERT_TRUE(controller.decide(OCSD_RESP_CONT).action == OpenCsdErrorController::Action::RecoverStream) + << "invalid packet header should be recoverable even when forwarding is suppressed"; + ASSERT_TRUE(controller.decide(OCSD_RESP_FATAL_SYS_ERR).action == OpenCsdErrorController::Action::Abort) + << "system-fatal response must not be reclassified as a stream recovery"; + + controller.beginDataPathCall(); + const ocsdError internalFailure(OCSD_ERR_SEV_ERROR, OCSD_ERR_MEM, "allocation failed"); + controller.LogError(source, &internalFailure); + ASSERT_TRUE(controller.decide(OCSD_RESP_FATAL_INVALID_DATA).action == OpenCsdErrorController::Action::Abort) + << "non-stream OpenCSD error should abort on a fatal response"; + ASSERT_TRUE(controller.decide(OCSD_RESP_ERR_CONT).action == OpenCsdErrorController::Action::Continue) + << "a non-stream OpenCSD error should continue when the datapath explicitly permits it"; + + controller.beginDataPathCall(); + controller.LogError(source, &recoverable); + controller.LogError(source, &internalFailure); + const auto mixedFailure = controller.decide(OCSD_RESP_FATAL_INVALID_DATA); + ASSERT_TRUE(mixedFailure.action == OpenCsdErrorController::Action::Abort) + << "a recoverable stream error must not hide a non-recoverable error from the same call"; + ASSERT_TRUE(mixedFailure.error.has_value() && mixedFailure.error->code == OCSD_ERR_MEM) + << "fatal mixed-error calls should identify the non-recoverable cause"; + ASSERT_TRUE(mixedFailure.errors.size() == 2U) << "mixed OpenCSD errors should all be retained"; + + controller.beginDataPathCall(); + const ocsdError warning(OCSD_ERR_SEV_WARN, OCSD_ERR_BAD_PACKET_SEQ, 789U, 1U, "warning-only packet observation"); + controller.LogError(source, &warning); + ASSERT_TRUE(controller.decide(OCSD_RESP_WARN_CONT).action == OpenCsdErrorController::Action::Continue) + << "warning-only packet observations should not trigger stream recovery"; + ASSERT_TRUE(controller.decide(OCSD_RESP_FATAL_INVALID_DATA).action == OpenCsdErrorController::Action::Abort) + << "a warning must not justify recovery from a fatal invalid-data response"; + + controller.beginDataPathCall(); + ASSERT_TRUE(controller.decide(OCSD_RESP_WAIT).action == OpenCsdErrorController::Action::Wait) + << "WAIT response should request a flush"; + ASSERT_TRUE(controller.decide(OCSD_RESP_CONT).action == OpenCsdErrorController::Action::Continue) + << "CONT response should continue"; +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerClassifiesErrorConditions) +{ + EXPECT_FALSE(OpenCsdErrorController::responseReportsError(OCSD_RESP_CONT)); + EXPECT_TRUE(OpenCsdErrorController::responseReportsError(OCSD_RESP_ERR_CONT)); + EXPECT_TRUE(OpenCsdErrorController::responseReportsError(OCSD_RESP_ERR_WAIT)); + EXPECT_TRUE(OpenCsdErrorController::responseReportsError(OCSD_RESP_FATAL_SYS_ERR)); + EXPECT_TRUE(OpenCsdErrorController::isRecoverableStreamError(OCSD_ERR_BAD_PACKET_SEQ)); + EXPECT_TRUE(OpenCsdErrorController::isRecoverableStreamError(OCSD_ERR_INVALID_PCKT_HDR)); + EXPECT_FALSE(OpenCsdErrorController::isRecoverableStreamError(OCSD_ERR_MEM)); +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerMapsDecisionDetails) +{ + OpenCsdErrorController::Decision noError; + noError.response = OCSD_RESP_CONT; + EXPECT_EQ(OpenCsdErrorController::errorOffset(noError, 42U), 42U); + EXPECT_EQ(OpenCsdErrorController::issueCode(noError), TraceIssueCode::OpenCsdDecodeError); + + const auto headerError = makeDecision(OCSD_RESP_ERR_CONT, OCSD_ERR_INVALID_PCKT_HDR, 17U); + EXPECT_EQ(OpenCsdErrorController::errorOffset(headerError, 42U), 17U); + EXPECT_EQ(OpenCsdErrorController::issueCode(headerError), TraceIssueCode::OpenCsdInvalidPacketHeader); + + const auto ordinaryError = makeDecision(OCSD_RESP_ERR_CONT, OCSD_ERR_MEM); + EXPECT_EQ(OpenCsdErrorController::errorOffset(ordinaryError, 42U), 42U); + EXPECT_EQ(OpenCsdErrorController::issueCode(ordinaryError), TraceIssueCode::OpenCsdDecodeError); +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerSummarizesKnownFailures) +{ + constexpr std::array summaries{ + std::pair{OCSD_ERR_BAD_PACKET_SEQ, "OpenCSD detected an invalid ITM packet sequence."}, + std::pair{OCSD_ERR_INVALID_PCKT_HDR, "OpenCSD detected an invalid ITM packet header."}, + std::pair{OCSD_ERR_NOT_INIT, "OpenCSD decoder is not initialized."}, + std::pair{OCSD_ERR_MEM, "OpenCSD decoder ran out of memory."}, + std::pair{OCSD_ERR_INVALID_PARAM_VAL, "OpenCSD rejected a decoder parameter."}, + std::pair{OCSD_ERR_INVALID_PARAM_TYPE, "OpenCSD rejected a decoder parameter."}, + std::pair{OCSD_ERR_FILE_ERROR, "OpenCSD could not read required input data."}, + std::pair{OCSD_ERR_RDR_FILE_NOT_FOUND, "OpenCSD could not read required input data."}, + std::pair{OCSD_ERR_DATA_DECODE_FATAL, "OpenCSD could not decode the trace data."}, + std::pair{OCSD_ERR_INVALID_ID, "OpenCSD decoder error."}, + }; + for (const auto& [code, summary] : summaries) { + EXPECT_EQ(OpenCsdErrorController::describeSummary(makeDecision(OCSD_RESP_ERR_CONT, code)), summary); + } + + auto offsetError = makeDecision(OCSD_RESP_ERR_CONT, OCSD_ERR_MEM, 0U); + EXPECT_EQ(OpenCsdErrorController::describeSummary(offsetError), "OpenCSD decoder ran out of memory at raw offset 0."); +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerSummarizesFatalResponses) +{ + constexpr std::array summaries{ + std::pair{OCSD_RESP_FATAL_NOT_INIT, "OpenCSD decoder is not initialized."}, + std::pair{OCSD_RESP_FATAL_INVALID_OP, "OpenCSD rejected a decoder operation."}, + std::pair{OCSD_RESP_FATAL_INVALID_PARAM, "OpenCSD rejected a decoder parameter."}, + std::pair{OCSD_RESP_FATAL_INVALID_DATA, "OpenCSD rejected invalid trace data."}, + std::pair{OCSD_RESP_FATAL_SYS_ERR, "OpenCSD reported a system error."}, + std::pair{OCSD_RESP_CONT, "OpenCSD decoder error."}, + }; + for (const auto& [response, summary] : summaries) { + OpenCsdErrorController::Decision decision; + decision.response = response; + EXPECT_EQ(OpenCsdErrorController::describeSummary(decision), summary); + } +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerTracksLoggerState) +{ + OpenCsdErrorController controller; + EXPECT_EQ(controller.GetErrorLogVerbosity(), OCSD_ERR_SEV_INFO); + EXPECT_EQ(controller.GetLastError(), nullptr); + EXPECT_EQ(controller.GetLastIDError(0U), nullptr); + EXPECT_EQ(controller.GetLastIDError(0x70U), nullptr); + EXPECT_EQ(controller.getOutputLogger(), nullptr); + controller.LogError(0U, nullptr); + EXPECT_EQ(controller.GetLastError(), nullptr); + + const ocsdError withoutIndex(OCSD_ERR_SEV_ERROR, OCSD_ERR_MEM, "allocation failed \r\n\t"); + controller.LogError(0U, &withoutIndex); + EXPECT_EQ(controller.GetLastError()->getErrorCode(), OCSD_ERR_MEM); + const auto firstDecision = controller.decide(OCSD_RESP_ERR_CONT); + ASSERT_TRUE(firstDecision.error.has_value()); + EXPECT_FALSE(firstDecision.error->hasIndex); + EXPECT_EQ(firstDecision.error->index, 0U); + EXPECT_EQ(firstDecision.error->message, "allocation failed"); + + const ocsdError traceError(OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 73U, 3U, "bad header"); + controller.LogError(0U, &traceError); + ASSERT_NE(controller.GetLastIDError(3U), nullptr); + EXPECT_EQ(controller.GetLastIDError(3U)->getErrorIndex(), 73U); + EXPECT_EQ(controller.GetLastIDError(4U), nullptr); +} + +TEST(CtraceUnitTests, testOpenCsdErrorControllerForwardsLoggerMessages) +{ + OpenCsdErrorController controller; + ocsdMsgLogger logger; + MessageSink sink; + logger.setStrOutFn(&sink); + controller.setOutputLogger(&logger); + EXPECT_EQ(controller.getOutputLogger(), &logger); + + const auto source = controller.RegisterErrorSource("ITM decoder"); + controller.LogMessage(source, OCSD_ERR_SEV_INFO, "configured"); + controller.LogMessage(99U, OCSD_ERR_SEV_ERROR, "fallback"); + EXPECT_NE(sink.messages().find("ITM decoder : configured"), std::string::npos); + EXPECT_NE(sink.messages().find("unknown : fallback"), std::string::npos); + + logger.setLogOpts(ocsdMsgLogger::OUT_NONE); + controller.LogMessage(source, OCSD_ERR_SEV_INFO, "muted"); + EXPECT_EQ(sink.messages().find("muted"), std::string::npos); + + controller.setOutputLogger(nullptr); + controller.LogMessage(source, OCSD_ERR_SEV_ERROR, "disabled"); + EXPECT_EQ(sink.messages().find("disabled"), std::string::npos); +} diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp new file mode 100644 index 000000000..648188bfa --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/OpenCsdItmDecoderTests.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdTestSupport.h" +#include "OpenCsdSessionTestSupport.h" +#include "TestSupport.h" + +#include + +#include "OpenCsdErrorController.h" +#include "OpenCsdItmDecoder.h" +#include "OpenCsdItmSession.h" +#include "OpenCsdPacketCollector.h" +#include "TraceEvent.h" +#include "opencsd/ocsd_if_types.h" + +#include +#include +#include +#include +#include + +using OpenCsdSessionTestSupport::ScriptedDecoderHarness; +using OpenCsdTestSupport::CollectingOpenCsdElementSink; + +TEST(CtraceUnitTests, testOpenCsdItmDecoderChunksAndFinishesOnce) +{ + ScriptedDecoderHarness harness; + harness.push(5000U); + EXPECT_EQ(harness.script().pushCalls, 2U); + EXPECT_EQ(harness.decoder().finish().bytesIn, 5000U); + EXPECT_EQ(harness.decoder().finish().bytesIn, 5000U); + EXPECT_EQ(harness.script().endCalls, 1U); + EXPECT_THROW(harness.push(1U), std::runtime_error); + EXPECT_TRUE(harness.sink().hasIssue(TraceIssueCode::DataLoss)); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderRecoversAndMarksConsumedDataLoss) +{ + ScriptedDecoderHarness harness; + harness.script().pushes = { + {OCSD_RESP_ERR_CONT, 2U, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 1U, "reserved header"}}}, + {OCSD_RESP_CONT, 2U, false, {}}, + {OCSD_RESP_CONT, 2U, true, {}}, + }; + harness.push(6U); + EXPECT_EQ(harness.decoder().finish().bytesIn, 6U); + EXPECT_EQ(harness.script().resetCalls, 1U); + EXPECT_TRUE(harness.sink().hasIssue(TraceIssueCode::OpenCsdInvalidPacketHeader)); + EXPECT_TRUE(harness.sink().hasIssue(TraceIssueCode::DataLoss)); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderMarksUnframedDataBeforeSync) +{ + ScriptedDecoderHarness harness; + harness.script().pushes = { + {OCSD_RESP_CONT, 2U, false, {}}, + {OCSD_RESP_CONT, 2U, true, {}}, + }; + harness.push(4U); + harness.decoder().finish(); + EXPECT_TRUE(harness.sink().hasIssue(TraceIssueCode::DataLoss)); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderFlushesWaitResponses) +{ + ScriptedDecoderHarness harness; + harness.script().pushes = { + {OCSD_RESP_WAIT, 1U, false, {}}, + {OCSD_RESP_WAIT, 1U, true, {}}, + }; + harness.script().flushes = {{OCSD_RESP_CONT}, {OCSD_RESP_CONT}}; + harness.push(2U); + harness.decoder().finish(); + EXPECT_EQ(harness.script().flushCalls, 2U); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderReportsWarningsAndImplicitErrors) +{ + ScriptedDecoderHarness harness; + harness.script().pushes = {{OCSD_RESP_ERR_CONT, + 1U, + false, + { + {OCSD_ERR_SEV_INFO, OCSD_ERR_FAIL, 0U, "info"}, + {OCSD_ERR_SEV_WARN, OCSD_ERR_BAD_PACKET_SEQ, 0U, "warning"}, + }}}; + harness.push(1U); + harness.decoder().finish(); + ASSERT_GE(harness.sink().elements().size(), 2U); + EXPECT_EQ(harness.sink().elements()[0].issueSeverity, TraceIssueSeverity::Warning); + EXPECT_EQ(harness.sink().elements()[1].issueSeverity, TraceIssueSeverity::Error); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderHandlesEndOfTraceDecisions) +{ + ScriptedDecoderHarness recovery; + recovery.script().ends = { + {OCSD_RESP_ERR_CONT, std::nullopt, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_BAD_PACKET_SEQ, 0U, "bad tail"}}}}; + EXPECT_NO_THROW(recovery.decoder().finish()); + EXPECT_TRUE(recovery.sink().hasIssue(TraceIssueCode::OpenCsdBadPacketSequence)); + + ScriptedDecoderHarness wait; + wait.script().ends = {{OCSD_RESP_WAIT}}; + wait.script().flushes = {{OCSD_RESP_CONT}}; + EXPECT_NO_THROW(wait.decoder().finish()); + EXPECT_EQ(wait.script().flushCalls, 1U); + + ScriptedDecoderHarness fatal; + fatal.script().ends = {{OCSD_RESP_FATAL_SYS_ERR}}; + EXPECT_THROW((void)fatal.decoder().finish(), OpenCsdFatalError); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderAbortsFatalAndNoProgressData) +{ + ScriptedDecoderHarness fatal; + fatal.script().pushes = {{OCSD_RESP_FATAL_SYS_ERR, 1U}}; + try { + fatal.push(1U); + FAIL() << "fatal OpenCSD response did not abort decoding"; + } catch (const OpenCsdFatalError& error) { + EXPECT_EQ(error.bytesProcessed(), 1U); + } + + ScriptedDecoderHarness stalled; + stalled.script().pushes = {{OCSD_RESP_CONT, 0U}, {OCSD_RESP_CONT, 0U}}; + EXPECT_THROW(stalled.push(1U), OpenCsdFatalError); + EXPECT_TRUE(stalled.sink().hasIssue(TraceIssueCode::OpenCsdNoProgress)); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderBoundsZeroProgressRecoveryAndWaitRetries) +{ + ScriptedDecoderHarness recovery; + recovery.script().pushes = { + {OCSD_RESP_ERR_CONT, 0U, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 0U, "bad packet"}}}, + {OCSD_RESP_ERR_CONT, 0U, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 0U, "bad packet"}}}, + }; + EXPECT_THROW(recovery.push(1U), OpenCsdFatalError); + EXPECT_EQ(recovery.script().pushCalls, 2U); + EXPECT_EQ(recovery.script().resetCalls, 1U); + EXPECT_TRUE(recovery.sink().hasIssue(TraceIssueCode::OpenCsdNoProgress)); + + ScriptedDecoderHarness wait; + wait.script().pushes = { + {OCSD_RESP_WAIT, 0U}, + {OCSD_RESP_WAIT, 0U}, + }; + wait.script().flushes = {{OCSD_RESP_CONT}}; + EXPECT_THROW(wait.push(1U), OpenCsdFatalError); + EXPECT_EQ(wait.script().pushCalls, 2U); + EXPECT_EQ(wait.script().flushCalls, 1U); + EXPECT_TRUE(wait.sink().hasIssue(TraceIssueCode::OpenCsdNoProgress)); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderAllowsZeroProgressRetriesToResume) +{ + ScriptedDecoderHarness harness; + harness.script().pushes = { + {OCSD_RESP_WAIT, 0U}, + {OCSD_RESP_CONT, 1U, true}, + }; + harness.script().flushes = {{OCSD_RESP_CONT}}; + + EXPECT_NO_THROW(harness.push(1U)); + EXPECT_EQ(harness.decoder().finish().bytesIn, 1U); + EXPECT_EQ(harness.script().pushCalls, 2U); + EXPECT_EQ(harness.script().flushCalls, 1U); + + ScriptedDecoderHarness recovery; + recovery.script().pushes = { + {OCSD_RESP_ERR_CONT, 0U, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 0U, "bad packet"}}}, + {OCSD_RESP_CONT, 1U, true}, + }; + EXPECT_NO_THROW(recovery.push(1U)); + EXPECT_EQ(recovery.decoder().finish().bytesIn, 1U); + EXPECT_EQ(recovery.script().pushCalls, 2U); + EXPECT_EQ(recovery.script().resetCalls, 1U); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderHandlesFlushRecoveryAndTimeout) +{ + ScriptedDecoderHarness recovery; + recovery.script().ends = {{OCSD_RESP_WAIT}}; + recovery.script().flushes = { + {OCSD_RESP_ERR_CONT, std::nullopt, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 0U, "bad flush"}}}}; + EXPECT_NO_THROW(recovery.decoder().finish()); + EXPECT_EQ(recovery.script().resetCalls, 1U); + + ScriptedDecoderHarness committed; + committed.script().ends = {{OCSD_RESP_WAIT}}; + committed.script().flushes = {{OCSD_RESP_CONT, std::nullopt, true}}; + EXPECT_NO_THROW(committed.decoder().finish()); + EXPECT_FALSE(committed.sink().elements().empty()); + + ScriptedDecoderHarness fatal; + fatal.script().ends = {{OCSD_RESP_WAIT}}; + fatal.script().flushes = {{OCSD_RESP_FATAL_SYS_ERR}}; + EXPECT_THROW((void)fatal.decoder().finish(), OpenCsdFatalError); + + ScriptedDecoderHarness timeout; + timeout.script().ends = {{OCSD_RESP_WAIT}}; + timeout.script().defaultFlushResponse = OCSD_RESP_WAIT; + EXPECT_THROW((void)timeout.decoder().finish(), OpenCsdFatalError); + EXPECT_EQ(timeout.script().flushCalls, 1024U); + EXPECT_TRUE(timeout.sink().hasIssue(TraceIssueCode::OpenCsdWaitTimeout)); +} + +TEST(CtraceUnitTests, testOpenCsdItmDecoderReportsResetAndInitializationFailures) +{ + ScriptedDecoderHarness reset; + reset.script().pushes = { + {OCSD_RESP_ERR_CONT, 1U, false, {{OCSD_ERR_SEV_ERROR, OCSD_ERR_INVALID_PCKT_HDR, 0U, "bad packet"}}}}; + reset.script().resets = {{OCSD_RESP_WAIT}}; + EXPECT_THROW(reset.push(1U), OpenCsdFatalError); + + CollectingOpenCsdElementSink nullSink; + const OpenCsdItmSessionFactory nullFactory = + [](OpenCsdPacketCollector&, OpenCsdErrorController&) -> std::unique_ptr { + return nullptr; + }; + EXPECT_THROW((void)OpenCsdItmDecoder(nullSink, nullFactory), OpenCsdFatalError); + EXPECT_TRUE(nullSink.hasIssue(TraceIssueCode::OpenCsdInitializationError)); + + CollectingOpenCsdElementSink errorSink; + const OpenCsdItmSessionFactory errorFactory = + [](OpenCsdPacketCollector&, OpenCsdErrorController&) -> std::unique_ptr { + throw OpenCsdItmSessionError("synthetic session setup failure"); + }; + EXPECT_THROW((void)OpenCsdItmDecoder(errorSink, errorFactory), OpenCsdFatalError); + EXPECT_TRUE(errorSink.hasIssue(TraceIssueCode::OpenCsdInitializationError)); +} + +TEST(CtraceUnitTests, testOpenCsdItmSessionAcceptsEmptyDataPathOperations) +{ + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector(sink); + OpenCsdErrorController errors; + OpenCsdItmSession session(collector, errors); + + EXPECT_NE(errors.decide(session.reset()).action, OpenCsdErrorController::Action::Abort); + EXPECT_NE(errors.decide(session.flush()).action, OpenCsdErrorController::Action::Abort); + EXPECT_NE(errors.decide(session.endOfTrace()).action, OpenCsdErrorController::Action::Abort); +} + +TEST(CtraceUnitTests, testOpenCsdSessionValidationRejectsInvalidApiResults) +{ + const std::uint32_t object = 1U; + EXPECT_NO_THROW(OpenCsdSessionValidation::requireObject(&object, "valid object")); + EXPECT_THROW(OpenCsdSessionValidation::requireObject(nullptr, "missing object"), OpenCsdItmSessionError); + + EXPECT_NO_THROW(OpenCsdSessionValidation::requireSuccess(OCSD_OK, "successful call")); + const auto message = captureExceptionMessage( + [] { OpenCsdSessionValidation::requireSuccess(OCSD_ERR_MEM, "decoder setup failed"); }); + ASSERT_TRUE(message.has_value()); + EXPECT_NE(message->find("OCSD_ERR_MEM"), std::string::npos); + EXPECT_NE(message->find("decoder setup failed"), std::string::npos); +} + +TEST(CtraceUnitTests, testOpenCsdItmSessionRejectsMissingDecoderRegistry) +{ + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector(sink); + OpenCsdErrorController errors; + const auto missingRegistry = []() -> OcsdLibDcdRegister* { return nullptr; }; + + EXPECT_THROW((void)OpenCsdItmSession(collector, errors, nullptr), OpenCsdItmSessionError); + EXPECT_THROW((void)OpenCsdItmSession(collector, errors, missingRegistry), OpenCsdItmSessionError); +} diff --git a/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp b/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp new file mode 100644 index 000000000..6e93be712 --- /dev/null +++ b/tools/ctrace/test/unit/src/decode/OpenCsdPacketCollectorTests.cpp @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "OpenCsdTestSupport.h" +#include "TestSupport.h" + +#include + +#include "csv/CsvRowMapper.h" +#include "OpenCsdPacketCollector.h" +#include "OpenCsdTraceElement.h" +#include "TraceEvent.h" +#include "common/trc_gen_elem.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/itm/trc_pkt_types_itm.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/trc_gen_elem_types.h" + +#include +#include +#include +#include +#include +#include +#include + +using OpenCsdTestSupport::CollectingOpenCsdElementSink; + +/** @brief Collects one element and then simulates a downstream failure. */ +class ThrowingTraceElementSink final : public CollectingOpenCsdElementSink { +public: + /** @brief Stores the element before throwing the synthetic failure. */ + void append(OpenCsdTraceElement element) override + { + CollectingOpenCsdElementSink::append(std::move(element)); + throw std::runtime_error("synthetic output failure"); + } +}; + +/** @brief Creates a generic OpenCSD ITM element for callback tests. */ +static OcsdTraceElement itmElement(swt_itm_type type, std::uint8_t source = 0U, std::uint8_t size = 0U, + std::uint32_t value = 0U, bool overflow = false) +{ + OcsdTraceElement element; + element.setType(OCSD_GEN_TRC_ELEM_ITMTRACE); + swt_itm_info info{}; + info.pkt_type = type; + info.payload_src_id = source; + info.payload_size = size; + info.value = value; + info.overflow = overflow; + element.setSWT_ITMInfo(info); + return element; +} + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorUsesReconstructedGlobalTimestamp) +{ + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector(sink); + + auto globalTimestamp = itmElement(TS_GLOBAL); + globalTimestamp.setTS(0xfedcba9876543210ULL, true); + + ASSERT_TRUE(collector.TraceElemIn(42U, 7U, globalTimestamp) == OCSD_RESP_CONT) + << "OpenCSD global timestamp collection failed"; + ASSERT_TRUE(sink.elements().size() == 1U) << "reconstructed global timestamp element missing"; + const auto element = sink.elements().front(); + ASSERT_TRUE(element.kind == OpenCsdTraceElement::Kind::GlobalTimestamp) + << "reconstructed global timestamp kind mismatch"; + ASSERT_TRUE(element.sourceIndex == 42U) << "reconstructed global timestamp source index mismatch"; + ASSERT_TRUE(element.traceBusId == 7U) << "OpenCSD Trace Bus ID was not preserved"; + ASSERT_TRUE(element.timestampValue == 0xfedcba9876543210ULL) << "reconstructed global timestamp value mismatch"; + ASSERT_TRUE(element.clockChange) << "reconstructed global timestamp clock-change flag missing"; + + ASSERT_TRUE(collector.TraceElemIn(43U, 0xffU, globalTimestamp) == OCSD_RESP_CONT) + << "OpenCSD global timestamp collection with missing source ID failed"; + ASSERT_TRUE(sink.elements().back().traceBusId == 0U) << "an unavailable OpenCSD Trace Bus ID must fall back to zero"; + + ItmTrcPacket rawGts1; + rawGts1.setPktType(ITM_PKT_TS_GLOBAL_1); + rawGts1.setValue(0x123456U, 4U); + collector.RawPacketDataMon(OCSD_OP_DATA, 43U, &rawGts1, 0U, nullptr); + + ItmTrcPacket rawGts2; + rawGts2.setPktType(ITM_PKT_TS_GLOBAL_2); + rawGts2.setExtValue(0x123456789ULL); + collector.RawPacketDataMon(OCSD_OP_DATA, 44U, &rawGts2, 0U, nullptr); + ASSERT_TRUE(sink.elements().size() == 2U) + << "raw GTS fragments must not be published as independent global timestamps"; + + TraceEvent packet{GlobalTimestampTraceEvent{element.timestampValue, false}}; + ASSERT_TRUE(CsvRowMapper::row(packet) == std::to_string(element.timestampValue) + ",,global_ts,,,,,") + << "CSV must preserve all 64 global timestamp bits in the cycles column"; +} + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsLocalTimestampRelations) +{ + const std::array, 4> cases{{ + {TS_SYNC, LocalTimestampRelation::Synchronous}, + {TS_DELAY, LocalTimestampRelation::TimestampDelayed}, + {TS_PKT_DELAY, LocalTimestampRelation::PayloadDelayed}, + {TS_PKT_TS_DELAY, LocalTimestampRelation::TimestampAndPayloadDelayed}, + }}; + + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector(sink); + for (std::size_t index = 0; index < cases.size(); ++index) { + auto timestamp = itmElement(cases[index].first); + timestamp.setTS(100U + index, false); + + ASSERT_TRUE(collector.TraceElemIn(index, 0U, timestamp) == OCSD_RESP_CONT) + << "OpenCSD local timestamp collection failed"; + } + + ASSERT_TRUE(sink.elements().size() == cases.size()) << "local timestamp relation count mismatch"; + for (std::size_t index = 0; index < cases.size(); ++index) { + ASSERT_TRUE(sink.elements()[index].kind == OpenCsdTraceElement::Kind::LocalTimestamp) + << "local timestamp kind mismatch"; + ASSERT_TRUE(sink.elements()[index].timestampRelation == cases[index].second) << "local timestamp relation mismatch"; + } +} + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorMapsPayloadAndRawPacketKinds) +{ + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector(sink); + + const auto software = itmElement(SWIT_PAYLOAD, 7U, 4U, 0x12345678U, true); + EXPECT_EQ(collector.TraceElemIn(10U, 3U, software), OCSD_RESP_CONT); + ASSERT_EQ(sink.elements().size(), 1U); + EXPECT_EQ(sink.elements().back().kind, OpenCsdTraceElement::Kind::Software); + EXPECT_EQ(sink.elements().back().channel, 7U); + EXPECT_EQ(sink.elements().back().size, 4U); + EXPECT_EQ(sink.elements().back().value, 0x12345678U); + EXPECT_TRUE(sink.elements().back().overflow); + + const auto hardware = itmElement(DWT_PAYLOAD, 9U, 2U, 0x1234U); + EXPECT_EQ(collector.TraceElemIn(11U, 4U, hardware), OCSD_RESP_CONT); + ASSERT_EQ(sink.elements().size(), 2U); + EXPECT_EQ(sink.elements().back().kind, OpenCsdTraceElement::Kind::Hardware); + EXPECT_EQ(sink.elements().back().discriminator, 9U); + + OcsdTraceElement unrelated; + unrelated.setType(OCSD_GEN_TRC_ELEM_NO_SYNC); + EXPECT_EQ(collector.TraceElemIn(12U, 5U, unrelated), OCSD_RESP_CONT); + EXPECT_EQ(sink.elements().size(), 2U); + + const auto unknown = itmElement(static_cast(7)); + EXPECT_EQ(collector.TraceElemIn(12U, 5U, unknown), OCSD_RESP_CONT); + EXPECT_EQ(sink.elements().size(), 2U); + + collector.RawPacketDataMon(OCSD_OP_DATA, 13U, nullptr, 0U, nullptr); + ItmTrcPacket packet; + packet.setPktType(ITM_PKT_ASYNC); + collector.RawPacketDataMon(OCSD_OP_RESET, 14U, &packet, 0U, nullptr); + EXPECT_EQ(sink.elements().size(), 2U); + + /** @brief Describes one raw-packet callback mapping test case. */ + struct RawPacketCase { + ocsd_itm_pkt_type type; + ocsd_datapath_op_t operation; + OpenCsdTraceElement::Kind kind; + const char* errorMessage; + std::optional issueCode; + }; + constexpr RawPacketCase rawPacketCases[]{ + {ITM_PKT_OVERFLOW, OCSD_OP_DATA, OpenCsdTraceElement::Kind::Overflow, "", std::nullopt}, + {ITM_PKT_RESERVED, OCSD_OP_DATA, OpenCsdTraceElement::Kind::Error, "Reserved ITM packet", + TraceIssueCode::OpenCsdDecodeError}, + {ITM_PKT_BAD_SEQUENCE, OCSD_OP_DATA, OpenCsdTraceElement::Kind::Error, "Bad ITM packet sequence", + TraceIssueCode::OpenCsdDecodeError}, + {ITM_PKT_INCOMPLETE_EOT, OCSD_OP_EOT, OpenCsdTraceElement::Kind::Error, "incomplete ITM packet at end of input", + TraceIssueCode::OpenCsdIncompleteTail}, + }; + for (std::size_t index = 0U; index < std::size(rawPacketCases); ++index) { + SCOPED_TRACE(index); + const auto& testCase = rawPacketCases[index]; + packet.setPktType(testCase.type); + collector.RawPacketDataMon(testCase.operation, 15U + index, &packet, 0U, nullptr); + ASSERT_EQ(sink.elements().size(), 3U + index); + EXPECT_EQ(sink.elements().back().kind, testCase.kind); + EXPECT_EQ(sink.elements().back().errorMessage, testCase.errorMessage); + EXPECT_EQ(sink.elements().back().issueCode, testCase.issueCode); + } + + packet.setPktType(ITM_PKT_SWIT); + collector.RawPacketDataMon(OCSD_OP_DATA, 19U, &packet, 0U, nullptr); + EXPECT_EQ(sink.elements().size(), 6U); +} + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorTransactionsPreserveOnlyCommittedElements) +{ + CollectingOpenCsdElementSink sink; + OpenCsdPacketCollector collector(sink); + EXPECT_NO_THROW(collector.rethrowOutputError()); + EXPECT_FALSE(collector.transactionFirstSourceOffset().has_value()); + + collector.beginTransaction(); + collector.appendDecodeError(5U, "decode", TraceIssueCode::DecodeError); + collector.prependDiscontinuity(4U, "gap", TraceIssueCode::DataLoss, 2U); + collector.prependDataLossError(3U, "loss", 3U); + EXPECT_EQ(collector.transactionElementCount(), 3U); + EXPECT_EQ(collector.transactionFirstSourceOffset(), 3U); + collector.commitTransactionBefore(5U); + ASSERT_EQ(sink.elements().size(), 1U); + EXPECT_EQ(sink.elements().front().kind, OpenCsdTraceElement::Kind::Discontinuity); + + collector.beginTransaction(); + collector.appendDecodeError(7U, "rolled back"); + collector.rollbackTransaction(); + EXPECT_EQ(sink.elements().size(), 1U); + + collector.prependDiscontinuity(8U, "outside", TraceIssueCode::DataLoss); + collector.prependDataLossError(9U, "outside loss", 4U); + ASSERT_EQ(sink.elements().size(), 3U); + EXPECT_EQ(sink.elements().back().rawBytesConsumed, 4U); + + collector.beginTransaction(); + collector.appendDecodeError(10U, "committed"); + collector.commitTransaction(); + ASSERT_EQ(sink.elements().size(), 4U); + EXPECT_EQ(sink.elements().back().sourceIndex, 10U); +} + +TEST(CtraceUnitTests, testOpenCsdPacketCollectorDefersOutputFailures) +{ + ThrowingTraceElementSink sink; + OpenCsdPacketCollector collector(sink); + const auto software = itmElement(SWIT_PAYLOAD, 1U, 1U, 42U); + + EXPECT_EQ(collector.TraceElemIn(1U, 1U, software), OCSD_RESP_FATAL_SYS_ERR); + EXPECT_EQ(collector.TraceElemIn(2U, 1U, software), OCSD_RESP_FATAL_SYS_ERR); + EXPECT_EQ(sink.elements().size(), 2U); + EXPECT_THROW(collector.rethrowOutputError(), std::runtime_error); + EXPECT_NO_THROW(collector.rethrowOutputError()); + + ItmTrcPacket packet; + packet.setPktType(ITM_PKT_ASYNC); + collector.RawPacketDataMon(OCSD_OP_DATA, 3U, &packet, 0U, nullptr); + collector.RawPacketDataMon(OCSD_OP_DATA, 4U, &packet, 0U, nullptr); + EXPECT_EQ(sink.elements().size(), 4U); + EXPECT_THROW(collector.rethrowOutputError(), std::runtime_error); +} diff --git a/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp b/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp new file mode 100644 index 000000000..710330f67 --- /dev/null +++ b/tools/ctrace/test/unit/src/diagnostics/DiagnosticsTests.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceIssueReporter.h" +#include +#include + +TEST(CtraceUnitTests, testDiagnosticCollection) +{ + CollectingDiagnosticSink sink; + sink.report({ + DiagnosticSink::Severity::Warning, + "timestamp discontinuity", + {{"offset", "42"}}, + }); + + ASSERT_TRUE(sink.events().size() == 1) << "diagnostic event count mismatch"; + ASSERT_TRUE(sink.events()[0].severity == DiagnosticSink::Severity::Warning) << "diagnostic severity mismatch"; + ASSERT_TRUE(toString(sink.events()[0].severity) == "warning") << "diagnostic severity text mismatch"; + ASSERT_TRUE(sink.failureCount() == 0U) << "warnings must not fail the command"; + + sink.report({ + DiagnosticSink::Severity::Error, + "generator could not create an irrelevant register", + {}, + DiagnosticSink::Impact::NonFailing, + }); + ASSERT_TRUE(sink.events().back().impact == DiagnosticSink::Impact::NonFailing && sink.failureCount() == 0U) + << "non-failing errors must retain error severity without failing the job"; + + sink.report({ + DiagnosticSink::Severity::Error, + "required input is missing", + }); + ASSERT_TRUE(sink.events().back().impact == DiagnosticSink::Impact::Failing && sink.failureCount() == 1U) + << "failing errors must fail the job"; +} + +TEST(CtraceUnitTests, testDiagnosticTextCoversSeverityAndFormatting) +{ + EXPECT_EQ(toString(DiagnosticSink::Severity::Info), "info"); + EXPECT_EQ(toString(DiagnosticSink::Severity::Warning), "warning"); + EXPECT_EQ(toString(DiagnosticSink::Severity::Error), "error"); + EXPECT_EQ(toString(static_cast(99)), "unknown"); + + StderrDiagnosticSink sink; + testing::internal::CaptureStderr(); + sink.report({DiagnosticSink::Severity::Info, "message with context", {{"argument", "--all"}}}); + sink.report({DiagnosticSink::Severity::Error, "plain message", {}, DiagnosticSink::Impact::Failing}); + const auto text = testing::internal::GetCapturedStderr(); + EXPECT_NE(text.find("[info] message with context: argument=--all"), std::string::npos); + EXPECT_NE(text.find("[error] plain message"), std::string::npos); + EXPECT_EQ(text.find("cli"), std::string::npos); + EXPECT_EQ(text.find("output/write"), std::string::npos); +} + +TEST(CtraceUnitTests, testTraceIssueReporterReportsEveryIssue) +{ + CollectingDiagnosticSink payloadIndependentDiagnostics; + TraceIssueReporter payloadIndependentReporter(payloadIndependentDiagnostics); + TraceEvent payloadIndependentError = issuePacket(TraceIssueCode::OpenCsdIncompleteTail); + payloadIndependentReporter.append(payloadIndependentError); + ASSERT_TRUE(payloadIndependentDiagnostics.events().size() == 1U) + << "decoder errors must remain visible independently of payload filtering"; + ASSERT_TRUE(payloadIndependentDiagnostics.failureCount() == 1U) + << "decoder errors must fail validation independently of payload filtering"; + + CollectingDiagnosticSink diagnostics; + TraceIssueReporter reporter(diagnostics); + + reporter.append(overflowPacket(10)); + reporter.append(overflowPacket(20)); + reporter.finish(); + reporter.finish(); + + TraceEvent dataLoss = issuePacket(TraceIssueCode::DataLoss, "trace data was lost"); + dataLoss.index = 12U; + std::get(dataLoss.payload).rawBytesConsumed = 3U; + reporter.append(dataLoss); + reporter.append(dataLoss); + + TraceEvent warning = issuePacket(TraceIssueCode::OpenCsdDecodeError, "decoder warning", TraceIssueSeverity::Warning); + reporter.append(warning); + + TraceEvent initializationError = issuePacket(TraceIssueCode::OpenCsdInitializationError, "decoder setup failed"); + reporter.append(initializationError); + + ASSERT_TRUE(diagnostics.events().size() == 5U) + << "TraceIssueReporter should report every error and warning occurrence"; + ASSERT_TRUE(diagnostics.events()[0].severity == DiagnosticSink::Severity::Warning) + << "TraceIssueReporter overflow severity mismatch"; + EXPECT_NE(diagnostics.events()[0].message.find("1 more occurred"), std::string::npos); + ASSERT_TRUE(diagnostics.events()[1].severity == DiagnosticSink::Severity::Error) + << "TraceIssueReporter data-loss severity mismatch"; + ASSERT_TRUE(diagnostics.events()[1].context.empty()) << "TraceIssueReporter context mismatch"; + ASSERT_TRUE(diagnostics.events()[1].message.find("3 raw bytes") != std::string::npos) + << "TraceIssueReporter should include the lost byte count"; + ASSERT_TRUE(diagnostics.events()[2].message == diagnostics.events()[1].message) + << "TraceIssueReporter repeated data-loss message mismatch"; + ASSERT_TRUE(diagnostics.events()[3].severity == DiagnosticSink::Severity::Warning) + << "TraceIssueReporter should preserve warning severity"; + ASSERT_TRUE(diagnostics.events()[4].context.empty()) << "TraceIssueReporter context mismatch"; + ASSERT_TRUE(diagnostics.failureCount() == 3U) << "TraceIssueReporter should classify decoder errors as failing"; +} + +TEST(CtraceUnitTests, testTraceIssueReporterFormatsUnknownOverflowTimestamp) +{ + CollectingDiagnosticSink diagnostics; + TraceIssueReporter reporter(diagnostics); + reporter.append(TraceEvent{OverflowTraceEvent{"overflow"}}); + reporter.finish(); + + ASSERT_EQ(diagnostics.events().size(), 1U); + EXPECT_NE(diagnostics.events().front().message.find("unknown cycle timestamp"), std::string::npos); + EXPECT_EQ(diagnostics.events().front().message.find("cycle timestamp 0"), std::string::npos); + EXPECT_EQ(diagnostics.events().front().message.find("0 more occurred"), std::string::npos); +} + +TEST(CtraceUnitTests, testTraceIssueReporterFormatsEveryErrorKind) +{ + CollectingDiagnosticSink diagnostics; + TraceIssueReporter reporter(diagnostics); + + /** @brief Describes one diagnostic formatting test case. */ + struct Case { + TraceIssueCode code; + const char* message; + }; + constexpr Case cases[]{ + {TraceIssueCode::OpenCsdBadPacketSequence, "invalid ITM packet sequence at raw offset 42"}, + {TraceIssueCode::OpenCsdInvalidPacketHeader, "invalid ITM packet header at raw offset 42"}, + {TraceIssueCode::OpenCsdIncompleteTail, "incomplete ITM packet starting at raw offset 42 at end of input"}, + {TraceIssueCode::OpenCsdNoProgress, "OpenCSD made no decode progress at raw offset 42"}, + {TraceIssueCode::OpenCsdWaitTimeout, "OpenCSD remained blocked while flushing pending data"}, + {TraceIssueCode::OpenCsdInitializationError, "OpenCSD initialization failed"}, + {TraceIssueCode::DecodeError, "trace decode error at raw offset 42"}, + {static_cast(255U), "trace decode error at raw offset 42"}, + }; + for (const auto& testCase : cases) { + auto event = issuePacket(testCase.code); + event.index = 42U; + reporter.append(event); + } + + auto dataLoss = issuePacket(TraceIssueCode::DataLoss); + dataLoss.index = 43U; + reporter.append(dataLoss); + + auto warningDataLoss = issuePacket(TraceIssueCode::DataLoss, "warning loss", TraceIssueSeverity::Warning); + reporter.append(warningDataLoss); + reporter.append(issuePacket(TraceIssueCode::DecodeError)); + + ASSERT_EQ(diagnostics.events().size(), std::size(cases) + 3U); + for (std::size_t index = 0U; index < std::size(cases); ++index) { + EXPECT_EQ(diagnostics.events()[index].message, cases[index].message); + } + EXPECT_NE(diagnostics.events()[std::size(cases)].message.find("raw offset 43"), std::string::npos); + EXPECT_EQ(diagnostics.events()[std::size(cases) + 1U].severity, DiagnosticSink::Severity::Warning); + EXPECT_EQ(diagnostics.events().back().message, "trace decode error at raw offset 0"); +} diff --git a/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp new file mode 100644 index 000000000..f9461d311 --- /dev/null +++ b/tools/ctrace/test/unit/src/model/TraceSelectionTests.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include +#include "TraceEvent.h" +#include "TraceSelection.h" +#include "TraceStreamId.h" +#include "TraceRunConfig.h" + +TEST(CtraceUnitTests, testTraceSelection) +{ + EXPECT_EQ(traceEventTypeName(TraceEventType::Count), "unknown"); + EXPECT_EQ(traceEventTypeName(static_cast(1000U)), "unknown"); + EXPECT_FALSE(CoreSight::isAtbTraceId(0U)); + EXPECT_TRUE(CoreSight::isAtbTraceId(1U)); + EXPECT_FALSE(CoreSight::isAtbTraceId(112U)); + EXPECT_TRUE(CoreSight::isItmStimulusPort(0U)); + EXPECT_TRUE(CoreSight::isItmStimulusPort(31U)); + EXPECT_FALSE(CoreSight::isItmStimulusPort(32U)); + EXPECT_TRUE(TraceRunSchema::isDwtDataType("unsigned int")); + EXPECT_TRUE(TraceRunSchema::isDwtDataType("signed int")); + EXPECT_TRUE(TraceRunSchema::isDwtDataType("float")); + TraceRunReference timestampReference; + timestampReference.ctraceRef = "timestamps"; + EXPECT_TRUE(TraceRunSchema::isTimestampReference(timestampReference)); + TraceRunReference itmReference; + itmReference.type = "itm"; + itmReference.ctraceRef = "itm"; + EXPECT_TRUE(TraceRunSchema::isProcessorItmReference(itmReference)); + TraceEvent itm = softwarePacket(1U); + itm.traceBusId = 1; + ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {}})) << "TraceSelection ITM type mismatch"; + ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{"dwt"}, {}})) + << "TraceSelection should reject unrelated DWT type"; + ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{}, {1U, 2U}})) << "TraceSelection stream-set mismatch"; + ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{}, {2U, 3U}})) + << "TraceSelection should reject other stream"; + ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {1U}})) + << "TraceSelection combined criteria mismatch"; + ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {2U}})) + << "TraceSelection combined stream mismatch"; + + itm.traceBusId = 0U; + ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {}})) + << "TraceSelection type selector must retain Trace Bus ID 0 input"; + ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{}, {1U}})) + << "a non-zero stream selector must not match Trace Bus ID 0 input"; + ASSERT_TRUE(traceEventSelectedForOutput(itm, TraceSelection{{}, {0U}})) + << "stream selector 0 must match unformatted single-source input"; + std::get(itm.payload).channel = 0; + ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{})) + << "TraceSelection must exclude software channel zero without selectors"; + ASSERT_TRUE(!traceEventSelectedForOutput(itm, TraceSelection{{"itm"}, {1U}})) + << "TraceSelection selectors must not enable software channel zero"; + + TraceEvent softwareError = issuePacket(TraceIssueCode::DecodeError); + ASSERT_TRUE(traceEventSelectedForOutput(softwareError, TraceSelection{})) + << "TraceSelection must retain decoder errors"; + + TraceEvent dwt{DwtDataTraceEvent{2U}}; + ASSERT_TRUE(traceEventSelectedForOutput(dwt, TraceSelection{{"dwt"}, {}})) << "TraceSelection DWT type mismatch"; + ASSERT_TRUE(traceEventSelectedForOutput(dwt, TraceSelection{{"itm", "dwt"}, {}})) + << "TraceSelection type OR mismatch"; + + TraceEvent exception{ExceptionTraceEvent{11U, ExceptionAction::Entered}}; + ASSERT_TRUE(traceEventSelectedForOutput(exception, TraceSelection{{"exception"}, {}})) + << "TraceSelection exception type mismatch"; +} diff --git a/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp new file mode 100644 index 000000000..9434254e1 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/OutputRequirementsTests.cpp @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include "TraceRunTestSupport.h" +#include +#include "CtraceRunMeta.h" +#include "OutputRequirements.h" +#include "TraceOutputConfig.h" +#include "TraceRunConfig.h" +#include +#include +#include +#include +#include +#include +#include + +/** @brief Creates an output request selecting test formats. */ +static TraceOutputRequest outputRequest(bool csv, bool ctf) +{ + return {csv, ctf, {}}; +} + +/** @brief Plans outputs using explicitly supplied trace metadata. */ +static TraceOutputPlan planOutputs(const TraceOutputRequest& request, const std::filesystem::path& rawInputPath, + const CtraceRunMeta& meta, DiagnosticSink& diagnostics) +{ + return planTraceOutputs(request, rawInputPath, meta, diagnostics); +} + +/** @brief Plans outputs after normalizing a trace-run configuration. */ +static TraceOutputPlan planOutputs(const TraceOutputRequest& request, const std::filesystem::path& rawInputPath, + const TraceRunConfig& config, DiagnosticSink& diagnostics) +{ + return planOutputs(request, rawInputPath, CtraceRunMeta::fromConfig(config), diagnostics); +} + +/** @brief Creates metadata satisfying the default backend requirements. */ +static TraceRunConfig backendRequirementsConfig() +{ + TraceRunConfig config; + config.path = "BackendRequirements.ctrace-run.yml"; + auto setup = TraceRunTestSupport::makeTimestampSetup(std::nullopt, 400000000U, 1U); + setup.data.push_back(TraceRunDataSetup{"double", 4U}); + config.setups.push_back(setup); + auto reference = TraceRunTestSupport::makeReference("dwt", std::nullopt, std::nullopt, {0U}, "opaque/dwt"); + reference.dataSetupIndex = 0U; + config.references.push_back(reference); + return config; +} + +TEST(CtraceUnitTests, testBackendRequirementsUsePerStreamMetadata) +{ + TraceRunConfig multicore; + multicore.path = "Multicore.ctrace-run.yml"; + auto core0 = TraceRunTestSupport::makeTimestampSetup("core0", 400000000U, 1U); + auto core1 = TraceRunTestSupport::makeTimestampSetup("core1", 400000000U, 4U); + multicore.setups = {core0, core1}; + + const auto core0Itm = TraceRunTestSupport::makeReference("itm", "core0", 1U, {1U}, "opaque/core0-route"); + const auto core1Itm = TraceRunTestSupport::makeReference("itm", "core1", 2U, {1U}, "opaque/core1-route"); + multicore.references = {core0Itm, core1Itm}; + + core1.timestamps = TraceRunTimestampSetup{400000000U, std::nullopt}; + multicore.setups = {core0, core1}; + const auto missingPrescalerMeta = CtraceRunMeta::fromConfig(multicore); + ASSERT_TRUE(missingPrescalerMeta.timestampPrescaler() == + std::optional(TraceRunSchema::kDefaultTimestampPrescaler)) + << "an omitted processor prescaler must resolve to the specified default"; + ASSERT_TRUE((missingPrescalerMeta.timestampPrescalersByTraceBusId() == + std::map({{1U, 1U}, {2U, 1U}}))) + << "the default prescaler must be retained per ATB stream"; + + core0.timestamps = TraceRunTimestampSetup{400000000U, 4U}; + multicore.setups = {core0, core1}; + const auto mixedMissingPrescalerMeta = CtraceRunMeta::fromConfig(multicore); + ASSERT_TRUE(mixedMissingPrescalerMeta.hasDistinctProcessorPrescalers()) + << "an explicit prescaler must be compared with another processor's default"; + ASSERT_TRUE((mixedMissingPrescalerMeta.timestampPrescalersByTraceBusId() == + std::map({{1U, 4U}, {2U, 1U}}))) + << "different explicit and default prescalers must remain stream-specific"; + + core1.timestamps = TraceRunTimestampSetup{200000000U, 1U}; + multicore.setups = {core0, core1}; + const auto distinctClockMeta = CtraceRunMeta::fromConfig(multicore); + ASSERT_TRUE(distinctClockMeta.timestampsByTraceBusId().at(1U).clockHz == std::optional(400000000U) && + distinctClockMeta.timestampsByTraceBusId().at(2U).clockHz == std::optional(200000000U)) + << "processor clocks must remain associated with their Trace Bus IDs"; + + auto ctfRequest = outputRequest(false, true); + CollectingDiagnosticSink allClockDiagnostics; + const auto allClockPlan = + planOutputs(ctfRequest, "captures/Multicore.SWO.raw", distinctClockMeta, allClockDiagnostics); + ASSERT_TRUE(!allClockPlan.ctf.has_value()) << "CTF must reject selected Trace Bus IDs with different clocks"; + allClockDiagnostics.singleEvent(); + + ctfRequest.selection.streams = {2U}; + CollectingDiagnosticSink selectedClockDiagnostics; + const auto selectedClockPlan = + planOutputs(ctfRequest, "captures/Multicore.SWO.raw", distinctClockMeta, selectedClockDiagnostics); + ASSERT_TRUE(selectedClockPlan.ctf.has_value() && selectedClockPlan.ctf->coreClockHz == 200000000U && + selectedClockDiagnostics.events().empty()) + << "a selected Trace Bus ID must use its processor's clock"; +} + +TEST(CtraceUnitTests, testDwtDataMetadataDefaultsAndValidation) +{ + TraceRunConfig config; + config.path = "DwtSize.ctrace-run.yml"; + auto setup = TraceRunTestSupport::makeTimestampSetup(std::nullopt, 100000000U, 1U); + setup.data = { + TraceRunDataSetup{}, + TraceRunDataSetup{"unsigned int", 1U}, + TraceRunDataSetup{"signed int", 2U}, + }; + config.setups.push_back(std::move(setup)); + for (std::uint32_t comparator = 0U; comparator < 3U; ++comparator) { + auto reference = TraceRunTestSupport::makeReference("dwt", std::nullopt, std::nullopt, {comparator}, + "opaque/dwt-" + std::to_string(comparator)); + reference.dataSetupIndex = comparator; + config.references.push_back(std::move(reference)); + } + const auto meta = CtraceRunMeta::fromConfig(config); + + const auto allRequest = outputRequest(true, true); + CollectingDiagnosticSink configurationDiagnostics; + const auto outputPlan = planOutputs(allRequest, "DwtSize.SWO.raw", meta, configurationDiagnostics); + ASSERT_TRUE(outputPlan.csv.has_value() && outputPlan.ctf.has_value()) + << "valid or missing DWT metadata must not disable CSV or CTF"; + ASSERT_TRUE(configurationDiagnostics.events().empty()) + << "valid or missing DWT metadata must not produce diagnostics"; + ASSERT_TRUE(outputPlan.ctf->sources.size() == 3U && outputPlan.ctf->sources[0].valueType == "unsigned int" && + outputPlan.ctf->sources[0].valueSize == 4U && outputPlan.ctf->sources[1].valueType == "unsigned int" && + outputPlan.ctf->sources[1].valueSize == 1U && outputPlan.ctf->sources[2].valueType == "signed int" && + outputPlan.ctf->sources[2].valueSize == 2U) + << "CTF data.symbol-type/data.symbol-size defaults or explicit values mismatch"; + + config.setups[0].data[1].symbolSize = 0U; + CollectingDiagnosticSink invalidSizeDiagnostics; + const auto invalidSizePlan = planOutputs(allRequest, "DwtSize.SWO.raw", config, invalidSizeDiagnostics); + ASSERT_TRUE(invalidSizePlan.csv.has_value() && !invalidSizePlan.ctf.has_value()) + << "invalid data.symbol-size must disable only CTF"; + invalidSizeDiagnostics.singleEvent(); +} + +TEST(CtraceUnitTests, testOutputRequirementsAreBackendSpecific) +{ + auto config = backendRequirementsConfig(); + + const auto csvRequest = outputRequest(true, false); + CollectingDiagnosticSink invalidTypeCsvDiagnostics; + const auto invalidTypeCsv = planOutputs(csvRequest, "BackendRequirements.SWO.raw", config, invalidTypeCsvDiagnostics); + ASSERT_TRUE(invalidTypeCsv.csv.has_value() && !invalidTypeCsv.ctf.has_value()) + << "an invalid explicit data.symbol-type must not disable CSV"; + ASSERT_TRUE(invalidTypeCsvDiagnostics.events().empty()) << "CSV must not inspect CTF-only data.symbol-type metadata"; + + const auto allRequest = outputRequest(true, true); + CollectingDiagnosticSink invalidTypeAllDiagnostics; + const auto invalidTypeAll = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, invalidTypeAllDiagnostics); + ASSERT_TRUE(invalidTypeAll.csv.has_value() && !invalidTypeAll.ctf.has_value()) + << "an invalid explicit data.symbol-type must disable only CTF for --all"; + invalidTypeAllDiagnostics.singleEvent(); + + config.setups[0].data[0].symbolType.reset(); + config.setups[0].data[0].symbolSize.reset(); + CollectingDiagnosticSink missingTypeDiagnostics; + const auto missingType = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, missingTypeDiagnostics); + ASSERT_TRUE(missingType.csv.has_value() && missingType.ctf.has_value()) + << "missing data.symbol-type/data.symbol-size must use CTF defaults and leave CSV enabled"; + ASSERT_TRUE( + (missingType.csv->outputPath == std::filesystem::path("BackendRequirements.SWO.csv") && + missingType.ctf->outputDirectory == std::filesystem::path("BackendRequirements.ctf") && + missingType.ctf->traceCompassXmlPath == std::filesystem::path("BackendRequirements.SWO.traceanalysis.xml") && + missingType.ctf->coreClockHz == 400000000U && missingType.ctf->sources.size() == 1U && + missingType.ctf->sources[0].valueType == "unsigned int" && missingType.ctf->sources[0].valueSize == 4U)) + << "output preflight must resolve artifact paths, clock, routes, and defaults"; + ASSERT_TRUE(missingTypeDiagnostics.events().empty()) + << "missing optional data.symbol-type/data.symbol-size must not produce diagnostics"; + + config.setups[0].data[0].symbolTypeError = "data.symbol-type must be scalar"; + CollectingDiagnosticSink malformedTypeDiagnostics; + const auto malformedType = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, malformedTypeDiagnostics); + ASSERT_TRUE(malformedType.csv.has_value() && !malformedType.ctf.has_value()) + << "malformed data.symbol-type must disable only CTF"; + + config.setups[0].data[0].symbolTypeError.reset(); + config.setups[0].data[0].symbolSizeError = "data.symbol-size must be unsigned"; + CollectingDiagnosticSink malformedSizeDiagnostics; + const auto malformedSize = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, malformedSizeDiagnostics); + ASSERT_TRUE(malformedSize.csv.has_value() && !malformedSize.ctf.has_value()) + << "malformed data.symbol-size must disable only CTF"; + malformedSizeDiagnostics.singleEvent(); +} + +TEST(CtraceUnitTests, testCtfOutputRequiresAValidClock) +{ + auto config = backendRequirementsConfig(); + config.setups[0].data[0] = TraceRunDataSetup{}; + const auto allRequest = outputRequest(true, true); + config.setups[0].timestamps->clockHz.reset(); + CollectingDiagnosticSink missingClockDiagnostics; + const auto missingClock = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, missingClockDiagnostics); + ASSERT_TRUE(missingClock.csv.has_value() && !missingClock.ctf.has_value()) + << "missing timestamps.clock must disable only CTF"; + + config.setups[0].timestamps->clockError = "timestamps.clock must be unsigned"; + CollectingDiagnosticSink malformedClockDiagnostics; + const auto malformedClock = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, malformedClockDiagnostics); + ASSERT_TRUE(malformedClock.csv.has_value() && !malformedClock.ctf.has_value()) + << "malformed timestamps.clock must disable only CTF"; + + config.setups[0].timestamps->clockError.reset(); + config.setups[0].timestamps->clockHz = 0U; + CollectingDiagnosticSink zeroClockDiagnostics; + const auto zeroClock = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, zeroClockDiagnostics); + ASSERT_TRUE(zeroClock.csv.has_value() && !zeroClock.ctf.has_value()) << "zero timestamps.clock must disable only CTF"; +} + +TEST(CtraceUnitTests, testOutputRequirementsHonorFiltersAndCheckOnlyMode) +{ + const auto config = backendRequirementsConfig(); + auto allRequest = outputRequest(true, true); + allRequest.selection.types = {"itm"}; + CollectingDiagnosticSink filteredDiagnostics; + const auto filtered = planOutputs(allRequest, "BackendRequirements.SWO.raw", config, filteredDiagnostics); + ASSERT_TRUE(filtered.csv.has_value() && filtered.ctf.has_value()) + << "filtered-out DWT type metadata must not affect either backend"; + ASSERT_TRUE(filteredDiagnostics.events().empty()) << "filtered-out invalid DWT metadata must not produce diagnostics"; + + CollectingDiagnosticSink checkOnlyDiagnostics; + const auto checkOnly = + planOutputs(outputRequest(false, false), "BackendRequirements.SWO.raw", config, checkOnlyDiagnostics); + ASSERT_TRUE(!checkOnly.hasRequestedOutputs() && checkOnlyDiagnostics.events().empty()) + << "check-only mode must not apply CSV or CTF metadata requirements"; +} + +TEST(CtraceUnitTests, testOutputPreflightRejectsAmbiguousRoutesForCtfOnly) +{ + TraceRunConfig config; + config.path = "AmbiguousRoutes.ctrace-run.yml"; + auto setup = TraceRunTestSupport::makeTimestampSetup(std::nullopt, 400000000U, 1U); + setup.data.push_back(TraceRunDataSetup{"unsigned int", 4U}); + config.setups.push_back(setup); + + auto first = TraceRunTestSupport::makeReference("dwt", std::nullopt, 1U, {0U}, "opaque/dwt-a"); + first.dataSetupIndex = 0U; + first.label = "core-one"; + TraceRunReference second = first; + second.label = "core-two"; + config.references = {first, second}; + + auto allRequest = outputRequest(true, true); + CollectingDiagnosticSink diagnostics; + const auto plan = planOutputs(allRequest, "captures/AmbiguousRoutes.SWO.raw", config, diagnostics); + ASSERT_TRUE(plan.csv.has_value() && !plan.ctf.has_value()) + << "conflicting CTF route labels must not disable independent CSV output"; + diagnostics.singleEvent(); + + config.references[1].stream = 2U; + config.references[1].label = "core-one"; + CollectingDiagnosticSink routeDiagnostics; + const auto routePlan = planOutputs(allRequest, "captures/AmbiguousRoutes.SWO.raw", config, routeDiagnostics); + ASSERT_TRUE(routePlan.csv.has_value() && routePlan.ctf.has_value() && routePlan.ctf->sources.size() == 2U) + << "CTF must retain equivalent routes with distinct Trace Bus IDs"; + ASSERT_TRUE(routeDiagnostics.events().empty()) + << "equivalent CTF metadata on distinct Trace Bus IDs must not be ambiguous"; + + TraceRunConfig processorConfig; + processorConfig.path = "AmbiguousProcessors.ctrace-run.yml"; + auto core0Setup = setup; + core0Setup.processorName = "core0"; + auto core1Setup = setup; + core1Setup.processorName = "core1"; + processorConfig.setups = {core0Setup, core1Setup}; + auto core0Reference = first; + core0Reference.processorName = "core0"; + auto core1Reference = first; + core1Reference.processorName = "core1"; + processorConfig.references = {core0Reference, core1Reference}; + CollectingDiagnosticSink processorDiagnostics; + const auto processorPlan = planOutputs(outputRequest(true, false), "captures/AmbiguousProcessors.SWO.raw", + processorConfig, processorDiagnostics); + ASSERT_TRUE(processorPlan.csv.has_value() && processorDiagnostics.events().empty()) + << "CSV must preserve raw stream/source values without consuming processor metadata"; + + allRequest.selection.streams = {1U}; + CollectingDiagnosticSink selectedDiagnostics; + const auto selectedPlan = planOutputs(allRequest, "captures/AmbiguousRoutes.SWO.raw", config, selectedDiagnostics); + ASSERT_TRUE(selectedPlan.csv.has_value() && selectedPlan.ctf.has_value() && selectedPlan.ctf->sources.size() == 1U && + selectedPlan.ctf->sources[0].label == std::optional("core-one")) + << "an explicit stream selection must produce one resolved CTF route"; + ASSERT_TRUE(selectedDiagnostics.events().empty()) + << "an unambiguous selected route must not produce preflight diagnostics"; + + config.setups[0].data.push_back(TraceRunDataSetup{"unsigned int", 2U}); + config.references[1].stream = 1U; + config.references[1].ctraceRef = "opaque/dwt-b"; + config.references[1].dataSetupIndex = 1U; + CollectingDiagnosticSink sizeDiagnostics; + const auto csvPlan = + planOutputs(outputRequest(true, false), "captures/AmbiguousRoutes.SWO.raw", config, sizeDiagnostics); + ASSERT_TRUE(csvPlan.csv.has_value()) << "CTF-only data.symbol-size metadata must not disable CSV"; + ASSERT_TRUE(sizeDiagnostics.events().empty()) << "CSV must not inspect CTF-only data.symbol-size metadata"; +} + +TEST(CtraceUnitTests, testOutputRequirementsValidateDefaultClockWithoutRoutes) +{ + const auto ctfRequest = outputRequest(false, true); + TraceRunConfig config; + config.path = "Clock.ctrace-run.yml"; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{}; + config.setups.push_back(setup); + + CollectingDiagnosticSink missingDiagnostics; + auto plan = planOutputs(ctfRequest, "Clock.SWO.raw", config, missingDiagnostics); + ASSERT_FALSE(plan.ctf.has_value()); + missingDiagnostics.singleEvent(); + + config.setups[0].timestamps->clockError = "clock must be an unsigned integer"; + CollectingDiagnosticSink malformedDiagnostics; + plan = planOutputs(ctfRequest, "Clock.SWO.raw", config, malformedDiagnostics); + ASSERT_FALSE(plan.ctf.has_value()); + malformedDiagnostics.singleEvent(); + + config.setups[0].timestamps->clockError.reset(); + config.setups[0].timestamps->clockHz = 0U; + CollectingDiagnosticSink zeroDiagnostics; + plan = planOutputs(ctfRequest, "Clock.SWO.raw", config, zeroDiagnostics); + ASSERT_FALSE(plan.ctf.has_value()); + zeroDiagnostics.singleEvent(); +} + +TEST(CtraceUnitTests, testOutputRequirementsRejectUnknownStreamWithMultipleClocks) +{ + TraceRunConfig config; + config.path = "Multicore.ctrace-run.yml"; + config.setups = { + TraceRunTestSupport::makeTimestampSetup("first", 100U, 1U), + TraceRunTestSupport::makeTimestampSetup("second", 200U, 1U), + }; + config.references = { + TraceRunTestSupport::makeReference("itm", "first", 1U, {1U}, "first/itm"), + TraceRunTestSupport::makeReference("itm", "second", 2U, {1U}, "second/itm"), + }; + + auto ctfRequest = outputRequest(false, true); + ctfRequest.selection.streams = {99U}; + CollectingDiagnosticSink diagnostics; + const auto plan = planOutputs(ctfRequest, "Multicore.SWO.raw", config, diagnostics); + ASSERT_FALSE(plan.ctf.has_value()); + diagnostics.singleEvent(); +} + +TEST(CtraceUnitTests, testOutputRequirementsRejectsInputWithoutArtifactName) +{ + CollectingDiagnosticSink diagnostics; + EXPECT_THROW((void)planTraceOutputs(outputRequest(true, false), {}, CtraceRunMeta::fromConfig({}), diagnostics), + std::runtime_error); +} diff --git a/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp new file mode 100644 index 000000000..49dabe0fe --- /dev/null +++ b/tools/ctrace/test/unit/src/output/TraceOutputLifecycleTests.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.h" +#include "TestSupport.h" +#include "TraceOutputTestSupport.h" +#include +#include "csv/CsvFileOutput.h" +#include "DiagnosticSink.h" +#include "TraceEvent.h" +#include "TraceOutput.h" +#include "TraceOutputLifecycle.h" +#include +#include +#include +#include +#include + +using TraceOutputTestSupport::TestTraceOutput; +using TraceOutputTestSupport::TestTraceOutputFailure; + +/** @brief Simulates failure while recording an output diagnostic. */ +class ThrowingDiagnosticSink final : public DiagnosticSink { +protected: + /** @brief Throws the synthetic diagnostic failure. */ + void write(const Event&) override + { + throw std::runtime_error("synthetic diagnostic failure"); + } +}; + +TEST(CtraceUnitTests, testTraceOutputLifecycleCompletesIndependentOutputs) +{ + const TemporaryTestPath temporaryPath("ctrace-output-lifecycle-test.csv"); + const auto& path = temporaryPath.path(); + writeTestFile(path, "old-output\n"); + + std::vector> outputs; + outputs.push_back(std::make_unique(TestTraceOutputFailure::Start)); + auto csv = std::make_unique(path); + outputs.push_back(std::move(csv)); + outputs.push_back(std::make_unique(TestTraceOutputFailure::Stop)); + + CollectingDiagnosticSink diagnostics; + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + TraceEvent packet = softwarePacket(1U, 1U, 'A'); + lifecycle.append(packet); + lifecycle.finish(); + ASSERT_TRUE(diagnostics.failureCount() == 2U) << "output lifecycle should report start and finalization failures"; + + const auto contents = readTestTextFile(path); + ASSERT_TRUE(contents.find("cycles,stream,type,source,value,pc,offset,note\n") == 0U && + contents.find(",,itm,1,0x41,,,\n") != std::string::npos) + << "output lifecycle should complete successful outputs despite another output failure"; +} + +TEST(CtraceUnitTests, testTraceOutputLifecycleReportsAbortFailures) +{ + std::vector> outputs; + outputs.push_back(std::make_unique(TestTraceOutputFailure::Abort)); + CollectingDiagnosticSink diagnostics; + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + lifecycle.abort(); + + ASSERT_TRUE(diagnostics.failureCount() == 1U && diagnostics.containsContext("phase", "abort")) + << "output lifecycle must report a failed direct-output cleanup"; +} + +TEST(CtraceUnitTests, testTraceOutputLifecycleReportsWriteFailuresAndFinishesOnce) +{ + std::vector> outputs; + auto failing = std::make_unique(TestTraceOutputFailure::Write, "synthetic.trace"); + auto* failingPointer = failing.get(); + outputs.push_back(std::move(failing)); + CollectingDiagnosticSink diagnostics; + TraceOutputLifecycle lifecycle(std::move(outputs), diagnostics); + lifecycle.append(softwarePacket(1U)); + lifecycle.append(softwarePacket(2U)); + lifecycle.finish(); + lifecycle.finish(); + + ASSERT_EQ(diagnostics.events().size(), 1U); + EXPECT_TRUE(failingPointer->aborted()); + EXPECT_EQ(diagnostics.events().front().message, + "trace output 'synthetic.trace' failed during write: intentional write failure"); +} + +TEST(CtraceUnitTests, testTraceOutputLifecycleContainsDiagnosticAndNonStandardFailures) +{ + std::vector> outputs; + outputs.push_back(std::make_unique(TestTraceOutputFailure::NonStandardStart)); + ThrowingDiagnosticSink diagnostics; + EXPECT_NO_THROW((void)TraceOutputLifecycle(std::move(outputs), diagnostics)); + EXPECT_EQ(diagnostics.failureCount(), 1U); + + std::vector> passiveOutputs; + passiveOutputs.push_back(std::make_unique()); + CollectingDiagnosticSink passiveDiagnostics; + TraceOutputLifecycle passive(std::move(passiveOutputs), passiveDiagnostics); + passive.finish(); + EXPECT_EQ(passiveDiagnostics.failureCount(), 0U); +} diff --git a/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp new file mode 100644 index 000000000..1980ca2e1 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/csv/CsvFileOutputTests.cpp @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.h" +#include "TestPlatform.h" +#include "TestSupport.h" +#include +#include "TraceEvent.h" +#include "TraceSelection.h" +#include "csv/CsvFileOutput.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Selects which operation fails in a synthetic CSV stream. */ +enum class CsvStreamFailure { + Write, + Flush, +}; + +/** @brief Implements write and flush failures for CSV lifecycle tests. */ +class FailingCsvStreamBuffer final : public std::streambuf { +public: + /** @brief Creates a buffer that fails at the selected operation. */ + explicit FailingCsvStreamBuffer(CsvStreamFailure failure) + : m_failure(failure) + { + } + +protected: + /** @brief Accepts or rejects one output character. */ + int_type overflow(int_type character) override + { + return m_failure == CsvStreamFailure::Write ? traits_type::eof() : character; + } + + /** @brief Accepts or rejects a stream flush. */ + int sync() override + { + return m_failure == CsvStreamFailure::Flush ? -1 : 0; + } + +private: + CsvStreamFailure m_failure; +}; + +/** @brief Provides a CSV stream around a synthetic failing stream buffer. */ +class FailingCsvStream final : public CsvFileOutput::Stream { +public: + /** @brief Creates a stream that fails at the selected operation. */ + explicit FailingCsvStream(CsvStreamFailure failure) + : m_buffer(failure), + m_stream(&m_buffer) + { + } + + /** @brief Returns the synthetic output stream. */ + std::ostream& output() override + { + return m_stream; + } + + /** @brief Flushes the synthetic output stream. */ + void close() override + { + m_stream.flush(); + } + +private: + FailingCsvStreamBuffer m_buffer; + std::ostream m_stream; +}; + +TEST(CtraceUnitTests, testCsvFileOutputCriteria) +{ + const TemporaryTestPath outputPath("ctrace-filtered-output.csv"); + CsvFileOutput output(outputPath.path(), TraceSelection{{"itm"}, {1U, 2U}}); + output.start(); + + const auto accepted = onStream(softwarePacket(1U, 1U, 0x41U), 2U); + output.writeEvent(accepted); + + for (const auto stream : {3U, 0U}) { + auto excluded = accepted; + excluded.traceBusId = static_cast(stream); + output.writeEvent(excluded); + } + + auto excludedChannel = accepted; + std::get(excludedChannel.payload).channel = 0U; + output.writeEvent(excludedChannel); + + output.writeEvent(onStream(issuePacket(TraceIssueCode::DecodeError), accepted.traceBusId)); + output.stop(); + + ASSERT_TRUE( + (readTestTextFile(outputPath.path()) == "cycles,stream,type,source,value,pc,offset,note\n,2,itm,1,0x41,,,\n")) + << "CsvFileOutput criteria mismatch"; + + const TemporaryTestPath errorOutputPath("ctrace-filtered-errors.csv"); + CsvFileOutput errorOutput(errorOutputPath.path(), TraceSelection{{"error"}, {}}); + errorOutput.start(); + errorOutput.writeEvent(issuePacket(TraceIssueCode::DecodeError, "decoder warning", TraceIssueSeverity::Warning)); + errorOutput.stop(); + ASSERT_TRUE(readTestTextFile(errorOutputPath.path()).find(",,error,,,,,decoder warning\n") != std::string::npos) + << "the error selector must include warning-severity decoder issue packets"; +} + +TEST(CtraceUnitTests, testCsvFileOutputMatchesSpecification) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-test.csv"); + const auto& csvPath = temporaryPath.path(); + + CsvFileOutput output(csvPath); + output.start(); + + output.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{ + 2U, + 4U, + 0xfffffdf9U, + AccessType::Read, + 0xfdf9U, + 0x08001234U, + }}, + 949338400U)); + output.writeEvent(atCycle(TraceEvent{ExceptionTraceEvent{11U, ExceptionAction::Entered}}, 950364820U)); + + output.stop(); + + const auto lines = readTestLines(csvPath); + + ASSERT_TRUE(lines.size() == 3U) << "CSV specification row count mismatch"; + ASSERT_TRUE(lines[0] == "cycles,stream,type,source,value,pc,offset,note") << "CSV specification header mismatch"; + ASSERT_TRUE(lines[1] == "949338400,,dwt,2,0xfffffdf9,0x08001234,0xfdf9,") << "CSV DWT row schema mismatch"; + ASSERT_TRUE(lines[2] == "950364820,,exception,11,0x1,,,") << "CSV exception state schema mismatch"; +} + +TEST(CtraceUnitTests, testCsvFileOutputWritesTraceIssues) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-issue-test.csv"); + const auto& csvPath = temporaryPath.path(); + + CsvFileOutput output(csvPath); + output.start(); + output.writeEvent(overflowPacket(1234)); + + output.writeEvent(atCycle(issuePacket(TraceIssueCode::DataLoss, "trace data lost before resynchronization"), 1235U)); + output.stop(); + + const auto lines = readTestLines(csvPath); + + ASSERT_TRUE(lines.size() == 3U) << "CSV issue row count mismatch"; + ASSERT_TRUE(lines[0] == "cycles,stream,type,source,value,pc,offset,note") << "CSV issue header mismatch"; + ASSERT_TRUE( + (lines[1] == "1234,,overflow,,,,,overflow: new timestamp segment; time across boundary may be unreliable")) + << "CSV overflow issue row mismatch"; + ASSERT_TRUE(lines[2] == "1235,,error,,,,,trace data lost before resynchronization") + << "CSV data-loss issue row mismatch"; +} + +TEST(CtraceUnitTests, testCsvFileOutputWritesDirectly) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-direct-test"); + const auto& testRoot = temporaryPath.path(); + const auto csvPath = testRoot / std::filesystem::u8path(u8"Gr\u00f6\u00dfe.csv"); + writeTestFile(csvPath, "old-output\n"); + + CsvFileOutput output(csvPath); + output.start(); + ASSERT_TRUE(std::filesystem::is_regular_file(csvPath)) + << "CSV start must replace the existing target with the direct output file"; + TraceEvent packet = softwarePacket(1U, 1U, 'A'); + output.writeEvent(packet); + output.abort(); + ASSERT_TRUE(!std::filesystem::exists(csvPath)) << "CSV abort must remove the incomplete direct output"; + + writeTestFile(csvPath, "stale-output\n"); + output.start(); + output.writeEvent(packet); + output.stop(); + const auto contents = readTestTextFile(csvPath); + ASSERT_TRUE(contents.find("cycles,stream,type") == 0 && contents.find("stale-output") == std::string::npos) + << "completed CSV output must directly replace the previous file"; + std::filesystem::remove(csvPath); + + std::filesystem::create_directory(csvPath); + const auto rejectedDirectory = throwsException([&] { + CsvFileOutput directoryOutput(csvPath); + directoryOutput.start(); + }); + ASSERT_TRUE(rejectedDirectory && std::filesystem::is_directory(csvPath)) + << "CSV output must not delete a directory occupying its target path"; + std::filesystem::remove_all(testRoot); + + std::filesystem::create_directories(testRoot / "working"); + std::filesystem::create_directories(testRoot / "captures"); + const auto parentRelativePath = testRoot / "working" / ".." / "captures" / "output.csv"; + CsvFileOutput parentRelativeOutput(parentRelativePath); + parentRelativeOutput.start(); + parentRelativeOutput.writeEvent(packet); + parentRelativeOutput.stop(); + ASSERT_TRUE(std::filesystem::is_regular_file(testRoot / "captures" / "output.csv")) + << "CSV output must accept a legitimate parent-relative trace path"; +} + +TEST(CtraceUnitTests, testCsvFileOutputReportsIdentityAndIgnoresClosedWrites) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-identity-test.csv"); + CsvFileOutput output(temporaryPath.path(), TraceSelection{{"itm"}, {1U}}); + EXPECT_EQ(output.backendName(), "csv"); + EXPECT_EQ(output.targetPath(), temporaryPath.path().string()); + + output.writeEvent(softwarePacket(1U)); + EXPECT_FALSE(std::filesystem::exists(temporaryPath.path())); + output.start(); + output.writeEvent(softwarePacket(2U)); + output.stop(); + EXPECT_EQ(readTestLines(temporaryPath.path()).size(), 1U); + output.stop(); + output.abort(); +} + +TEST(CtraceUnitTests, testCsvFileOutputRejectsUnsafeAndInvalidParents) +{ + for (const auto& path : {std::filesystem::path{}, std::filesystem::path("."), std::filesystem::path("..")}) { + CsvFileOutput output(path); + EXPECT_THROW(output.start(), std::invalid_argument); + } + + const TemporaryTestPath regularParent("ctrace-csv-regular-parent-test"); + writeTestFile(regularParent.path(), "not a directory"); + CsvFileOutput childOfFile(regularParent.path() / "output.csv"); + EXPECT_THROW(childOfFile.start(), std::runtime_error); + + const TemporaryTestPath longNameRoot("ctrace-csv-long-name-test"); + longNameRoot.createDirectory(); + CsvFileOutput overlyLongName(longNameRoot.path() / std::string(1024U, 'x')); + EXPECT_THROW(overlyLongName.start(), std::runtime_error); +} + +TEST(CtraceUnitTests, testCsvFileOutputSupportsParentlessPath) +{ + const auto path = std::filesystem::path("ctrace-parentless-output-test.csv"); + std::error_code ignored; + std::filesystem::remove(path, ignored); + CsvFileOutput output(path); + output.start(); + output.writeEvent(softwarePacket(1U)); + output.stop(); + EXPECT_TRUE(std::filesystem::is_regular_file(path)); + std::filesystem::remove(path, ignored); +} + +TEST(CtraceUnitTests, testCsvFileOutputReportsInjectedStreamFailures) +{ + const TemporaryTestPath temporaryPath("ctrace-csv-stream-failure-test.csv"); + + const auto factory = [](CsvStreamFailure failure) { + return [failure](const std::filesystem::path&) { return std::make_unique(failure); }; + }; + + CsvFileOutput writeFailure(temporaryPath.path(), {}, factory(CsvStreamFailure::Write)); + EXPECT_THROW(writeFailure.start(), std::runtime_error); + + CsvFileOutput flushFailure(temporaryPath.path(), {}, factory(CsvStreamFailure::Flush)); + flushFailure.start(); + EXPECT_THROW(flushFailure.stop(), std::runtime_error); + + CsvFileOutput openFailure(temporaryPath.path(), {}, + [](const std::filesystem::path&) { return std::unique_ptr{}; }); + EXPECT_THROW(openFailure.start(), std::runtime_error); + + EXPECT_THROW((void)CsvFileOutput(temporaryPath.path(), {}, {}), std::invalid_argument); +} + +TEST(CtraceUnitTests, testCsvFileOutputReportsPseudoFilesystemOpenFailure) +{ + if (!TestPlatform::supports(TestPlatformCapability::LinuxSpecialFiles)) { + GTEST_SKIP(); + } + CsvFileOutput output(TestPlatform::creationFailurePath("ctrace-coverage-output.csv")); + EXPECT_THROW(output.start(), std::runtime_error); +} + +TEST(CtraceUnitTests, testCsvFileOutputReportsPermissionFailures) +{ + if (!TestPlatform::supports(TestPlatformCapability::PosixPermissions)) { + GTEST_SKIP(); + } + const TemporaryTestPath temporaryPath("ctrace-csv-permission-failure-test"); + const auto& root = temporaryPath.createDirectory(); + const auto outputPath = root / "output.csv"; + + { + CsvFileOutput output(outputPath); + output.start(); + std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + } + std::filesystem::permissions(root, std::filesystem::perms::owner_all); + EXPECT_TRUE(std::filesystem::is_regular_file(outputPath)); + + std::filesystem::remove(outputPath); + std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + CsvFileOutput createFailure(root / "missing" / "output.csv"); + EXPECT_THROW(createFailure.start(), std::runtime_error); + std::filesystem::permissions(root, std::filesystem::perms::owner_all); +} diff --git a/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp new file mode 100644 index 000000000..bc5733fb8 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/csv/CsvRowMapperTests.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include +#include "TraceEvent.h" +#include "TraceSelection.h" +#include "csv/CsvRowMapper.h" +#include +#include +#include +#include +#include +#include + +TEST(CtraceUnitTests, testCsvRowMapperAndTraceEventSchema) +{ + const std::array expectedTypes{{ + "itm", + "dwt", + "event", + "pmu", + "exception", + "pcsample", + "global_ts", + "overflow", + "error", + }}; + ASSERT_TRUE(kTraceEventTypeNames == expectedTypes) << "trace event type names mismatch"; + for (const auto type : kTraceEventTypeNames) { + ASSERT_TRUE(parseTraceEventType(type).has_value()) << "declared trace event type should parse"; + } + ASSERT_TRUE(!parseTraceEventType("timestamp").has_value()) << "undeclared trace event type should be rejected"; + const std::vector>> semanticTypes{ + {TraceEvent{SoftwareTraceEvent{}}, TraceEventType::Itm}, + {TraceEvent{DwtDataTraceEvent{}}, TraceEventType::Dwt}, + {TraceEvent{DwtAddressTraceEvent{0U, DwtPcTraceLocation{0U}}}, TraceEventType::Dwt}, + {TraceEvent{ExceptionTraceEvent{}}, TraceEventType::Exception}, + {TraceEvent{DwtEventTraceEvent{}}, std::nullopt}, + {TraceEvent{PmuTraceEvent{}}, std::nullopt}, + {TraceEvent{LocalTimestampTraceEvent{}}, std::nullopt}, + {TraceEvent{GlobalTimestampTraceEvent{}}, TraceEventType::GlobalTimestamp}, + {TraceEvent{OverflowTraceEvent{}}, TraceEventType::Overflow}, + {TraceEvent{SyncTraceEvent{}}, std::nullopt}, + {TraceEvent{TraceIssueEvent{}}, TraceEventType::Error}, + }; + for (const auto& [event, expectedType] : semanticTypes) { + ASSERT_TRUE(traceEventType(event) == expectedType) << "semantic TraceEvent type mapping mismatch"; + } + + ASSERT_TRUE(CsvRowMapper::header() == "cycles,stream,type,source,value,pc,offset,note") + << "CSV schema header integration mismatch"; + ASSERT_TRUE( + (CsvRowMapper::row(TraceEvent{ExceptionTraceEvent{11U, ExceptionAction::Entered}}) == ",,exception,11,0x1,,,")) + << "CSV exception value mismatch"; + ASSERT_TRUE(CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 1U, 0x0aU, AccessType::Write}}) == ",,dwt,0,0x0a,,,") + << "CSV must render the raw hexadecimal DWT value with the one-byte SWO width"; + ASSERT_TRUE( + (CsvRowMapper::row(TraceEvent{DwtDataTraceEvent{0U, 2U, 0x0aU, AccessType::Write}}) == ",,dwt,0,0x000a,,,")) + << "CSV must render the raw hexadecimal DWT value with the two-byte SWO width"; + ASSERT_TRUE(CsvRowMapper::row(softwarePacket(1U)) == ",,itm,1,0x00,,,") + << "CSV must leave the stream field empty for unformatted input"; +} + +TEST(CtraceUnitTests, testCsvRowMapperCoversAddressAndExceptionVariants) +{ + EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtAddressTraceEvent{2U, DwtOffsetTraceLocation{0xabcdU}}}), + ",,dwt,2,,,0xabcd,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtAddressTraceEvent{3U, DwtPcAndOffsetTraceLocation{0x1234U, 0x56U}}}), + ",,dwt,3,,0x00001234,0x0056,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{ExceptionTraceEvent{1U, ExceptionAction::Exited}}), ",,exception,1,0x2,,,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{ExceptionTraceEvent{1U, ExceptionAction::Returned}}), ",,exception,1,0x3,,,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{ExceptionTraceEvent{1U, ExceptionAction::Unknown}}), ",,exception,1,,,,"); + EXPECT_EQ(CsvRowMapper::row(TraceEvent{ExceptionTraceEvent{1U, static_cast(99)}}), + ",,exception,1,0x0,,,"); +} + +TEST(CtraceUnitTests, testCsvRowMapperEscapesDiagnosticText) +{ + const auto issue = onStream(issuePacket(TraceIssueCode::DecodeError, "comma, quote \" and\nnewline"), 7U); + EXPECT_EQ(CsvRowMapper::row(issue), ",7,error,,,,,\"comma, quote \"\" and\nnewline\""); + + EXPECT_EQ(CsvRowMapper::row(atCycle(TraceEvent{GlobalTimestampTraceEvent{123U, false}}, 99U)), "123,,global_ts,,,,,"); +} + +TEST(CtraceUnitTests, testCsvRowMapperHandlesInternalAndCustomOverflowEvents) +{ + EXPECT_EQ(CsvRowMapper::row(TraceEvent{DwtEventTraceEvent{0U, 1U, 1U}}), ",,,,,,,"); + TraceEvent overflow{OverflowTraceEvent{"custom overflow"}}; + EXPECT_EQ(CsvRowMapper::row(overflow), ",,overflow,,,,,custom overflow"); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp new file mode 100644 index 000000000..1413abcb7 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfBundleOutputTests.cpp @@ -0,0 +1,729 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +// CTF bundle metadata, event handling, and direct-output lifecycle tests. +#include "CtfTestSupport.h" +#include "TestPlatform.h" +#include "TestSupport.h" +#include "TraceRunTestSupport.h" + +#include + +#include "ctf/CtfBundleOutput.h" +#include "ctf/CtfSchema.h" +#include "CtraceRunMeta.h" +#include "OutputRequirements.h" +#include "TestPath.h" +#include "TraceEvent.h" +#include "TraceOutputConfig.h" +#include "TraceRunConfig.h" +#include "TraceSelection.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using CtfTestSupport::parseCtfRecords; +using CtfTestSupport::readCtfRecords; +using CtfTestSupport::readLe16; +using CtfTestSupport::readLe64; +using CtfTestSupport::requireFirstCtfRecord; +using CtfTestSupport::requireSingleItmEvent; + +/** @brief Derives the Trace Compass XML path used by bundle tests. */ +static std::filesystem::path testTraceCompassXmlPath(const std::filesystem::path& outputDirectory) +{ + auto basePath = outputDirectory; + if (basePath.extension() == ".ctf") { + basePath.replace_extension(); + } + basePath += ".SWO.traceanalysis.xml"; + return basePath; +} + +/** @brief Creates a CTF bundle configuration with test defaults. */ +static CtfOutputConfig makeCtfBundleConfig(const std::filesystem::path& outputDirectory, std::uint64_t coreClockHz) +{ + return CtfOutputConfig(outputDirectory, testTraceCompassXmlPath(outputDirectory), coreClockHz, {}, {}); +} + +/** @brief Requires all files of a completed CTF bundle. */ +static void requireCompleteCtfBundle(const std::filesystem::path& ctfDirectory, const std::filesystem::path& xmlPath, + const std::string& message) +{ + ASSERT_TRUE(std::filesystem::is_regular_file(ctfDirectory / "metadata") && + std::filesystem::is_regular_file(ctfDirectory / "stream_0") && std::filesystem::is_regular_file(xmlPath)) + << message; +} + +/** @brief Owns the temporary directory used by one CTF output test. */ +class TemporaryCtfOutput { +public: + /** @brief Creates a temporary CTF output path. */ + explicit TemporaryCtfOutput(const std::string& name) + : m_root(name), + m_outputDirectory(m_root.path() / "output.ctf") + { + } + + /** @brief Returns the temporary output directory. */ + const std::filesystem::path& outputDirectory() const + { + return m_outputDirectory; + } + +private: + TemporaryTestPath m_root; + std::filesystem::path m_outputDirectory; +}; + +/** @brief Converts normalized trace-run source metadata for output tests. */ +static ResolvedTraceSource resolvedSource(const CtraceRunSourceMeta& source) +{ + return { + source.type, + source.source, + source.traceBusId, + source.label, + source.symbolAddress, + source.valueType, + static_cast(source.valueSize), + }; +} + +/** @brief Reads compact exception records from a test CTF stream. */ +static std::vector readCtfExceptionRecords(const std::filesystem::path& streamPath) +{ + std::vector records; + for (const auto& record : readCtfRecords(streamPath)) { + if (record.id == CtfSchema::value(CtfSchema::EventId::Exception)) { + const auto number = readLe16(record.payload, 0U); + const auto action = record.payload[2U]; + records.push_back(std::to_string(number) + ":" + std::to_string(static_cast(action))); + } + } + return records; +} + +/** @brief Reads trace-status reasons while requiring no other event types. */ +static std::vector readOnlyCtfTraceStatusReasons(const std::filesystem::path& streamPath) +{ + std::vector reasons; + for (const auto& record : readCtfRecords(streamPath)) { + if (record.id != CtfSchema::value(CtfSchema::EventId::TraceStatus)) { + throw std::runtime_error("expected only CTF trace-status events"); + } + reasons.push_back(record.payload.front()); + } + return reasons; +} + +/** @brief Reads the first encoded DWT value tag from a test stream. */ +static std::uint8_t readFirstCtfDwtValueTag(const std::filesystem::path& streamPath) +{ + const auto records = readCtfRecords(streamPath); + const auto& record = requireFirstCtfRecord(records, CtfSchema::EventId::DwtValue, "expected CTF DWT event missing"); + return record.payload[2U]; +} + +TEST(CtraceUnitTests, testCtfBundleOutputExceptionContext) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-exception-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + CtfBundleOutput output(std::move(options)); + output.start(); + output.writeEvent(exceptionPacket(15, ExceptionAction::Entered, 100)); + output.writeEvent(exceptionPacket(54, ExceptionAction::Entered, 200)); + output.writeEvent(exceptionPacket(54, ExceptionAction::Exited, 300)); + output.writeEvent(exceptionPacket(15, ExceptionAction::Returned, 400)); + output.writeEvent(exceptionPacket(15, ExceptionAction::Exited, 500)); + output.stop(); + + const auto records = readCtfExceptionRecords(outputDir / "stream_0"); + const auto metadata = readTestTextFile(outputDir / "metadata"); + + ASSERT_TRUE(metadata.find("\"entered\" = 0") != std::string::npos) << "CTF exception entered label mismatch"; + ASSERT_TRUE(metadata.find("\"exited\" = 1") != std::string::npos) << "CTF exception exited label mismatch"; + + ASSERT_TRUE(records == std::vector({ + "0:0", + "0:1", + "15:0", + "15:1", + "54:0", + "54:1", + "15:0", + "15:1", + "0:0", + })) + << "CtfBundleOutput exception active-context records mismatch"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputUsesCtraceRunMeta) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-ctrace-run-meta-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + TraceRunConfig traceRun; + traceRun.path = "Board.ctrace-run.yml"; + auto signedByteReference = TraceRunTestSupport::makeReference("dwt", std::nullopt, 7U, {0U}, "opaque/signed-byte"); + signedByteReference.dataSetupIndex = 0U; + signedByteReference.label = "Sine"; + traceRun.references.push_back(signedByteReference); + + auto reference = TraceRunTestSupport::makeReference("dwt", std::nullopt, std::nullopt, {2U}, "opaque/current"); + reference.dataSetupIndex = 2U; + reference.label = "Current\n\t\"\\\x01"; + reference.symbolAddress = 0x24000e88U; + traceRun.references.push_back(reference); + TraceRunSetup setup; + setup.data.resize(3U); + setup.data[0].symbolType = "signed int"; + setup.data[0].symbolSize = 1U; + setup.data[2].symbolType = "signed int"; + setup.data[2].symbolSize = 4U; + setup.timestamps = TraceRunTimestampSetup{280000000U, 1U}; + traceRun.setups.push_back(std::move(setup)); + + CollectingDiagnosticSink preflightDiagnostics; + auto outputPlan = planTraceOutputs({false, true, {}}, outputDir.parent_path() / "output.SWO.raw", + CtraceRunMeta::fromConfig(traceRun), preflightDiagnostics); + ASSERT_TRUE(outputPlan.ctf.has_value() && preflightDiagnostics.events().empty()) << "resolved CTF source missing"; + auto options = std::move(*outputPlan.ctf); + ASSERT_TRUE(!options.sources.empty()) << "resolved CTF source missing"; + ASSERT_TRUE(options.sources.front().traceBusId == 7U) << "resolved CTF source must retain its Trace Bus ID"; + ASSERT_TRUE(options.sources.front().valueType == "signed int") << "resolved CTF source must retain its value type"; + CtfBundleOutput output(std::move(options)); + output.start(); + output.writeEvent(atCycle(onStream(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}, 7U), 100U)); + output.stop(); + + const auto metadata = readTestTextFile(outputDir / "metadata"); + const auto stream = readTestBinaryFile(outputDir / "stream_0"); + + ASSERT_TRUE(metadata.find("freq = 280000000") != std::string::npos) << "CTF trace-run clock mismatch"; + ASSERT_TRUE(metadata.find("cmsis_dwt0_value_type = \"signed int\"") != std::string::npos) + << "CTF signed-byte source type mismatch"; + ASSERT_TRUE(metadata.find("cmsis_dwt2_value_type = \"signed int\"") != std::string::npos) + << "CTF trace-run type mismatch"; + ASSERT_TRUE(metadata.find("cmsis_dwt2_address_start = \"0x24000E88\"") != std::string::npos) + << "CTF trace-run start address mismatch"; + ASSERT_TRUE(metadata.find("cmsis_dwt2_address_end = \"0x24000E8B\"") != std::string::npos) + << "CTF trace-run end address mismatch"; + ASSERT_TRUE(metadata.find("\"Current\\n\\t\\\"\\\\\\x01\" = 2") != std::string::npos) + << "CTF trace-run label escaping mismatch"; + + const auto records = parseCtfRecords(stream); + ASSERT_TRUE(records.front().id == CtfSchema::value(CtfSchema::EventId::TraceStatus)) + << "CTF trace-start event missing"; + const auto& dwtRecord = + requireFirstCtfRecord(records, CtfSchema::EventId::DwtValue, "expected CTF DWT value event missing"); + ASSERT_TRUE(dwtRecord.traceBusId == 7U) << "CTF event context must preserve the CoreSight Trace Bus ID"; + ASSERT_TRUE(dwtRecord.payload[2U] == 0U) << "CTF one-byte int payload must select the i8 variant"; + ASSERT_TRUE(dwtRecord.payload[3U] == 0xffU) << "CTF signed-byte payload mismatch"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputDefaultsDwtValueType) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-default-dwt-type-test"); + const auto& root = temporaryPath.path(); + + TraceRunConfig defaultTraceRun; + auto defaultReference = + TraceRunTestSupport::makeReference("dwt", std::nullopt, std::nullopt, {0U}, "opaque/default-dwt"); + defaultReference.dataSetupIndex = 0U; + defaultTraceRun.references.push_back(defaultReference); + const auto defaultMeta = CtraceRunMeta::fromConfig(defaultTraceRun); + ASSERT_TRUE(defaultMeta.sources().size() == 1U && defaultMeta.sources().front().valueType == "unsigned int" && + defaultMeta.sources().front().valueSize == 4U) + << "missing DWT data.symbol-type/data.symbol-size must default to unsigned int/4"; + + const auto defaultOutputDir = root / "default"; + auto defaultOptions = makeCtfBundleConfig(defaultOutputDir, 1000000U); + defaultOptions.sources = {resolvedSource(defaultMeta.sources().front())}; + CollectingDiagnosticSink diagnostics; + CtfBundleOutput defaultOutput(std::move(defaultOptions), &diagnostics); + defaultOutput.start(); + defaultOutput.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}); + defaultOutput.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 2U, 0xffffU, AccessType::Write}}); + defaultOutput.stop(); + ASSERT_TRUE(readFirstCtfDwtValueTag(defaultOutputDir / "stream_0") == 5U) + << "default DWT metadata must select the unsigned 32-bit CTF variant"; + const auto& sizeWarning = diagnostics.singleEvent(); + ASSERT_TRUE(sizeWarning.severity == DiagnosticSink::Severity::Warning && diagnostics.failureCount() == 0U) + << "DWT size mismatch must be reported once per channel"; + + TraceRunConfig signedTraceRun; + TraceRunSetup signedSetup; + signedSetup.data.push_back(TraceRunDataSetup{"signed int", 1U}); + signedTraceRun.setups.push_back(std::move(signedSetup)); + signedTraceRun.references.push_back(defaultReference); + const auto signedMeta = CtraceRunMeta::fromConfig(signedTraceRun); + ASSERT_TRUE(signedMeta.sources().size() == 1U) << "signed DWT source missing"; + const auto signedOutputDir = root / "signed"; + auto signedOptions = makeCtfBundleConfig(signedOutputDir, 1000000U); + signedOptions.sources = {resolvedSource(signedMeta.sources().front())}; + CollectingDiagnosticSink signedDiagnostics; + CtfBundleOutput signedOutput(std::move(signedOptions), &signedDiagnostics); + signedOutput.start(); + signedOutput.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Write}}); + signedOutput.stop(); + ASSERT_TRUE(readFirstCtfDwtValueTag(signedOutputDir / "stream_0") == 0U) + << "explicit signed int/1 metadata must select the signed 8-bit CTF variant"; + ASSERT_TRUE(signedDiagnostics.events().empty()) << "matching DWT sizes must not produce a warning"; + + TraceRunConfig traceRun; + traceRun.path = "ambiguous-streams.ctrace-run.yml"; + TraceRunSetup setup; + setup.data.push_back(TraceRunDataSetup{"signed int", 4U}); + traceRun.setups.push_back(std::move(setup)); + auto first = TraceRunTestSupport::makeReference("dwt", std::nullopt, 1U, {0U}, "opaque/dwt-route"); + first.dataSetupIndex = 0U; + first.label = "core-one"; + TraceRunReference second = first; + second.stream = 2U; + second.label = "core-two"; + traceRun.references = {first, second}; + const auto meta = CtraceRunMeta::fromConfig(traceRun); + ASSERT_TRUE(meta.sources().size() == 2U && meta.sources().front().traceBusId == 1U && + meta.sources().front().label == std::optional("core-one")) + << "trace-run metadata must preserve the exact DWT stream route"; +} + +TEST(CtraceUnitTests, testCtfWarningsRemainVisibleWithoutResettingContext) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-warning-test"); + const auto& root = temporaryPath.path(); + const auto filteredDir = root / "filtered.ctf"; + const auto contextDir = root / "context.ctf"; + const auto dataLossDir = root / "data-loss.ctf"; + + auto filteredOptions = makeCtfBundleConfig(filteredDir, 1000000U); + filteredOptions.selection.types.push_back("error"); + CtfBundleOutput filtered(std::move(filteredOptions)); + filtered.start(); + TraceEvent warning = issuePacket(TraceIssueCode::OpenCsdDecodeError, "decoder warning", TraceIssueSeverity::Warning); + filtered.writeEvent(warning); + filtered.stop(); + ASSERT_TRUE(readOnlyCtfTraceStatusReasons(filteredDir / "stream_0") == + std::vector({CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)})) + << "--type error must retain decoder warnings in CTF"; + + CtfBundleOutput context(makeCtfBundleConfig(contextDir, 1000000U)); + context.start(); + context.writeEvent(exceptionPacket(15U, ExceptionAction::Entered, 10U)); + context.writeEvent(warning); + context.writeEvent(exceptionPacket(54U, ExceptionAction::Entered, 20U)); + context.stop(); + ASSERT_TRUE(readCtfExceptionRecords(contextDir / "stream_0") == + std::vector({"0:0", "0:1", "15:0", "15:1", "54:0"})) + << "a decoder warning must not reset the active CTF exception context"; + + auto dataLossOptions = makeCtfBundleConfig(dataLossDir, 1000000U); + dataLossOptions.selection.types.push_back("exception"); + CtfBundleOutput dataLoss(std::move(dataLossOptions)); + dataLoss.start(); + dataLoss.writeEvent(exceptionPacket(15U, ExceptionAction::Entered, 10U)); + dataLoss.writeEvent(issuePacket(TraceIssueCode::DataLoss, "decoder data loss")); + dataLoss.writeEvent(exceptionPacket(15U, ExceptionAction::Returned, 20U)); + dataLoss.stop(); + ASSERT_TRUE(readCtfExceptionRecords(dataLossDir / "stream_0") == + std::vector({"0:0", "0:1", "15:0", "15:1", "15:0"})) + << "filtered data-loss must still reset the CTF exception context"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputTypeFilterExcludesSyntheticEvents) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-filter-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("itm"); + CtfBundleOutput output(std::move(options)); + output.start(); + + output.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 100U)); + output.writeEvent(exceptionPacket(15, ExceptionAction::Entered, 200)); + output.writeEvent(overflowPacket(300)); + auto error = atCycle(issuePacket(TraceIssueCode::OpenCsdBadPacketSequence), 400U); + error.quality = TraceQuality{true, false, 1U}; + output.writeEvent(error); + output.stop(); + + requireSingleItmEvent(outputDir / "stream_0", 1U, + "CTF itm filter should exclude synthetic events and errors of other packet types"); +} + +TEST(CtraceUnitTests, testCtfGlobalTimestampDoesNotEstablishLocalTimeQuality) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-global-timestamp-quality-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("itm"); + CtfBundleOutput output(std::move(options)); + output.start(); + + output.writeEvent(atCycle(TraceEvent{GlobalTimestampTraceEvent{1234U, false}}, 42U)); + output.writeEvent(softwarePacket(1U, 1U, 'A')); + output.stop(); + + const auto records = readCtfRecords(outputDir / "stream_0"); + constexpr std::uint8_t timestampReliable = 1U << 1U; + constexpr std::uint8_t beforeFirstLocalTimestamp = 1U << 2U; + ASSERT_TRUE(records.size() == 1U && records.front().id == CtfSchema::value(CtfSchema::EventId::Itm)) + << "CTF ITM event missing after global timestamp"; + ASSERT_TRUE(records.front().payload[3U] == (timestampReliable | beforeFirstLocalTimestamp)) + << "global timestamp must not mark following samples as locally timestamped"; +} + +TEST(CtraceUnitTests, testCtfHoldsRegressingEventTimestamps) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-regressing-timestamp-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("itm"); + CtfBundleOutput output(std::move(options)); + output.start(); + output.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 100U)); + output.writeEvent(atCycle(softwarePacket(1U, 1U, 'B'), 10U)); + output.stop(); + + const auto records = readCtfRecords(outputDir / "stream_0"); + ASSERT_TRUE(records.size() == 2U && records[0].timestamp == 100U) << "first CTF event timestamp mismatch"; + ASSERT_TRUE(records[1].timestamp == 100U) << "CTF event timestamps must not regress"; +} + +TEST(CtraceUnitTests, testCtfGlobalTimestampEvent) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-global-timestamp-event-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + options.selection.types.push_back("global_ts"); + CtfBundleOutput output(std::move(options)); + output.start(); + constexpr std::uint64_t timestampValue = 0x123456789abcdef0ULL; + output.writeEvent(TraceEvent{GlobalTimestampTraceEvent{timestampValue, true}}); + output.stop(); + + const auto records = readCtfRecords(outputDir / "stream_0"); + const auto metadata = readTestTextFile(outputDir / "metadata"); + + ASSERT_TRUE(records.size() == 1U) << "CTF global timestamp filter emitted unrelated events"; + const auto& record = records.front(); + ASSERT_TRUE(record.id == CtfSchema::value(CtfSchema::EventId::GlobalTimestamp)) + << "CTF global timestamp event ID mismatch"; + ASSERT_TRUE(readLe64(record.payload, 0U) == timestampValue) << "CTF global timestamp value mismatch"; + ASSERT_TRUE(record.payload[8U] == 1U) << "CTF global timestamp clock-change flag mismatch"; + ASSERT_TRUE(metadata.find("name = \"GLOBAL_TIMESTAMP\"") != std::string::npos) + << "CTF global timestamp metadata missing"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputExcludesSoftwareChannelZero) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-channel-zero-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + TraceRunConfig traceRun; + traceRun.setups.push_back(TraceRunTestSupport::makeTimestampSetup(std::nullopt, 1000000U)); + auto channelZero = TraceRunTestSupport::makeReference("itm", std::nullopt, std::nullopt, {0U}, "opaque/channel-zero"); + channelZero.label = "Console"; + traceRun.references.push_back(std::move(channelZero)); + TraceSelection selection{{"itm", "error"}, {}}; + CollectingDiagnosticSink preflightDiagnostics; + auto outputPlan = planTraceOutputs({false, true, selection}, outputDir.parent_path() / "output.SWO.raw", + CtraceRunMeta::fromConfig(traceRun), preflightDiagnostics); + ASSERT_TRUE(outputPlan.ctf.has_value() && outputPlan.ctf->sources.empty() && preflightDiagnostics.events().empty()) + << "CTF preflight must exclude software channel zero metadata"; + auto options = std::move(*outputPlan.ctf); + CtfBundleOutput output(std::move(options)); + output.start(); + output.writeEvent(atCycle(softwarePacket(0U, 1U, 'A'), 100U)); + output.writeEvent(atCycle(issuePacket(TraceIssueCode::OpenCsdIncompleteTail), 101U)); + output.stop(); + ASSERT_TRUE(readOnlyCtfTraceStatusReasons(outputDir / "stream_0") == + std::vector({CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)})) + << "CTF must exclude software channel zero payload but retain its decoder errors"; + std::vector bundleEntries; + for (const auto& entry : std::filesystem::directory_iterator(outputDir)) { + bundleEntries.push_back(entry.path().filename().string()); + } + std::sort(bundleEntries.begin(), bundleEntries.end()); + ASSERT_TRUE(bundleEntries == std::vector({"metadata", "stream_0"})) + << "CTF bundle must contain only metadata and the primary stream"; + + const auto metadata = readTestTextFile(outputDir / "metadata"); + ASSERT_TRUE(metadata.find("ITM" + std::to_string(0)) == std::string::npos) + << "CTF metadata must not register software channel zero"; + ASSERT_TRUE(metadata.find("cmsis_itm0_") == std::string::npos && metadata.find("Console") == std::string::npos) + << "CTF analysis metadata must not expose configured software channel zero"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputAbortRemovesPartialBundle) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-direct-abort-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + const auto xmlPath = testTraceCompassXmlPath(outputDir); + writeTestFile(outputDir / "old-marker", "old"); + writeTestFile(xmlPath, "old-xml"); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + CtfBundleOutput output(std::move(options)); + output.start(); + ASSERT_TRUE(!std::filesystem::exists(outputDir / "old-marker") && + std::filesystem::is_regular_file(outputDir / "stream_0") && readTestTextFile(xmlPath) != "old-xml") + << "CTF start must remove both previous targets and write directly to their final paths"; + + TraceEvent software = softwarePacket(1U, 1U, 'A'); + output.writeEvent(software); + output.abort(); + ASSERT_TRUE(!std::filesystem::exists(outputDir) && !std::filesystem::exists(xmlPath)) + << "CTF abort must remove the incomplete direct output"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputReplacesExistingBundleAtStart) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-direct-replace-test"); + const auto& outputDir = temporaryOutput.outputDirectory(); + const auto xmlPath = testTraceCompassXmlPath(outputDir); + writeTestFile(outputDir / "old-marker", "old"); + writeTestFile(xmlPath, "old-xml"); + + auto options = makeCtfBundleConfig(outputDir, 1000000U); + CtfBundleOutput output(std::move(options)); + output.start(); + const auto xml = readTestTextFile(xmlPath); + ASSERT_TRUE(!std::filesystem::exists(outputDir / "old-marker") && + std::filesystem::is_regular_file(outputDir / "stream_0") && + !std::filesystem::exists(outputDir / "metadata") && xml != "old-xml" && !xml.empty()) + << "CTF start must replace existing output before decoding begins"; + + output.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 10U)); + output.stop(); + ASSERT_TRUE(std::filesystem::is_regular_file(outputDir / "metadata") && + std::filesystem::file_size(outputDir / "stream_0") > 0U && std::filesystem::is_regular_file(xmlPath)) + << "CTF stop must complete the directly written bundle"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputRejectsOverlappingTargetsBeforeDeletion) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-overlapping-targets-test"); + const auto& root = temporaryPath.path(); + const auto ctfDirectory = root / "Bundle.ctf"; + const auto nestedXml = ctfDirectory / "Bundle.SWO.traceanalysis.xml"; + writeTestFile(ctfDirectory / "old-marker", "old-ctf"); + writeTestFile(nestedXml, "old-xml"); + + const auto rejected = throwsException( + [&] { CtfBundleOutput output(CtfOutputConfig(ctfDirectory, nestedXml, 1000000U, {}, {})); }); + ASSERT_TRUE(rejected && readTestTextFile(ctfDirectory / "old-marker") == "old-ctf" && + readTestTextFile(nestedXml) == "old-xml") + << "overlapping CTF targets must be rejected before either existing target is deleted"; + + const auto wrongTypeCtf = root / "WrongType.ctf"; + const auto wrongTypeXml = root / "WrongType.SWO.traceanalysis.xml"; + writeTestFile(wrongTypeCtf, "not-a-directory"); + std::filesystem::create_directory(wrongTypeXml); + const auto rejectedWrongTypes = throwsException([&] { + CtfBundleOutput output(CtfOutputConfig(wrongTypeCtf, wrongTypeXml, 1000000U, {}, {})); + output.start(); + }); + ASSERT_TRUE(rejectedWrongTypes && readTestTextFile(wrongTypeCtf) == "not-a-directory" && + std::filesystem::is_directory(wrongTypeXml)) + << "CTF start must reject unexpected target types before deleting either target"; + + const auto rejectedCaseInsensitiveOverlap = throwsException([&] { + CtfBundleOutput output( + CtfOutputConfig(root / "Bundle.ctf", root / "BUNDLE.CTF" / "Bundle.SWO.traceanalysis.xml", 1000000U, {}, {})); + }); + ASSERT_TRUE(rejectedCaseInsensitiveOverlap) << "CTF target overlap checks must conservatively ignore ASCII case"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputOwnsDirectLifecycle) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-bundle-lifecycle-test"); + const auto& testRoot = temporaryPath.path(); + const auto root = testRoot / std::filesystem::u8path(u8"Gr\u00f6\u00dfe"); + const auto ctfDirectory = root / "Bundle.ctf"; + const auto xmlPath = root / "Bundle.SWO.traceanalysis.xml"; + + auto config = makeCtfBundleConfig(ctfDirectory, 1000000U); + CtfBundleOutput output(std::move(config)); + output.start(); + const auto software = atCycle(softwarePacket(1U, 1U, 'A'), 10U); + output.writeEvent(software); + output.stop(); + + requireCompleteCtfBundle(ctfDirectory, xmlPath, "CTF bundle must support Unicode output paths"); + ASSERT_TRUE(!readTestTextFile(xmlPath).empty()) << "CTF bundle did not write the Trace Compass XML"; + + writeTestFile(ctfDirectory / "stale-marker", "stale"); + writeTestFile(xmlPath, "stale-xml"); + output.start(); + ASSERT_TRUE(!std::filesystem::exists(ctfDirectory / "stale-marker") && + !std::filesystem::exists(ctfDirectory / "metadata") && readTestTextFile(xmlPath) != "stale-xml") + << "restarting CTF output must delete the previous bundle before writing"; + output.writeEvent(software); + output.stop(); + requireCompleteCtfBundle(ctfDirectory, xmlPath, "restarted CTF bundle must complete normally"); + + std::filesystem::create_directories(testRoot / "working"); + std::filesystem::create_directories(testRoot / "captures"); + const auto relativeRoot = testRoot / "working" / ".." / "captures"; + const auto relativeCtf = relativeRoot / "Relative.ctf"; + CtfBundleOutput relativeOutput(makeCtfBundleConfig(relativeCtf, 1000000U)); + relativeOutput.start(); + relativeOutput.writeEvent(software); + relativeOutput.stop(); + ASSERT_TRUE(std::filesystem::is_regular_file(testRoot / "captures" / "Relative.ctf" / "metadata") && + std::filesystem::is_regular_file(testRoot / "captures" / "Relative.SWO.traceanalysis.xml")) + << "CTF output must accept a legitimate parent-relative trace path"; +} + +TEST(CtraceUnitTests, testCtfBundleOutputReportsIdentityAndSupportsInactiveStop) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-identity-test"); + const auto& outputDirectory = temporaryOutput.outputDirectory(); + CtfBundleOutput output(makeCtfBundleConfig(outputDirectory, 1000000U)); + EXPECT_EQ(output.backendName(), "ctf"); + EXPECT_EQ(output.targetPath(), outputDirectory.string()); + output.stop(); + output.writeEvent(softwarePacket(1U)); + EXPECT_FALSE(std::filesystem::exists(outputDirectory)); +} + +TEST(CtraceUnitTests, testCtfBundleOutputRejectsUnsafeTargets) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-unsafe-targets-test"); + const auto safeCtf = temporaryPath.path() / "safe.ctf"; + const auto safeXml = temporaryPath.path() / "safe.xml"; + for (const auto& unsafe : {std::filesystem::path{}, std::filesystem::path("."), std::filesystem::path("..")}) { + EXPECT_THROW((void)CtfBundleOutput(CtfOutputConfig(unsafe, safeXml, 1000000U, {}, {})), std::invalid_argument); + EXPECT_THROW((void)CtfBundleOutput(CtfOutputConfig(safeCtf, unsafe, 1000000U, {}, {})), std::invalid_argument); + } +} + +TEST(CtraceUnitTests, testCtfBundleOutputRejectsInvalidExistingXmlAndLongPaths) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-invalid-existing-output-test"); + const auto ctfDirectory = temporaryPath.path() / "output.ctf"; + const auto xmlDirectory = temporaryPath.path() / "output.xml"; + std::filesystem::create_directories(xmlDirectory); + CtfBundleOutput directoryXml(CtfOutputConfig(ctfDirectory, xmlDirectory, 1000000U, {}, {})); + EXPECT_THROW(directoryXml.start(), std::runtime_error); + + const auto longName = std::string(1024U, 'x'); + CtfBundleOutput longCtf( + CtfOutputConfig(temporaryPath.path() / longName, temporaryPath.path() / "long-ctf.xml", 1000000U, {}, {})); + EXPECT_THROW(longCtf.start(), std::runtime_error); + CtfBundleOutput longXml( + CtfOutputConfig(temporaryPath.path() / "long-xml.ctf", temporaryPath.path() / longName, 1000000U, {}, {})); + EXPECT_THROW(longXml.start(), std::runtime_error); +} + +TEST(CtraceUnitTests, testCtfBundleOutputCleansUpAfterMetadataFailure) +{ + const TemporaryCtfOutput temporaryOutput("ctrace-ctf-metadata-failure-test"); + const auto& outputDirectory = temporaryOutput.outputDirectory(); + const auto xmlPath = testTraceCompassXmlPath(outputDirectory); + CtfBundleOutput output(makeCtfBundleConfig(outputDirectory, 1000000U)); + output.start(); + std::filesystem::create_directory(outputDirectory / "metadata"); + EXPECT_THROW(output.stop(), std::runtime_error); + EXPECT_FALSE(std::filesystem::exists(outputDirectory)); + EXPECT_FALSE(std::filesystem::exists(xmlPath)); +} + +TEST(CtraceUnitTests, testCtfBundleOutputCleansUpAfterTraceCompassStartFailure) +{ + const TemporaryTestPath root("ctrace-ctf-start-failure-test"); + root.createDirectory(); + const auto blockedParent = root.path() / "not-a-directory"; + writeTestFile(blockedParent, "file"); + const auto outputDirectory = root.path() / "trace.ctf"; + const auto xmlPath = blockedParent / "trace.xml"; + + CtfBundleOutput output(CtfOutputConfig(outputDirectory, xmlPath, 1000000U, {}, {})); + EXPECT_THROW(output.start(), std::runtime_error); + EXPECT_FALSE(std::filesystem::exists(outputDirectory)); +} + +TEST(CtraceUnitTests, testCtfBundleOutputReportsPseudoFilesystemStartFailure) +{ + if (!TestPlatform::supports(TestPlatformCapability::LinuxSpecialFiles)) { + GTEST_SKIP(); + } + const TemporaryTestPath temporaryPath("ctrace-ctf-pseudo-filesystem-test"); + const auto outputDirectory = temporaryPath.path() / "output.ctf"; + CtfBundleOutput output(CtfOutputConfig( + outputDirectory, TestPlatform::creationFailurePath("ctrace-coverage-output.xml"), 1000000U, {}, {})); + EXPECT_THROW(output.start(), std::runtime_error); + EXPECT_FALSE(std::filesystem::exists(outputDirectory)); +} + +TEST(CtraceUnitTests, testCtfBundleOutputReportsPermissionFailures) +{ + if (!TestPlatform::supports(TestPlatformCapability::PosixPermissions)) { + GTEST_SKIP(); + } + const TemporaryTestPath temporaryPath("ctrace-ctf-permission-failure-test"); + const auto& root = temporaryPath.createDirectory(); + + const auto destructorCtf = root / "destructor.ctf"; + const auto destructorXml = root / "destructor.xml"; + { + CtfBundleOutput output(CtfOutputConfig(destructorCtf, destructorXml, 1000000U, {}, {})); + output.start(); + std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + } + std::filesystem::permissions(root, std::filesystem::perms::owner_all); + EXPECT_TRUE(std::filesystem::exists(destructorCtf)); + EXPECT_TRUE(std::filesystem::exists(destructorXml)); + std::filesystem::remove_all(destructorCtf); + std::filesystem::remove(destructorXml); + + const auto existingCtf = root / "existing.ctf"; + const auto existingXml = root / "existing.xml"; + writeTestFile(existingCtf / "marker", "existing"); + std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + CtfBundleOutput removeDirectoryFailure(CtfOutputConfig(existingCtf, existingXml, 1000000U, {}, {})); + EXPECT_THROW(removeDirectoryFailure.start(), std::runtime_error); + std::filesystem::permissions(root, std::filesystem::perms::owner_all); + std::filesystem::remove_all(existingCtf); + + const auto writableParent = root / "writable"; + const auto blockedParent = root / "blocked"; + std::filesystem::create_directories(writableParent); + std::filesystem::create_directories(blockedParent); + const auto blockedXml = blockedParent / "existing.xml"; + writeTestFile(blockedXml, "existing"); + std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + CtfBundleOutput removeFileFailure(CtfOutputConfig(writableParent / "output.ctf", blockedXml, 1000000U, {}, {})); + EXPECT_THROW(removeFileFailure.start(), std::runtime_error); + std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); + + const auto cleanupCtf = writableParent / "cleanup.ctf"; + std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + CtfBundleOutput startCleanupFailure(CtfOutputConfig(cleanupCtf, blockedParent / "new.xml", 1000000U, {}, {})); + EXPECT_THROW(startCleanupFailure.start(), std::runtime_error); + EXPECT_FALSE(std::filesystem::exists(cleanupCtf)); + std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); + + const auto blockedCtf = blockedParent / "new.ctf"; + std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); + CtfBundleOutput createDirectoryFailure(CtfOutputConfig(blockedCtf, writableParent / "new.xml", 1000000U, {}, {})); + EXPECT_THROW(createDirectoryFailure.start(), std::runtime_error); + std::filesystem::permissions(blockedParent, std::filesystem::perms::owner_all); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp new file mode 100644 index 000000000..99a7ae444 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfEncoderTests.cpp @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfTestSupport.h" +#include "TestSupport.h" + +#include + +#include "ctf/CtfEncoder.h" +#include "ctf/CtfSchema.h" +#include "TestPath.h" +#include "TraceEvent.h" +#include "TraceOutputConfig.h" +#include "TraceSelection.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using CtfTestSupport::CtfRecord; +using CtfTestSupport::kCtfEventOffset; +using CtfTestSupport::kCtfPacketContextSize; +using CtfTestSupport::kCtfPacketHeaderSize; +using CtfTestSupport::kCtfPacketSize; +using CtfTestSupport::readCtfRecords; +using CtfTestSupport::readLe16; +using CtfTestSupport::readLe32; +using CtfTestSupport::requireFirstCtfRecord; +using CtfTestSupport::requireSingleItmEvent; + +/** @brief Formats an encoded CTF UUID for comparison. */ +static std::string formatCtfUuid(const std::vector& bytes, std::size_t offset) +{ + static constexpr char hexDigits[] = "0123456789abcdef"; + std::string result; + result.reserve(36U); + for (std::size_t index = 0; index < 16U; ++index) { + if (index == 4U || index == 6U || index == 8U || index == 10U) { + result.push_back('-'); + } + const auto byte = bytes[offset + index]; + result.push_back(hexDigits[(byte >> 4U) & 0x0fU]); + result.push_back(hexDigits[byte & 0x0fU]); + } + return result; +} + +/** @brief Creates resolved DWT source metadata for encoder tests. */ +static ResolvedTraceSource resolvedDwtSource(std::uint32_t comparator, std::uint8_t traceBusId, std::string type, + std::uint8_t size) +{ + ResolvedTraceSource source; + source.type = "dwt"; + source.source = comparator; + source.traceBusId = traceBusId; + source.valueType = std::move(type); + source.valueSize = size; + return source; +} + +TEST(CtraceUnitTests, testCtfEncoderWritesOnlyIntoProvidedDirectory) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-boundary-test"); + const auto& root = temporaryPath.path(); + const auto missingDirectory = root / "missing"; + const auto outputDirectory = root / "provided"; + + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"itm"}, {}}, + {}, + }); + const auto rejectedMissingDirectory = throwsException([&] { encoder.start(missingDirectory); }); + ASSERT_TRUE(rejectedMissingDirectory && !std::filesystem::exists(missingDirectory)) + << "CtfEncoder must not create or own its output directory"; + + std::filesystem::create_directories(outputDirectory); + encoder.start(outputDirectory); + encoder.writeEvent(atCycle(softwarePacket(1U, 1U, 'A'), 10U)); + encoder.stop(); + ASSERT_TRUE(std::filesystem::is_regular_file(outputDirectory / "metadata") && + std::filesystem::is_regular_file(outputDirectory / "stream_0")) + << "CtfEncoder must encode metadata and stream data into the provided directory"; + requireSingleItmEvent(outputDirectory / "stream_0", 1U, "CtfEncoder encoded an unexpected ITM event"); + + encoder.abort(); + ASSERT_TRUE(std::filesystem::is_regular_file(outputDirectory / "metadata")) + << "CtfEncoder abort must not delete a directory owned by its caller"; +} + +TEST(CtraceUnitTests, testCtfEncoderPacketBoundaryAndUuid) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-packet-boundary-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"itm"}, {}}, + {}, + }); + encoder.start(outputDirectory); + + // A one-byte ITM event occupies 21 bytes. Exactly 3118 events fit after + // the 56-byte header/context; event 3119 starts packet 2. + constexpr std::size_t eventsInFirstPacket = 3118U; + for (std::size_t index = 0; index <= eventsInFirstPacket; ++index) { + encoder.writeEvent(atCycle(softwarePacket(1U, 1U, static_cast(index)), index + 1U)); + } + encoder.stop(); + + const auto stream = readTestBinaryFile(outputDirectory / "stream_0"); + const auto metadata = readTestTextFile(outputDirectory / "metadata"); + + constexpr std::size_t eventSize = 21U; + constexpr auto firstContentSize = kCtfPacketHeaderSize + kCtfPacketContextSize + eventsInFirstPacket * eventSize; + constexpr std::uint32_t ctfMagic = 0xc1fc1fc1U; + ASSERT_TRUE(stream.size() == kCtfPacketSize * 2U) << "CTF packet rollover must emit two complete 64-KiB packets"; + ASSERT_TRUE(readLe32(stream, 0U) == ctfMagic && readLe32(stream, kCtfPacketSize) == ctfMagic) + << "CTF packet rollover emitted an invalid packet header"; + ASSERT_TRUE(readLe32(stream, kCtfPacketHeaderSize) == kCtfPacketSize * 8U && + readLe32(stream, kCtfPacketHeaderSize + 4U) == firstContentSize * 8U) + << "first CTF packet content size mismatch at rollover"; + ASSERT_TRUE(readLe32(stream, kCtfPacketSize + kCtfPacketHeaderSize + 4U) == (kCtfEventOffset + eventSize) * 8U) + << "second CTF packet content size mismatch"; + ASSERT_TRUE(readLe32(stream, kCtfPacketHeaderSize + 28U) == 0U && + readLe32(stream, kCtfPacketSize + kCtfPacketHeaderSize + 28U) == 1U) + << "CTF packet sequence must advance across a 64-KiB boundary"; + ASSERT_TRUE(std::equal(stream.begin() + 4U, stream.begin() + 20U, stream.begin() + kCtfPacketSize + 4U)) + << "CTF packet UUID must remain stable across packet rollover"; + + const auto uuid = formatCtfUuid(stream, 4U); + ASSERT_TRUE(metadata.find("uuid = \"" + uuid + "\";") != std::string::npos) + << "CTF metadata UUID must match the binary packet UUID"; + ASSERT_TRUE((stream[4U + 6U] & 0xf0U) == 0x40U && (stream[4U + 8U] & 0xc0U) == 0x80U) + << "CTF UUID must use RFC 4122 version-4 and variant bits"; + + encoder.abort(); +} + +TEST(CtraceUnitTests, testCtfEncoderDwtAddressEncoding) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-encoder-dwt-address-test"); + const auto& outputDirectory = temporaryPath.createDirectory(); + + CtfEncoder encoder(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"dwt"}, {}}, + {}, + }); + encoder.start(outputDirectory); + encoder.writeEvent(atCycle(TraceEvent{DwtAddressTraceEvent{ + 3U, + DwtPcAndOffsetTraceLocation{0x12345678U, 0x0000abcdU}, + }}, + 99U)); + encoder.stop(); + + const auto records = readCtfRecords(outputDirectory / "stream_0"); + ASSERT_EQ(records.size(), 1U); + const auto& record = records.front(); + ASSERT_TRUE(record.id == CtfSchema::value(CtfSchema::EventId::DwtAddress)) << "CTF DWT address event ID mismatch"; + ASSERT_TRUE(record.timestamp == 99U) << "CTF DWT address timestamp mismatch"; + ASSERT_TRUE(record.payload.size() == 14U) << "CTF DWT address event payload size mismatch"; + ASSERT_TRUE(record.payload[0U] == 3U && record.payload[1U] == 1U && record.payload[2U] == 1U) + << "CTF DWT address comparator or presence flags mismatch"; + ASSERT_TRUE(readLe32(record.payload, 3U) == 0x12345678U && readLe16(record.payload, 7U) == 0xabcdU) + << "CTF DWT PC/address payload mismatch"; + + encoder.abort(); +} + +TEST(CtraceUnitTests, testCtfEncoderRejectsInvalidClockAndPayloadMetadata) +{ + EXPECT_THROW((void)CtfEncoder(CtfEncoderConfig{}), std::invalid_argument); + + const TemporaryTestPath temporaryPath("ctrace-ctf-invalid-payload-test"); + temporaryPath.createDirectory(); + CtfEncoder invalidItm(CtfEncoderConfig{1000000U, TraceSelection{{"itm"}, {}}, {}}); + invalidItm.stop(); + invalidItm.writeEvent(softwarePacket(1U)); + invalidItm.start(temporaryPath.path()); + EXPECT_THROW(invalidItm.writeEvent(softwarePacket(1U, 3U, 0U)), std::runtime_error); + invalidItm.abort(); + + CtfEncoder invalidDwt(CtfEncoderConfig{ + 1000000U, + TraceSelection{{"dwt"}, {}}, + {resolvedDwtSource(0U, 1U, "unsupported", 3U)}, + }); + invalidDwt.start(temporaryPath.path()); + EXPECT_THROW(invalidDwt.writeEvent(onStream(TraceEvent{DwtDataTraceEvent{0U, 1U, 0U, AccessType::Read}}, 1U)), + std::runtime_error); + invalidDwt.abort(); +} + +TEST(CtraceUnitTests, testCtfEncoderWritesAllDwtValueVariants) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-value-variants-test"); + temporaryPath.createDirectory(); + std::vector sources{ + resolvedDwtSource(0U, 1U, "signed int", 2U), resolvedDwtSource(1U, 1U, "float", 4U), + resolvedDwtSource(2U, 1U, "signed int", 4U), resolvedDwtSource(3U, 7U, "unsigned int", 1U), + resolvedDwtSource(4U, 1U, "signed int", 1U), resolvedDwtSource(4U, 2U, "signed int", 1U), + }; + sources.push_back(resolvedDwtSource(99U, 7U, "unsigned int", 1U)); + + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"dwt"}, {}}, sources}); + encoder.start(temporaryPath.path()); + + auto signed16 = atCycle( + onStream(TraceEvent{DwtDataTraceEvent{0U, 2U, 0xff80U, AccessType::Write, 0x1234U, 0x08000000U}}, 1U), 10U); + signed16.quality = TraceQuality{false, true, 0U}; + encoder.writeEvent(signed16); + + encoder.writeEvent(atCycle(onStream(TraceEvent{DwtDataTraceEvent{1U, 4U, 0x3f800000U, AccessType::Read}}, 1U), 11U)); + encoder.writeEvent(atCycle(onStream(TraceEvent{DwtDataTraceEvent{2U, 4U, 0xffffffffU, AccessType::Read}}, 1U), 12U)); + encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{3U, 1U, 0x12U, AccessType::Read}}, 13U)); + encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{4U, 1U, 0xffU, AccessType::Read}}, 14U)); + encoder.writeEvent(atCycle(TraceEvent{DwtDataTraceEvent{6U, 4U, 0x12345678U, AccessType::Read}}, 15U)); + + encoder.stop(); + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + ASSERT_EQ(records.size(), 6U); + for (const auto& record : records) { + EXPECT_EQ(record.id, CtfSchema::value(CtfSchema::EventId::DwtValue)); + } + EXPECT_EQ(records[0].timestamp, 10U); + EXPECT_EQ(records[0].traceBusId, 1U); + EXPECT_EQ(records[0].payload[0U], 0U); + EXPECT_EQ(records[0].payload[1U], CtfSchema::value(CtfSchema::DwtAccess::Write)); + EXPECT_EQ(records[0].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Signed16)); + EXPECT_EQ(readLe16(records[0].payload, 3U), 0xff80U); + EXPECT_EQ(records[0].payload[5U], 1U); + EXPECT_EQ(readLe32(records[0].payload, 6U), 0x08000000U); + EXPECT_EQ(records[0].payload[10U], 1U); + EXPECT_EQ(readLe16(records[0].payload, 11U), 0x1234U); + + EXPECT_EQ(records[1].timestamp, 11U); + EXPECT_EQ(records[1].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Float32)); + EXPECT_EQ(readLe32(records[1].payload, 3U), 0x3f800000U); + EXPECT_EQ(records[2].timestamp, 12U); + EXPECT_EQ(records[2].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Signed32)); + EXPECT_EQ(readLe32(records[2].payload, 3U), 0xffffffffU); + EXPECT_EQ(records[3].timestamp, 13U); + EXPECT_EQ(records[3].traceBusId, 0U); + EXPECT_EQ(records[3].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Unsigned8)); + EXPECT_EQ(records[3].payload[3U], 0x12U); + // The unformatted stream must retain equivalent metadata from both configured routes. + EXPECT_EQ(records[4].timestamp, 14U); + EXPECT_EQ(records[4].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Signed8)); + EXPECT_EQ(records[4].payload[3U], 0xffU); + EXPECT_EQ(records[5].timestamp, 15U); + EXPECT_EQ(records[5].payload[2U], CtfSchema::value(CtfSchema::ValueTag::Unsigned32)); + EXPECT_EQ(readLe32(records[5].payload, 3U), 0x12345678U); +} + +TEST(CtraceUnitTests, testCtfEncoderRejectsConflictingUnformattedDwtRoutes) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-conflicting-dwt-routes-test"); + temporaryPath.createDirectory(); + const std::vector sources{ + resolvedDwtSource(0U, 1U, "signed int", 1U), + resolvedDwtSource(0U, 2U, "float", 4U), + }; + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"dwt"}, {}}, sources}); + encoder.start(temporaryPath.path()); + EXPECT_TRUE( + throwsWithMessage([&] { encoder.writeEvent(TraceEvent{DwtDataTraceEvent{0U, 1U, 0xffU, AccessType::Read}}); }, + "conflicting metadata for unformatted dwt source 0")); + encoder.abort(); +} + +TEST(CtraceUnitTests, testCtfEncoderIgnoresUnselectedStreamTimeAndQuality) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-filtered-stream-state-test"); + temporaryPath.createDirectory(); + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{"itm"}, {1U}}, {}}); + encoder.start(temporaryPath.path()); + + auto excludedTimestamp = atCycle(onStream(TraceEvent{LocalTimestampTraceEvent{}}, 2U), 900U); + excludedTimestamp.quality = TraceQuality{false, true, 0U}; + encoder.writeEvent(excludedTimestamp); + auto excludedOverflow = atCycle(onStream(TraceEvent{OverflowTraceEvent{}}, 2U), 1000U); + excludedOverflow.quality = TraceQuality{true, false, 99U}; + encoder.writeEvent(excludedOverflow); + + auto selected = atCycle(onStream(softwarePacket(1U, 1U, 'A'), 1U), 10U); + selected.quality = TraceQuality{false, true, 0U}; + encoder.writeEvent(selected); + encoder.stop(); + + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + ASSERT_EQ(records.size(), 1U); + EXPECT_EQ(records[0].id, CtfSchema::value(CtfSchema::EventId::Itm)); + EXPECT_EQ(records[0].timestamp, 10U); + EXPECT_EQ(records[0].traceBusId, 1U); + EXPECT_EQ(records[0].payload[3U], CtfSchema::SampleFlagTimestampReliable); + EXPECT_EQ(readLe32(records[0].payload, 4U), 0U); +} + +TEST(CtraceUnitTests, testCtfEncoderStreamSelectionKeepsStartAndResyncContext) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-selected-stream-status-test"); + temporaryPath.createDirectory(); + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{{}, {3U}}, {}}); + encoder.start(temporaryPath.path()); + encoder.writeEvent(onStream(exceptionPacket(15U, ExceptionAction::Entered, 10U), 3U)); + encoder.writeEvent(atCycle(onStream(TraceEvent{SyncTraceEvent{}}, 3U), 11U)); + encoder.writeEvent(onStream(exceptionPacket(54U, ExceptionAction::Entered, 20U), 3U)); + encoder.stop(); + + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + std::vector statusReasons; + std::vector> exceptionRecords; + for (const auto& record : records) { + EXPECT_EQ(record.traceBusId, 3U); + if (record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus)) { + statusReasons.push_back(record.payload[0U]); + } else if (record.id == CtfSchema::value(CtfSchema::EventId::Exception)) { + exceptionRecords.emplace_back(record.timestamp, std::to_string(readLe16(record.payload, 0U)) + ":" + + std::to_string(record.payload[2U])); + } + } + EXPECT_EQ(statusReasons, std::vector({ + CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), + CtfSchema::value(CtfSchema::TraceStatusReason::Resync), + })); + EXPECT_EQ(exceptionRecords, (std::vector>({ + {0U, "0:0"}, + {10U, "0:1"}, + {10U, "15:0"}, + {20U, "15:1"}, + {20U, "54:0"}, + }))); +} + +TEST(CtraceUnitTests, testCtfEncoderTracksLocalTimeAndUnqualifiedOverflow) +{ + const TemporaryTestPath temporaryPath("ctrace-ctf-time-quality-test"); + temporaryPath.createDirectory(); + CtfEncoder encoder(CtfEncoderConfig{1000000U, TraceSelection{}, {}}); + encoder.start(temporaryPath.path()); + + encoder.writeEvent(atCycle(onStream(TraceEvent{LocalTimestampTraceEvent{}}, 3U), 20U)); + encoder.writeEvent(onStream(TraceEvent{OverflowTraceEvent{}}, 3U)); + + auto saturated = atCycle(onStream(softwarePacket(1U, 1U, 0U), 3U), 21U); + saturated.quality = TraceQuality{true, true, std::numeric_limits::max()}; + encoder.writeEvent(saturated); + encoder.stop(); + encoder.stop(); + + const auto records = readCtfRecords(temporaryPath.path() / "stream_0"); + const auto& itm = requireFirstCtfRecord(records, CtfSchema::EventId::Itm, "saturated CTF ITM sample missing"); + EXPECT_EQ(itm.timestamp, 21U); + EXPECT_EQ(itm.traceBusId, 3U); + EXPECT_EQ(itm.payload[3U], CtfSchema::SampleFlagOverflow | CtfSchema::SampleFlagTimestampReliable); + EXPECT_EQ(readLe32(itm.payload, 4U), std::numeric_limits::max()); + + const auto overflowStatus = std::find_if(records.begin(), records.end(), [](const CtfRecord& record) { + return record.id == CtfSchema::value(CtfSchema::EventId::TraceStatus) && record.traceBusId == 3U && + record.payload[0U] == CtfSchema::value(CtfSchema::TraceStatusReason::Overflow); + }); + ASSERT_NE(overflowStatus, records.end()); + EXPECT_EQ(readLe32(overflowStatus->payload, 1U), 1U); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp new file mode 100644 index 000000000..43962bdd8 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfMetadataWriterTests.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.h" +#include "TestPlatform.h" +#include "TestSupport.h" + +#include + +#include "ctf/CtfMetadataWriter.h" +#include "ctf/CtfSchema.h" +#include "ctf/TraceCompassXmlWriter.h" +#include "TraceOutputConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST(CtraceUnitTests, testCtfMetadataWriterEscapesAndDeduplicatesSourceLabels) +{ + const TemporaryTestPath path("ctrace-metadata-writer"); + path.createDirectory(); + const std::vector sources{ + {"itm", 1U, 1U, std::string("ITM3"), std::nullopt, "unsigned int", 4U}, + {"itm", 2U, 1U, std::string("ITM3_1"), std::nullopt, "unsigned int", 4U}, + {"itm", 3U, 1U, std::string("ITM3"), std::nullopt, "unsigned int", 4U}, + {"itm", 4U, 1U, std::string("line\rbreak"), std::nullopt, "unsigned int", 4U}, + {"itm", 5U, 1U, std::nullopt, std::nullopt, "unsigned int", 4U}, + {"itm", 6U, 1U, std::string("ITM3"), std::nullopt, "unsigned int", 4U}, + {"future", 7U, 1U, std::string("ignored"), std::nullopt, "unsigned int", 4U}, + {"dwt", 0U, 1U, std::nullopt, std::numeric_limits::max(), "unsigned int", 4U}, + }; + + CtfMetadataWriter::write(path.path(), "00000000-0000-4000-8000-000000000000", 1000000U, sources, + {8U, 10U, 13U, 16U, 54U}); + const auto metadata = readTestTextFile(path.path() / "metadata"); + EXPECT_NE(metadata.find("ITM3_2"), std::string::npos); + EXPECT_NE(metadata.find("\"ITM6\" = 6"), std::string::npos); + EXPECT_NE(metadata.find("line\\rbreak"), std::string::npos); + EXPECT_NE(metadata.find("\"Reserved 8\" = 8"), std::string::npos); + EXPECT_NE(metadata.find("\"Reserved 10\" = 10"), std::string::npos); + EXPECT_NE(metadata.find("\"Reserved 13\" = 13"), std::string::npos); + EXPECT_NE(metadata.find("\"External IRQ 0\" = 16"), std::string::npos); + EXPECT_NE(metadata.find("\"External IRQ 38\" = 54"), std::string::npos); +} + +TEST(CtraceUnitTests, testCtfMetadataWriterRejectsMissingOutputDirectory) +{ + const TemporaryTestPath path("ctrace-metadata-writer-missing"); + EXPECT_THROW(CtfMetadataWriter::write(path.path(), "uuid", 1U, {}, {}), std::runtime_error); +} + +TEST(CtraceUnitTests, testTraceCompassXmlWriterRejectsDirectoryTarget) +{ + const TemporaryTestPath path("ctrace-trace-compass-directory-target"); + path.createDirectory(); + EXPECT_THROW(TraceCompassXmlWriter::writeFile(path.path()), std::runtime_error); +} + +TEST(CtraceUnitTests, testTraceCompassXmlWriterSupportsParentlessTarget) +{ + const auto path = std::filesystem::path("ctrace-parentless-trace-compass.xml"); + std::error_code ignored; + std::filesystem::remove(path, ignored); + EXPECT_NO_THROW(TraceCompassXmlWriter::writeFile(path)); + EXPECT_TRUE(std::filesystem::is_regular_file(path)); + std::filesystem::remove(path, ignored); +} + +TEST(CtraceUnitTests, testTraceCompassXmlUsesCurrentCtfEvents) +{ + const TemporaryTestPath path("ctrace-trace-compass-schema.xml"); + TraceCompassXmlWriter::writeFile(path.path()); + const auto xml = readTestTextFile(path.path()); + constexpr std::array visualizedEvents{ + CtfSchema::EventId::Itm, CtfSchema::EventId::DwtValue, CtfSchema::EventId::DwtAddress, + CtfSchema::EventId::Exception, CtfSchema::EventId::TraceStatus, + }; + + for (const auto eventId : visualizedEvents) { + EXPECT_NE(xml.find("eventName=\"" + std::string(CtfSchema::eventName(eventId)) + "\""), std::string::npos); + } +} + +TEST(CtraceUnitTests, testCtfTextWritersReportDeviceWriteFailures) +{ + if (!TestPlatform::supports(TestPlatformCapability::LinuxSpecialFiles)) { + GTEST_SKIP(); + } + const TemporaryTestPath path("ctrace-metadata-device-failure"); + path.createDirectory(); + std::filesystem::create_symlink(TestPlatform::writeFailurePath(), path.path() / "metadata"); + EXPECT_THROW(CtfMetadataWriter::write(path.path(), "uuid", 1U, {}, {}), std::runtime_error); + EXPECT_THROW(TraceCompassXmlWriter::writeFile(TestPlatform::writeFailurePath()), std::runtime_error); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp new file mode 100644 index 000000000..45cd49180 --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfSchemaTests.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" + +#include + +#include "ctf/CtfExceptionLaneTracker.h" +#include "ctf/CtfSchema.h" +#include "TraceEvent.h" + +#include +#include +#include +#include +#include + +TEST(CtraceUnitTests, testCtfSchemaUsesDenseIdentifiers) +{ + constexpr std::array eventIds{ + CtfSchema::value(CtfSchema::EventId::Itm), CtfSchema::value(CtfSchema::EventId::DwtValue), + CtfSchema::value(CtfSchema::EventId::DwtAddress), CtfSchema::value(CtfSchema::EventId::TraceStatus), + CtfSchema::value(CtfSchema::EventId::Exception), CtfSchema::value(CtfSchema::EventId::GlobalTimestamp), + }; + constexpr std::array statusReasons{ + CtfSchema::value(CtfSchema::TraceStatusReason::TraceStart), + CtfSchema::value(CtfSchema::TraceStatusReason::Resync), + CtfSchema::value(CtfSchema::TraceStatusReason::Overflow), + CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError), + CtfSchema::value(CtfSchema::TraceStatusReason::DataLoss), + }; + constexpr std::array exceptionActions{ + CtfSchema::value(CtfSchema::ExceptionAction::Entered), + CtfSchema::value(CtfSchema::ExceptionAction::Exited), + }; + constexpr std::array valueTags{ + CtfSchema::value(CtfSchema::ValueTag::Signed8), CtfSchema::value(CtfSchema::ValueTag::Unsigned8), + CtfSchema::value(CtfSchema::ValueTag::Signed16), CtfSchema::value(CtfSchema::ValueTag::Unsigned16), + CtfSchema::value(CtfSchema::ValueTag::Signed32), CtfSchema::value(CtfSchema::ValueTag::Unsigned32), + CtfSchema::value(CtfSchema::ValueTag::Float32), + }; + + for (std::size_t index = 0U; index < eventIds.size(); ++index) { + EXPECT_EQ(eventIds[index], static_cast(index)); + } + for (std::size_t index = 0U; index < statusReasons.size(); ++index) { + EXPECT_EQ(statusReasons[index], static_cast(index)); + } + for (std::size_t index = 0U; index < exceptionActions.size(); ++index) { + EXPECT_EQ(exceptionActions[index], static_cast(index)); + } + for (std::size_t index = 0U; index < valueTags.size(); ++index) { + EXPECT_EQ(valueTags[index], static_cast(index)); + } +} + +TEST(CtraceUnitTests, testCtfValueTypes) +{ + EXPECT_EQ(CtfSchema::eventName(static_cast(255U)), "UNKNOWN"); + /** @brief Describes one supported CTF value representation. */ + struct SupportedType { + const char* name; + CtfSchema::ValueTag tag; + std::uint8_t size; + }; + const std::vector supported{ + {"unsigned int", CtfSchema::ValueTag::Unsigned8, 1U}, {"unsigned int", CtfSchema::ValueTag::Unsigned16, 2U}, + {"unsigned int", CtfSchema::ValueTag::Unsigned32, 4U}, {"signed int", CtfSchema::ValueTag::Signed8, 1U}, + {"signed int", CtfSchema::ValueTag::Signed16, 2U}, {"signed int", CtfSchema::ValueTag::Signed32, 4U}, + {"float", CtfSchema::ValueTag::Float32, 4U}, + }; + for (const auto& item : supported) { + const auto* resolved = CtfSchema::valueVariantForTraceRunType(item.name, item.size); + ASSERT_TRUE(resolved != nullptr) << std::string("supported CTF data type was rejected: ") + item.name; + ASSERT_TRUE(resolved->tag == item.tag) << std::string("CTF type tag mismatch: ") + item.name; + ASSERT_TRUE(resolved->byteSize == item.size) << std::string("CTF type size mismatch: ") + item.name; + } + + ASSERT_TRUE(CtfSchema::valueVariantForTraceRunType("uint", 4U) == nullptr) + << "unsupported CTF type alias was accepted"; + ASSERT_TRUE(CtfSchema::valueVariantForTraceRunType("int", 4U) == nullptr) + << "unsupported signed type alias was accepted"; + ASSERT_TRUE(CtfSchema::valueVariantForTraceRunType("float32", 4U) == nullptr) + << "unsupported float type alias was accepted"; + ASSERT_TRUE(CtfSchema::valueVariantForTraceRunType("double", 4U) == nullptr) << "unsupported CTF type was accepted"; + ASSERT_TRUE(CtfSchema::valueVariantForTraceRunType("float", 2U) == nullptr) + << "incompatible CTF float width was accepted"; + ASSERT_TRUE(CtfSchema::valueVariantForTraceRunType("unsigned int", 8U) == nullptr) + << "invalid CTF data size was accepted"; +} + +TEST(CtraceUnitTests, testCtfExceptionLaneTracker) +{ + CtfExceptionLaneTracker tracker; + std::vector records; + const auto emit = [&records](std::uint32_t number, CtfExceptionLaneTracker::RecordAction action) { + records.push_back(std::to_string(number) + + (action == CtfExceptionLaneTracker::RecordAction::Enter ? ":enter" : ":exit")); + }; + + tracker.startThreadMode(emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Exited}, emit); + tracker.consume(ExceptionTraceEvent{54, ExceptionAction::Entered}, emit); + tracker.consume(ExceptionTraceEvent{54, ExceptionAction::Exited}, emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Returned}, emit); + tracker.consume(ExceptionTraceEvent{15, ExceptionAction::Exited}, emit); + tracker.consume(ExceptionTraceEvent{3, ExceptionAction::Exited}, emit); + + ASSERT_TRUE(records == std::vector({ + "0:enter", + "0:exit", + "3:enter", + "3:exit", + "15:enter", + "15:exit", + "3:enter", + "3:exit", + "54:enter", + "54:exit", + "3:enter", + "3:exit", + "0:enter", + })) + << "CtfExceptionLaneTracker nested and tail-chain records mismatch"; + ASSERT_TRUE(tracker.observedExceptionNumbers() == std::vector({0U, 3U, 15U, 54U})) + << "CtfExceptionLaneTracker observed lanes mismatch"; + + tracker.resetForDiscontinuity(emit); + ASSERT_TRUE(records.back() == "0:exit") << "CtfExceptionLaneTracker discontinuity must close the active lane"; + const auto recordCount = records.size(); + tracker.consume(ExceptionTraceEvent{0, ExceptionAction::Unknown}, emit); + ASSERT_TRUE(records.size() == recordCount) << "CtfExceptionLaneTracker must ignore unknown exception actions"; + + CtfExceptionLaneTracker resumedTracker; + resumedTracker.startThreadMode(emit); + ASSERT_TRUE(resumedTracker.observedExceptionNumbers() == std::vector({0U})) + << "a new CtfExceptionLaneTracker must start with an empty lane history"; + + records.clear(); + resumedTracker.consume(ExceptionTraceEvent{3, ExceptionAction::Entered}, emit); + resumedTracker.consume(ExceptionTraceEvent{15, ExceptionAction::Entered}, emit); + resumedTracker.consume(ExceptionTraceEvent{15, ExceptionAction::Exited}, emit); + const auto preemptedRecordCount = records.size(); + resumedTracker.consume(ExceptionTraceEvent{3, ExceptionAction::Exited}, emit); + ASSERT_TRUE(records.size() == preemptedRecordCount) + << "CtfExceptionLaneTracker must not exit a preempted context before its return packet"; + resumedTracker.consume(ExceptionTraceEvent{3, ExceptionAction::Returned}, emit); + resumedTracker.consume(ExceptionTraceEvent{3, ExceptionAction::Exited}, emit); + ASSERT_TRUE(records.size() == preemptedRecordCount + 2U && records[records.size() - 2U] == "3:exit" && + records.back() == "0:enter") + << "CtfExceptionLaneTracker must exit a resumed context"; + + CtfExceptionLaneTracker returnTracker; + records.clear(); + returnTracker.consume(ExceptionTraceEvent{3, ExceptionAction::Entered}, emit); + returnTracker.consume(ExceptionTraceEvent{15, ExceptionAction::Entered}, emit); + returnTracker.consume(ExceptionTraceEvent{3, ExceptionAction::Returned}, emit); + EXPECT_EQ(records.back(), "3:enter"); +} diff --git a/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp b/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp new file mode 100644 index 000000000..5eddc528d --- /dev/null +++ b/tools/ctrace/test/unit/src/output/ctf/CtfStreamWriterTests.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtfTestSupport.h" +#include "TestPath.h" +#include "TestPlatform.h" + +#include + +#include "ctf/CtfSchema.h" +#include "ctf/CtfStreamWriter.h" + +#include +#include + +TEST(CtraceUnitTests, testCtfStreamWriterHandlesInactiveAndEmptyStreams) +{ + CtfStreamWriter writer; + EXPECT_NO_THROW(writer.close()); + EXPECT_NO_THROW(writer.writeRecord(1U, 1U, 1U, 0U, [](CtfStreamWriter::Record&) {})); + + const TemporaryTestPath path("ctrace-empty-stream"); + writer.open(path.path(), 7U); + EXPECT_FALSE(writer.uuidString().empty()); + EXPECT_NO_THROW(writer.close()); +} + +TEST(CtraceUnitTests, testCtfStreamWriterValidatesDeclaredPayloadSize) +{ + const TemporaryTestPath path("ctrace-invalid-record-stream"); + CtfStreamWriter writer; + writer.open(path.path(), 7U); + + EXPECT_THROW(writer.writeRecord(1U, 1U, 1U, 65536U, [](CtfStreamWriter::Record&) {}), std::invalid_argument); + EXPECT_THROW(writer.writeRecord(1U, 1U, 1U, 1U, [](CtfStreamWriter::Record&) {}), std::logic_error); + EXPECT_THROW(writer.writeRecord(1U, 1U, 1U, 1U, [](CtfStreamWriter::Record& record) { record.writeU16(0x1234U); }), + std::logic_error); + writer.abort(); +} + +TEST(CtraceUnitTests, testCtfStreamWriterHoldsRegressingTimestamps) +{ + const TemporaryTestPath path("ctrace-monotonic-stream"); + CtfStreamWriter writer; + writer.open(path.path(), 7U); + const auto eventId = CtfSchema::value(CtfSchema::EventId::TraceStatus); + const auto writePayload = [](CtfStreamWriter::Record& record) { + record.writeU8(CtfSchema::value(CtfSchema::TraceStatusReason::DecodeError)); + record.writeU32(1U); + }; + writer.writeRecord(eventId, 100U, 1U, 5U, writePayload); + writer.writeRecord(eventId, 50U, 1U, 5U, writePayload); + writer.close(); + + const auto records = CtfTestSupport::readCtfRecords(path.path()); + ASSERT_EQ(records.size(), 2U); + EXPECT_EQ(records[0].timestamp, 100U); + EXPECT_EQ(records[1].timestamp, 100U); +} + +TEST(CtraceUnitTests, testCtfStreamWriterReportsDeviceWriteFailures) +{ + if (!TestPlatform::supports(TestPlatformCapability::LinuxSpecialFiles)) { + GTEST_SKIP(); + } + CtfStreamWriter writer; + writer.open(TestPlatform::writeFailurePath(), 7U); + writer.writeRecord(1U, 1U, 1U, 1U, [](CtfStreamWriter::Record& record) { record.writeU8(1U); }); + EXPECT_THROW(writer.close(), std::runtime_error); +} diff --git a/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp new file mode 100644 index 000000000..59e3f1161 --- /dev/null +++ b/tools/ctrace/test/unit/src/tracerun/CtraceRunMetaTests.cpp @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestSupport.h" +#include "TraceRunTestSupport.h" +#include +#include "CtraceRunMeta.h" +#include "TraceRunConfig.h" +#include +#include +#include +#include +#include +#include + +using TraceRunTestSupport::makeReference; +using TraceRunTestSupport::makeTimestampSetup; + +/** @brief Tests whether metadata normalization rejects a configuration. */ +static bool metaRejects(const TraceRunConfig& config, std::string_view message) +{ + return throwsWithMessage([&config] { (void)CtraceRunMeta::fromConfig(config); }, message); +} + +TEST(CtraceUnitTests, testTimestampPrescalerMetadataDefaults) +{ + const auto ctraceRunMeta = CtraceRunMeta::fromConfig(TraceRunConfig{}); + ASSERT_TRUE(!ctraceRunMeta.timestampPrescaler().has_value()) + << "a missing timestamp setup must leave the metadata prescaler unspecified"; + + TraceRunConfig incompleteTraceRun; + TraceRunSetup incompleteSetup; + incompleteSetup.timestamps = TraceRunTimestampSetup{}; + incompleteTraceRun.setups.push_back(incompleteSetup); + const auto missingPrescalerMeta = CtraceRunMeta::fromConfig(incompleteTraceRun); + ASSERT_TRUE(missingPrescalerMeta.timestampPrescaler() == + std::optional(TraceRunSchema::kDefaultTimestampPrescaler)) + << "timestamp setup must resolve an omitted prescaler to the specified default"; + TraceRunConfig traceRun; + TraceRunSetup setup; + setup.timestamps = TraceRunTimestampSetup{std::nullopt, 4U}; + traceRun.setups.push_back(setup); + const auto prescalerOnlyMeta = CtraceRunMeta::fromConfig(traceRun); + ASSERT_TRUE(prescalerOnlyMeta.timestampPrescaler() == std::optional(4U)) + << "explicit timestamp prescaler metadata mismatch"; +} + +TEST(CtraceUnitTests, testCtraceRunMetaRejectsInvalidReferences) +{ + /** @brief Describes one invalid reference and its expected diagnostic. */ + struct Case { + TraceRunReference reference; + const char* message; + }; + std::vector cases; + cases.push_back({makeReference("dwt", std::nullopt, 1U, {1U, 1U}), "duplicate value in source array"}); + cases.push_back( + {makeReference("itm", std::nullopt, 0U, {1U}), "stream must be a CoreSight ATB trace ID between 1 and 111"}); + cases.push_back({makeReference("itm", std::nullopt, 1U, {32U}), "ITM source must be between 0 and 31"}); + + for (const auto& testCase : cases) { + TraceRunConfig config; + config.path = "trace.yml"; + config.references = {testCase.reference}; + EXPECT_TRUE(metaRejects(config, testCase.message)); + + config.references.front().line = 7U; + EXPECT_TRUE(metaRejects(config, "trace.yml(7)")); + } + + TraceRunConfig diagnosed; + diagnosed.references.push_back(makeReference("itm", std::nullopt, 0U, {99U})); + diagnosed.references.front().error = "producer rejected this route"; + EXPECT_NO_THROW((void)CtraceRunMeta::fromConfig(diagnosed)); + + TraceRunConfig diagnosedBinding; + diagnosedBinding.path = "trace.yml"; + diagnosedBinding.setups.push_back(makeTimestampSetup("core")); + diagnosedBinding.references.push_back(makeReference("itm", "unusable", 0U, {99U})); + diagnosedBinding.references.front().error = "producer rejected this route"; + const auto meta = CtraceRunMeta::fromConfig(diagnosedBinding); + EXPECT_EQ(meta.processorCount(), 1U); + EXPECT_TRUE(meta.sources().empty()); + EXPECT_TRUE(meta.timestampsByTraceBusId().empty()); +} + +TEST(CtraceUnitTests, testCtraceRunMetaExpandsItmAndDwtSourceArrays) +{ + TraceRunConfig config; + config.references.push_back(makeReference("itm", std::nullopt, 1U, {1U, 2U})); + config.references.push_back(makeReference("dwt", std::nullopt, 1U, {3U, 4U})); + + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.sources().size(), 4U); + EXPECT_EQ(meta.sources()[0].type, "itm"); + EXPECT_EQ(meta.sources()[0].source, 1U); + EXPECT_EQ(meta.sources()[1].type, "itm"); + EXPECT_EQ(meta.sources()[1].source, 2U); + EXPECT_EQ(meta.sources()[2].type, "dwt"); + EXPECT_EQ(meta.sources()[2].source, 3U); + EXPECT_EQ(meta.sources()[3].type, "dwt"); + EXPECT_EQ(meta.sources()[3].source, 4U); +} + +TEST(CtraceUnitTests, testCtraceRunMetaRejectsInvalidPrescalers) +{ + TraceRunConfig config; + config.path = "trace.yml"; + config.setups.push_back(makeTimestampSetup(std::nullopt, 1U, 2U)); + EXPECT_TRUE(metaRejects(config, "ctrace-setup timestamps.itm-prescaler must be one of")); + + config.setups.front().timestamps->line = 12U; + EXPECT_TRUE(metaRejects(config, "trace.yml(12): 'timestamps.itm-prescaler' must be one of")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaRejectsDuplicateSetups) +{ + TraceRunConfig duplicate; + duplicate.path = "trace.yml"; + duplicate.setups = {makeTimestampSetup(std::nullopt), makeTimestampSetup(std::nullopt)}; + EXPECT_TRUE(metaRejects(duplicate, "duplicate active ctrace-setup for pname ''")); + duplicate.setups.back().line = 9U; + EXPECT_TRUE(metaRejects(duplicate, "duplicate active 'ctrace-setup' for pname ''")); + + TraceRunConfig unnamedMultiProcessor; + unnamedMultiProcessor.path = "trace.yml"; + unnamedMultiProcessor.setups = {makeTimestampSetup("a"), makeTimestampSetup(std::nullopt)}; + EXPECT_TRUE(metaRejects(unnamedMultiProcessor, + "pname is required for every ctrace-setup in a multi-processor configuration")); +} + +TEST(CtraceUnitTests, testCtraceRunMetaWarnsForCrossRootProcessorIdentityConflicts) +{ + TraceRunConfig multiUnnamed; + multiUnnamed.path = "trace.yml"; + multiUnnamed.setups = {makeTimestampSetup("a"), makeTimestampSetup("b")}; + multiUnnamed.references.push_back(makeReference("itm", std::nullopt, 1U, {1U})); + multiUnnamed.references.front().line = 17U; + auto diagnosedReference = makeReference("itm", "a", 0U, {99U}); + diagnosedReference.error = "producer rejected this route"; + multiUnnamed.references.push_back(diagnosedReference); + const auto unnamedMeta = CtraceRunMeta::fromConfig(multiUnnamed); + EXPECT_EQ(unnamedMeta.processorCount(), 2U); + EXPECT_TRUE(unnamedMeta.sources().empty()); + ASSERT_EQ(unnamedMeta.warnings().size(), 1U); + EXPECT_NE(unnamedMeta.warnings().front().message.find("without pname"), std::string::npos); + ASSERT_EQ(unnamedMeta.warnings().front().context.size(), 4U); + EXPECT_EQ(unnamedMeta.warnings().front().context[2], (std::pair{"line", "17"})); + + TraceRunConfig multiUnmatched = multiUnnamed; + multiUnmatched.references.front().processorName = "c"; + const auto unmatchedMeta = CtraceRunMeta::fromConfig(multiUnmatched); + EXPECT_TRUE(unmatchedMeta.sources().empty()); + ASSERT_EQ(unmatchedMeta.warnings().size(), 1U); + EXPECT_NE(unmatchedMeta.warnings().front().message.find("no matching ctrace-setup"), std::string::npos); +} + +TEST(CtraceUnitTests, testCtraceRunMetaWarnsForSingleSetupIdentityConflicts) +{ + TraceRunConfig namedSetup; + namedSetup.path = "trace.yml"; + namedSetup.setups.push_back(makeTimestampSetup("a")); + namedSetup.references.push_back(makeReference("itm", "b", 1U, {1U})); + const auto namedMeta = CtraceRunMeta::fromConfig(namedSetup); + EXPECT_TRUE(namedMeta.sources().empty()); + ASSERT_EQ(namedMeta.warnings().size(), 1U); + + TraceRunConfig unnamedSetup; + unnamedSetup.path = "trace.yml"; + unnamedSetup.setups.push_back(makeTimestampSetup(std::nullopt)); + unnamedSetup.references = { + makeReference("itm", "a", 1U, {1U}), + makeReference("itm", "b", 2U, {2U}), + }; + const auto unnamedMeta = CtraceRunMeta::fromConfig(unnamedSetup); + EXPECT_EQ(unnamedMeta.processorCount(), 1U); + EXPECT_EQ(unnamedMeta.sources().size(), 2U); + ASSERT_EQ(unnamedMeta.warnings().size(), 1U); + + TraceRunConfig referencesOnly; + referencesOnly.path = "trace.yml"; + referencesOnly.references = unnamedSetup.references; + referencesOnly.references.push_back(makeReference("dwt", std::nullopt, 3U, {0U})); + EXPECT_TRUE(metaRejects(referencesOnly, "pname is required for every ctrace-ref")); + + referencesOnly.references.pop_back(); + const auto meta = CtraceRunMeta::fromConfig(referencesOnly); + EXPECT_EQ(meta.processorCount(), 2U); + EXPECT_FALSE(meta.timestampClockHz().has_value()); +} + +TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctProcessorSettings) +{ + TraceRunConfig config; + config.path = "trace.yml"; + config.setups = { + makeTimestampSetup("a", 100U, 4U, 1U), + makeTimestampSetup("b", 200U, 4U, 2U), + }; + config.references = { + makeReference("itm", "a", 5U, {1U}), + makeReference("itm", "b", 5U, {2U}), + }; + + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_FALSE(meta.timestampClockHz().has_value()); + EXPECT_EQ(meta.timestampPrescaler(), std::optional(4U)); + EXPECT_FALSE(meta.itmEnableMask().has_value()); + ASSERT_EQ(meta.itmEnableMasksByTraceBusId().size(), 1U); + EXPECT_EQ(meta.itmEnableMasksByTraceBusId().at(5U), 1U); + ASSERT_EQ(meta.timestampsByTraceBusId().size(), 1U); + EXPECT_EQ(meta.timestampsByTraceBusId().at(5U).clockHz, std::optional(100U)); + EXPECT_FALSE(meta.timestampsByTraceBusId().at(5U).clockError.has_value()); + EXPECT_EQ(meta.warnings().size(), 2U); +} + +TEST(CtraceUnitTests, testCtraceRunMetaMapsDistinctPrescalersPerStream) +{ + TraceRunConfig config; + config.setups = { + makeTimestampSetup("a", 100U, 4U), + makeTimestampSetup("b", 100U, 16U), + }; + config.references = { + makeReference("itm", "a", 5U, {1U}), + makeReference("itm", "b", 6U, {2U}), + }; + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_TRUE(meta.hasDistinctProcessorPrescalers()); + EXPECT_FALSE(meta.timestampPrescaler().has_value()); + EXPECT_EQ(meta.timestampPrescalersByTraceBusId().at(5U), 4U); + EXPECT_EQ(meta.timestampPrescalersByTraceBusId().at(6U), 16U); + + config.path = "trace.yml"; + config.references.back().stream = 5U; + config.references.back().line = 23U; + const auto conflictingMeta = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(conflictingMeta.timestampPrescalersByTraceBusId().at(5U), 4U); + EXPECT_EQ(conflictingMeta.warnings().size(), 2U); +} + +TEST(CtraceUnitTests, testCtraceRunMetaResolvesDwtDataAndDefaults) +{ + TraceRunConfig config; + config.path = "trace.yml"; + auto setup = makeTimestampSetup("core"); + setup.data.resize(2U); + setup.data[0].symbolType = "signed int"; + setup.data[0].symbolSize = 2U; + setup.data[0].symbolTypeError = "type warning"; + setup.data[0].symbolSizeError = "size warning"; + config.setups.push_back(setup); + + auto configured = makeReference("dwt", "core", 1U, {0U}, "core/data#0"); + configured.dataSetupIndex = 0U; + configured.symbolAddress = 0x20000000U; + auto missingIndex = makeReference("dwt", "core", 1U, {1U}, "core/data"); + auto outOfRange = makeReference("dwt", "core", 1U, {2U}, "core/data#9"); + outOfRange.dataSetupIndex = 9U; + config.references = {configured, missingIndex, outOfRange}; + + const auto meta = CtraceRunMeta::fromConfig(config); + ASSERT_EQ(meta.sources().size(), 3U); + EXPECT_EQ(meta.configPath(), "trace.yml"); + EXPECT_EQ(meta.sources()[0].valueType, "signed int"); + EXPECT_EQ(meta.sources()[0].valueSize, 2U); + EXPECT_EQ(meta.sources()[0].symbolAddress, std::optional(0x20000000U)); + EXPECT_EQ(meta.sources()[0].symbolTypeError, std::optional("type warning")); + EXPECT_EQ(meta.sources()[0].symbolSizeError, std::optional("size warning")); + EXPECT_EQ(meta.sources()[1].valueType, std::string(TraceRunSchema::kDefaultDwtDataType)); + EXPECT_EQ(meta.sources()[2].valueSize, TraceRunSchema::kDefaultDwtDataSize); +} + +TEST(CtraceUnitTests, testCtraceRunMetaIgnoresInactiveSetups) +{ + TraceRunConfig config; + TraceRunSetup inactive; + inactive.processorName = "unused"; + inactive.data.resize(1U); + config.setups.push_back(inactive); + config.references.push_back(makeReference("dwt", "other", 1U, {0U}, "other/data#4")); + config.references.front().dataSetupIndex = 4U; + + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(meta.processorCount(), 1U); + EXPECT_EQ(meta.sources().front().valueType, std::string(TraceRunSchema::kDefaultDwtDataType)); +} diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp new file mode 100644 index 000000000..5a86a9084 --- /dev/null +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunConfigReaderTests.cpp @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "CtraceRunMeta.h" +#include "TestPath.h" +#include "TestSupport.h" +#include "YmlTraceRunConfigReader.h" + +#include + +#include +#include +#include +#include +#include +#include + +/** @brief Owns one temporary trace-run file used by YAML reader tests. */ +class TraceRunFixture { +public: + /** @brief Creates a fixture with a process-specific file path. */ + explicit TraceRunFixture(const std::string& name) + : m_root(name), + m_path(m_root.path() / "Board.ctrace-run.yml") + { + } + + /** @brief Reads the current file content. */ + TraceRunConfig read() const + { + return YmlTraceRunConfigReader().read(m_path.string()); + } + + /** @brief Writes and reads the supplied YAML document. */ + TraceRunConfig read(std::string_view yaml) + { + write(yaml); + return read(); + } + + /** @brief Returns the error produced by reading the current file. */ + std::string error() const + { + return captureExceptionMessage([this] { (void)read(); }).value_or(""); + } + + /** @brief Writes YAML and returns its reader error. */ + std::string error(std::string_view yaml) + { + write(yaml); + return error(); + } + +private: + /** @brief Writes YAML content to the fixture file. */ + void write(std::string_view yaml) + { + writeTestFile(m_path, std::string(yaml)); + } + + TemporaryTestPath m_root; + std::filesystem::path m_path; +}; + +/** @brief Requires YAML input to produce expected reader error text. */ +static void expectReadError(TraceRunFixture& file, std::string_view yaml, std::string_view expected) +{ + const auto error = file.error(yaml); + EXPECT_NE(error.find(expected), std::string::npos) << "unexpected result for:\n" << yaml << "\nerror: " << error; +} + +TEST(CtraceUnitTests, TraceRunReaderParsesConsumedFields) +{ + TraceRunFixture file("ctrace-run-reader-consumed-fields-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-setup: + - pname: core0 + timestamps: + clock: 400000000 + itm-prescaler: 4 + itm: + enable: 0x00000006 + data: + - symbol-type: signed int + symbol-size: 1 + ctrace-refs: + - ctrace-ref: core0/itm + pname: core0 + type: itm + stream: 2 + source: 1 + label: Console + - ctrace-ref: core0/data#0 + pname: core0 + type: dwt + stream: 2 + source: 0 + symbol-address: 0x20000100 + label: Current +)yml"); + ASSERT_EQ(config.references.size(), 2U); + ASSERT_EQ(config.setups.size(), 1U); + + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(meta.processorCount(), 1U); + EXPECT_EQ(meta.timestampClockHz(), std::optional(400000000U)); + EXPECT_EQ(meta.timestampPrescaler(), std::optional(4U)); + ASSERT_EQ(meta.itmEnableMasksByTraceBusId().at(2U), 0x00000006U); + + ASSERT_EQ(meta.sources().size(), 2U); + const auto& itm = meta.sources()[0]; + EXPECT_EQ(itm.type, "itm"); + EXPECT_EQ(itm.traceBusId, 2U); + EXPECT_EQ(itm.source, 1U); + EXPECT_EQ(itm.label, std::optional("Console")); + + const auto& dwt = meta.sources()[1]; + EXPECT_EQ(dwt.type, "dwt"); + EXPECT_EQ(dwt.traceBusId, 2U); + EXPECT_EQ(dwt.source, 0U); + EXPECT_EQ(dwt.valueType, "signed int"); + EXPECT_EQ(dwt.valueSize, 1U); + EXPECT_EQ(dwt.symbolAddress, std::optional(0x20000100U)); + EXPECT_EQ(dwt.label, std::optional("Current")); +} + +TEST(CtraceUnitTests, TraceRunReaderAcceptsScalarAndArraySourceNotation) +{ + TraceRunFixture file("ctrace-run-reader-consumed-fields-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-refs: + - ctrace-ref: relevant/itm + type: itm + source: [1, 2] + - ctrace-ref: relevant/dwt-scalar + type: dwt + source: 3 + - ctrace-ref: relevant/dwt-array + type: dwt + source: [4, 5] + - ctrace-ref: ignored/unsupported + type: unsupported +)yml"); + ASSERT_EQ(config.references.size(), 3U); + EXPECT_TRUE(config.references[0].sources == std::vector({1U, 2U})); + EXPECT_TRUE(config.references[1].sources == std::vector({3U})); + EXPECT_TRUE(config.references[2].sources == std::vector({4U, 5U})); +} + +TEST(CtraceUnitTests, TraceRunReaderAcceptsProcessorItmReferenceWithoutEnabledChannels) +{ + TraceRunFixture file("ctrace-run-reader-empty-itm-reference-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-setup: + - pname: core0 + itm: + enable: 0 + ctrace-refs: + - ctrace-ref: core0/itm + pname: core0 + type: itm + stream: 2 +)yml"); + + ASSERT_EQ(config.references.size(), 1U); + EXPECT_TRUE(config.references.front().sources.empty()); + + const auto meta = CtraceRunMeta::fromConfig(config); + EXPECT_EQ(meta.processorCount(), 1U); + EXPECT_TRUE(meta.sources().empty()); + EXPECT_EQ(meta.itmEnableMask(), std::optional(0U)); + ASSERT_EQ(meta.itmEnableMasksByTraceBusId().size(), 1U); + EXPECT_EQ(meta.itmEnableMasksByTraceBusId().at(2U), 0U); +} + +TEST(CtraceUnitTests, TraceRunReaderReportsDocumentErrors) +{ + TraceRunFixture file("ctrace-run-reader-document-errors-test"); + EXPECT_EQ(captureExceptionMessage([] { (void)YmlTraceRunConfigReader().read(""); }), + std::optional("trace-run configuration path is empty")); + EXPECT_NE(file.error().find("failed to parse trace-run configuration"), std::string::npos); + + expectReadError(file, "ctrace-run: [\n", "failed to parse trace-run configuration"); + expectReadError(file, "ctrace-run: {}\n---\nctrace-run: {}\n", "expected exactly one YAML document"); + expectReadError(file, "[]\n", "expected a YAML map containing 'ctrace-run'"); + expectReadError(file, "{}\n", "missing top-level 'ctrace-run' node"); + expectReadError(file, "ctrace-run: {}\nctrace-run: {}\n", "map keys must be unique"); + expectReadError(file, "ctrace-run: []\n", "top-level 'ctrace-run' node must be a map"); +} + +TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedReferenceContainers) +{ + TraceRunFixture file("ctrace-run-reader-reference-container-errors-test"); + expectReadError(file, "ctrace-run: {}\n", "missing required 'ctrace-refs' array"); + expectReadError(file, "ctrace-run:\n ctrace-refs: {}\n", "'ctrace-refs' must be an array"); + expectReadError(file, "ctrace-run:\n ctrace-refs: []\n ctrace-refs: []\n", "map keys must be unique"); + expectReadError(file, "ctrace-run:\n ctrace-refs: [invalid]\n", "each 'ctrace-refs' entry must be a map"); + expectReadError(file, "ctrace-run:\n ctrace-refs:\n - { ctrace-ref: core/itm, source: 1 }\n", + "missing required 'type' scalar"); + expectReadError(file, "ctrace-run:\n ctrace-refs:\n - { type: [], ctrace-ref: core/itm, source: 1 }\n", + "missing required 'type' scalar"); + expectReadError(file, "ctrace-run:\n ctrace-refs:\n - { type: '', ctrace-ref: core/itm, source: 1 }\n", + "missing required 'type' scalar"); + expectReadError(file, "ctrace-run:\n ctrace-refs:\n - type: itm\n source: 1\n", + "missing required 'ctrace-ref' scalar"); + expectReadError(file, "ctrace-run:\n ctrace-refs:\n - { type: itm, ctrace-ref: [], source: 1 }\n", + "missing required 'ctrace-ref' scalar"); + expectReadError(file, "ctrace-run:\n ctrace-refs:\n - { type: itm, ctrace-ref: '', source: 1 }\n", + "missing required 'ctrace-ref' scalar"); +} + +TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedReferenceRoutes) +{ + TraceRunFixture file("ctrace-run-reader-reference-route-errors-test"); + /** @brief Describes malformed route fields and their expected errors. */ + struct Case { + const char* fields; + const char* error; + }; + constexpr Case cases[] = { + {"pname: []\n source: 1", "'pname' must be a scalar string"}, + {"stream: []\n source: 1", "'stream' must be a scalar unsigned integer"}, + {"source: {}", "'source' must be an array"}, + {"source: null", "'source' must be an unsigned integer"}, + {"source: 0x", "'source' must be an unsigned integer"}, + {"source: -1", "'source' must be an unsigned integer in range"}, + {"source: 4294967296", "'source' must be an unsigned integer in range"}, + {"source: [1, {}]", "each 'source' entry must be an unsigned integer"}, + {"source: [1, '']", "each 'source' entry must be an unsigned integer"}, + {"source: 1\n source: 2", "map keys must be unique"}, + {"source: 1\n info: []", "'info' must be a scalar message"}, + {"source: 1\n warning: {}", "'warning' must be a scalar message"}, + {"source: 1\n error: []", "'error' must be a scalar message"}, + }; + for (const auto& testCase : cases) { + const auto yaml = std::string("ctrace-run:\n ctrace-refs:\n - type: itm\n ctrace-ref: core/itm\n ") + + testCase.fields + "\n"; + expectReadError(file, yaml, testCase.error); + } +} + +TEST(CtraceUnitTests, TraceRunReaderPreservesDiagnosticReferences) +{ + TraceRunFixture file("ctrace-run-reader-diagnostic-references-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-refs: + - { type: event, ctrace-ref: core/event, pname: core0, info: note } + - { type: pmu, ctrace-ref: core/pmu, pname: core1, warning: warning } + - { type: pcsample, ctrace-ref: core/pc, pname: null, error: unavailable } + - { type: dwt, ctrace-ref: core/data#, source: 0, symbol-address: invalid, error: diagnostic } + - { type: dwt, ctrace-ref: core/data#x, stream: [], source: 0, error: malformed } + - { type: dwt, ctrace-ref: core/notdata#2, error: unrouted } + - { type: itm, ctrace-ref: core/itm0, source: 0, error: disabled } + - { type: itm, ctrace-ref: core/itm1, source: 1, error: usable, label: null } +)yml"); + ASSERT_EQ(config.references.size(), 8U); + EXPECT_EQ(config.references[0].processorName, std::optional("core0")); + EXPECT_EQ(config.references[1].processorName, std::optional("core1")); + EXPECT_FALSE(config.references[2].processorName.has_value()); + EXPECT_FALSE(config.references[3].symbolAddress.has_value()); + EXPECT_FALSE(config.references[3].dataSetupIndex.has_value()); + EXPECT_FALSE(config.references[4].stream.has_value()); + EXPECT_FALSE(config.references[4].dataSetupIndex.has_value()); + EXPECT_TRUE(config.references[6].sources == std::vector{0U}); + EXPECT_EQ(config.references[7].label, std::optional("")); +} + +TEST(CtraceUnitTests, TraceRunReaderParsesTimestampSetupVariants) +{ + TraceRunFixture file("ctrace-run-reader-timestamp-variants-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-setup: + - { timestamps: null } + - { timestamps: invalid } + - { timestamps: [] } + - timestamps: { clock: [] } + - timestamps: { clock: invalid } + - timestamps: { clock: null } + - timestamps: { clock: 0X10, itm-prescaler: 0x4 } + ctrace-refs: [] +)yml"); + ASSERT_EQ(config.setups.size(), 7U); + EXPECT_FALSE(config.setups[0].timestamps->clockError.has_value()); + EXPECT_EQ(config.setups[1].timestamps->clockError, std::optional("'timestamps' must be empty or a map")); + EXPECT_EQ(config.setups[2].timestamps->clockError, std::optional("'timestamps' must be empty or a map")); + EXPECT_EQ(config.setups[3].timestamps->clockError, + std::optional("'timestamps.clock' must be a scalar unsigned integer")); + EXPECT_TRUE(config.setups[4].timestamps->clockError.has_value()); + EXPECT_TRUE(config.setups[5].timestamps->clockError.has_value()); + EXPECT_EQ(config.setups[6].timestamps->clockHz, std::optional(16U)); + EXPECT_EQ(config.setups[6].timestamps->timestampPrescaler, std::optional(4U)); + + expectReadError(file, "ctrace-run:\n ctrace-setup:\n - timestamps: { clock: 1, clock: 2 }\n", + "map keys must be unique"); + expectReadError(file, "ctrace-run:\n ctrace-setup:\n - timestamps: {}\n timestamps: {}\n", + "map keys must be unique"); +} + +TEST(CtraceUnitTests, TraceRunReaderRejectsMalformedConsumedSetups) +{ + TraceRunFixture file("ctrace-run-reader-setup-errors-test"); + constexpr std::string_view prefix = "ctrace-run:\n ctrace-refs: []\n ctrace-setup:\n - "; + /** @brief Describes malformed setup fields and their expected errors. */ + struct Case { + const char* setup; + const char* error; + }; + constexpr Case cases[] = { + {"timestamps: { itm-prescaler: [] }", "'timestamps.itm-prescaler' must be a scalar unsigned integer"}, + {"timestamps: { itm-prescaler: invalid }", "'itm-prescaler' must be an unsigned integer in range"}, + {"itm: []", "'itm' must be a map containing 'enable'"}, + {"itm: {}", "'itm.enable' is required"}, + {"itm: { enable: [] }", "'itm.enable' is required"}, + {"itm: { enable: '' }", "'itm.enable' is required"}, + {"itm: { enable: invalid }", "'itm.enable' must be an unsigned integer in range"}, + {"itm: { enable: 1, enable: 2 }", "map keys must be unique"}, + }; + for (const auto& testCase : cases) { + expectReadError(file, std::string(prefix) + testCase.setup + "\n", testCase.error); + } + + expectReadError(file, "ctrace-run:\n ctrace-refs: []\n ctrace-setup: {}\n", + "'ctrace-setup' must be an array"); + expectReadError(file, "ctrace-run:\n ctrace-refs: []\n ctrace-setup: [invalid]\n", + "each 'ctrace-setup' entry must be a map"); + + expectReadError(file, R"yml(ctrace-run: + ctrace-refs: + - { type: dwt, ctrace-ref: core/data#0, source: 0 } + ctrace-setup: + - pname: [] + data: [{}] +)yml", + "'pname' must be a scalar string"); +} + +TEST(CtraceUnitTests, TraceRunReaderParsesReferencedDataVariants) +{ + TraceRunFixture file("ctrace-run-reader-data-variants-test"); + const auto config = file.read(R"yml(ctrace-run: + ctrace-refs: + - { type: dwt, ctrace-ref: core0/data#1, pname: core0, source: 0 } + - { type: dwt, ctrace-ref: core0/data#2, pname: core0, source: 1 } + - { type: dwt, ctrace-ref: core0/data#3, pname: core0, source: 2 } + - { type: dwt, ctrace-ref: core0/data#4, pname: core0, source: 3 } + - { type: dwt, ctrace-ref: core0/data#5, pname: core0, source: 4 } + - { type: dwt, ctrace-ref: core1/data#0, pname: core1, source: 0 } + - { type: dwt, ctrace-ref: core2/data#9, pname: core2, source: 0 } + - { type: dwt, ctrace-ref: data#0, source: 6 } + - { type: dwt, ctrace-ref: data#9, source: 7 } + ctrace-setup: + - pname: core0 + data: + - {} + - not-a-map + - { symbol-type: [] } + - { symbol-size: [] } + - { symbol-type: null, symbol-size: null } + - { symbol-size: invalid } + - {} + - pname: core1 + data: not-an-array + - pname: core2 + data: [{}] + - data: + - { symbol-type: null, symbol-size: null } + - data: [{}] +)yml"); + ASSERT_EQ(config.setups.size(), 4U); + ASSERT_EQ(config.setups[0].data.size(), 7U); + EXPECT_FALSE(config.setups[0].data[1].symbolType.has_value()); + EXPECT_EQ(config.setups[0].data[2].symbolTypeError, + std::optional("'data.symbol-type' must be a scalar string")); + EXPECT_EQ(config.setups[0].data[3].symbolSizeError, + std::optional("'data.symbol-size' must be a scalar unsigned integer")); + EXPECT_FALSE(config.setups[0].data[4].symbolType.has_value()); + EXPECT_FALSE(config.setups[0].data[4].symbolSize.has_value()); + EXPECT_TRUE(config.setups[0].data[5].symbolSizeError.has_value()); + EXPECT_TRUE(config.setups[2].data[0].symbolType == std::nullopt); + EXPECT_TRUE(config.setups[2].data[0].symbolSize == std::nullopt); + + expectReadError(file, R"yml(ctrace-run: + ctrace-refs: [{ type: dwt, ctrace-ref: data#0, source: 0 }] + ctrace-setup: + - data: [{ symbol-type: one, symbol-type: two }] +)yml", + "map keys must be unique"); +} + +TEST(CtraceUnitTests, TraceRunReaderSkipsDisabledSetup) +{ + TraceRunFixture file("ctrace-run-reader-disabled-setup-test"); + EXPECT_TRUE(file.read(R"yml(ctrace-run: + ctrace-setup: + - disable: + timestamps: + clock: 400000000 + ctrace-refs: [] +)yml") + .setups.empty()); +} diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp new file mode 100644 index 000000000..c32329d25 --- /dev/null +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPath.h" +#include "TestSupport.h" +#include +#include "TraceRunDiscovery.h" +#include +#include +#include +#include +#include + +TEST(CtraceUnitTests, testTraceRunDiscovery) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-run-discovery-test"); + const auto& traceDir = temporaryPath.path(); + writeTestFile(traceDir / "Beta.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Alpha.SWO.raw"); + writeTestFile(traceDir / "Alpha.TB.raw"); + writeTestFile(traceDir / "Alpha.ER.raw"); + writeTestFile(traceDir / "Alpha.swo.raw"); + writeTestFile(traceDir / "Alpha.custom.raw"); + writeTestFile(traceDir / "unrelated.raw"); + writeTestFile(traceDir / "Alpha..raw"); + std::filesystem::create_directories(traceDir / "ignored.ctrace-run.yml"); + std::filesystem::create_directories(traceDir / "Alpha.SWO.raw.dir"); + + const auto batch = TraceRunDiscovery::selectConfigFiles(traceDir, std::nullopt); + ASSERT_TRUE(batch.size() == 2U) << "TraceRunDiscovery batch configuration count mismatch"; + ASSERT_TRUE(batch[0].filename() == "Alpha.ctrace-run.yml") << "TraceRunDiscovery batch sort mismatch"; + ASSERT_TRUE(batch[1].filename() == "Beta.ctrace-run.yml") << "TraceRunDiscovery second batch item mismatch"; + + const auto selected = TraceRunDiscovery::selectConfigFiles(traceDir, std::string("Alpha")); + ASSERT_TRUE(selected.size() == 1U) << "TraceRunDiscovery target selection count mismatch"; + ASSERT_TRUE(selected[0].filename() == "Alpha.ctrace-run.yml") << "TraceRunDiscovery target path mismatch"; + ASSERT_TRUE(TraceRunDiscovery::solutionSetName(selected[0]) == "Alpha") << "TraceRunDiscovery solution-set mismatch"; + + const auto rawInputs = TraceRunDiscovery::rawInputs(selected[0]); + ASSERT_TRUE(rawInputs.size() == 3U) << "TraceRunDiscovery must accept only the specified trace channels"; + ASSERT_TRUE(rawInputs[0].channel == "ER") << "TraceRunDiscovery ER channel mismatch"; + ASSERT_TRUE(rawInputs[1].channel == "SWO") << "TraceRunDiscovery SWO channel mismatch"; + ASSERT_TRUE(rawInputs[2].channel == "TB") << "TraceRunDiscovery TB channel mismatch"; + + const std::vector unsafeTargets{ + "", + ".", + "..", + "../Alpha", + "..\\Alpha", + "/Alpha", + "C:Alpha", + "C:/Alpha", + "C:\\Alpha", + "\\\\server\\share", + "Alpha:stream", + "Alpha?Beta", + "Alpha.", + "Alpha ", + "NUL", + "con.txt", + "PRN", + "AUX", + "CONIN$", + "CONOUT$", + "COM1.log", + "LPT9", + "bad\x01name", + }; + for (const auto& unsafeTarget : unsafeTargets) { + ASSERT_TRUE(throwsWithMessage([&] { (void)TraceRunDiscovery::selectConfigFiles(traceDir, unsafeTarget); }, + "solution-set name")) + << "TraceRunDiscovery should reject unsafe target name: " + unsafeTarget; + } + + EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(traceDir, std::string("Missing")), std::runtime_error); + EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(traceDir, std::string("ABCD")), std::runtime_error); + EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(traceDir, std::string("COM0")), std::runtime_error); + EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(traceDir / "missing", std::nullopt), std::runtime_error); + EXPECT_THROW((void)TraceRunDiscovery::solutionSetName("wrong.yml"), std::runtime_error); + EXPECT_THROW((void)TraceRunDiscovery::solutionSetName(".ctrace-run.yml"), std::runtime_error); + + const TemporaryTestPath emptyPath("ctrace-trace-run-discovery-empty-test"); + emptyPath.createDirectory(); + writeTestFile(emptyPath.path() / ".ctrace-run.yml"); + EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(emptyPath.path(), std::nullopt), std::runtime_error); + EXPECT_TRUE(TraceRunDiscovery::rawInputs("parentless.ctrace-run.yml").empty()); +} diff --git a/tools/ctrace/test/unit/support/CtfTestSupport.h b/tools/ctrace/test/unit/support/CtfTestSupport.h new file mode 100644 index 000000000..2598d531a --- /dev/null +++ b/tools/ctrace/test/unit/support/CtfTestSupport.h @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_CTFTESTSUPPORT_H +#define CTRACE_TEST_UNIT_SUPPORT_CTFTESTSUPPORT_H + +#include "TestSupport.h" +#include "ctf/CtfSchema.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CtfTestSupport { + +inline constexpr std::size_t kCtfPacketHeaderSize = 24U; +inline constexpr std::size_t kCtfPacketContextSize = 32U; +inline constexpr std::size_t kCtfEventHeaderSize = 13U; +inline constexpr std::size_t kCtfEventOffset = kCtfPacketHeaderSize + kCtfPacketContextSize; +inline constexpr std::size_t kCtfPacketSize = 65536U; + +/** @brief Stores one CTF event parsed by a unit test. */ +struct CtfRecord { + std::uint32_t id; + std::uint64_t timestamp; + std::uint8_t traceBusId; + std::vector payload; +}; + +/** @brief Reads an unsigned little-endian integer from test stream bytes. */ +template inline Integer readLittleEndian(const std::vector& bytes, std::size_t offset) +{ + static_assert(std::is_integral_v && std::is_unsigned_v); + require(offset <= bytes.size() && sizeof(Integer) <= bytes.size() - offset, "CTF test read exceeds stream data"); + Integer value = 0U; + for (std::size_t byte = 0U; byte < sizeof(Integer); ++byte) { + value |= static_cast(bytes[offset + byte]) << (byte * 8U); + } + return value; +} + +/** @brief Reads a 16-bit little-endian test value. */ +inline std::uint16_t readLe16(const std::vector& bytes, std::size_t offset) +{ + return readLittleEndian(bytes, offset); +} + +/** @brief Reads a 32-bit little-endian test value. */ +inline std::uint32_t readLe32(const std::vector& bytes, std::size_t offset) +{ + return readLittleEndian(bytes, offset); +} + +/** @brief Reads a 64-bit little-endian test value. */ +inline std::uint64_t readLe64(const std::vector& bytes, std::size_t offset) +{ + return readLittleEndian(bytes, offset); +} + +/** @brief Returns the encoded size of a test CTF value variant. */ +inline std::size_t ctfValueSize(std::uint8_t tag) +{ + static constexpr std::array sizes{{1U, 1U, 2U, 2U, 4U, 4U, 4U}}; + require(tag < sizes.size(), "CTF test parser encountered an invalid value tag"); + return sizes[tag]; +} + +/** @brief Determines one encoded CTF event payload size. */ +inline std::size_t ctfPayloadSize(const std::vector& bytes, std::size_t payloadOffset, + std::size_t contentEnd, std::uint32_t eventId) +{ + const auto requirePayload = [&](std::size_t size) { + require(payloadOffset <= contentEnd && size <= contentEnd - payloadOffset, + "CTF test parser encountered a truncated event payload"); + }; + + if (eventId == CtfSchema::value(CtfSchema::EventId::Itm)) { + requirePayload(2U); + return 7U + ctfValueSize(bytes[payloadOffset + 1U]); + } + if (eventId == CtfSchema::value(CtfSchema::EventId::DwtValue)) { + requirePayload(3U); + auto size = 3U + ctfValueSize(bytes[payloadOffset + 2U]); + requirePayload(size + 1U); + const auto hasPc = bytes[payloadOffset + size]; + require(hasPc <= 1U, "CTF test parser encountered an invalid DWT PC presence flag"); + size += 1U + (hasPc != 0U ? 4U : 0U); + requirePayload(size + 1U); + const auto hasAddress = bytes[payloadOffset + size]; + require(hasAddress <= 1U, "CTF test parser encountered an invalid DWT address presence flag"); + size += 1U + (hasAddress != 0U ? 2U : 0U); + return size + 5U; + } + if (eventId == CtfSchema::value(CtfSchema::EventId::TraceStatus) || + eventId == CtfSchema::value(CtfSchema::EventId::Exception)) { + return 5U; + } + if (eventId == CtfSchema::value(CtfSchema::EventId::DwtAddress)) { + return 14U; + } + if (eventId == CtfSchema::value(CtfSchema::EventId::GlobalTimestamp)) { + return 9U; + } + require(false, "CTF test parser encountered an unknown event ID"); + return 0U; +} + +/** @brief Parses all records from an encoded test CTF stream. */ +inline std::vector parseCtfRecords(const std::vector& bytes) +{ + std::vector records; + for (std::size_t packetStart = 0U; packetStart + kCtfEventOffset <= bytes.size(); packetStart += kCtfPacketSize) { + const auto contentBits = readLe32(bytes, packetStart + kCtfPacketHeaderSize + 4U); + require(contentBits % 8U == 0U, "CTF packet content size must be byte-aligned"); + const auto contentEnd = packetStart + static_cast(contentBits / 8U); + require(contentEnd >= packetStart + kCtfEventOffset && contentEnd <= bytes.size() && + contentEnd <= packetStart + kCtfPacketSize, + "CTF packet content size exceeds the packet"); + + auto offset = packetStart + kCtfEventOffset; + while (offset < contentEnd) { + require(kCtfEventHeaderSize <= contentEnd - offset, "CTF test parser encountered a truncated event header"); + const auto id = readLe32(bytes, offset); + const auto timestamp = readLe64(bytes, offset + 4U); + const auto traceBusId = bytes[offset + 12U]; + const auto payloadOffset = offset + kCtfEventHeaderSize; + const auto payloadSize = ctfPayloadSize(bytes, payloadOffset, contentEnd, id); + require(payloadSize <= contentEnd - payloadOffset, "CTF event payload exceeds packet content"); + records.push_back({ + id, + timestamp, + traceBusId, + {bytes.begin() + static_cast(payloadOffset), + bytes.begin() + static_cast(payloadOffset + payloadSize)}, + }); + offset = payloadOffset + payloadSize; + } + } + return records; +} + +/** @brief Reads and parses records from a test CTF stream file. */ +inline std::vector readCtfRecords(const std::filesystem::path& streamPath) +{ + return parseCtfRecords(readTestBinaryFile(streamPath)); +} + +/** @brief Returns the first record with a required CTF event ID. */ +inline const CtfRecord& requireFirstCtfRecord(const std::vector& records, CtfSchema::EventId id, + const std::string& message) +{ + const auto expected = CtfSchema::value(id); + const auto found = std::find_if(records.begin(), records.end(), + [expected](const CtfRecord& record) { return record.id == expected; }); + require(found != records.end(), message); + return *found; +} + +/** @brief Requires a stream containing one ITM event on the expected channel. */ +inline void requireSingleItmEvent(const std::filesystem::path& streamPath, std::uint8_t expectedChannel, + const std::string& message) +{ + const auto records = readCtfRecords(streamPath); + require(records.size() == 1U && records.front().id == CtfSchema::value(CtfSchema::EventId::Itm) && + !records.front().payload.empty() && records.front().payload.front() == expectedChannel, + message); +} + +} // namespace CtfTestSupport + +#endif // CTRACE_TEST_UNIT_SUPPORT_CTFTESTSUPPORT_H diff --git a/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h b/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h new file mode 100644 index 000000000..0114aeed5 --- /dev/null +++ b/tools/ctrace/test/unit/support/OpenCsdSessionTestSupport.h @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_OPENCSDSESSIONTESTSUPPORT_H +#define CTRACE_TEST_UNIT_SUPPORT_OPENCSDSESSIONTESTSUPPORT_H + +#include "OpenCsdTestSupport.h" + +#include "OpenCsdErrorController.h" +#include "OpenCsdItmDecoder.h" +#include "OpenCsdItmSession.h" +#include "OpenCsdPacketCollector.h" +#include "common/ocsd_error.h" +#include "opencsd/ocsd_if_types.h" +#include "opencsd/itm/trc_pkt_elem_itm.h" +#include "opencsd/itm/trc_pkt_types_itm.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace OpenCsdSessionTestSupport { + +/** @brief Describes one OpenCSD error emitted by a scripted session step. */ +struct ScriptedError { + ocsd_err_severity_t severity = OCSD_ERR_SEV_ERROR; + ocsd_err_t code = OCSD_ERR_FAIL; + std::uint64_t index = 0U; + std::string message; +}; + +/** @brief Describes one response returned by a scripted OpenCSD session. */ +struct SessionStep { + /** @brief Creates a scripted response and its optional side effects. */ + SessionStep(ocsd_datapath_resp_t stepResponse = OCSD_RESP_CONT, + std::optional stepProcessed = std::nullopt, bool stepEmitSync = false, + std::vector stepErrors = {}) + : response(stepResponse), + processed(stepProcessed), + emitSync(stepEmitSync), + errors(std::move(stepErrors)) + { + } + + ocsd_datapath_resp_t response = OCSD_RESP_CONT; + std::optional processed; + bool emitSync = false; + std::vector errors; +}; + +/** @brief Stores the queued operations and call counters of a scripted session. */ +struct SessionScript { + std::deque pushes; + std::deque flushes; + std::deque resets; + std::deque ends; + ocsd_datapath_resp_t defaultFlushResponse = OCSD_RESP_CONT; + std::uint32_t pushCalls = 0U; + std::uint32_t flushCalls = 0U; + std::uint32_t resetCalls = 0U; + std::uint32_t endCalls = 0U; +}; + +/** @brief Implements a deterministic OpenCSD session for decoder unit tests. */ +class ScriptedOpenCsdSession final : public OpenCsdItmSessionInterface { +public: + /** @brief Creates a session backed by shared scripted state. */ + ScriptedOpenCsdSession(std::shared_ptr script, OpenCsdPacketCollector& collector, + OpenCsdErrorController& errors) + : m_script(std::move(script)), + m_collector(collector), + m_errors(errors) + { + } + + /** @brief Applies the next scripted data response. */ + ocsd_datapath_resp_t pushData(ocsd_trc_index_t index, std::uint32_t size, const std::uint8_t*, + std::uint32_t& processed) override + { + ++m_script->pushCalls; + auto step = take(m_script->pushes, SessionStep{}); + processed = step.processed.value_or(size); + apply(step, index); + return step.response; + } + + /** @brief Applies the next scripted flush response. */ + ocsd_datapath_resp_t flush() override + { + ++m_script->flushCalls; + auto step = take(m_script->flushes, SessionStep{m_script->defaultFlushResponse}); + apply(step, 0U); + return step.response; + } + + /** @brief Applies the next scripted reset response. */ + ocsd_datapath_resp_t reset() override + { + ++m_script->resetCalls; + auto step = take(m_script->resets, SessionStep{}); + apply(step, 0U); + return step.response; + } + + /** @brief Applies the next scripted end-of-trace response. */ + ocsd_datapath_resp_t endOfTrace() override + { + ++m_script->endCalls; + auto step = take(m_script->ends, SessionStep{}); + apply(step, 0U); + return step.response; + } + +private: + /** @brief Removes and returns the next scripted step or a fallback. */ + static SessionStep take(std::deque& steps, SessionStep fallback) + { + if (steps.empty()) { + return fallback; + } + auto step = std::move(steps.front()); + steps.pop_front(); + return step; + } + + /** @brief Applies scripted errors and synchronization side effects. */ + void apply(const SessionStep& step, ocsd_trc_index_t index) + { + for (const auto& error : step.errors) { + const ocsdError reported(error.severity, error.code, error.index, 1U, error.message); + m_errors.LogError(0U, &reported); + } + if (step.emitSync) { + ItmTrcPacket packet; + packet.setPktType(ITM_PKT_ASYNC); + m_collector.RawPacketDataMon(OCSD_OP_DATA, index, &packet, 0U, nullptr); + } + } + + std::shared_ptr m_script; + OpenCsdPacketCollector& m_collector; + OpenCsdErrorController& m_errors; +}; + +/** @brief Creates a session factory sharing the supplied script. */ +inline OpenCsdItmSessionFactory scriptedFactory(const std::shared_ptr& script) +{ + return [script](OpenCsdPacketCollector& collector, + OpenCsdErrorController& errors) -> std::unique_ptr { + return std::make_unique(script, collector, errors); + }; +} + +/** @brief Bundles a scripted session, decoder, and collecting sink for tests. */ +class ScriptedDecoderHarness { +public: + /** @brief Creates a decoder connected to a new empty session script. */ + ScriptedDecoderHarness() + : m_script(std::make_shared()), + m_decoder(m_sink, scriptedFactory(m_script)) + { + } + + /** @brief Pushes a zero-filled raw byte block through the decoder. */ + void push(std::uint32_t size) + { + m_input.assign(size, 0U); + m_decoder.push(m_input.data(), size); + } + + /** @brief Returns the collecting element sink. */ + OpenCsdTestSupport::CollectingOpenCsdElementSink& sink() + { + return m_sink; + } + + /** @brief Returns the mutable session script. */ + SessionScript& script() + { + return *m_script; + } + + /** @brief Returns the decoder under test. */ + OpenCsdItmDecoder& decoder() + { + return m_decoder; + } + +private: + OpenCsdTestSupport::CollectingOpenCsdElementSink m_sink; + std::shared_ptr m_script; + OpenCsdItmDecoder m_decoder; + std::vector m_input; +}; + +} // namespace OpenCsdSessionTestSupport + +#endif // CTRACE_TEST_UNIT_SUPPORT_OPENCSDSESSIONTESTSUPPORT_H diff --git a/tools/ctrace/test/unit/support/OpenCsdTestSupport.h b/tools/ctrace/test/unit/support/OpenCsdTestSupport.h new file mode 100644 index 000000000..890552645 --- /dev/null +++ b/tools/ctrace/test/unit/support/OpenCsdTestSupport.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_OPENCSDTESTSUPPORT_H +#define CTRACE_TEST_UNIT_SUPPORT_OPENCSDTESTSUPPORT_H + +#include "OpenCsdTraceElement.h" +#include "TraceEvent.h" + +#include +#include +#include + +namespace OpenCsdTestSupport { + +/** @brief Collects OpenCSD elements emitted during a unit test. */ +class CollectingOpenCsdElementSink : public OpenCsdTraceElementSink { +public: + /** @brief Stores one emitted OpenCSD element. */ + void append(OpenCsdTraceElement element) override + { + m_elements.push_back(std::move(element)); + } + + /** @brief Tests whether the collected elements include an issue code. */ + bool hasIssue(TraceIssueCode code) const + { + for (const auto& element : m_elements) { + if (element.issueCode.has_value() && *element.issueCode == code) { + return true; + } + } + return false; + } + + /** @brief Returns all collected OpenCSD elements. */ + const std::vector& elements() const + { + return m_elements; + } + +private: + std::vector m_elements; +}; + +/** @brief Creates a basic OpenCSD element for a test. */ +inline OpenCsdTraceElement openCsdElement(OpenCsdTraceElement::Kind kind, std::uint64_t index = 0U, + std::uint8_t stream = 0U) +{ + OpenCsdTraceElement element; + element.kind = kind; + element.sourceIndex = index; + element.traceBusId = stream; + return element; +} + +/** @brief Creates an OpenCSD software element for a test. */ +inline OpenCsdTraceElement openCsdSoftwareElement(std::uint32_t channel, std::uint32_t value = 0U, + std::uint64_t index = 0U, std::uint8_t stream = 0U, + std::uint8_t size = 1U) +{ + auto element = openCsdElement(OpenCsdTraceElement::Kind::Software, index, stream); + element.channel = channel; + element.size = size; + element.value = value; + return element; +} + +/** @brief Creates an OpenCSD local timestamp element for a test. */ +inline OpenCsdTraceElement +openCsdTimestampElement(std::uint64_t tcyc, std::uint64_t index = 0U, std::uint8_t stream = 0U, + LocalTimestampRelation relation = LocalTimestampRelation::Synchronous) +{ + auto element = openCsdElement(OpenCsdTraceElement::Kind::LocalTimestamp, index, stream); + element.timestampRelation = relation; + element.tcyc = tcyc; + return element; +} + +} // namespace OpenCsdTestSupport + +#endif // CTRACE_TEST_UNIT_SUPPORT_OPENCSDTESTSUPPORT_H diff --git a/tools/ctrace/test/unit/support/TestPath.h b/tools/ctrace/test/unit/support/TestPath.h new file mode 100644 index 000000000..1778d8206 --- /dev/null +++ b/tools/ctrace/test/unit/support/TestPath.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_TESTPATH_H +#define CTRACE_TEST_UNIT_SUPPORT_TESTPATH_H + +#include +#include +#include +#include +#include +#include +#include + +/** @brief Creates a process-specific path in the system temporary directory. */ +inline std::filesystem::path testTemporaryPath(const std::string& name) +{ + static std::atomic sequence{0U}; + const auto timestamp = std::chrono::steady_clock::now().time_since_epoch().count(); + const auto uniqueId = sequence.fetch_add(1U, std::memory_order_relaxed); + return std::filesystem::temp_directory_path() / + (name + '-' + std::to_string(timestamp) + '-' + std::to_string(uniqueId)); +} + +/** @brief Owns and removes one process-specific temporary test path. */ +class TemporaryTestPath { +public: + /** @brief Prepares an empty path with the supplied test name. */ + explicit TemporaryTestPath(const std::string& name) + : m_path(testTemporaryPath(name)) + { + std::error_code error; + std::filesystem::remove_all(m_path, error); + if (error) { + throw std::runtime_error("failed to prepare temporary test path: " + m_path.string()); + } + } + + /** @brief Removes the temporary path and its contents. */ + ~TemporaryTestPath() + { + std::error_code ignored; + std::filesystem::remove_all(m_path, ignored); + } + + /** @brief Disables copying to preserve unique path ownership. */ + TemporaryTestPath(const TemporaryTestPath&) = delete; + /** @brief Disables copy assignment to preserve unique path ownership. */ + TemporaryTestPath& operator=(const TemporaryTestPath&) = delete; + + /** @brief Returns the owned temporary path. */ + const std::filesystem::path& path() const + { + return m_path; + } + + /** @brief Creates and returns the owned path as a directory. */ + const std::filesystem::path& createDirectory() const + { + std::filesystem::create_directories(m_path); + return m_path; + } + +private: + std::filesystem::path m_path; +}; + +#endif // CTRACE_TEST_UNIT_SUPPORT_TESTPATH_H diff --git a/tools/ctrace/test/unit/support/TestPlatform.h b/tools/ctrace/test/unit/support/TestPlatform.h new file mode 100644 index 000000000..5c9ceb080 --- /dev/null +++ b/tools/ctrace/test/unit/support/TestPlatform.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_TESTPLATFORM_H +#define CTRACE_TEST_UNIT_SUPPORT_TESTPLATFORM_H + +#include +#include + +/** @brief Identifies target-platform facilities used by portable unit tests. */ +enum class TestPlatformCapability { + DirectoryReadFailure, + LinuxSpecialFiles, + PosixPermissions, +}; + +/** @brief Provides target-platform test services without exposing platform macros to tests. */ +class TestPlatform final { +public: + /** @brief Reports whether the target supports the requested test facility. */ + static bool supports(TestPlatformCapability capability) noexcept; + + /** @brief Returns a target path whose writes fail after opening. */ + static std::filesystem::path writeFailurePath(); + + /** @brief Returns a target path that rejects creation of the named file. */ + static std::filesystem::path creationFailurePath(std::string_view fileName); + +private: + /** @brief Prevents construction of this stateless test utility. */ + TestPlatform() = delete; +}; + +#endif // CTRACE_TEST_UNIT_SUPPORT_TESTPLATFORM_H diff --git a/tools/ctrace/test/unit/support/TestSupport.h b/tools/ctrace/test/unit/support/TestSupport.h new file mode 100644 index 000000000..287dffda6 --- /dev/null +++ b/tools/ctrace/test/unit/support/TestSupport.h @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_TESTSUPPORT_H +#define CTRACE_TEST_UNIT_SUPPORT_TESTSUPPORT_H + +#include "DiagnosticSink.h" +#include "TraceEvent.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @brief Throws from low-level test support where fatal GoogleTest macros cannot terminate the caller. */ +inline void require(bool condition, const std::string& message) +{ + if (!condition) { + throw std::runtime_error(message); + } +} + +/** @brief Executes an action and captures a matching exception message. */ +template +inline std::optional captureExceptionMessage(Function&& action) +{ + try { + std::forward(action)(); + } catch (const Exception& error) { + return error.what(); + } + return std::nullopt; +} + +/** @brief Tests whether an action throws the requested exception type. */ +template inline bool throwsException(Function&& action) +{ + return captureExceptionMessage(std::forward(action)).has_value(); +} + +/** @brief Tests whether an exception message contains expected text. */ +template +inline bool throwsWithMessage(Function&& action, std::string_view expected) +{ + const auto message = captureExceptionMessage(std::forward(action)); + return message.has_value() && message->find(expected) != std::string::npos; +} + +/** @brief Collects structured diagnostics emitted during a unit test. */ +class CollectingDiagnosticSink final : public DiagnosticSink { +public: + /** @brief Returns all collected diagnostic events. */ + const std::vector& events() const + { + return m_events; + } + + /** @brief Tests whether a diagnostic message contains the requested text. */ + bool containsMessage(std::string_view text) const + { + for (const auto& event : m_events) { + if (event.message.find(text) != std::string::npos) { + return true; + } + } + return false; + } + + /** @brief Returns the only collected event. */ + const Event& singleEvent() const + { + require(m_events.size() == 1U, "expected exactly one diagnostic event"); + return m_events.front(); + } + + /** @brief Tests whether a diagnostic contains one context entry. */ + bool containsContext(std::string_view key, std::string_view value) const + { + for (const auto& event : m_events) { + for (const auto& [contextKey, contextValue] : event.context) { + if (contextKey == key && contextValue == value) { + return true; + } + } + } + return false; + } + +protected: + /** @brief Stores one reported diagnostic event. */ + void write(const Event& event) override + { + m_events.push_back(event); + } + +private: + std::vector m_events; +}; + +/** @brief Collects semantic trace events emitted during a unit test. */ +class CollectingEventSink final : public TraceEventSink { +public: + /** @brief Stores one emitted semantic event. */ + void append(const TraceEvent& event) override + { + m_events.push_back(event); + } + + /** @brief Returns all collected semantic events. */ + const std::vector& events() const + { + return m_events; + } + + /** @brief Returns mutable collected events for ownership transfer in tests. */ + std::vector& events() + { + return m_events; + } + +private: + std::vector m_events; +}; + +/** @brief Creates a binary test file with the supplied contents. */ +inline void writeTestFile(const std::filesystem::path& path, const std::string& contents = {}) +{ + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out) { + throw std::runtime_error("failed to create test file: " + path.string()); + } + out << contents; +} + +/** @brief Reads a complete test file as text. */ +inline std::string readTestTextFile(const std::filesystem::path& path) +{ + std::ifstream in(path, std::ios::binary); + if (!in) { + throw std::runtime_error("failed to read test file: " + path.string()); + } + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +/** @brief Reads all lines from a test text file. */ +inline std::vector readTestLines(const std::filesystem::path& path) +{ + std::ifstream in(path); + if (!in) { + throw std::runtime_error("failed to read test file: " + path.string()); + } + std::vector lines; + for (std::string line; std::getline(in, line);) { + lines.push_back(std::move(line)); + } + return lines; +} + +/** @brief Reads a complete test file as bytes. */ +inline std::vector readTestBinaryFile(const std::filesystem::path& path) +{ + std::ifstream in(path, std::ios::binary); + if (!in) { + throw std::runtime_error("failed to read binary test file: " + path.string()); + } + return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; +} + +/** @brief Creates a timestamped exception event for a test. */ +inline TraceEvent exceptionPacket(std::uint32_t number, ExceptionAction action, std::uint64_t tcyc = 0) +{ + TraceEvent packet{ExceptionTraceEvent{number, action}}; + packet.tcyc = tcyc; + return packet; +} + +/** @brief Creates a timestamped overflow event for a test. */ +inline TraceEvent overflowPacket(std::uint64_t tcyc) +{ + TraceEvent packet{OverflowTraceEvent{}}; + packet.tcyc = tcyc; + packet.quality = TraceQuality{true, false, 1U}; + return packet; +} + +/** @brief Creates an ITM software event for a test. */ +inline TraceEvent softwarePacket(std::uint32_t channel, std::uint8_t size = 1, std::uint32_t value = 0) +{ + return TraceEvent{SoftwareTraceEvent{channel, size, value}}; +} + +/** @brief Assigns a cycle timestamp to a copied event. */ +inline TraceEvent atCycle(TraceEvent event, std::uint64_t tcyc) +{ + event.tcyc = tcyc; + return event; +} + +/** @brief Assigns a Trace Bus ID to a copied event. */ +inline TraceEvent onStream(TraceEvent event, std::uint8_t traceBusId) +{ + event.traceBusId = traceBusId; + return event; +} + +/** @brief Creates a decoder issue event for a test. */ +inline TraceEvent issuePacket(TraceIssueCode code, std::string message = {}, + TraceIssueSeverity severity = TraceIssueSeverity::Error) +{ + return TraceEvent{TraceIssueEvent{ + code, + severity, + std::move(message), + }}; +} + +#endif // CTRACE_TEST_UNIT_SUPPORT_TESTSUPPORT_H diff --git a/tools/ctrace/test/unit/support/TraceOutputTestSupport.h b/tools/ctrace/test/unit/support/TraceOutputTestSupport.h new file mode 100644 index 000000000..07d3d8e6b --- /dev/null +++ b/tools/ctrace/test/unit/support/TraceOutputTestSupport.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_TRACEOUTPUTTESTSUPPORT_H +#define CTRACE_TEST_UNIT_SUPPORT_TRACEOUTPUTTESTSUPPORT_H + +#include "TraceOutput.h" +#include "TraceEvent.h" + +#include +#include +#include +#include + +namespace TraceOutputTestSupport { + +/** @brief Selects the lifecycle operation where a test output fails. */ +enum class TestTraceOutputFailure { None, Start, Stop, Abort, Write, NonStandardStart }; + +/** @brief Implements a configurable trace output for lifecycle unit tests. */ +class TestTraceOutput final : public TraceOutput { +public: + /** @brief Creates a test output with an optional failure point and target. */ + explicit TestTraceOutput(TestTraceOutputFailure failure = TestTraceOutputFailure::None, std::string target = {}) + : m_failure(failure), + m_target(std::move(target)) + { + } + + /** @brief Creates a test output that records lifecycle calls. */ + explicit TestTraceOutput(std::vector& calls) + : m_calls(&calls) + { + } + + /** @brief Records start and optionally throws the configured failure. */ + void start() override + { + record("start"); + if (m_failure == TestTraceOutputFailure::NonStandardStart) { + throw 42; + } + failAt(TestTraceOutputFailure::Start, "intentional start failure"); + TraceOutput::start(); + } + + /** @brief Records stop and optionally throws the configured failure. */ + void stop() override + { + record("stop"); + failAt(TestTraceOutputFailure::Stop, "intentional stop failure"); + TraceOutput::stop(); + } + + /** @brief Records abort and optionally throws the configured failure. */ + void abort() override + { + record("abort"); + failAt(TestTraceOutputFailure::Abort, "intentional abort cleanup failure"); + m_aborted = true; + } + + /** @brief Records an event write and optionally throws the configured failure. */ + void writeEvent(const TraceEvent&) override + { + record("write"); + failAt(TestTraceOutputFailure::Write, "intentional write failure"); + } + + /** @brief Returns the configured output target. */ + std::string targetPath() const override + { + return m_target.empty() ? TraceOutput::targetPath() : m_target; + } + + /** @brief Reports whether abort completed successfully. */ + bool aborted() const + { + return m_aborted; + } + +private: + /** @brief Records a lifecycle call when call collection is enabled. */ + void record(const char* call) + { + if (m_calls != nullptr) { + m_calls->emplace_back(call); + } + } + + /** @brief Throws when the supplied operation is the configured failure point. */ + void failAt(TestTraceOutputFailure point, const char* message) const + { + if (m_failure == point) { + throw std::runtime_error(message); + } + } + + TestTraceOutputFailure m_failure = TestTraceOutputFailure::None; + std::string m_target; + std::vector* m_calls = nullptr; + bool m_aborted = false; +}; + +} // namespace TraceOutputTestSupport + +#endif // CTRACE_TEST_UNIT_SUPPORT_TRACEOUTPUTTESTSUPPORT_H diff --git a/tools/ctrace/test/unit/support/TraceRunTestSupport.h b/tools/ctrace/test/unit/support/TraceRunTestSupport.h new file mode 100644 index 000000000..69dea6ae7 --- /dev/null +++ b/tools/ctrace/test/unit/support/TraceRunTestSupport.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_TEST_UNIT_SUPPORT_TRACERUNTESTSUPPORT_H +#define CTRACE_TEST_UNIT_SUPPORT_TRACERUNTESTSUPPORT_H + +#include "TraceRunConfig.h" + +#include +#include +#include +#include +#include + +namespace TraceRunTestSupport { + +/** @brief Creates a trace-run reference with common test defaults. */ +inline TraceRunReference makeReference(std::string type, std::optional processorName, + std::optional stream, std::vector sources, + std::string ctraceRef = "route") +{ + TraceRunReference reference; + reference.type = std::move(type); + reference.processorName = std::move(processorName); + reference.stream = stream; + reference.sources = std::move(sources); + reference.ctraceRef = std::move(ctraceRef); + return reference; +} + +/** @brief Creates a timestamp setup with optional ITM configuration. */ +inline TraceRunSetup makeTimestampSetup(std::optional processorName, + std::optional clock = 1U, + std::optional prescaler = 1U, + std::optional enableMask = std::nullopt) +{ + TraceRunSetup setup; + setup.processorName = std::move(processorName); + setup.timestamps = TraceRunTimestampSetup{clock, prescaler}; + if (enableMask.has_value()) { + setup.itm = TraceRunItmSetup{*enableMask}; + } + return setup; +} + +} // namespace TraceRunTestSupport + +#endif // CTRACE_TEST_UNIT_SUPPORT_TRACERUNTESTSUPPORT_H diff --git a/tools/ctrace/test/unit/support/retarget/linux/TestPlatform.cpp b/tools/ctrace/test/unit/support/retarget/linux/TestPlatform.cpp new file mode 100644 index 000000000..88d1b9e73 --- /dev/null +++ b/tools/ctrace/test/unit/support/retarget/linux/TestPlatform.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPlatform.h" + +#include +#include +#include + +bool TestPlatform::supports(TestPlatformCapability capability) noexcept +{ + switch (capability) { + case TestPlatformCapability::DirectoryReadFailure: + case TestPlatformCapability::LinuxSpecialFiles: + case TestPlatformCapability::PosixPermissions: + return true; + } + return false; +} + +std::filesystem::path TestPlatform::writeFailurePath() +{ + return "/dev/full"; +} + +std::filesystem::path TestPlatform::creationFailurePath(std::string_view fileName) +{ + return std::filesystem::path("/proc") / std::string(fileName); +} diff --git a/tools/ctrace/test/unit/support/retarget/posix/TestPlatform.cpp b/tools/ctrace/test/unit/support/retarget/posix/TestPlatform.cpp new file mode 100644 index 000000000..a87147ecc --- /dev/null +++ b/tools/ctrace/test/unit/support/retarget/posix/TestPlatform.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPlatform.h" + +#include +#include + +bool TestPlatform::supports(TestPlatformCapability capability) noexcept +{ + return capability == TestPlatformCapability::PosixPermissions; +} + +std::filesystem::path TestPlatform::writeFailurePath() +{ + return {}; +} + +std::filesystem::path TestPlatform::creationFailurePath(std::string_view) +{ + return {}; +} diff --git a/tools/ctrace/test/unit/support/retarget/windows/TestPlatform.cpp b/tools/ctrace/test/unit/support/retarget/windows/TestPlatform.cpp new file mode 100644 index 000000000..b7f4620d8 --- /dev/null +++ b/tools/ctrace/test/unit/support/retarget/windows/TestPlatform.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TestPlatform.h" + +#include +#include + +bool TestPlatform::supports(TestPlatformCapability) noexcept +{ + return false; +} + +std::filesystem::path TestPlatform::writeFailurePath() +{ + return {}; +} + +std::filesystem::path TestPlatform::creationFailurePath(std::string_view) +{ + return {}; +}