diff --git a/.bazelversion b/.bazelversion index e7fdef7..acd405b 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -8.4.2 +8.6.0 diff --git a/examples/BUILD b/examples/BUILD new file mode 100644 index 0000000..c05ec12 --- /dev/null +++ b/examples/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +alias( + name = "cpp_basic", + actual = "//score/test_scenarios_cpp:basic", + visibility = ["//visibility:public"], +) + +alias( + name = "rust_basic", + actual = "//score/test_scenarios_rust:basic", + visibility = ["//visibility:public"], +) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..ef842cb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,42 @@ +# testing_tools Examples + +Example programs demonstrating `test_scenarios_cpp` and `test_scenarios_rust`. C++ examples are +located in [score/test_scenarios_cpp/examples](../score/test_scenarios_cpp/examples), and Rust +examples in [score/test_scenarios_rust/examples](../score/test_scenarios_rust/examples). In this +directory there are Bazel aliases for convenience. + +## Running Examples + +```bash +bazel run //examples:cpp_basic -- --list-scenarios +bazel run //examples:cpp_basic -- --name list.enumerate --input '{"items":["a","b","c"]}' +bazel run //examples:cpp_basic -- --name list.require_non_empty --input '{"items":["a"]}' +bazel run //examples:cpp_basic -- --name list.require_non_empty --input '{"items":[]}' + +bazel run //examples:rust_basic -- --list-scenarios +bazel run //examples:rust_basic -- --name list.enumerate --input '{"items":["a","b","c"]}' +bazel run //examples:rust_basic -- --name list.require_non_empty --input '{"items":["a"]}' +bazel run //examples:rust_basic -- --name list.require_non_empty --input '{"items":[]}' +``` + +## Available Examples + +### basic (C++ and Rust) + +A foundational example demonstrating the core building blocks of the library: + +- Implementing `Scenario` — both scenarios parse a JSON `{"items": [...]}` input (failing + genuinely on malformed JSON or a missing `items` field). `list.enumerate` logs each item with + its index via structured tracing; `list.require_non_empty` fails genuinely if `items` is empty, + otherwise logs the count +- Structured logging via the library's own tracing support (`TRACING_INFO` in C++, + `tracing::info!` over a subscriber built with `create_tracing_subscriber()` in Rust), with + timestamps from the library's monotonic clock +- Grouping scenarios with `ScenarioGroupImpl`, including a nested group (`list`) to show how + nesting produces dotted scenario names (`list.enumerate`, `list.require_non_empty`) +- Wiring a `TestContext` and running it through `run_cli_app`, which provides + `--list-scenarios`, `--name`, and `--input` for free + +**Key concepts:** `Scenario`, `ScenarioGroupImpl` (including nested groups), `TestContext`, +`run_cli_app`, structured tracing with a monotonic clock, JSON input parsing, CLI argument +handling, error propagation diff --git a/score/test_scenarios_cpp/BUILD b/score/test_scenarios_cpp/BUILD index 983eaf9..6df3fb4 100644 --- a/score/test_scenarios_cpp/BUILD +++ b/score/test_scenarios_cpp/BUILD @@ -10,6 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") load("//tools/lint:linters.bzl", "clang_tidy_test") @@ -39,6 +40,16 @@ cc_library( deps = ["@nlohmann_json//:json"], ) +cc_binary( + name = "basic", + srcs = ["examples/basic.cpp"], + visibility = ["//visibility:public"], + deps = [ + ":test_scenarios_cpp", + "@nlohmann_json//:json", + ], +) + cc_test( name = "tests", srcs = [ diff --git a/score/test_scenarios_cpp/examples/basic.cpp b/score/test_scenarios_cpp/examples/basic.cpp new file mode 100644 index 0000000..2ecc2db --- /dev/null +++ b/score/test_scenarios_cpp/examples/basic.cpp @@ -0,0 +1,94 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +const std::string kTargetName{"examples::basic::list"}; + +std::vector parse_items(const std::string& input) { + nlohmann::json parsed; + try { + parsed = nlohmann::json::parse(input); + } catch (const nlohmann::json::exception& e) { + throw std::runtime_error{"invalid input: " + std::string{e.what()}}; + } + if (!parsed.contains("items")) { + throw std::runtime_error{"invalid input: missing 'items' field"}; + } + return parsed.at("items").get>(); +} + +// Logs each item with its index via structured tracing. +class EnumerateScenario final : public Scenario { + public: + std::string name() const override { return "enumerate"; } + + void run(const std::string& input) const override { + auto items{parse_items(input)}; + for (std::size_t index = 0; index < items.size(); ++index) { + TRACING_INFO(kTargetName, std::pair{std::string{"index"}, index}, + std::pair{std::string{"item"}, items[index]}); + } + } +}; + +// Fails if the input has no items. +class RequireNonEmptyScenario final : public Scenario { + public: + std::string name() const override { return "require_non_empty"; } + + void run(const std::string& input) const override { + auto items{parse_items(input)}; + if (items.empty()) { + throw std::runtime_error{"items must not be empty"}; + } + TRACING_INFO(kTargetName, std::pair{std::string{"count"}, items.size()}); + } +}; + +} // namespace + +int main(int argc, char* argv[]) { + std::vector raw_arguments{argv, argv + argc}; + + ScenarioGroup::Ptr list_group{new ScenarioGroupImpl{ + "list", + std::vector{std::make_shared(), + std::make_shared()}, + std::vector{}}}; + ScenarioGroup::Ptr root_group{new ScenarioGroupImpl{ + "root", std::vector{}, std::vector{list_group}}}; + TestContext test_context{root_group}; + + try { + run_cli_app(raw_arguments, test_context); + return 0; + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return 1; + } +} diff --git a/score/test_scenarios_rust/BUILD b/score/test_scenarios_rust/BUILD index 03e6f16..8908563 100644 --- a/score/test_scenarios_rust/BUILD +++ b/score/test_scenarios_rust/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") rust_library( name = "test_scenarios_rust", @@ -22,6 +22,18 @@ rust_library( ], ) +rust_binary( + name = "basic", + srcs = ["examples/basic.rs"], + visibility = ["//visibility:public"], + deps = [ + ":test_scenarios_rust", + "@score_crates//:serde", + "@score_crates//:serde_json", + "@score_crates//:tracing", + ], +) + rust_test( name = "tests", crate = ":test_scenarios_rust", diff --git a/score/test_scenarios_rust/Cargo.lock b/score/test_scenarios_rust/Cargo.lock index aa92dbb..e044baa 100644 --- a/score/test_scenarios_rust/Cargo.lock +++ b/score/test_scenarios_rust/Cargo.lock @@ -146,6 +146,8 @@ dependencies = [ name = "test_scenarios_rust" version = "0.3.1" dependencies = [ + "serde", + "serde_json", "tracing", "tracing-subscriber", ] diff --git a/score/test_scenarios_rust/Cargo.toml b/score/test_scenarios_rust/Cargo.toml index c3100d3..ec5625b 100644 --- a/score/test_scenarios_rust/Cargo.toml +++ b/score/test_scenarios_rust/Cargo.toml @@ -4,5 +4,7 @@ version = "0.3.1" edition = "2021" [dependencies] +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["json"] } diff --git a/score/test_scenarios_rust/examples/basic.rs b/score/test_scenarios_rust/examples/basic.rs new file mode 100644 index 0000000..ba34c0f --- /dev/null +++ b/score/test_scenarios_rust/examples/basic.rs @@ -0,0 +1,90 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use std::process::ExitCode; + +use serde::Deserialize; +use tracing::info; + +use test_scenarios_rust::cli::{create_tracing_subscriber, run_cli_app}; +use test_scenarios_rust::scenario::{Scenario, ScenarioGroupImpl}; +use test_scenarios_rust::test_context::TestContext; + +#[derive(Deserialize)] +struct ItemsInput { + items: Vec, +} + +impl ItemsInput { + fn parse(input: &str) -> Result { + serde_json::from_str(input).map_err(|e| format!("invalid input: {e}")) + } +} + +/// Logs each item with its index via structured tracing. +struct EnumerateScenario; + +impl Scenario for EnumerateScenario { + fn name(&self) -> &str { + "enumerate" + } + + fn run(&self, input: &str) -> Result<(), String> { + let parsed = ItemsInput::parse(input)?; + for (index, item) in parsed.items.iter().enumerate() { + info!(index, item = item.as_str()); + } + Ok(()) + } +} + +/// Fails if the input has no items. +struct RequireNonEmptyScenario; + +impl Scenario for RequireNonEmptyScenario { + fn name(&self) -> &str { + "require_non_empty" + } + + fn run(&self, input: &str) -> Result<(), String> { + let parsed = ItemsInput::parse(input)?; + if parsed.items.is_empty() { + return Err("items must not be empty".to_string()); + } + info!(count = parsed.items.len()); + Ok(()) + } +} + +fn main() -> ExitCode { + tracing::subscriber::set_global_default(create_tracing_subscriber()) + .expect("Setting default subscriber failed!"); + + let raw_arguments: Vec = std::env::args().collect(); + + let list_group = ScenarioGroupImpl::new( + "list", + vec![Box::new(EnumerateScenario), Box::new(RequireNonEmptyScenario)], + Vec::new(), + ); + let root_group = ScenarioGroupImpl::new("root", Vec::new(), vec![Box::new(list_group)]); + let test_context = TestContext::new(Box::new(root_group)); + + match run_cli_app(&raw_arguments, &test_context) { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("{e}"); + ExitCode::FAILURE + } + } +}