From d8dec8ae050bc2125b166ed3bbf3f425f5850c47 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Fri, 23 Oct 2026 08:17:43 +0200 Subject: [PATCH 1/2] feat(ctrace): add raw input channel selection --- tools/ctrace/README.md | 20 +- tools/ctrace/docs/architecture.md | 10 +- tools/ctrace/docs/constraints.md | 22 +- tools/ctrace/docs/todo.md | 8 +- tools/ctrace/src/CMakeLists.txt | 2 + tools/ctrace/src/cli/CliOptions.h | 2 + tools/ctrace/src/cli/CliParser.cpp | 40 +++- .../ctrace/src/control/TraceDirectoryJob.cpp | 47 +++- tools/ctrace/src/control/TraceDirectoryJob.h | 6 +- tools/ctrace/src/model/DiagnosticMessages.cpp | 10 + tools/ctrace/src/model/DiagnosticMessages.h | 4 + tools/ctrace/src/model/MessageCatalog.inc | 2 + tools/ctrace/src/model/TraceInputChannel.cpp | 36 +++ tools/ctrace/src/model/TraceInputChannel.h | 18 ++ .../ctrace/src/tracerun/TraceRunDiscovery.cpp | 49 +--- tools/ctrace/src/tracerun/TraceRunDiscovery.h | 14 +- tools/ctrace/test/data/README.md | 2 +- tools/ctrace/test/integration/README.md | 2 + .../test/integration/src/CtraceIntegTests.cpp | 220 ++++++++++++++++++ .../test/unit/src/cli/CliParserTests.cpp | 69 +++++- .../src/control/TraceDirectoryJobTests.cpp | 159 ++++++++++++- .../src/tracerun/TraceRunDiscoveryTests.cpp | 62 +++-- 22 files changed, 694 insertions(+), 110 deletions(-) create mode 100644 tools/ctrace/src/model/TraceInputChannel.cpp create mode 100644 tools/ctrace/src/model/TraceInputChannel.h diff --git a/tools/ctrace/README.md b/tools/ctrace/README.md index b9dda9af8..6f827786a 100644 --- a/tools/ctrace/README.md +++ b/tools/ctrace/README.md @@ -10,6 +10,7 @@ The [architecture description](docs/architecture.md) documents the supported fea ctrace [options] -t, --target Process one solution set; otherwise process all + -c, --channel Select one input channel; repeat to select several --csv Generate CSV output --ctf Generate CTF and Trace Compass XML output -a, --all Generate all output formats @@ -25,6 +26,10 @@ 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. Run `ctrace --help` for the current option details. +`--channel` selects exact, case-sensitive names (`SWO`, `TB`, `TB_`). Repeat it for multiple channels, +e.g. `ctrace .trace --all -c SWO -c TB_ETB_0`; duplicates are ignored. Default: all supported channels. +`TB` selects only the unnamed buffer; `--type` and `--stream` filter events within the selected inputs. + Normal CLI diagnostics use the same compact trace messages as CSV. Add `--verbose` (or `-v`) to include detailed reasons, raw positions with bounded inspection windows, native decoder codes, packet previews and technical run information on stderr. This option does not change CSV or CTF output, event selection or the exit status. @@ -50,9 +55,9 @@ For `ctrace .trace --target Board --all`, the supported input produces: Board.traceanalysis.xml # graphical views from eligible completed CTF bundles ``` -Discovery processes every existing `Board.SWO.raw`, `Board.TB.raw`, and `Board.TB_.raw` input independently, -one after another. `--target Board` selects the solution set and all its supported inputs. Each input gets its own -CSV and CTF bundle, using `.` as their common base name. CTF uses +Discovery processes selected `Board.SWO.raw`, `Board.TB.raw`, and `Board.TB_.raw` inputs independently, +one after another. `--target Board` selects the solution set; `--channel` further restricts its inputs. Each input gets +its own CSV and CTF bundle, using `.` as their common base name. CTF uses this channel-qualified name even for a single input; older `Board.ctf` bundles are neither reused nor removed. The [CTF profile](docs/ctf-format.md#files-and-common-structure) records the required specification alignment. @@ -65,6 +70,10 @@ remains valid CTF but is omitted from the XML with a warning. Independent single their clock UUIDs keep sources separate without synchronizing or rebasing their timestamps. No old per-channel XML files are migrated or deleted. An XML failure leaves completed CSV and CTF outputs intact. +With `--channel`, sets without matches are skipped and their outputs preserved. Each requested channel must exist +in at least one selected set; otherwise ctrace returns `1` after processing available matches. Excluded CSV/CTF +files remain untouched and are omitted from regenerated XML. + Without a format declaration, each SWO input defaults to unformatted ITM and each TB or named-TB input defaults to formatted CoreSight. Missing or null `trace-format` uses these channel-based defaults; an explicit value overrides the format for every supported input in that trace-run: @@ -81,8 +90,9 @@ memory-aligned CoreSight frames; there is no public `trace-framing` field yet. S [constraints](docs/constraints.md) for the full discovery, routing, and compatibility contract. -Event Recorder (`Board.ER.raw`) remains unsupported and is skipped with a warning. A solution set without a supported -input reports an error. A failure in one input leaves the remaining inputs and solution sets available for processing; +Event Recorder (`Board.ER.raw`) remains unsupported: unfiltered discovery skips it with a warning, and `--channel ER` +is rejected. Without a channel filter, a solution set without a supported input reports an error. +A failure in one input leaves the remaining inputs and solution sets available for processing; the command returns non-zero if any input fails. This also applies when validating without output options. ## Incomplete captures diff --git a/tools/ctrace/docs/architecture.md b/tools/ctrace/docs/architecture.md index 7a5f23963..ee33ed73b 100644 --- a/tools/ctrace/docs/architecture.md +++ b/tools/ctrace/docs/architecture.md @@ -20,7 +20,7 @@ Backend-specific representations are documented in the [CTF profile](ctf-format. PC sampling accepts a four-byte PC or the one-byte status markers `0x00` (`CPU Sleeping`) and Armv8-M `0xff` (`Trace prohibited`). Status markers preserve route, timestamp, and sample quality without a PC address. -Every matching SWO, TB, and named-TB raw input is processed independently and sequentially for each selected trace-run +Every selected SWO, TB, and named-TB raw input is processed independently and sequentially for each selected trace-run configuration. Formatted input distributes bytes to configured ITM routes by Trace Bus ID; unformatted input uses one synthetic route. Other protocols require explicit decoder integration, not guesses based on observed IDs. Deferred inputs and decoders are tracked in the [TODO list](todo.md). @@ -30,7 +30,7 @@ formats independent of OpenCSD and allows another raw trace channel to reuse the ## How it works at a glance -`ctrace` processes one solution set at a time and runs a separate file job for each supported raw input in that set. +`ctrace` processes one solution set at a time and runs a separate file job for each selected raw input in that set. The [README](../README.md#trace-directory) describes how configuration, raw inputs, and generated outputs are grouped by solution-set and channel names. The following path runs once per input, with fresh decoder and output state. @@ -86,8 +86,9 @@ there is deliberately no common base class for all decode stages. Input selection and route binding belong to `tracerun` and complete before each input's decoder and output backends are constructed. Target-level XML preparation precedes these per-input jobs. +With `--channel`, `TraceDirectoryJob` skips sets without matching inputs before YAML parsing and output preparation. The provisional, ctrace-private `trace-format` declaration describes effective capture bytes rather than target -capability or file identity. Discovery collects every matching SWO, TB, and named-TB input, independently of whether +capability or file identity. Discovery selects matching SWO, TB, and named-TB inputs independently of whether the format is declared. Each input is preflighted and normalized separately. An explicit format applies to every input; otherwise SWO defaults to `unformatted` and TB or named-TB to `formatted`. The format is resolved before `CtraceRunMeta` normalizes that input's routes, so the decoder and route model use the same effective format. This @@ -219,8 +220,7 @@ byte. Packet rollback remains separate from the fatal-input output policy descri 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) and - [`TraceRunDiscovery.cpp`](../src/tracerun/TraceRunDiscovery.cpp) to see how a solution set and YAML select all - matching SWO/TB inputs, each with its own preflighted descriptor and file job. + [`TraceRunDiscovery.cpp`](../src/tracerun/TraceRunDiscovery.cpp) for target/channel selection and input preflight. 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), diff --git a/tools/ctrace/docs/constraints.md b/tools/ctrace/docs/constraints.md index ee656b017..cef551f9b 100644 --- a/tools/ctrace/docs/constraints.md +++ b/tools/ctrace/docs/constraints.md @@ -41,18 +41,23 @@ configuration with trace communication, separate from trace-source setup. channel-based default. Missing or null selects `unformatted` for SWO and `formatted` for TB or named-TB without an Error. Discovery resolves this effective format separately for each input before route normalization. An explicit override applies to every supported input associated with that trace-run configuration. -- Every existing `.SWO.raw`, `.TB.raw`, and `.TB_.raw` is processed independently and sequentially. - `--target` selects a solution set, including all its supported inputs. A set without an eligible input reports an error. +- Selected `.SWO.raw`, `.TB.raw`, and `.TB_.raw` inputs are processed independently and sequentially. + `--target` selects a solution set. Without `--channel`, all its supported inputs are selected and a set without an + eligible input reports an error. Each input must be a regular, readable file and is opened during its own preflight, before that input's decoder and - output backends are constructed. Target XML preparation precedes these per-input jobs. Event Recorder input remains - diagnosed and excluded from processing. + output backends are constructed. Target XML preparation precedes these per-input jobs. +- `--channel` / `-c` accepts exact, case-sensitive names: `SWO`, `TB`, or `TB_` with suffix `[A-Za-z0-9_-]+`. + Repeated options form a union without duplicates; `TB` excludes named buffers. `ER` is unsupported: + the CLI rejects it, while unfiltered discovery warns and skips it. +- With a channel filter, sets without matches are skipped before YAML parsing. Each requested channel must match + a filename within the selected target scope; missing channels cause exit status `1` after processing the matches. + Matched inputs report YAML/preflight failures without a missing-channel error. `--type` and `--stream` filter + events within selected inputs. - Input preflight, route normalization, decoding, and output failures do not prevent processing the remaining inputs or solution sets. Each input has independent decoder and output state; any failing input contributes to a non-zero command exit status. A configuration read failure prevents processing that set but does not stop later sets. - The standardized `trace-buffer` selection belongs to solution/build-run producer configuration, not to the - `*.ctrace-run.yml` file consumed by ctrace. The caller-facing selection and format/framing contract remains - follow-up work; this does not imply adding fields to `*.ctrace-run.yml`. Current discovery processes all supported - matching files and applies the channel-based defaults above. + `*.ctrace-run.yml` file consumed by ctrace. The caller-facing format/framing contract remains follow-up work. - Formatted input globally uses 16-byte memory-aligned CoreSight frames. Its length must be a multiple of 16, and it contains neither FSYNC nor HSYNC framing. Ctrace does not parse or emit a `trace-framing` YAML field; supporting another framing mode requires a public trace contract first. @@ -199,8 +204,9 @@ configuration with trace communication, separate from trace-source setup. only CTF metadata successfully finalized in that invocation, never stale bundles discovered on disk. Recoverable decoder errors do not exclude an otherwise completed bundle. An XML failure does not remove completed CTF or CSV; an input failure does not remove other inputs' contributions. No per-channel XML is generated or cleaned up. + Excluded inputs retain their CSV/CTF files. Sets without selected inputs retain all outputs, including XML. - 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. +- Validation-only mode creates no output. - Apart from the explicitly retained CSV after a fatal decode abort, cleanup of incomplete output artifacts is attempted after failure, and cleanup failures are reported. Incompatible existing output filesystem types are rejected before replacement. CTF bundles and target XML use distinct sibling paths derived from the trace-run diff --git a/tools/ctrace/docs/todo.md b/tools/ctrace/docs/todo.md index 2f2330d5b..c99422456 100644 --- a/tools/ctrace/docs/todo.md +++ b/tools/ctrace/docs/todo.md @@ -8,14 +8,10 @@ ## Inputs and time correlation -- [ ] Align caller-facing raw-input/channel selection and transport format/framing with the outcome of +- [ ] Align caller-facing transport format/framing with the outcome of [cmsis-toolbox #699](https://github.com/Open-CMSIS-Pack/cmsis-toolbox/pull/699), also tracked by [vscode-cmsis-debugger #1150](https://github.com/Open-CMSIS-Pack/vscode-cmsis-debugger/issues/1150). - TB input/CSV naming support tracked by - [devtools #2573](https://github.com/Open-CMSIS-Pack/devtools/issues/2573) is complete and that issue is closed; - public channel selection and format/framing remain separate follow-up work. Keep trace communication separate - from trace-source setup and decide how to replace the temporary `trace-format` override without assuming its - standardization. + Replace the provisional `trace-format` override while keeping trace communication separate from trace-source setup. - [ ] Align the published CTF bundle path with ctrace's `..ctf` naming, including single-input runs. - [ ] Support FSYNC and FSYNC+HSYNC formatted input after a public framing contract is specified. - [ ] Define cross-stream clock correlation and offsets once a common producer time reference is available. diff --git a/tools/ctrace/src/CMakeLists.txt b/tools/ctrace/src/CMakeLists.txt index f2530ec32..aa597b122 100644 --- a/tools/ctrace/src/CMakeLists.txt +++ b/tools/ctrace/src/CMakeLists.txt @@ -8,6 +8,7 @@ set(CTRACE_MODEL_HEADER_FILES model/MessageCatalog.inc model/Messages.h model/TraceEvent.h + model/TraceInputChannel.h model/TraceMessages.h model/TraceRoute.h model/TraceSelection.h @@ -86,6 +87,7 @@ source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" add_library(ctrace-model STATIC model/DiagnosticMessages.cpp model/Messages.cpp + model/TraceInputChannel.cpp model/TraceMessages.cpp model/TraceSelection.cpp ${CTRACE_MODEL_HEADER_FILES} diff --git a/tools/ctrace/src/cli/CliOptions.h b/tools/ctrace/src/cli/CliOptions.h index e9579bfce..5849d8ff4 100644 --- a/tools/ctrace/src/cli/CliOptions.h +++ b/tools/ctrace/src/cli/CliOptions.h @@ -12,6 +12,7 @@ #include #include +#include /** @brief Selects the trace output formats requested on the command line. */ enum class OutputFormat { @@ -25,6 +26,7 @@ enum class OutputFormat { struct CliOptions { std::optional traceDir; std::optional targetName; + std::vector channels; OutputFormat outputFormat = OutputFormat::None; TraceSelection selection; bool help = false; diff --git a/tools/ctrace/src/cli/CliParser.cpp b/tools/ctrace/src/cli/CliParser.cpp index a9abf54f8..254ace5b4 100644 --- a/tools/ctrace/src/cli/CliParser.cpp +++ b/tools/ctrace/src/cli/CliParser.cpp @@ -10,6 +10,7 @@ #include "DiagnosticMessages.h" #include "CliOptions.h" #include "ProductInfo.h" +#include "TraceInputChannel.h" #include "TraceSelection.h" #include "TraceStreamId.h" @@ -136,14 +137,23 @@ static void configureCliParser(cxxopts::Options& parser) 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")( - "v,verbose", "Show detailed diagnostics and processing information"); - parser.add_options("Hidden")("h,help", "Print help"); + 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()) + ("c,channel", "Select an input channel (repeatable; default: all)", + cxxopts::value(), "name") + ("V,version", "Print version") + ("v,verbose", "Show detailed diagnostics and processing information"); + + parser.add_options("Hidden") + ("h,help", "Print help"); } /** @brief Parses normalized command-line arguments into ctrace options. */ @@ -175,6 +185,15 @@ static CliOptions parseCliArgs(const std::vector& arguments) if (parsed.count("target") != 0U) { options.targetName = parsed["target"].as(); } + // Read repeated scalars without cxxopts' comma splitting. + for (const auto& argument : parsed.arguments()) { + if (argument.key() == "channel") { + if (argument.value().empty() || startsWithDash(argument.value())) { + throw std::runtime_error(missingArgumentValueMessage("--channel")); + } + options.channels.push_back(argument.value()); + } + } if (parsed.count("type") != 0U) { for (const auto& type : parsed["type"].as>()) { options.selection.types.push_back(type); @@ -201,6 +220,11 @@ static void validateCliOptions(const CliOptions& options) if (options.help) { return; } + for (const auto& channel : options.channels) { + if (!isSupportedTraceChannel(channel)) { + throw std::runtime_error(channelArgumentMessage(channel)); + } + } for (const auto& type : options.selection.types) { if (!parseTraceEventType(type).has_value()) { throw std::runtime_error(typeArgumentMessage(type, traceEventTypeList(", "))); diff --git a/tools/ctrace/src/control/TraceDirectoryJob.cpp b/tools/ctrace/src/control/TraceDirectoryJob.cpp index a79407ffa..d7853d013 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.cpp +++ b/tools/ctrace/src/control/TraceDirectoryJob.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -154,15 +155,35 @@ void TraceDirectoryJob::run() throw std::runtime_error(formatMessage(MessageId::TraceDirectoryRequired)); } + std::set unmatchedChannels(m_options.channels.begin(), m_options.channels.end()); for (const auto& configFile : configFiles) { - processConfigFile(configFile); + processConfigFile(configFile, unmatchedChannels); + } + for (const auto& channel : unmatchedChannels) { + std::vector> context{ + {"channel", channel}, {"traceDir", *m_options.traceDir}}; + if (m_options.targetName.has_value()) { + context.emplace_back("target", *m_options.targetName); + } + m_diagnostics.report({DiagnosticSink::Severity::Error, missingChannelInputMessage(channel), std::move(context)}); } } -void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFile) +void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFile, + std::set& unmatchedChannels) { const auto solutionSet = TraceRunDiscovery::solutionSetName(configFile); try { + std::vector inputs; + if (!m_options.channels.empty()) { + inputs = TraceRunDiscovery::selectInputs(configFile, m_options.channels); + for (const auto& input : inputs) { + unmatchedChannels.erase(input.channel); + } + if (inputs.empty()) { + return; + } + } auto config = m_configReader.read(configFile.string()); DiagnosticSink::Event selected{ DiagnosticSink::Severity::Info, @@ -177,17 +198,19 @@ void TraceDirectoryJob::processConfigFile(const std::filesystem::path& configFil selected.visibility = DiagnosticSink::Visibility::Verbose; m_diagnostics.report(selected); reportConsumedReferenceDiagnostics(config, m_diagnostics); - const auto inputs = TraceRunDiscovery::selectInputs(config, [&](const auto& rawInput) { - m_diagnostics.report({ - DiagnosticSink::Severity::Warning, - formatMessage(MessageId::SkippingExcludedTraceChannel), - { - {"solutionSet", solutionSet}, - {"channel", rawInput.channel}, - {"path", rawInput.path.string()}, - }, + if (m_options.channels.empty()) { + inputs = TraceRunDiscovery::selectInputs(config.path, {}, [&](const auto& rawInput) { + m_diagnostics.report({ + DiagnosticSink::Severity::Warning, + formatMessage(MessageId::SkippingExcludedTraceChannel), + { + {"solutionSet", solutionSet}, + {"channel", rawInput.channel}, + {"path", rawInput.path.string()}, + }, + }); }); - }); + } processInputs(config, inputs, configFile.parent_path() / (solutionSet + ".traceanalysis.xml")); } catch (const std::exception& error) { m_diagnostics.report({ diff --git a/tools/ctrace/src/control/TraceDirectoryJob.h b/tools/ctrace/src/control/TraceDirectoryJob.h index f93cef2a8..cefdf17df 100644 --- a/tools/ctrace/src/control/TraceDirectoryJob.h +++ b/tools/ctrace/src/control/TraceDirectoryJob.h @@ -15,6 +15,8 @@ #include #include +#include +#include #include struct TraceRunConfig; @@ -40,8 +42,8 @@ class TraceDirectoryJob { void run(); private: - /** @brief Reads one configuration and processes all its supported raw inputs. */ - void processConfigFile(const std::filesystem::path& configFile); + /** @brief Processes matching inputs and records requested channels found. */ + void processConfigFile(const std::filesystem::path& configFile, std::set& unmatchedChannels); /** @brief Collects completed per-input CTF metadata into one target-level XML. */ void processInputs(const TraceRunConfig& config, const std::vector& inputs, const std::filesystem::path& xmlPath); diff --git a/tools/ctrace/src/model/DiagnosticMessages.cpp b/tools/ctrace/src/model/DiagnosticMessages.cpp index 4a5d065a8..34b53d9fb 100644 --- a/tools/ctrace/src/model/DiagnosticMessages.cpp +++ b/tools/ctrace/src/model/DiagnosticMessages.cpp @@ -62,6 +62,16 @@ std::string targetArgumentMessage(std::string_view value) return formatMessage(MessageId::DiagnosticTargetArgument, MessageStyle::Detailed, {value}); } +std::string channelArgumentMessage(std::string_view value) +{ + return formatMessage(MessageId::DiagnosticChannelArgument, MessageStyle::Detailed, {value}); +} + +std::string missingChannelInputMessage(std::string_view value) +{ + return formatMessage(MessageId::DiagnosticMissingChannelInput, MessageStyle::Detailed, {value}); +} + std::string configFieldMessage(std::string_view field, ConfigFieldProblem problem) { switch (problem) { diff --git a/tools/ctrace/src/model/DiagnosticMessages.h b/tools/ctrace/src/model/DiagnosticMessages.h index 5120c2fd0..bd5175fe0 100644 --- a/tools/ctrace/src/model/DiagnosticMessages.h +++ b/tools/ctrace/src/model/DiagnosticMessages.h @@ -31,6 +31,10 @@ std::string positionalArgumentMessage(std::string_view argument); std::string streamArgumentMessage(std::string_view value); std::string typeArgumentMessage(std::string_view value, std::string_view accepted); std::string targetArgumentMessage(std::string_view value); +std::string channelArgumentMessage(std::string_view value); + +/** @brief Formats a missing-channel diagnostic. */ +std::string missingChannelInputMessage(std::string_view value); /** @brief Selects the validation applied to one named YAML field. */ enum class ConfigFieldProblem { diff --git a/tools/ctrace/src/model/MessageCatalog.inc b/tools/ctrace/src/model/MessageCatalog.inc index 5ca39053c..8e765e5b2 100644 --- a/tools/ctrace/src/model/MessageCatalog.inc +++ b/tools/ctrace/src/model/MessageCatalog.inc @@ -677,6 +677,8 @@ CTRACE_MESSAGE(DiagnosticPositionalArgument, "Unexpected positional argument: {0 CTRACE_MESSAGE(DiagnosticStreamArgument, "--stream must be a CoreSight Trace Bus ID between 0 and 111, got {0}", nullptr) CTRACE_MESSAGE(DiagnosticTypeArgument, "Invalid --type value: {0} (accepted: {1})", nullptr) CTRACE_MESSAGE(DiagnosticTargetArgument, "--target must be a solution-set name, got {0}", nullptr) +CTRACE_MESSAGE(DiagnosticChannelArgument, "Invalid --channel value: {0} (accepted: SWO, TB, TB_; name uses letters, digits, '_' or '-')", nullptr) +CTRACE_MESSAGE(DiagnosticMissingChannelInput, "no raw trace input found for requested channel {0}", nullptr) CTRACE_MESSAGE(DiagnosticConfigFieldScalarString, "'{0}' must be a scalar string", nullptr) CTRACE_MESSAGE(DiagnosticConfigFieldScalarUnsigned, "'{0}' must be a scalar unsigned integer", nullptr) CTRACE_MESSAGE(DiagnosticConfigFieldUnsigned, "'{0}' must be an unsigned integer", nullptr) diff --git a/tools/ctrace/src/model/TraceInputChannel.cpp b/tools/ctrace/src/model/TraceInputChannel.cpp new file mode 100644 index 000000000..777069990 --- /dev/null +++ b/tools/ctrace/src/model/TraceInputChannel.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#include "TraceInputChannel.h" + +#include +#include + +/** @brief Checks the CMSIS RestrictedString character set. */ +static bool isRestrictedStringCharacter(const char character) +{ + return (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || character == '_' || character == '-'; +} + +bool isTraceBufferChannel(std::string_view value) +{ + constexpr std::string_view namedPrefix = "TB_"; + if (value == "TB") { + return true; + } + if (value.size() <= namedPrefix.size() || value.substr(0U, namedPrefix.size()) != namedPrefix) { + return false; + } + const auto name = value.substr(namedPrefix.size()); + return std::all_of(name.begin(), name.end(), isRestrictedStringCharacter); +} + +bool isSupportedTraceChannel(std::string_view value) +{ + return value == "SWO" || isTraceBufferChannel(value); +} diff --git a/tools/ctrace/src/model/TraceInputChannel.h b/tools/ctrace/src/model/TraceInputChannel.h new file mode 100644 index 000000000..04913c468 --- /dev/null +++ b/tools/ctrace/src/model/TraceInputChannel.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Arm Limited. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * Generated with AI + */ + +#ifndef CTRACE_SRC_MODEL_TRACEINPUTCHANNEL_H +#define CTRACE_SRC_MODEL_TRACEINPUTCHANNEL_H + +#include + +/** @brief Recognizes TB and named trace buffers. */ +bool isTraceBufferChannel(std::string_view value); +/** @brief Recognizes supported raw input channels. */ +bool isSupportedTraceChannel(std::string_view value); + +#endif // CTRACE_SRC_MODEL_TRACEINPUTCHANNEL_H diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp index e3a93f5bb..5365cd7c8 100644 --- a/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.cpp @@ -8,6 +8,7 @@ #include "TraceRunDiscovery.h" #include "DiagnosticMessages.h" +#include "TraceInputChannel.h" #include #include @@ -64,39 +65,6 @@ static bool endsWith(const std::string_view& value, const std::string_view& suff return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix; } -/** @brief Tests whether a byte belongs to the CMSIS RestrictedString character set. */ -static bool isRestrictedStringCharacter(const char character) -{ - return (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z') || - (character >= '0' && character <= '9') || character == '_' || character == '-'; -} - -/** @brief Tests whether a channel names one specification-defined Trace Buffer. */ -static bool isTraceBufferChannel(const std::string_view& value) -{ - constexpr std::string_view namedPrefix = "TB_"; - if (value == "TB") { - return true; - } - if (value.size() <= namedPrefix.size() || value.substr(0U, namedPrefix.size()) != namedPrefix) { - return false; - } - const auto name = value.substr(namedPrefix.size()); - return std::all_of(name.begin(), name.end(), isRestrictedStringCharacter); -} - -/** @brief Tests whether a file extension names a recognized trace channel. */ -static bool isTraceChannel(const std::string_view& value) -{ - return value == "SWO" || value == "ER" || isTraceBufferChannel(value); -} - -/** @brief Tests input eligibility independently of an optional byte-format override. */ -static bool isEligibleTraceChannel(const std::string_view& value) -{ - return value == "SWO" || isTraceBufferChannel(value); -} - /** @brief Tests whether a solution-set name is reserved by Windows. */ static bool isWindowsReservedFilename(const std::string_view& value) { @@ -199,7 +167,7 @@ std::vector TraceRunDiscovery::rawInputs(const std::filesystem continue; } const auto channel = filename.substr(prefix.size(), channelSize); - if (!isTraceChannel(channel)) { + if (channel != "ER" && !isSupportedTraceChannel(channel)) { continue; } inputs.push_back({entry.path(), channel}); @@ -209,17 +177,20 @@ std::vector TraceRunDiscovery::rawInputs(const std::filesystem return inputs; } -std::vector TraceRunDiscovery::selectInputs(const TraceRunConfig& config, +std::vector TraceRunDiscovery::selectInputs(const std::filesystem::path& configFile, + const std::vector& channels, const SkippedTraceRunInputSink& skippedInputSink) { - if (config.path.empty()) { + if (configFile.empty()) { throw std::runtime_error(formatMessage(MessageId::TraceConfigurationSourcePathMissing)); } - const std::filesystem::path configFile(config.path); const auto rawInputs = TraceRunDiscovery::rawInputs(configFile); std::vector eligible; for (const auto& rawInput : rawInputs) { - if (isEligibleTraceChannel(rawInput.channel)) { + if (!channels.empty() && std::find(channels.begin(), channels.end(), rawInput.channel) == channels.end()) { + continue; + } + if (isSupportedTraceChannel(rawInput.channel)) { eligible.push_back(rawInput); } else if (skippedInputSink) { skippedInputSink(rawInput); @@ -227,7 +198,7 @@ std::vector TraceRunDiscovery::selectInputs(const TraceRunConf } const auto solutionSet = solutionSetName(configFile); - if (eligible.empty()) { + if (eligible.empty() && channels.empty()) { throw std::runtime_error(pathDiagnosticMessage(PathDiagnosticCode::RawInputMissing, solutionSet)); } return eligible; diff --git a/tools/ctrace/src/tracerun/TraceRunDiscovery.h b/tools/ctrace/src/tracerun/TraceRunDiscovery.h index 40339880e..263fcb5e7 100644 --- a/tools/ctrace/src/tracerun/TraceRunDiscovery.h +++ b/tools/ctrace/src/tracerun/TraceRunDiscovery.h @@ -91,13 +91,15 @@ class TraceRunDiscovery final { */ static std::string solutionSetName(const std::filesystem::path& configFile); /** - * @brief Selects every supported raw input associated with one trace-run configuration. - * @param config Parsed trace-run configuration with its source path and optional format override. - * @param skippedInputSink Optional observer for recognized inputs excluded from selection. - * @return Deterministically ordered inputs for independent preflight and decoding. - * @throws std::runtime_error If the source path is missing or no eligible input exists. + * @brief Selects raw inputs for one trace-run configuration. + * @param configFile Trace-run configuration path. + * @param channels Exact names; empty selects all supported channels. + * @param skippedInputSink Receives matching unsupported inputs. + * @return Inputs in path order; may be empty with a channel filter. + * @throws std::runtime_error If configFile is empty or no unfiltered input is eligible. */ - static std::vector selectInputs(const TraceRunConfig& config, + static std::vector selectInputs(const std::filesystem::path& configFile, + const std::vector& channels = {}, const SkippedTraceRunInputSink& skippedInputSink = {}); /** * @brief Preflights one selected raw input, resolves its format, and normalizes its routes. diff --git a/tools/ctrace/test/data/README.md b/tools/ctrace/test/data/README.md index e3efcb32e..0c8f2dca7 100644 --- a/tools/ctrace/test/data/README.md +++ b/tools/ctrace/test/data/README.md @@ -69,7 +69,7 @@ generated raw hash, and test matrix. [CtraceIntegTests.cpp](../integration/src/CtraceIntegTests.cpp) creates one trace-run configuration per target with SWO, TB and TB_ETB captures. Distinct ITM values and timestamps verify independent decoding, default formats, -type/stream filters, target selection, and continuation after channel errors. +type/stream filters, target and exact channel selection, and continuation after channel errors. CSV and CTF retain channel-qualified names; graphical views are collected in one `.traceanalysis.xml` without per-channel XML files. diff --git a/tools/ctrace/test/integration/README.md b/tools/ctrace/test/integration/README.md index 7f2349e21..8fee8b6c0 100644 --- a/tools/ctrace/test/integration/README.md +++ b/tools/ctrace/test/integration/README.md @@ -30,6 +30,8 @@ on two healthy three-channel targets. It checks successful completion and exactl six CSV files, six CTF bundles and two target XML files by name and file type; output contents are covered by the other tests. +Channel-selection tests cover all output modes, combined filters, missing channels, error isolation and artifact preservation. + ## Decode errors and retained output The formatted recovery tests verify that a damaged route can resynchronize diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index 93654a3e9..df21c01f8 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -1849,6 +1849,226 @@ TEST_F(CtraceIntegTests, GeneratesAllChannelOutputsForAllTargets) EXPECT_EQ(outputs, expectedOutputs); } +TEST_F(CtraceIntegTests, SelectsExactChannelsInEveryOutputMode) +{ + struct Mode { + std::string_view name; + std::string_view option; + bool csv; + bool ctf; + }; + constexpr std::array modes{{ + {"check", {}, false, false}, {"csv", "--csv", true, false}, + {"ctf", "--ctf", false, true}, {"all", "--all", true, true}, + }}; + const std::array, 4U> selections{{ + {"SWO"}, {"TB"}, {"TB_ETB"}, {"SWO", "TB_ETB", "SWO"}, + }}; + for (std::size_t selection = 0U; selection < selections.size(); ++selection) { + const auto& channels = selections[selection]; + const std::set uniqueChannels(channels.begin(), channels.end()); + for (const auto& mode : modes) { + const auto directory = workDirectory() / std::to_string(selection) / mode.name; + SCOPED_TRACE(directory.string()); + writeMultipleChannelFixture(directory, "Selected"); + writeMultipleChannelFixture(directory, "Other"); + std::vector arguments{"ctrace", "-c", channels.front(), directory.string(), + "--target", "Selected", "--verbose"}; + for (std::size_t index = 1U; index < channels.size(); ++index) { + arguments.emplace_back("--channel=" + channels[index]); + } + if (!mode.option.empty()) { + arguments.emplace_back(mode.option); + } + const auto result = run(std::move(arguments)); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + expectNotContains(result.stderrText, "[error]"); + EXPECT_EQ(countOccurrences(result.stderrText, "[info] raw input inspection context"), uniqueChannels.size()); + for (const auto& fixture : kChannelFixtures) { + const bool selected = uniqueChannels.count(std::string(fixture.channel)) != 0U; + expectChannelArtifacts(directory, "Selected", fixture, selected && mode.csv, selected && mode.ctf); + expectChannelArtifacts(directory, "Other", fixture, false, false); + } + const auto xmlPath = directory / "Selected.traceanalysis.xml"; + EXPECT_EQ(std::filesystem::exists(xmlPath), mode.ctf); + EXPECT_FALSE(std::filesystem::exists(directory / "Other.traceanalysis.xml")); + if (mode.ctf) { + const auto xml = readTestTextFile(xmlPath); + for (const auto& channel : uniqueChannels) { + expectCaptureView(xml, singleCtfClockUuid(directory / ("Selected." + channel + ".ctf")), + channel == "SWO" ? 0U : 1U, "PC_SAMPLE"); + } + EXPECT_EQ(traceCompassDefinitionIds(xml).size(), uniqueChannels.size() + 1U); + } + } + } +} + +TEST_F(CtraceIntegTests, SelectsChannelsAcrossSparseTargetsBeforeReadingUnusedConfigurations) +{ + writeMultipleChannelFixture(workDirectory(), "Alpha"); + writeMultipleChannelFixture(workDirectory(), "Beta"); + writeMultipleChannelFixture(workDirectory(), "Ignored"); + for (const auto name : {"Alpha.TB.raw", "Alpha.TB_ETB.raw", "Beta.SWO.raw", "Beta.TB.raw", + "Ignored.SWO.raw", "Ignored.TB_ETB.raw"}) { + ASSERT_TRUE(std::filesystem::remove(workDirectory() / name)); + } + writeTestFile(workDirectory() / "Ignored.ctrace-run.yml", "ctrace-run: ["); + writeTestFile(workDirectory() / "Ignored.ER.raw", "unsupported input"); + writeTestFile(workDirectory() / "Ignored.traceanalysis.xml", "existing XML"); + + const auto result = run({"ctrace", workDirectory().string(), "--all", "--channel", "SWO", "-c", "TB_ETB"}); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + expectNotContains(result.stderrText, "Ignored"); + for (const auto& fixture : kChannelFixtures) { + expectChannelArtifacts(workDirectory(), "Alpha", fixture, fixture.channel == "SWO", fixture.channel == "SWO"); + expectChannelArtifacts(workDirectory(), "Beta", fixture, fixture.channel == "TB_ETB", fixture.channel == "TB_ETB"); + expectChannelArtifacts(workDirectory(), "Ignored", fixture, false, false); + } + expectNonEmptyFile(workDirectory() / "Alpha.traceanalysis.xml"); + expectNonEmptyFile(workDirectory() / "Beta.traceanalysis.xml"); + EXPECT_EQ(readTestTextFile(workDirectory() / "Ignored.traceanalysis.xml"), "existing XML"); +} + +TEST_F(CtraceIntegTests, ReportsMissingChannelsWithinSelectedTargetAndCompletesMatches) +{ + writeMultipleChannelFixture(workDirectory(), "Selected"); + writeMultipleChannelFixture(workDirectory(), "Other"); + ASSERT_TRUE(std::filesystem::remove(workDirectory() / "Selected.TB_ETB.raw")); + + const auto result = run({"ctrace", workDirectory().string(), "--target", "Selected", "--all", + "-c", "SWO", "--channel", "TB_ETB", "-c", "TB_ETB"}); + EXPECT_EQ(result.exitCode, 1) << result.stderrText; + EXPECT_EQ(countOccurrences(result.stderrText, "no raw trace input found for requested channel TB_ETB"), 1U); + for (const auto& fixture : kChannelFixtures) { + expectChannelArtifacts(workDirectory(), "Selected", fixture, fixture.channel == "SWO", fixture.channel == "SWO"); + expectChannelArtifacts(workDirectory(), "Other", fixture, false, false); + } + expectNonEmptyFile(workDirectory() / "Selected.traceanalysis.xml"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Other.traceanalysis.xml")); +} + +TEST_F(CtraceIntegTests, PreservesOutputsAndSkipsYamlWhenChannelSelectionIsEmpty) +{ + for (const auto option : {"", "--csv", "--ctf", "--all"}) { + const auto directory = workDirectory() / (std::string_view(option).empty() ? "check" : option); + SCOPED_TRACE(option); + writeMultipleChannelFixture(directory, "Selected"); + writeTestFile(directory / "Selected.ctrace-run.yml", "ctrace-run: ["); + writeTestFile(directory / "Selected.SWO.csv", "existing CSV"); + writeTestFile(directory / "Selected.SWO.ctf" / "metadata", "existing metadata"); + writeTestFile(directory / "Selected.traceanalysis.xml", "existing XML"); + std::vector arguments{"ctrace", directory.string(), "--target", "Selected", "-c", "TB_Missing"}; + if (*option != '\0') { + arguments.emplace_back(option); + } + const auto result = run(std::move(arguments)); + EXPECT_EQ(result.exitCode, 1) << result.stderrText; + EXPECT_EQ(countOccurrences(result.stderrText, "[error]"), 1U); + expectContains(result.stderrText, "no raw trace input found for requested channel TB_Missing"); + EXPECT_EQ(readTestTextFile(directory / "Selected.SWO.csv"), "existing CSV"); + EXPECT_EQ(readTestTextFile(directory / "Selected.SWO.ctf" / "metadata"), "existing metadata"); + EXPECT_EQ(readTestTextFile(directory / "Selected.traceanalysis.xml"), "existing XML"); + EXPECT_FALSE(std::filesystem::exists(directory / "Selected.TB_Missing.csv")); + EXPECT_FALSE(std::filesystem::exists(directory / "Selected.TB_Missing.ctf")); + } +} + +TEST_F(CtraceIntegTests, PreservesUnselectedArtifactsAndExcludesOldBundlesFromXml) +{ + for (const bool selectUnreadable : {false, true}) { + const auto directory = workDirectory() / (selectUnreadable ? "selected" : "excluded"); + SCOPED_TRACE(directory.string()); + writeMultipleChannelFixture(directory, "Selected"); + auto result = run({"ctrace", directory.string(), "--all"}); + ASSERT_EQ(result.exitCode, 0) << result.stderrText; + const auto tbCsv = readTestTextFile(directory / "Selected.TB.csv"); + const auto tbMetadata = readTestTextFile(directory / "Selected.TB.ctf" / "metadata"); + const auto tbStream = readTestBinaryFile(directory / "Selected.TB.ctf" / "stream_1"); + const auto namedCsv = readTestTextFile(directory / "Selected.TB_ETB.csv"); + const auto namedMetadata = readTestTextFile(directory / "Selected.TB_ETB.ctf" / "metadata"); + const auto namedStream = readTestBinaryFile(directory / "Selected.TB_ETB.ctf" / "stream_1"); + const auto tbClock = singleCtfClockUuid(directory / "Selected.TB.ctf"); + const auto namedClock = singleCtfClockUuid(directory / "Selected.TB_ETB.ctf"); + std::filesystem::rename(directory / "Selected.TB.raw", directory / "original.raw"); + ASSERT_TRUE(std::filesystem::create_directory(directory / "Selected.TB.raw")); + + std::vector arguments{"ctrace", directory.string(), "--all", "-c", "SWO"}; + if (selectUnreadable) { + arguments.insert(arguments.end(), {"-c", "TB"}); + } + result = run(std::move(arguments)); + EXPECT_EQ(result.exitCode, selectUnreadable ? 1 : 0) << result.stderrText; + expectNotContains(result.stderrText, "no raw trace input found"); + if (selectUnreadable) { + expectContains(result.stderrText, "not a regular file"); + expectContains(result.stderrText, "inputChannel=TB"); + } else { + expectNotContains(result.stderrText, "[error]"); + } + expectChannelArtifacts(directory, "Selected", kChannelFixtures[0U], true, true); + EXPECT_EQ(readTestTextFile(directory / "Selected.TB.csv"), tbCsv); + EXPECT_EQ(readTestTextFile(directory / "Selected.TB.ctf" / "metadata"), tbMetadata); + EXPECT_EQ(readTestBinaryFile(directory / "Selected.TB.ctf" / "stream_1"), tbStream); + EXPECT_EQ(readTestTextFile(directory / "Selected.TB_ETB.csv"), namedCsv); + EXPECT_EQ(readTestTextFile(directory / "Selected.TB_ETB.ctf" / "metadata"), namedMetadata); + EXPECT_EQ(readTestBinaryFile(directory / "Selected.TB_ETB.ctf" / "stream_1"), namedStream); + const auto xml = readTestTextFile(directory / "Selected.traceanalysis.xml"); + expectCaptureView(xml, singleCtfClockUuid(directory / "Selected.SWO.ctf"), 0U, "PC_SAMPLE"); + expectNotContains(xml, tbClock); + expectNotContains(xml, namedClock); + EXPECT_EQ(traceCompassDefinitionIds(xml).size(), 2U); + } +} + +TEST_F(CtraceIntegTests, ReportsSelectedConfigurationFailureWithoutMissingChannelError) +{ + writeMultipleChannelFixture(workDirectory(), "Broken"); + writeMultipleChannelFixture(workDirectory(), "Healthy"); + writeTestFile(workDirectory() / "Broken.ctrace-run.yml", "ctrace-run: ["); + writeTestFile(workDirectory() / "Broken.traceanalysis.xml", "existing XML"); + + const auto result = run({"ctrace", workDirectory().string(), "--all", "-c", "TB"}); + EXPECT_EQ(result.exitCode, 1) << result.stderrText; + expectContains(result.stderrText, "[error]"); + expectContains(result.stderrText, "Broken.ctrace-run.yml"); + expectNotContains(result.stderrText, "no raw trace input found"); + for (const auto& fixture : kChannelFixtures) { + expectChannelArtifacts(workDirectory(), "Broken", fixture, false, false); + expectChannelArtifacts(workDirectory(), "Healthy", fixture, fixture.channel == "TB", fixture.channel == "TB"); + } + EXPECT_EQ(readTestTextFile(workDirectory() / "Broken.traceanalysis.xml"), "existing XML"); + expectNonEmptyFile(workDirectory() / "Healthy.traceanalysis.xml"); +} + +TEST_F(CtraceIntegTests, IntersectsChannelSelectionWithTargetTypeAndStreamFilters) +{ + writeMultipleChannelFixture(workDirectory(), "Selected"); + writeMultipleChannelFixture(workDirectory(), "Other"); + const auto result = run({"ctrace", workDirectory().string(), "--target", "Selected", "--all", + "-c", "SWO", "--channel", "TB_ETB", "--type", "itm", "--stream", "1"}); + EXPECT_EQ(result.exitCode, 0) << result.stderrText; + EXPECT_EQ(readTestTextFile(workDirectory() / "Selected.SWO.csv"), + "cycles,stream,type,index,value,pc,address,note\n"); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Selected.SWO.ctf" / "stream_0")); + expectNotContains(readTestTextFile(workDirectory() / "Selected.SWO.ctf" / "metadata"), "\nstream {"); + expectChannelArtifacts(workDirectory(), "Selected", kChannelFixtures[1U], false, false); + EXPECT_EQ(traceCsvRows(readTestTextFile(workDirectory() / "Selected.TB_ETB.csv")), + "cycles,stream,type,index,value,pc,address,note\n3,1,itm,1,0x43,,,\n"); + const auto records = CtfTestSupport::readCtfRecords(workDirectory() / "Selected.TB_ETB.ctf" / "stream_1", + CtfStreamWriter::EventContextLayout::RouteLabeled); + ASSERT_EQ(records.size(), 1U); + EXPECT_EQ(records.front().id, CtfSchema::value(CtfSchema::EventId::Itm)); + EXPECT_EQ(records.front().timestamp, 3U); + ASSERT_GE(records.front().payload.size(), 3U); + EXPECT_EQ(records.front().payload[2U], 0x43U); + for (const auto& fixture : kChannelFixtures) { + expectChannelArtifacts(workDirectory(), "Other", fixture, false, false); + } + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Selected.traceanalysis.xml")); + EXPECT_FALSE(std::filesystem::exists(workDirectory() / "Other.traceanalysis.xml")); +} + TEST_F(CtraceIntegTests, ContinuesOtherChannelsAndTargetsAfterIncompleteFormattedFrame) { writeMultipleChannelFixture(workDirectory(), "Alpha"); diff --git a/tools/ctrace/test/unit/src/cli/CliParserTests.cpp b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp index 7432d9cd8..8f7bf2144 100644 --- a/tools/ctrace/test/unit/src/cli/CliParserTests.cpp +++ b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp @@ -10,6 +10,7 @@ #include "CliOptions.h" #include "CliParser.h" #include +#include #include #include #include @@ -44,7 +45,7 @@ static bool validationErrorContains(std::initializer_list arguments /** @brief Tests whether parsing the supplied command line fails. */ static bool parseFails(std::initializer_list arguments) { - return throwsException([&arguments] { (void)parse(arguments); }); + return throwsException([&arguments] { (void)parse(arguments); }); } /** @brief Requires a command line to select the expected output format. */ @@ -89,6 +90,7 @@ TEST(CtraceUnitTests, testCliParserOutputOptions) "Filter output for specific packet types (default: all)", "Filter output for specific streams (default: all)", "Specify a trace solution-set (default: all)", + "Select an input channel (repeatable; default: all)", "Print version", "Show detailed diagnostics and processing information", }) { @@ -104,6 +106,71 @@ TEST(CtraceUnitTests, testCliParserOutputOptions) << "CliParser trailing trace directory must preserve selector values"; } +TEST(CtraceUnitTests, testCliParserRepeatsExactInputChannelsIndependentlyOfOutputFilters) +{ + EXPECT_TRUE(parseAndValidate({"ctrace", ".trace"}).channels.empty()); + + const auto selected = parseAndValidate( + {"ctrace", ".trace", "--target", "Board.Debug", "--type", "dwt", "itm", "-c", "SWO", "--stream", "1", "2", + "--channel=TB", "-c", "TB_ETB-0", "--channel", "TB_buffer_1", "-c", "SWO", "--all"}); + EXPECT_EQ(selected.channels, (std::vector{"SWO", "TB", "TB_ETB-0", "TB_buffer_1", "SWO"})); + EXPECT_EQ(selected.traceDir, std::optional(".trace")); + EXPECT_EQ(selected.targetName, std::optional("Board.Debug")); + EXPECT_EQ(selected.outputFormat, OutputFormat::All); + EXPECT_EQ(selected.selection.types, (std::vector{"dwt", "itm"})); + EXPECT_EQ(selected.selection.streams, (std::vector{1U, 2U})); + + const auto trailingTraceDirectory = + parseAndValidate({"ctrace", "--channel", "TB_ETB_0", ".trace", "-c", "SWO"}); + EXPECT_EQ(trailingTraceDirectory.traceDir, std::optional(".trace")); + EXPECT_EQ(trailingTraceDirectory.channels, (std::vector{"TB_ETB_0", "SWO"})); + + const auto delimitedTraceDirectory = parseAndValidate({"ctrace", "-c", "SWO", "--", ".trace"}); + EXPECT_EQ(delimitedTraceDirectory.traceDir, std::optional(".trace")); + EXPECT_EQ(delimitedTraceDirectory.channels, (std::vector{"SWO"})); + EXPECT_NE(CliParser::helpString().find("--channel"), std::string::npos); +} + +TEST(CtraceUnitTests, testCliParserRejectsUnsupportedOrMalformedInputChannels) +{ + for (const auto* channel : {"ER", "swo", "SwO", "tb", "TB_", "TBish", "TB_bad.name", "TB_bad name", + "TB_\xC3\x84", "TB_../other", "TB_bad/name", "TB_bad\\name", "TB_*", "SWO,TB", + "TB_ETB_0,", " SWO", "SWO "}) { + SCOPED_TRACE(channel); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "--channel", channel}, "Invalid --channel value:")); + } + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "-c", "ER", "-c", "SWO"}, + "Invalid --channel value: ER")); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "--channel=SWO,TB", "-c", "SWO"}, + "Invalid --channel value: SWO,TB")); +} + +TEST(CtraceUnitTests, testCliParserRequiresOneValuePerInputChannelOption) +{ + EXPECT_TRUE(parseFails({"ctrace", ".trace", "--channel"})); + EXPECT_TRUE(parseFails({"ctrace", ".trace", "-c"})); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "--channel="}, "Missing value for --channel")); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "-c", ""}, "Missing value for --channel")); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "--channel", "--csv"}, "Missing value for --channel")); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "-c", "--"}, "Missing value for --channel")); + EXPECT_TRUE(validationErrorContains({"ctrace", ".trace", "-c", "SWO", "TB"}, + "Unexpected positional argument: TB")); +} + +TEST(CtraceUnitTests, testCliParserInputChannelsPreserveHelpAndVersionValidation) +{ + const auto help = parseAndValidate({"ctrace", "--help", "--channel", "ER"}); + EXPECT_TRUE(help.help); + EXPECT_EQ(help.channels, (std::vector{"ER"})); + EXPECT_TRUE(parseFails({"ctrace", "--help", "--channel="})); + + const auto version = parseAndValidate({"ctrace", "--version", "-c", "SWO"}); + EXPECT_TRUE(version.version); + EXPECT_EQ(version.channels, (std::vector{"SWO"})); + EXPECT_TRUE(validationErrorContains({"ctrace", "--version", "--channel", "ER"}, + "Invalid --channel value: ER")); +} + TEST(CtraceUnitTests, testCliParserVerboseDoesNotChangeVersionOrOutputSelection) { EXPECT_FALSE(parseAndValidate({"ctrace", ".trace"}).verbose); diff --git a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp index 05e338f68..02d9ffae3 100644 --- a/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp +++ b/tools/ctrace/test/unit/src/control/TraceDirectoryJobTests.cpp @@ -20,6 +20,7 @@ #include "TraceDirectoryJob.h" #include "TraceRunConfig.h" #include "TraceRunConfigReader.h" +#include "YmlTraceRunConfigReader.h" #include "opencsd/ocsd_if_types.h" #include #include @@ -165,6 +166,162 @@ TEST(CtraceUnitTests, testTraceDirectoryTargetAndOutputNames) EXPECT_FALSE(std::filesystem::exists(traceDir / "Alpha.ER.csv")); } +TEST(CtraceUnitTests, testTraceDirectoryChannelSelectionProcessesDisjointTargetsAndPreservesExcludedOutputs) +{ + const TemporaryTestPath temporaryPath("ctrace-directory-channel-selection-test"); + const auto& traceDir = temporaryPath.path(); + writeTraceInputs(traceDir, {"Alpha"}); + writeTestFile(traceDir / "Beta.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Beta.TB.raw"); + writeTestFile(traceDir / "Other.ctrace-run.yml", "unselected config"); + writeTestFile(traceDir / "Other.TB_MTB.raw", "unselected raw"); + writeTestFile(traceDir / "Other.traceanalysis.xml", "unselected XML"); + std::filesystem::create_directory(traceDir / "Alpha.TB_MTB.raw"); + writeTestFile(traceDir / "Alpha.TB_MTB.csv", "unselected CSV"); + writeTestFile(traceDir / "Alpha.TB_MTB.ctf" / "sentinel", "unselected CTF"); + writeTestFile(traceDir / "Alpha.ER.raw", "unsupported"); + + auto config = defaultTraceRunConfig(); + config.traceFormat = TraceRunFormat::Unformatted; + CliOptions options; + options.traceDir = traceDir.string(); + options.channels = {"TB", "SWO", "SWO"}; + options.outputFormat = OutputFormat::All; + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_EQ(diagnostics.failureCount(), 0U); + EXPECT_EQ(reader.paths(), (std::vector{(traceDir / "Alpha.ctrace-run.yml").string(), + (traceDir / "Beta.ctrace-run.yml").string()})); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Alpha.SWO.csv")); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Beta.TB.csv")); + EXPECT_EQ(readTestTextFile(traceDir / "Alpha.TB_MTB.csv"), "unselected CSV"); + EXPECT_EQ(readTestTextFile(traceDir / "Alpha.TB_MTB.ctf" / "sentinel"), "unselected CTF"); + EXPECT_EQ(readTestTextFile(traceDir / "Other.traceanalysis.xml"), "unselected XML"); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Other.TB_MTB.csv")); + EXPECT_FALSE(diagnostics.containsMessage("skipping raw trace channel")); + EXPECT_EQ(std::count_if(diagnostics.events().begin(), diagnostics.events().end(), [](const auto& event) { + return event.message == "applied ctrace-run meta"; + }), 2); +} + +TEST(CtraceUnitTests, testTraceDirectoryChannelSelectionSkipsMalformedConfigsWithoutMatchingInputs) +{ + const TemporaryTestPath temporaryPath("ctrace-directory-channel-malformed-config-test"); + const auto& traceDir = temporaryPath.path(); + writeTestFile(traceDir / "Selected.ctrace-run.yml", "ctrace-run:\n ctrace-refs: []\n"); + writeTestFile(traceDir / "Selected.SWO.raw"); + writeTestFile(traceDir / "Skipped.ctrace-run.yml", "ctrace-run: ["); + writeTestFile(traceDir / "Skipped.TB.raw", "invalid raw"); + writeTestFile(traceDir / "Skipped.traceanalysis.xml", "unselected XML"); + + CliOptions options; + options.traceDir = traceDir.string(); + options.channels = {"SWO"}; + options.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink diagnostics; + YmlTraceRunConfigReader reader; + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_EQ(diagnostics.failureCount(), 0U); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Selected.SWO.csv")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Skipped.TB.csv")); + EXPECT_EQ(readTestTextFile(traceDir / "Skipped.traceanalysis.xml"), "unselected XML"); + EXPECT_FALSE(diagnostics.containsContext("solutionSet", "Skipped")); +} + +TEST(CtraceUnitTests, testTraceDirectoryChannelSelectionCountsInputsBeforeConfigAndPreflightFailures) +{ + for (const auto failure : {"configuration", "preflight"}) { + SCOPED_TRACE(failure); + const TemporaryTestPath temporaryPath("ctrace-directory-channel-failure-test"); + const auto& traceDir = temporaryPath.path(); + const std::string validConfig = "ctrace-run:\n trace-format: unformatted\n ctrace-refs: []\n"; + const bool invalidConfig = std::string_view(failure) == "configuration"; + writeTestFile(traceDir / "Alpha.ctrace-run.yml", invalidConfig ? "ctrace-run: [" : validConfig); + if (invalidConfig) { + writeTestFile(traceDir / "Alpha.SWO.raw"); + } else { + std::filesystem::create_directory(traceDir / "Alpha.SWO.raw"); + } + writeTestFile(traceDir / "Beta.ctrace-run.yml", validConfig); + writeTestFile(traceDir / "Beta.TB.raw"); + + CliOptions options; + options.traceDir = traceDir.string(); + options.channels = {"SWO", "TB"}; + options.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink diagnostics; + YmlTraceRunConfigReader reader; + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_EQ(diagnostics.failureCount(), 1U); + EXPECT_FALSE(diagnostics.containsMessage("no raw trace input found for requested channel")); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Alpha.SWO.csv")); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Beta.TB.csv")); + EXPECT_TRUE(diagnostics.containsMessage(invalidConfig ? "failed to parse trace-run configuration" + : "not a regular file")); + } +} + +TEST(CtraceUnitTests, testTraceDirectoryChannelSelectionReportsMissingChannelsWithinTargetScope) +{ + for (const auto& target : {std::optional{}, {"Alpha"}}) { + SCOPED_TRACE(target.value_or("")); + const TemporaryTestPath temporaryPath("ctrace-directory-channel-missing-scope-test"); + const auto& traceDir = temporaryPath.path(); + writeTraceInputs(traceDir, {"Alpha"}); + writeTestFile(traceDir / "Beta.ctrace-run.yml", "ctrace-run:\n"); + writeTestFile(traceDir / "Beta.TB.raw"); + auto config = defaultTraceRunConfig(); + config.traceFormat = TraceRunFormat::Unformatted; + + CliOptions options; + options.traceDir = traceDir.string(); + options.targetName = target; + options.channels = {"SWO", "TB", "TB_Absent", "TB_Absent"}; + options.outputFormat = OutputFormat::Csv; + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader(config); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_EQ(diagnostics.failureCount(), target.has_value() ? 2U : 1U); + EXPECT_EQ(reader.paths().size(), target.has_value() ? 1U : 2U); + EXPECT_TRUE(std::filesystem::is_regular_file(traceDir / "Alpha.SWO.csv")); + EXPECT_EQ(std::filesystem::exists(traceDir / "Beta.TB.csv"), !target.has_value()); + const auto* missing = findDiagnostic(diagnostics, "no raw trace input found for requested channel TB_Absent"); + ASSERT_NE(missing, nullptr); + EXPECT_EQ(missing->severity, DiagnosticSink::Severity::Error); + EXPECT_EQ(missing->impact, DiagnosticSink::Impact::Failing); + EXPECT_TRUE(diagnostics.containsContext("traceDir", traceDir.string())); + EXPECT_EQ(diagnostics.containsContext("target", "Alpha"), target.has_value()); + EXPECT_EQ(diagnostics.containsContext("channel", "TB"), target.has_value()); + } +} + +TEST(CtraceUnitTests, testTraceDirectoryChannelSelectionDoesNotPrepareEmptyTargets) +{ + const TemporaryTestPath temporaryPath("ctrace-directory-channel-empty-selection-test"); + const auto& traceDir = temporaryPath.path(); + writeTestFile(traceDir / "Skipped.ctrace-run.yml", "invalid config"); + writeTestFile(traceDir / "Skipped.TB.raw", "unselected raw"); + writeTestFile(traceDir / "Skipped.traceanalysis.xml", "unselected XML"); + CliOptions options; + options.traceDir = traceDir.string(); + options.channels = {"SWO", "SWO"}; + options.outputFormat = OutputFormat::All; + CollectingDiagnosticSink diagnostics; + TestTraceRunConfigReader reader({}, true); + TraceDirectoryJob(options, diagnostics, reader).run(); + + EXPECT_TRUE(reader.paths().empty()); + EXPECT_EQ(diagnostics.failureCount(), 1U); + EXPECT_TRUE(diagnostics.containsMessage("no raw trace input found for requested channel SWO")); + EXPECT_EQ(readTestTextFile(traceDir / "Skipped.traceanalysis.xml"), "unselected XML"); + EXPECT_FALSE(std::filesystem::exists(traceDir / "Skipped.TB.csv")); +} + TEST(CtraceUnitTests, testTraceDirectoryBatchCheckAndExplicitConfig) { const TemporaryTestPath temporaryPath("ctrace-trace-directory-check-test"); @@ -766,7 +923,7 @@ TEST(CtraceUnitTests, testInputSelectionAndPreflightNeverConstructDecoder) options.outputFormat = OutputFormat::All; const auto run = [&](const std::filesystem::path& configFile, TraceRunConfig config) { config.path = configFile.string(); - const auto inputs = TraceRunDiscovery::selectInputs(config); + const auto inputs = TraceRunDiscovery::selectInputs(config.path); auto input = TraceRunDiscovery::resolveInput(std::move(config), inputs.at(0U)); FileDecodeJob(options, std::move(input), diagnostics, factory).run(); }; diff --git a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp index bb216665a..bb2759bc6 100644 --- a/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp +++ b/tools/ctrace/test/unit/src/tracerun/TraceRunDiscoveryTests.cpp @@ -44,7 +44,7 @@ static TraceRunConfig inputConfig(const std::filesystem::path& configFile, /** @brief Selects and preflights the sole input belonging to a single-file test fixture. */ static TraceRunInputDescriptor resolveOnlyInput(const TraceRunConfig& config) { - const auto inputs = TraceRunDiscovery::selectInputs(config); + const auto inputs = TraceRunDiscovery::selectInputs(config.path); EXPECT_EQ(inputs.size(), 1U); return TraceRunDiscovery::resolveInput(config, inputs.at(0U)); } @@ -83,19 +83,16 @@ TEST(CtraceUnitTests, testTraceRunDiscovery) ASSERT_TRUE(selected[0].filename() == "Alpha.ctrace-run.yml") << "TraceRunDiscovery target path mismatch"; ASSERT_TRUE(TraceRunDiscovery::solutionSetName(selected[0]) == "Alpha") << "TraceRunDiscovery solution-set mismatch"; - for (const auto& format : {std::optional{}, {TraceRunFormat::Unformatted}, - {TraceRunFormat::Formatted}}) { - std::vector skippedChannels; - const auto inputs = TraceRunDiscovery::selectInputs( - inputConfig(selected[0], format), [&](const auto& input) { skippedChannels.push_back(input.channel); }); - const std::vector expected{"SWO", "TB", "TB_ETB-0", "TB_ETB_0", "TB_MTB"}; - ASSERT_EQ(inputs.size(), expected.size()); - for (std::size_t index = 0U; index < inputs.size(); ++index) { - EXPECT_EQ(inputs[index].channel, expected[index]); - EXPECT_EQ(inputs[index].path, traceDir / ("Alpha." + expected[index] + ".raw")); - } - EXPECT_EQ(skippedChannels, (std::vector{"ER"})); + std::vector skippedChannels; + const auto inputs = TraceRunDiscovery::selectInputs( + selected[0], {}, [&](const auto& input) { skippedChannels.push_back(input.channel); }); + const std::vector expected{"SWO", "TB", "TB_ETB-0", "TB_ETB_0", "TB_MTB"}; + ASSERT_EQ(inputs.size(), expected.size()); + for (std::size_t index = 0U; index < inputs.size(); ++index) { + EXPECT_EQ(inputs[index].channel, expected[index]); + EXPECT_EQ(inputs[index].path, traceDir / ("Alpha." + expected[index] + ".raw")); } + EXPECT_EQ(skippedChannels, (std::vector{"ER"})); const std::vector unsafeTargets{ "", @@ -141,6 +138,39 @@ TEST(CtraceUnitTests, testTraceRunDiscovery) EXPECT_THROW((void)TraceRunDiscovery::selectConfigFiles(emptyPath.path(), std::nullopt), std::runtime_error); } +TEST(CtraceUnitTests, testTraceRunDiscoverySelectsExactChannelsWithoutPreflightingExcludedInputs) +{ + const TemporaryTestPath temporaryPath("ctrace-trace-run-channel-selection-test"); + const auto& traceDir = temporaryPath.createDirectory(); + const auto configFile = traceDir / "Selected.ctrace-run.yml"; + for (const auto* channel : {"SWO", "TB", "TB_ETB-0", "ER"}) { + writeTestFile(traceDir / (std::string("Selected.") + channel + ".raw")); + } + std::filesystem::create_directory(traceDir / "Selected.TB_MTB.raw"); + + std::vector skippedChannels; + const auto inputs = TraceRunDiscovery::selectInputs( + configFile, {"TB_ETB-0", "SWO", "SWO"}, + [&](const auto& input) { skippedChannels.push_back(input.channel); }); + ASSERT_EQ(inputs.size(), 2U); + EXPECT_EQ(inputs[0].channel, "SWO"); + EXPECT_EQ(inputs[1].channel, "TB_ETB-0"); + EXPECT_TRUE(skippedChannels.empty()); + + const auto plainBuffer = TraceRunDiscovery::selectInputs(configFile, {"TB"}); + ASSERT_EQ(plainBuffer.size(), 1U); + EXPECT_EQ(plainBuffer[0].channel, "TB"); + EXPECT_TRUE(TraceRunDiscovery::selectInputs(configFile, {"swo"}).empty()); + EXPECT_TRUE(TraceRunDiscovery::selectInputs(configFile, {"TB_Absent"}).empty()); + EXPECT_TRUE(TraceRunDiscovery::selectInputs(traceDir / "Absent.ctrace-run.yml", {"SWO"}).empty()); + + const auto nonRegular = TraceRunDiscovery::selectInputs(configFile, {"TB_MTB"}); + ASSERT_EQ(nonRegular.size(), 1U); + EXPECT_TRUE(throwsWithMessage( + [&] { (void)TraceRunDiscovery::resolveInput(inputConfig(configFile), nonRegular.front()); }, + "not a regular file")); +} + TEST(CtraceUnitTests, testTraceRunDiscoveryResolvesOnePreflightedInput) { const TemporaryTestPath temporaryPath("ctrace-trace-run-input-resolution-test"); @@ -193,7 +223,7 @@ TEST(CtraceUnitTests, testTraceRunDiscoveryResolvesChannelDefaultsAndExplicitOve for (const auto& overrideFormat : {std::optional{}, {TraceRunFormat::Unformatted}, {TraceRunFormat::Formatted}}) { const auto config = inputConfig(root / "Default.ctrace-run.yml", overrideFormat); - const auto inputs = TraceRunDiscovery::selectInputs(config); + const auto inputs = TraceRunDiscovery::selectInputs(config.path); ASSERT_EQ(inputs.size(), 3U); for (const auto& input : inputs) { SCOPED_TRACE(input.path.string()); @@ -241,7 +271,7 @@ TEST(CtraceUnitTests, testTraceRunDiscoveryRejectsInvalidInputSelectionBeforePre "configuration has no source path")); const auto missingConfig = root / "Missing.ctrace-run.yml"; - EXPECT_TRUE(throwsWithMessage([&] { (void)TraceRunDiscovery::selectInputs(inputConfig(missingConfig)); }, + EXPECT_TRUE(throwsWithMessage([&] { (void)TraceRunDiscovery::selectInputs(missingConfig); }, "no eligible raw trace input")); const auto eventRecorderConfig = root / "ErOnly.ctrace-run.yml"; @@ -250,7 +280,7 @@ TEST(CtraceUnitTests, testTraceRunDiscoveryRejectsInvalidInputSelectionBeforePre std::vector skippedChannels; EXPECT_TRUE(throwsWithMessage( [&] { - (void)TraceRunDiscovery::selectInputs(inputConfig(eventRecorderConfig, TraceRunFormat::Formatted), + (void)TraceRunDiscovery::selectInputs(eventRecorderConfig, {}, [&](const auto& input) { skippedChannels.push_back(input.channel); }); }, "no eligible raw trace input")); From a4ce86a12592e12c8dcedc236e15481111feb0a2 Mon Sep 17 00:00:00 2001 From: Thorsten de Buhr Date: Fri, 23 Oct 2026 14:23:55 +0200 Subject: [PATCH 2/2] test(ctrace): reuse channel selection assertions --- .../test/integration/src/CtraceIntegTests.cpp | 136 ++++++++---------- .../test/unit/src/cli/CliParserTests.cpp | 5 +- 2 files changed, 62 insertions(+), 79 deletions(-) diff --git a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp index df21c01f8..62051f89e 100644 --- a/tools/ctrace/test/integration/src/CtraceIntegTests.cpp +++ b/tools/ctrace/test/integration/src/CtraceIntegTests.cpp @@ -38,6 +38,18 @@ struct RunResult { std::string stderrText; }; +struct OutputModeCase { + std::string_view name; + std::string_view option; + bool csv; + bool ctf; +}; + +constexpr std::array kOutputModes{{ + {"check", {}, false, false}, {"csv", "--csv", true, false}, + {"ctf", "--ctf", false, true}, {"all", "--all", true, true}, +}}; + class CtraceIntegTests : public testing::Test { protected: void SetUp() override @@ -633,6 +645,28 @@ void expectChannelArtifacts(const std::filesystem::path& directory, std::string_ } } +/** @brief Checks selected fixture channels and their shared target XML. */ +void expectTargetArtifacts(const std::filesystem::path& directory, std::string_view solutionSet, + const std::set& channels, const OutputModeCase& mode) +{ + SCOPED_TRACE(std::string(solutionSet)); + for (const auto& fixture : kChannelFixtures) { + const bool selected = channels.count(std::string(fixture.channel)) != 0U; + expectChannelArtifacts(directory, solutionSet, fixture, selected && mode.csv, selected && mode.ctf); + } + const auto xmlPath = directory / (std::string(solutionSet) + ".traceanalysis.xml"); + const bool xmlExpected = mode.ctf && !channels.empty(); + EXPECT_EQ(std::filesystem::exists(xmlPath), xmlExpected); + if (xmlExpected) { + const auto xml = readTestTextFile(xmlPath); + for (const auto& channel : channels) { + const auto capture = std::string(solutionSet) + "." + channel + ".ctf"; + expectCaptureView(xml, singleCtfClockUuid(directory / capture), channel == "SWO" ? 0U : 1U, "PC_SAMPLE"); + } + EXPECT_EQ(traceCompassDefinitionIds(xml).size(), channels.size() + 1U); + } +} + /** @brief Creates exactly one trace-run and SWO/TB inputs with disjoint graphical topics. */ void writeSwoAndTbTopicFixture(const std::filesystem::path& directory, std::string_view target, bool formattedSwo = false) @@ -1771,18 +1805,8 @@ TEST_F(CtraceIntegTests, ConvertsLegacyUnformattedSwoFixtureToGoldenOutputs) TEST_F(CtraceIntegTests, ProcessesEveryChannelOfSelectedTargetInEachOutputMode) { - struct Mode { - std::string_view name; - std::string_view option; - bool csv; - bool ctf; - }; - constexpr std::array modes{{ - {"check", {}, false, false}, {"csv", "--csv", true, false}, - {"ctf", "--ctf", false, true}, {"all", "--all", true, true}, - }}; for (const bool nullFormat : {false, true}) { - for (const auto& mode : modes) { + for (const auto& mode : kOutputModes) { const auto directory = workDirectory() / (nullFormat ? "null" : "absent") / mode.name; SCOPED_TRACE(directory.string()); writeMultipleChannelFixture(directory, "Selected", nullFormat); @@ -1794,22 +1818,8 @@ TEST_F(CtraceIntegTests, ProcessesEveryChannelOfSelectedTargetInEachOutputMode) const auto result = run(std::move(arguments)); EXPECT_EQ(result.exitCode, 0) << result.stderrText; expectNotContains(result.stderrText, "[error]"); - for (const auto& fixture : kChannelFixtures) { - expectChannelArtifacts(directory, "Selected", fixture, mode.csv, mode.ctf); - expectChannelArtifacts(directory, "Other", fixture, false, false); - } - const auto xmlPath = directory / "Selected.traceanalysis.xml"; - EXPECT_EQ(std::filesystem::exists(xmlPath), mode.ctf); - EXPECT_FALSE(std::filesystem::exists(directory / "Other.traceanalysis.xml")); - if (mode.ctf) { - const auto xml = readTestTextFile(xmlPath); - for (const auto& fixture : kChannelFixtures) { - const auto capture = "Selected." + std::string(fixture.channel) + ".ctf"; - expectCaptureView(xml, singleCtfClockUuid(directory / capture), fixture.channel == "SWO" ? 0U : 1U, - "PC_SAMPLE"); - } - traceCompassDefinitionIds(xml); - } + expectTargetArtifacts(directory, "Selected", {"SWO", "TB", "TB_ETB"}, mode); + expectTargetArtifacts(directory, "Other", {}, mode); EXPECT_FALSE(std::filesystem::exists(directory / "Selected.ctf")); } } @@ -1851,23 +1861,13 @@ TEST_F(CtraceIntegTests, GeneratesAllChannelOutputsForAllTargets) TEST_F(CtraceIntegTests, SelectsExactChannelsInEveryOutputMode) { - struct Mode { - std::string_view name; - std::string_view option; - bool csv; - bool ctf; - }; - constexpr std::array modes{{ - {"check", {}, false, false}, {"csv", "--csv", true, false}, - {"ctf", "--ctf", false, true}, {"all", "--all", true, true}, - }}; const std::array, 4U> selections{{ {"SWO"}, {"TB"}, {"TB_ETB"}, {"SWO", "TB_ETB", "SWO"}, }}; for (std::size_t selection = 0U; selection < selections.size(); ++selection) { const auto& channels = selections[selection]; const std::set uniqueChannels(channels.begin(), channels.end()); - for (const auto& mode : modes) { + for (const auto& mode : kOutputModes) { const auto directory = workDirectory() / std::to_string(selection) / mode.name; SCOPED_TRACE(directory.string()); writeMultipleChannelFixture(directory, "Selected"); @@ -1884,22 +1884,8 @@ TEST_F(CtraceIntegTests, SelectsExactChannelsInEveryOutputMode) EXPECT_EQ(result.exitCode, 0) << result.stderrText; expectNotContains(result.stderrText, "[error]"); EXPECT_EQ(countOccurrences(result.stderrText, "[info] raw input inspection context"), uniqueChannels.size()); - for (const auto& fixture : kChannelFixtures) { - const bool selected = uniqueChannels.count(std::string(fixture.channel)) != 0U; - expectChannelArtifacts(directory, "Selected", fixture, selected && mode.csv, selected && mode.ctf); - expectChannelArtifacts(directory, "Other", fixture, false, false); - } - const auto xmlPath = directory / "Selected.traceanalysis.xml"; - EXPECT_EQ(std::filesystem::exists(xmlPath), mode.ctf); - EXPECT_FALSE(std::filesystem::exists(directory / "Other.traceanalysis.xml")); - if (mode.ctf) { - const auto xml = readTestTextFile(xmlPath); - for (const auto& channel : uniqueChannels) { - expectCaptureView(xml, singleCtfClockUuid(directory / ("Selected." + channel + ".ctf")), - channel == "SWO" ? 0U : 1U, "PC_SAMPLE"); - } - EXPECT_EQ(traceCompassDefinitionIds(xml).size(), uniqueChannels.size() + 1U); - } + expectTargetArtifacts(directory, "Selected", uniqueChannels, mode); + expectTargetArtifacts(directory, "Other", {}, mode); } } } @@ -1950,17 +1936,17 @@ TEST_F(CtraceIntegTests, ReportsMissingChannelsWithinSelectedTargetAndCompletesM TEST_F(CtraceIntegTests, PreservesOutputsAndSkipsYamlWhenChannelSelectionIsEmpty) { - for (const auto option : {"", "--csv", "--ctf", "--all"}) { - const auto directory = workDirectory() / (std::string_view(option).empty() ? "check" : option); - SCOPED_TRACE(option); + for (const auto& mode : kOutputModes) { + const auto directory = workDirectory() / mode.name; + SCOPED_TRACE(mode.name); writeMultipleChannelFixture(directory, "Selected"); writeTestFile(directory / "Selected.ctrace-run.yml", "ctrace-run: ["); writeTestFile(directory / "Selected.SWO.csv", "existing CSV"); writeTestFile(directory / "Selected.SWO.ctf" / "metadata", "existing metadata"); writeTestFile(directory / "Selected.traceanalysis.xml", "existing XML"); std::vector arguments{"ctrace", directory.string(), "--target", "Selected", "-c", "TB_Missing"}; - if (*option != '\0') { - arguments.emplace_back(option); + if (!mode.option.empty()) { + arguments.emplace_back(mode.option); } const auto result = run(std::move(arguments)); EXPECT_EQ(result.exitCode, 1) << result.stderrText; @@ -1982,14 +1968,16 @@ TEST_F(CtraceIntegTests, PreservesUnselectedArtifactsAndExcludesOldBundlesFromXm writeMultipleChannelFixture(directory, "Selected"); auto result = run({"ctrace", directory.string(), "--all"}); ASSERT_EQ(result.exitCode, 0) << result.stderrText; - const auto tbCsv = readTestTextFile(directory / "Selected.TB.csv"); - const auto tbMetadata = readTestTextFile(directory / "Selected.TB.ctf" / "metadata"); - const auto tbStream = readTestBinaryFile(directory / "Selected.TB.ctf" / "stream_1"); - const auto namedCsv = readTestTextFile(directory / "Selected.TB_ETB.csv"); - const auto namedMetadata = readTestTextFile(directory / "Selected.TB_ETB.ctf" / "metadata"); - const auto namedStream = readTestBinaryFile(directory / "Selected.TB_ETB.ctf" / "stream_1"); - const auto tbClock = singleCtfClockUuid(directory / "Selected.TB.ctf"); - const auto namedClock = singleCtfClockUuid(directory / "Selected.TB_ETB.ctf"); + std::vector>> previousArtifacts; + std::vector previousClocks; + for (const auto channel : {"TB", "TB_ETB"}) { + const auto base = std::string("Selected.") + channel; + const auto ctfDirectory = directory / (base + ".ctf"); + for (const auto& path : {directory / (base + ".csv"), ctfDirectory / "metadata", ctfDirectory / "stream_1"}) { + previousArtifacts.emplace_back(path, readTestBinaryFile(path)); + } + previousClocks.push_back(singleCtfClockUuid(ctfDirectory)); + } std::filesystem::rename(directory / "Selected.TB.raw", directory / "original.raw"); ASSERT_TRUE(std::filesystem::create_directory(directory / "Selected.TB.raw")); @@ -2007,16 +1995,14 @@ TEST_F(CtraceIntegTests, PreservesUnselectedArtifactsAndExcludesOldBundlesFromXm expectNotContains(result.stderrText, "[error]"); } expectChannelArtifacts(directory, "Selected", kChannelFixtures[0U], true, true); - EXPECT_EQ(readTestTextFile(directory / "Selected.TB.csv"), tbCsv); - EXPECT_EQ(readTestTextFile(directory / "Selected.TB.ctf" / "metadata"), tbMetadata); - EXPECT_EQ(readTestBinaryFile(directory / "Selected.TB.ctf" / "stream_1"), tbStream); - EXPECT_EQ(readTestTextFile(directory / "Selected.TB_ETB.csv"), namedCsv); - EXPECT_EQ(readTestTextFile(directory / "Selected.TB_ETB.ctf" / "metadata"), namedMetadata); - EXPECT_EQ(readTestBinaryFile(directory / "Selected.TB_ETB.ctf" / "stream_1"), namedStream); + for (const auto& [path, bytes] : previousArtifacts) { + EXPECT_EQ(readTestBinaryFile(path), bytes) << path; + } const auto xml = readTestTextFile(directory / "Selected.traceanalysis.xml"); expectCaptureView(xml, singleCtfClockUuid(directory / "Selected.SWO.ctf"), 0U, "PC_SAMPLE"); - expectNotContains(xml, tbClock); - expectNotContains(xml, namedClock); + for (const auto& clock : previousClocks) { + expectNotContains(xml, clock); + } EXPECT_EQ(traceCompassDefinitionIds(xml).size(), 2U); } } diff --git a/tools/ctrace/test/unit/src/cli/CliParserTests.cpp b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp index 8f7bf2144..5cd402c5a 100644 --- a/tools/ctrace/test/unit/src/cli/CliParserTests.cpp +++ b/tools/ctrace/test/unit/src/cli/CliParserTests.cpp @@ -35,10 +35,7 @@ static CliOptions parseAndValidate(std::initializer_list arguments) static bool validationErrorContains(std::initializer_list arguments, std::string_view expected) { return throwsWithMessage( - [&arguments] { - const auto options = parse(arguments); - CliParser::validate(options); - }, + [&arguments] { (void)parseAndValidate(arguments); }, expected); }