Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
o introduces `TransactionName` enum with `TransactionOnly` and `InheritNameByRequests` variants
- [#578](https://github.com/tag1consulting/goose/pull/578) add type-safe client builder for cookie configuration, optimize startup with shared clients
- [#629](https://github.com/tag1consulting/goose/pull/629) add `--pdf-print-html` option to generate printer-friendly HTML optimized for PDF conversion; provides two-step PDF workflow without requiring Chromium dependencies; add `--pdf-timeout` option for configurable Chrome timeout in direct PDF generation (10-300s, default: 60)
- [#655](https://github.com/tag1consulting/goose/pull/655) fix scenario filtering to use exact matching instead of substring matching; introduce wildcard support with `*` character for pattern matching; update documentation with examples of both exact (`--scenarios scenarioname`) and wildcard (`--scenarios pattern*`) usage

## 0.18.1 August 14, 2025
- [#634](https://github.com/tag1consulting/goose/pull/634) add killswitch mechanism for programmatic test termination
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ tokio = { version = "1", features = [
tokio-tungstenite = "0.27"
tungstenite = "0.27"
url = "2"
wildcard = "0.3"
headless_chrome = { version = "1.0", features = ["fetch"], optional = true }
urlencoding = { version = "2.1", optional = true }

Expand Down
15 changes: 12 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,23 @@ impl FromStr for Scenarios {
for line in lines {
// Ignore white space an case.
let scenario = line.trim().to_lowercase();
// Valid scenario names are alphanumeric only.
if scenario.chars().all(char::is_alphanumeric) {
// Valid scenario names are alphanumeric or contain wildcards (*, ?, []).
if scenario.chars().all(|c| {
char::is_alphanumeric(c)
|| c == '*'
|| c == '?'
|| c == '['
|| c == ']'
|| c == '-'
|| c == '!'
|| c == '_'
}) {
active.push(scenario);
} else {
// Logger isn't initialized yet, provide helpful debug output.
eprintln!("ERROR: invalid `configuration.scenarios` value: '{line}'");
eprintln!(" Expected format: --scenarios \"{{one}},{{two}},{{three}}\"");
eprintln!(" {{one}}, {{two}}, {{three}}, etc must be alphanumeric");
eprintln!(" {{one}}, {{two}}, {{three}}, etc must be alphanumeric or contain wildcards (*, ?, [abc], [a-z])");
eprintln!(" To view valid scenario names invoke `--scenarios-list`");
return Err(GooseError::InvalidOption {
option: "`configuration.scenarios".to_string(),
Expand Down
25 changes: 17 additions & 8 deletions src/docs/goose-book/src/getting-started/scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ Scenarios:

## Running Scenarios By Machine Name

It is now possible to run any subset of the above scenarios by passing a comma separated list of machine names with the `--scenarios` run time option. Goose will match what you have typed against any machine name containing all or some of the typed text, so you do not have to type the full name. For example, to run only the two anonymous Scenarios, you could add `--scenarios anon`:
It is now possible to run any subset of the above scenarios by passing a comma separated list of machine names with the `--scenarios` run time option. Goose uses exact matching by default, but supports wildcard pattern matching using the `*` character. For example, to run only the two anonymous Scenarios using a wildcard pattern, you could add `--scenarios anon*`:

```bash,ignore
% cargo run --release --example umami -- --hatch-rate 10 --scenarios anon
% cargo run --release --example umami -- --hatch-rate 10 --scenarios "anon*"
Finished release [optimized] target(s) in 0.15s
Running `target/release/examples/umami --hatch-rate 10 --scenarios anon`
Running `target/release/examples/umami --hatch-rate 10 --scenarios anon*`
05:50:17 [INFO] Output verbosity level: INFO
05:50:17 [INFO] Logfile verbosity level: WARN
05:50:17 [INFO] users defaulted to number of CPUs = 10
05:50:17 [INFO] hatch_rate = 10
05:50:17 [INFO] iterations = 0
05:50:17 [INFO] scenarios = Scenarios { active: ["anon"] }
05:50:17 [INFO] scenarios = Scenarios { active: ["anon*"] }
05:50:17 [INFO] host for Anonymous English user configured: https://drupal-9.ddev.site/
05:50:17 [INFO] host for Anonymous Spanish user configured: https://drupal-9.ddev.site/
05:50:17 [INFO] host for Admin user configured: https://drupal-9.ddev.site/
Expand All @@ -63,19 +63,26 @@ It is now possible to run any subset of the above scenarios by passing a comma s
^C05:50:18 [WARN] caught ctrl-c, stopping...
```

Or, to run only the "Anonymous Spanish user" and "Admin user" Scenarios, you could add `--senarios "spanish,admin"`:
### Exact vs Wildcard Matching

Goose supports two types of scenario matching:

- **Exact matching**: `--scenarios adminuser` matches only the scenario with machine name `adminuser`
- **Wildcard matching**: `--scenarios admin*` matches all scenarios whose machine names start with `admin`

For example, to run only the "Anonymous Spanish user" scenario exactly, you could use `--scenarios anonymousspanishuser`. Or to run multiple specific scenarios, you could use `--scenarios "anonymousspanishuser,adminuser"`:

```bash,ignore
% cargo run --release --example umami -- --hatch-rate 10 --scenarios "spanish,admin"
% cargo run --release --example umami -- --hatch-rate 10 --scenarios "anonymousspanishuser,adminuser"
Compiling goose v0.18.1 (/Users/jandrews/devel/goose)
Finished release [optimized] target(s) in 11.79s
Running `target/release/examples/umami --hatch-rate 10 --scenarios spanish,admin`
Running `target/release/examples/umami --hatch-rate 10 --scenarios anonymousspanishuser,adminuser`
05:53:45 [INFO] Output verbosity level: INFO
05:53:45 [INFO] Logfile verbosity level: WARN
05:53:45 [INFO] users defaulted to number of CPUs = 10
05:53:45 [INFO] hatch_rate = 10
05:53:45 [INFO] iterations = 0
05:53:45 [INFO] scenarios = Scenarios { active: ["spanish", "admin"] }
05:53:45 [INFO] scenarios = Scenarios { active: ["anonymousspanishuser", "adminuser"] }
05:53:45 [INFO] host for Anonymous English user configured: https://drupal-9.ddev.site/
05:53:45 [INFO] host for Anonymous Spanish user configured: https://drupal-9.ddev.site/
05:53:45 [INFO] host for Admin user configured: https://drupal-9.ddev.site/
Expand All @@ -97,4 +104,6 @@ Or, to run only the "Anonymous Spanish user" and "Admin user" Scenarios, you cou
^C05:53:46 [WARN] caught ctrl-c, stopping...
```

You can also mix exact and wildcard matching in the same command: `--scenarios "adminuser,anon*"` would run the exact `adminuser` scenario plus all scenarios starting with `anon`.

When the load test completes, you can refer to the [Scenario metrics](./metrics.html#scenarios) to confirm which Scenarios were enabled, and which were not.
29 changes: 19 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

#[macro_use]
extern crate log;
extern crate wildcard;

pub mod client;
pub mod config;
Expand Down Expand Up @@ -742,19 +743,27 @@ impl GooseAttack {

/// Internal helper to determine if the scenario is currently active.
fn scenario_is_active(&self, scenario: &Scenario) -> bool {
// All scenarios are enabled by default.
// If no scenarios are configured, all are active.
if self.configuration.scenarios.active.is_empty() {
true
// Returns true or false depending on if the machine name is included in the
return true;
}
// Otherwise, check to see if this scenario is active, matching against
// configured `--scenarios`.
} else {
for active in &self.configuration.scenarios.active {
if scenario.machine_name.contains(active) {
return true;
}
self.configuration.scenarios.active.iter().any(|active| {
if active.contains('*') {
self.wildcard_match(active, &scenario.machine_name)
} else {
scenario.machine_name == *active
}
// No matches found, this scenario is not active.
false
})
}

/// Helper function to perform wildcard matching using the wildcard crate.
fn wildcard_match(&self, pattern: &str, text: &str) -> bool {
// Convert strings to bytes for the wildcard crate
match wildcard::Wildcard::new(pattern.as_bytes()) {
Ok(wildcard) => wildcard.is_match(text.as_bytes()),
Err(_) => false, // If pattern is invalid, fall back to no match
}
}

Expand Down
Loading
Loading