Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bazelversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.4.2
8.6.0
24 changes: 24 additions & 0 deletions examples/BUILD
Original file line number Diff line number Diff line change
@@ -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"],
)
42 changes: 42 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions score/test_scenarios_cpp/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 = [
Expand Down
94 changes: 94 additions & 0 deletions score/test_scenarios_cpp/examples/basic.cpp
Original file line number Diff line number Diff line change
@@ -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 <cli.hpp>
#include <scenario.hpp>
#include <test_context.hpp>
#include <tracing.hpp>

#include <nlohmann/json.hpp>

#include <cstddef>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

namespace {

const std::string kTargetName{"examples::basic::list"};

std::vector<std::string> 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<std::vector<std::string>>();
}

// 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<std::string> raw_arguments{argv, argv + argc};

ScenarioGroup::Ptr list_group{new ScenarioGroupImpl{
"list",
std::vector<Scenario::Ptr>{std::make_shared<EnumerateScenario>(),
std::make_shared<RequireNonEmptyScenario>()},
std::vector<ScenarioGroup::Ptr>{}}};
ScenarioGroup::Ptr root_group{new ScenarioGroupImpl{
"root", std::vector<Scenario::Ptr>{}, std::vector<ScenarioGroup::Ptr>{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;
}
}
14 changes: 13 additions & 1 deletion score/test_scenarios_rust/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions score/test_scenarios_rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions score/test_scenarios_rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
90 changes: 90 additions & 0 deletions score/test_scenarios_rust/examples/basic.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

impl ItemsInput {
fn parse(input: &str) -> Result<Self, String> {
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<String> = 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
}
}
}
Loading