goose/goose.rs
1//! Helpers and objects for building Goose load tests.
2//!
3//! Goose manages load tests with a series of objects:
4//!
5//! - [`Scenario`] each user is assigned a scenario, which is a collection of transactions.
6//! - [`Transaction`] transactions define one or more web requests and are assigned to scenarios.
7//! - [`GooseUser`] a user state responsible for repeatedly running all transactions in the assigned scenario.
8//! - [`GooseRequestMetric`] optional metrics collected for each URL/method pair.
9//!
10//! ## Creating Scenarios
11//!
12//! A [`Scenario`](./struct.Scenario.html) is created by passing in a `&str` name to the `new` function, for example:
13//!
14//! ```rust
15//! use goose::prelude::*;
16//!
17//! let mut loadtest_transactions = scenario!("LoadtestTransactions");
18//! ```
19//!
20//! ### Scenario Weight
21//!
22//! A weight can be applied to a scenario, controlling how often it is assigned to
23//! [`GooseUser`](../goose/struct.GooseUser.html) threads. The larger the integer value
24//! of weight, the more the scenario will be assigned to user threads. In the following
25//! example, `FooTransactions` will be assigned to users twice as often as `Bar`
26//! Transactions. We could have just added a weight of `2` to `FooTransactions` and left
27//! the default weight of `1` assigned to `BarTransactions` for the same weighting:
28//!
29//! ```rust
30//! use goose::prelude::*;
31//!
32//! #[tokio::main]
33//! async fn main() -> Result<(), GooseError> {
34//! let mut foo_transactions = scenario!("FooTransactions").set_weight(10)?;
35//! let mut bar_transactions = scenario!("BarTransactions").set_weight(5)?;
36//!
37//! Ok(())
38//! }
39//! ```
40//!
41//! ### Scenario Host
42//!
43//! A default host can be assigned to a scenario, which will be used only if the `--host`
44//! CLI option is not set at run-time. For example, this can configure your load test to
45//! run against your local development environment by default, allowing the `--host` option
46//! to override host when you want to load test production. You can also assign different
47//! hosts to different scenario if this is desirable:
48//!
49//! ```rust
50//! use goose::prelude::*;
51//!
52//! let mut foo_transactions = scenario!("FooTransactions").set_host("http://www.local");
53//! let mut bar_transactions = scenario!("BarTransactions").set_host("http://www2.local");
54//! ```
55//!
56//! ### Scenario Wait Time
57//!
58//! Wait time is specified as a low-high Duration range. Each time a transaction completes in the
59//! scenario, the user will pause for a random number of milliseconds inclusively between
60//! the low and high wait times. In the following example, users loading `foo` transactions will
61//! sleep 0 to 2.5 seconds after each transaction completes, and users loading `bar` transactions will
62//! always sleep 5 seconds after each transaction completes.
63//!
64//! ```rust
65//! use goose::prelude::*;
66//! use std::time::Duration;
67//!
68//! let mut foo_transactions = scenario!("FooTransactions").set_wait_time(Duration::from_secs(0), Duration::from_millis(2500)).unwrap();
69//! let mut bar_transactions = scenario!("BarTransactions").set_wait_time(Duration::from_secs(5), Duration::from_secs(5)).unwrap();
70//! ```
71//! ## Creating Transactions
72//!
73//! A [`Transaction`](./struct.Transaction.html) must include a pointer to a function which
74//! will be executed each time the transaction is run.
75//!
76//! ```rust
77//! use goose::prelude::*;
78//!
79//! let mut a_transaction = transaction!(transaction_function);
80//!
81//! /// A very simple transaction that loads the front page.
82//! async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
83//! let _goose = user.get("").await?;
84//!
85//! Ok(())
86//! }
87//! ```
88//!
89//! ### Transaction Name
90//!
91//! A name can be assigned to a transaction, and will be displayed in metrics about all requests
92//! made by the transaction.
93//!
94//! ```rust
95//! use goose::prelude::*;
96//!
97//! let mut a_transaction = transaction!(transaction_function).set_name("a");
98//!
99//! /// A very simple transaction that loads the front page.
100//! async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
101//! let _goose = user.get("").await?;
102//!
103//! Ok(())
104//! }
105//! ```
106//!
107//! ### Transaction Weight
108//!
109//! Individual transactions can be assigned a weight, controlling how often the transaction runs. The
110//! larger the value of weight, the more it will run. In the following example, `a_transaction`
111//! runs 3 times as often as `b_transaction`:
112//!
113//! ```rust
114//! use goose::prelude::*;
115//!
116//! #[tokio::main]
117//! async fn main() -> Result<(), GooseError> {
118//! let mut a_transaction = transaction!(a_transaction_function).set_weight(9)?;
119//! let mut b_transaction = transaction!(b_transaction_function).set_weight(3)?;
120//!
121//! Ok(())
122//! }
123//!
124//! /// A very simple transaction that loads the "a" page.
125//! async fn a_transaction_function(user: &mut GooseUser) -> TransactionResult {
126//! let _goose = user.get("a/").await?;
127//!
128//! Ok(())
129//! }
130//!
131//! /// Another very simple transaction that loads the "b" page.
132//! async fn b_transaction_function(user: &mut GooseUser) -> TransactionResult {
133//! let _goose = user.get("b/").await?;
134//!
135//! Ok(())
136//! }
137//! ```
138//!
139//! ### Transaction Sequence
140//!
141//! Transactions can also be configured to run in a sequence. For example, a transaction with a
142//! sequence value of `1` will always run before a transaction with a sequence value of `2`. Weight can
143//! be applied to sequenced transactions, so for example a transaction with a weight of `2` and a sequence
144//! of `1` will run two times before a transaction with a sequence of `2`. Scenarios can contain
145//! transactions with sequence values and without sequence values, and in this case all transactions with
146//! a sequence value will run before transactions without a sequence value. In the following example,
147//! `a_transaction` runs before `b_transaction`, which runs before `c_transaction`:
148//!
149//! ```rust
150//! use goose::prelude::*;
151//!
152//! let mut a_transaction = transaction!(a_transaction_function).set_sequence(1);
153//! let mut b_transaction = transaction!(b_transaction_function).set_sequence(2);
154//! let mut c_transaction = transaction!(c_transaction_function);
155//!
156//! /// A very simple transaction that loads the "a" page.
157//! async fn a_transaction_function(user: &mut GooseUser) -> TransactionResult {
158//! let _goose = user.get("a/").await?;
159//!
160//! Ok(())
161//! }
162//!
163//! /// Another very simple transaction that loads the "b" page.
164//! async fn b_transaction_function(user: &mut GooseUser) -> TransactionResult {
165//! let _goose = user.get("b/").await?;
166//!
167//! Ok(())
168//! }
169//!
170//! /// Another very simple transaction that loads the "c" page.
171//! async fn c_transaction_function(user: &mut GooseUser) -> TransactionResult {
172//! let _goose = user.get("c/").await?;
173//!
174//! Ok(())
175//! }
176//! ```
177//!
178//! ### Transaction On Start
179//!
180//! Transactions can be flagged to only run when a user first starts. This can be useful if you'd
181//! like your load test to use a logged-in user. It is possible to assign sequences and weights
182//! to [`on_start`](./struct.Transaction.html#method.set_on_start) functions if you want to have
183//! multiple transactions run in a specific order at start time, and/or the transactions to run multiple times.
184//! A transaction can be flagged to run both on start and on stop.
185//!
186//! ```rust
187//! use goose::prelude::*;
188//!
189//! let mut a_transaction = transaction!(a_transaction_function).set_sequence(1).set_on_start();
190//!
191//! /// A very simple transaction that loads the "a" page.
192//! async fn a_transaction_function(user: &mut GooseUser) -> TransactionResult {
193//! let _goose = user.get("a/").await?;
194//!
195//! Ok(())
196//! }
197//! ```
198//!
199//! ### Transaction On Stop
200//!
201//! Transactions can be flagged to only run when a user stops. This can be useful if you'd like your
202//! load test to simulate a user logging out when it finishes. It is possible to assign sequences
203//! and weights to [`on_stop`](./struct.Transaction.html#method.set_on_stop) functions if you want to
204//! have multiple transactions run in a specific order at stop time, and/or the transactions to run multiple
205//! times. A transaction can be flagged to run both on start and on stop.
206//!
207//! ```rust
208//! use goose::prelude::*;
209//!
210//! let mut b_transaction = transaction!(b_transaction_function).set_sequence(2).set_on_stop();
211//!
212//! /// Another very simple transaction that loads the "b" page.
213//! async fn b_transaction_function(user: &mut GooseUser) -> TransactionResult {
214//! let _goose = user.get("b/").await?;
215//!
216//! Ok(())
217//! }
218//! ```
219//!
220//! ## Controlling User
221//!
222//! When Goose starts, it creates one or more [`GooseUser`](./struct.GooseUser.html)s,
223//! assigning a single [`Scenario`](./struct.Scenario.html) to each. This user is
224//! then used to generate load. Behind the scenes, Goose is leveraging the
225//! [`reqwest::client`](https://docs.rs/reqwest/*/reqwest/struct.Client.html)
226//! to load web pages, and Goose can therefor do anything [`reqwest`](https://docs.rs/reqwest/)
227//! can do.
228//!
229//! The most common request types are [`GET`](./struct.GooseUser.html#method.get) and
230//! [`POST`](./struct.GooseUser.html#method.post), but [`HEAD`](./struct.GooseUser.html#method.head),
231//! PUT, PATCH and [`DELETE`](./struct.GooseUser.html#method.delete) are also supported.
232//!
233//! ### GET
234//!
235//! A helper to make a `GET` request of a path and collect relevant metrics.
236//! Automatically prepends the correct host.
237//!
238//! ```rust
239//! use goose::prelude::*;
240//!
241//! let mut transaction = transaction!(get_function);
242//!
243//! /// A very simple transaction that makes a GET request.
244//! async fn get_function(user: &mut GooseUser) -> TransactionResult {
245//! let _goose = user.get("path/to/foo/").await?;
246//!
247//! Ok(())
248//! }
249//! ```
250//!
251//! The returned response is a [`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)
252//! struct. You can use it as you would any Reqwest Response.
253//!
254//!
255//! ### POST
256//!
257//! A helper to make a `POST` request of a string value to the path and collect relevant
258//! metrics. Automatically prepends the correct host. The returned response is a
259//! [`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)
260//!
261//! ```rust
262//! use goose::prelude::*;
263//!
264//! let mut transaction = transaction!(post_function);
265//!
266//! /// A very simple transaction that makes a POST request.
267//! async fn post_function(user: &mut GooseUser) -> TransactionResult {
268//! let _goose = user.post("path/to/foo/", "string value to post").await?;
269//!
270//! Ok(())
271//! }
272//! ```
273//!
274//! ## License
275//!
276//! Copyright 2020-2023 Jeremy Andrews
277//!
278//! Licensed under the Apache License, Version 2.0 (the "License");
279//! you may not use this file except in compliance with the License.
280//! You may obtain a copy of the License at
281//!
282//! <http://www.apache.org/licenses/LICENSE-2.0>
283//!
284//! Unless required by applicable law or agreed to in writing, software
285//! distributed under the License is distributed on an "AS IS" BASIS,
286//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
287//! See the License for the specific language governing permissions and
288//! limitations under the License.
289
290use downcast_rs::{impl_downcast, Downcast};
291use regex::Regex;
292use reqwest::{header, Client, ClientBuilder, Method, RequestBuilder, Response};
293use serde::{Deserialize, Serialize};
294use std::fmt::{Debug, Formatter};
295use std::hash::{Hash, Hasher};
296use std::sync::Arc;
297use std::time::Duration;
298use std::{fmt, str};
299use std::{future::Future, pin::Pin, time::Instant};
300use url::Url;
301
302use crate::logger::GooseLog;
303use crate::metrics::{
304 GooseCoordinatedOmissionMitigation, GooseMetric, GooseRawRequest, GooseRequestMetric,
305 TransactionDetail,
306};
307use crate::{GooseConfiguration, GooseError, WeightedTransactions};
308
309/// By default Goose sets the following User-Agent header when making requests.
310static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
311
312/// By default Goose times out requests after 60,000 milliseconds.
313static GOOSE_REQUEST_TIMEOUT: u64 = 60_000;
314
315/// `transaction!(foo)` expands to `Transaction::new(foo)`, but also does some boxing to work around a limitation in the compiler.
316#[macro_export]
317macro_rules! transaction {
318 ($transaction_func:ident) => {
319 Transaction::new(std::sync::Arc::new(move |s| {
320 std::boxed::Box::pin($transaction_func(s))
321 }))
322 };
323}
324
325/// `scenario!("foo")` expands to `Scenario::new("foo")`.
326#[macro_export]
327macro_rules! scenario {
328 ($name:tt) => {
329 Scenario::new($name)
330 };
331}
332
333/// Goose transactions return a result, which is empty on success, or contains a boxed
334/// [`TransactionError`](./enum.TransactionError.html) on error.
335pub type TransactionResult = Result<(), Box<TransactionError>>;
336
337/// An enumeration of all errors a [`Transaction`](./struct.Transaction.html) can return.
338#[derive(Debug)]
339pub enum TransactionError {
340 /// Wraps a [`reqwest::Error`](https://docs.rs/reqwest/*/reqwest/struct.Error.html).
341 Reqwest(reqwest::Error),
342 /// Wraps a [`url::ParseError`](https://docs.rs/url/*/url/enum.ParseError.html).
343 Url(url::ParseError),
344 /// The request failed.
345 RequestFailed {
346 /// The [`GooseRequestMetric`](./struct.GooseRequestMetric.html) that failed.
347 raw_request: GooseRequestMetric,
348 },
349 /// The request was canceled. This happens when the throttle is enabled and the load
350 /// test finishes.
351 RequestCanceled {
352 /// Wraps a [`flume::SendError`](https://docs.rs/flume/*/flume/struct.SendError.html),
353 /// a [`GooseRequestMetric`](./struct.GooseRequestMetric.html) has not yet been constructed.
354 source: flume::SendError<bool>,
355 },
356 /// There was an error sending the metrics for a request to the parent thread.
357 MetricsFailed {
358 /// Wraps a [`flume::SendError`](https://docs.rs/flume/*/flume/struct.SendError.html),
359 /// which contains the [`GooseMetric`](../metrics/enum.GooseMetric.html) that wasn't sent.
360 source: flume::SendError<GooseMetric>,
361 },
362 /// There was an error sending debug information to the logger thread.
363 LoggerFailed {
364 /// Wraps a [`flume::SendError`](https://docs.rs/flume/*/flume/struct.SendError.html),
365 /// which contains the [`GooseDebug`](./struct.GooseDebug.html) that wasn't sent.
366 source: flume::SendError<Option<GooseLog>>,
367 },
368 /// Attempted an unrecognized HTTP request method.
369 InvalidMethod {
370 /// The unrecognized HTTP request method.
371 method: Method,
372 },
373}
374/// Implement a helper to provide a text description of all possible types of errors.
375impl TransactionError {
376 fn describe(&self) -> &str {
377 match *self {
378 TransactionError::Reqwest(_) => "reqwest::Error",
379 TransactionError::Url(_) => "url::ParseError",
380 TransactionError::RequestFailed { .. } => "request failed",
381 TransactionError::RequestCanceled { .. } => {
382 "request canceled because throttled load test ended"
383 }
384 TransactionError::MetricsFailed { .. } => "failed to send metrics to parent thread",
385 TransactionError::LoggerFailed { .. } => "failed to send log message to logger thread",
386 TransactionError::InvalidMethod { .. } => "unrecognized HTTP request method",
387 }
388 }
389}
390
391/// Implement format trait to allow displaying errors.
392impl fmt::Display for TransactionError {
393 // Implement display of error with `{}` marker.
394 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
395 match *self {
396 TransactionError::Reqwest(ref source) => {
397 write!(f, "TransactionError: {} ({})", self.describe(), source)
398 }
399 TransactionError::Url(ref source) => {
400 write!(f, "TransactionError: {} ({})", self.describe(), source)
401 }
402 TransactionError::RequestCanceled { ref source } => {
403 write!(f, "TransactionError: {} ({})", self.describe(), source)
404 }
405 TransactionError::MetricsFailed { ref source } => {
406 write!(f, "TransactionError: {} ({})", self.describe(), source)
407 }
408 TransactionError::LoggerFailed { ref source } => {
409 write!(f, "TransactionError: {} ({})", self.describe(), source)
410 }
411 _ => write!(f, "TransactionError: {}", self.describe()),
412 }
413 }
414}
415
416// Define the lower level source of this error, if any.
417impl std::error::Error for TransactionError {
418 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
419 match *self {
420 TransactionError::Reqwest(ref source) => Some(source),
421 TransactionError::Url(ref source) => Some(source),
422 TransactionError::RequestCanceled { ref source } => Some(source),
423 TransactionError::MetricsFailed { ref source } => Some(source),
424 TransactionError::LoggerFailed { ref source } => Some(source),
425 _ => None,
426 }
427 }
428}
429
430/// Auto-convert Reqwest errors.
431impl From<reqwest::Error> for TransactionError {
432 fn from(err: reqwest::Error) -> TransactionError {
433 TransactionError::Reqwest(err)
434 }
435}
436
437/// Auto-convert Reqwest errors to boxed TransactionError.
438impl From<reqwest::Error> for Box<TransactionError> {
439 fn from(value: reqwest::Error) -> Self {
440 Box::new(TransactionError::Reqwest(value))
441 }
442}
443
444/// Auto-convert Url errors.
445impl From<url::ParseError> for TransactionError {
446 fn from(err: url::ParseError) -> TransactionError {
447 TransactionError::Url(err)
448 }
449}
450
451/// When the throttle is enabled and the load test ends, the throttle channel is
452/// shut down. This causes a
453/// [`flume::SendError`](https://docs.rs/flume/*/flume/struct.SendError.html),
454/// which gets automatically converted to `RequestCanceled`.
455/// [`RequestCanceled`](./enum.TransactionError.html#variant.RequestCanceled)
456impl From<flume::SendError<bool>> for TransactionError {
457 fn from(source: flume::SendError<bool>) -> TransactionError {
458 TransactionError::RequestCanceled { source }
459 }
460}
461
462/// Attempt to send metrics to the parent thread failed.
463impl From<flume::SendError<GooseMetric>> for TransactionError {
464 fn from(source: flume::SendError<GooseMetric>) -> TransactionError {
465 TransactionError::MetricsFailed { source }
466 }
467}
468
469/// Attempt to send logs to the logger thread failed.
470impl From<flume::SendError<Option<GooseLog>>> for TransactionError {
471 fn from(source: flume::SendError<Option<GooseLog>>) -> TransactionError {
472 TransactionError::LoggerFailed { source }
473 }
474}
475
476/// An individual scenario.
477#[derive(Clone, Hash)]
478pub struct Scenario {
479 /// The name of the scenario.
480 pub name: String,
481 /// Auto-generated machine name of the scenario.
482 pub machine_name: String,
483 /// An integer reflecting where this scenario lives in the internal
484 /// [`GooseAttack`](../struct.GooseAttack.html)`.scenarios` vector.
485 pub scenarios_index: usize,
486 /// An integer value that controls the frequency that this scenario will be assigned to a user.
487 pub weight: usize,
488 /// A [`Duration`](https://doc.rust-lang.org/std/time/struct.Duration.html) range defining the
489 /// minimum and maximum time a [`GooseUser`] should sleep after running a transaction.
490 pub transaction_wait: Option<(Duration, Duration)>,
491 /// A vector containing one copy of each [`Transaction`](./struct.Transaction.html) that will
492 /// run by users running this scenario.
493 pub transactions: Vec<Transaction>,
494 /// A fully scheduled and weighted vector of integers (pointing to
495 /// [`Transaction`](./struct.Transaction.html)s and [`Transaction`](./struct.Transaction.html) names.
496 pub weighted_transactions: WeightedTransactions,
497 /// A vector of vectors of integers, controlling the sequence and order
498 /// [`on_start`](./struct.Transaction.html#method.set_on_start)
499 /// [`Transaction`](./struct.Transaction.html)s are run when the user first starts.
500 pub weighted_on_start_transactions: WeightedTransactions,
501 /// A vector of vectors of integers, controlling the sequence and order
502 /// [`on_stop`](./struct.Transaction.html#method.set_on_stop)
503 /// [`Transaction`](./struct.Transaction.html)s are run when the user first starts.
504 pub weighted_on_stop_transactions: WeightedTransactions,
505 /// An optional default host to run this `Scenario` against.
506 pub host: Option<String>,
507}
508impl Scenario {
509 /// Creates a new [`Scenario`](./struct.Scenario.html). Once created, a
510 /// [`Transaction`](./struct.Transaction.html) must be assigned to it, and finally it must
511 /// be registered with the [`GooseAttack`](../struct.GooseAttack.html) object. The
512 /// returned object must be stored in a mutable value.
513 ///
514 /// # Example
515 /// ```rust
516 /// use goose::prelude::*;
517 ///
518 /// let mut example_transactions = scenario!("ExampleTransactions");
519 /// ```
520 pub fn new(name: &str) -> Self {
521 trace!("new scenario: name: {}", &name);
522 Scenario {
523 name: name.to_string(),
524 machine_name: Scenario::get_machine_name(name),
525 scenarios_index: usize::MAX,
526 weight: 1,
527 transaction_wait: None,
528 transactions: Vec::new(),
529 weighted_transactions: Vec::new(),
530 weighted_on_start_transactions: Vec::new(),
531 weighted_on_stop_transactions: Vec::new(),
532 host: None,
533 }
534 }
535
536 /// An internal helper to convert a Scenario name to a machine name which is only
537 /// alphanumerics.
538 fn get_machine_name(name: &str) -> String {
539 // Remove all non-alphanumeric characters.
540 let re = Regex::new("[^a-zA-Z0-9]+").unwrap();
541 let alphanumeric = re.replace_all(name, "");
542 // Convert to lower case.
543 alphanumeric.to_lowercase()
544 }
545
546 /// Registers a [`Transaction`](./struct.Transaction.html) with a
547 /// [`Scenario`](./struct.Scenario.html), where it is stored in the
548 /// [`Scenario`](./struct.Scenario.html)`.transactions` vector. The function
549 /// associated with the transaction will be run during the load test.
550 ///
551 /// # Example
552 /// ```rust
553 /// use goose::prelude::*;
554 ///
555 /// let mut example_transactions = scenario!("ExampleTransactions");
556 /// example_transactions.register_transaction(transaction!(a_transaction_function));
557 ///
558 /// /// A very simple transaction that loads the "a" page.
559 /// async fn a_transaction_function(user: &mut GooseUser) -> TransactionResult {
560 /// let _goose = user.get("a/").await?;
561 ///
562 /// Ok(())
563 /// }
564 /// ```
565 pub fn register_transaction(mut self, mut transaction: Transaction) -> Self {
566 trace!("{} register_transaction: {}", self.name, transaction.name);
567 transaction.transactions_index = self.transactions.len();
568 self.transactions.push(transaction);
569 self
570 }
571
572 /// Sets a weight on a scenario. The larger the value of weight, the more often the scenario will
573 /// be assigned to users. For example, if you have scenario foo with a weight of 3, and scenario
574 /// bar with a weight of 1, and you spin up a load test with 8 users, 6 of them will be running
575 /// the foo scenario, and 2 will be running the bar scenario.
576 ///
577 /// # Example
578 /// ```rust
579 /// use goose::prelude::*;
580 ///
581 /// #[tokio::main]
582 /// async fn main() -> Result<(), GooseError> {
583 /// let mut example_transactions = scenario!("ExampleTransactions").set_weight(3)?;
584 ///
585 /// Ok(())
586 /// }
587 /// ```
588 pub fn set_weight(mut self, weight: usize) -> Result<Self, GooseError> {
589 trace!("{} set_weight: {}", self.name, weight);
590 if weight == 0 {
591 return Err(GooseError::InvalidWeight {
592 weight,
593 detail: ("Weight must be set to at least 1.".to_string()),
594 });
595 }
596 self.weight = weight;
597
598 Ok(self)
599 }
600
601 /// Set a default host for the scenario. If no `--host` flag is set when running the load test, this
602 /// host will be pre-pended on all requests. For example, this can configure your load test to run
603 /// against your local development environment by default, and the `--host` option could be used to
604 /// override host when running the load test against production.
605 ///
606 /// # Example
607 /// ```rust
608 /// use goose::prelude::*;
609 ///
610 /// let mut example_transactions = scenario!("ExampleTransactions").set_host("http://10.1.1.42");
611 /// ```
612 pub fn set_host(mut self, host: &str) -> Self {
613 trace!("{} set_host: {}", self.name, host);
614 // Host validation happens in main() at startup.
615 self.host = Some(host.to_string());
616 self
617 }
618
619 /// Configure a senario to to pause after running each transaction. The length of the pause will be randomly
620 /// selected from `min_wait` to `max_wait` inclusively.
621 ///
622 /// # Example
623 /// ```rust
624 /// use goose::prelude::*;
625 /// use std::time::Duration;
626 ///
627 /// #[tokio::main]
628 /// async fn main() -> Result<(), GooseError> {
629 /// scenario!("ExampleTransactions").set_wait_time(Duration::from_secs(0), Duration::from_secs(1))?;
630 ///
631 /// Ok(())
632 /// }
633 /// ```
634 pub fn set_wait_time(
635 mut self,
636 min_wait: Duration,
637 max_wait: Duration,
638 ) -> Result<Self, GooseError> {
639 trace!(
640 "{} set_wait time: min: {:?} max: {:?}",
641 self.name,
642 min_wait,
643 max_wait
644 );
645 if min_wait.as_millis() > max_wait.as_millis() {
646 return Err(GooseError::InvalidWaitTime {
647 min_wait,
648 max_wait,
649 detail:
650 "The min_wait option can not be set to a larger value than the max_wait option."
651 .to_string(),
652 });
653 }
654 self.transaction_wait = Some((min_wait, max_wait));
655
656 Ok(self)
657 }
658}
659
660/// Commands sent from the parent thread to the user threads, and from the manager to the
661/// worker processes.
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
663pub enum GooseUserCommand {
664 /// Tell worker process to pause load test.
665 Wait,
666 /// Tell worker process to start load test.
667 Run,
668 /// Tell user thread or worker process to exit.
669 Exit,
670}
671
672/// Supported HTTP methods.
673#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)]
674pub enum GooseMethod {
675 Delete,
676 Get,
677 Head,
678 Patch,
679 Post,
680 Put,
681}
682/// Display method in upper case.
683impl fmt::Display for GooseMethod {
684 // Implement display of `GooseMethod` with `{}` marker.
685 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
686 match self {
687 GooseMethod::Delete => write!(f, "DELETE"),
688 GooseMethod::Get => write!(f, "GET"),
689 GooseMethod::Head => write!(f, "HEAD"),
690 GooseMethod::Patch => write!(f, "PATCH"),
691 GooseMethod::Post => write!(f, "POST"),
692 GooseMethod::Put => write!(f, "PUT"),
693 }
694 }
695}
696
697/// Convert [`http::method::Method`](https://docs.rs/http/*/http/method/struct.Method.html)
698/// to [`GooseMethod`](./enum.GooseMethod.html).
699pub fn goose_method_from_method(method: Method) -> Result<GooseMethod, Box<TransactionError>> {
700 Ok(match method {
701 Method::DELETE => GooseMethod::Delete,
702 Method::GET => GooseMethod::Get,
703 Method::HEAD => GooseMethod::Head,
704 Method::PATCH => GooseMethod::Patch,
705 Method::POST => GooseMethod::Post,
706 Method::PUT => GooseMethod::Put,
707 _ => {
708 return Err(Box::new(TransactionError::InvalidMethod { method }));
709 }
710 })
711}
712
713/// The response to a [`GooseRequestMetric`].
714#[derive(Debug)]
715pub struct GooseResponse {
716 /// The request that this is a response to.
717 pub request: GooseRequestMetric,
718 /// The response.
719 pub response: Result<Response, reqwest::Error>,
720}
721impl GooseResponse {
722 pub fn new(request: GooseRequestMetric, response: Result<Response, reqwest::Error>) -> Self {
723 GooseResponse { request, response }
724 }
725}
726
727/// Object created by [`log_debug()`](struct.GooseUser.html#method.log_debug) and written
728/// to log to assist in debugging.
729#[derive(Debug, Deserialize, Serialize)]
730pub struct GooseDebug {
731 /// String to identify the source of the log message.
732 pub tag: String,
733 /// Optional request made.
734 pub request: Option<GooseRequestMetric>,
735 /// Optional headers returned by server.
736 pub header: Option<String>,
737 /// Optional body text returned by server.
738 pub body: Option<String>,
739}
740impl GooseDebug {
741 fn new(
742 tag: &str,
743 request: Option<&GooseRequestMetric>,
744 header: Option<&header::HeaderMap>,
745 body: Option<&str>,
746 ) -> Self {
747 GooseDebug {
748 // Convert tag from &str to string.
749 tag: tag.to_string(),
750 // If request is defined, clone it.
751 request: request.cloned(),
752 // If header is defined, convert it to a string.
753 header: header.map(|h| format!("{:?}", h)),
754 // If header is defined, convert from &str to string.
755 body: body.map(|b| b.to_string()),
756 }
757 }
758}
759
760/// Used internally by Coordinated Omission Mitigation, tracks the cadence between when the same request
761/// is made as Goose loops through a Scenario.
762#[derive(Debug, Clone)]
763struct GooseRequestCadence {
764 /// The last time this GooseUser lopped through its Transactions.
765 last_time: std::time::Instant,
766 /// Total milliseconds of delays followed each Transaction. This has to be substracted out as it's
767 /// not impacted by the upstream server and it can change randomly affecting the cadence.
768 delays_since_last_time: u64,
769 /// How many times this GooseUser has looped through all of its Transactions.
770 counter: u64,
771 /// The minimum time taken to loop through all Transactions.
772 minimum_cadence: u64,
773 /// The maximum time taken to loop through all Transactions.
774 maximum_cadence: u64,
775 /// Average amount of time taken to loop through all Transactions.
776 average_cadence: u64,
777 /// Total amount of time spent processing Transactions.
778 total_elapsed: u64,
779 /// If non-zero, the length of the server slowdown detected by the Goose Coordinated
780 /// Omission Mitigation in milliseconds.
781 coordinated_omission_mitigation: u64,
782 /// The expected cadence to loop through all Transactions.
783 user_cadence: u64,
784 /// If -1 coordinated_omission_mitigation was never enabled. Otherwise is a counter of how
785 /// many times the mitigation triggered.
786 coordinated_omission_counter: isize,
787}
788impl GooseRequestCadence {
789 // Return a new, empty RequestCadence object.
790 fn new() -> GooseRequestCadence {
791 GooseRequestCadence {
792 last_time: std::time::Instant::now(),
793 delays_since_last_time: 0,
794 counter: 0,
795 minimum_cadence: 0,
796 maximum_cadence: 0,
797 average_cadence: 0,
798 total_elapsed: 0,
799 coordinated_omission_mitigation: 0,
800 user_cadence: 0,
801 coordinated_omission_counter: -1,
802 }
803 }
804}
805
806/// A marker trait representing user data of any type
807/// ([generic](https://doc.rust-lang.org/rust-by-example/generics.html)) that can
808/// be added to any [`GooseUser`](../goose/struct.GooseUser.html). The format of
809/// the data stored in `GooseUserData` must be defined in your load test, and by
810/// default supports any type that supports
811/// [`Send`](https://doc.rust-lang.org/std/marker/trait.Send.html) and
812/// [`Sync`](https://doc.rust-lang.org/std/marker/trait.Sync.html).
813///
814/// Stored in the [`GooseUser`] object in a private `session_data` field. Per-user
815/// session data is stored by invoking [`GooseUser::set_session_data`]. The session
816/// data can be accessed by invoking [`GooseUser::get_session_data`],
817/// [`GooseUser::get_session_data_mut`], [`GooseUser::get_session_data_unchecked`],
818/// or [`GooseUser::get_session_data_unchecked_mut`].
819///
820/// For an example, see
821/// [`examples/simple_with_session`](https://github.com/tag1consulting/goose/blob/main/examples/simple_with_session.rs).
822pub trait GooseUserData: Downcast + Send + Sync + 'static {}
823impl_downcast!(GooseUserData);
824impl<T: Send + Sync + 'static> GooseUserData for T {}
825
826trait CloneGooseUserData {
827 fn clone_goose_user_data(&self) -> Box<dyn GooseUserData>;
828}
829
830impl<T> CloneGooseUserData for T
831where
832 T: GooseUserData + Clone + 'static,
833{
834 fn clone_goose_user_data(&self) -> Box<dyn GooseUserData> {
835 Box::new(self.clone())
836 }
837}
838
839impl Clone for Box<dyn GooseUserData> {
840 fn clone(&self) -> Self {
841 self.clone_goose_user_data()
842 }
843}
844
845/// An individual user state, repeatedly running all [`Transaction`](./struct.Transaction.html)s
846/// in a specific [`Scenario`](./struct.Scenario.html).
847#[derive(Debug)]
848pub struct GooseUser {
849 /// The Instant when this `GooseUser` client started.
850 pub started: Instant,
851 /// How many iterations of the scenario this GooseUser has run.
852 pub(crate) iterations: usize,
853 /// An index into the internal [`GooseAttack`](../struct.GooseAttack.html)`.scenarios`
854 /// vector, indicating which [`Scenario`](./struct.Scenario.html) is running.
855 pub(crate) scenarios_index: usize,
856 /// The Scenario this GooseUser is running.
857 pub(crate) scenario_name: String,
858 /// An optional index into [`Scenario`]`.transaction`, indicating which transaction this is.
859 pub(crate) transaction_index: Option<String>,
860 /// Current transaction name, if set.
861 pub(crate) transaction_name: Option<String>,
862 /// Client used to make requests, managing sessions and cookies.
863 pub client: Client,
864 /// The base URL to prepend to all relative paths.
865 pub base_url: Url,
866 /// A local copy of the global [`GooseConfiguration`](../struct.GooseConfiguration.html).
867 pub config: GooseConfiguration,
868 /// Channel to logger.
869 pub logger: Option<flume::Sender<Option<GooseLog>>>,
870 /// Channel to throttle.
871 pub throttle: Option<flume::Sender<bool>>,
872 /// Normal transactions are optionally throttled,
873 /// [`test_start`](../struct.GooseAttack.html#method.test_start) and
874 /// [`test_stop`](../struct.GooseAttack.html#method.test_stop) transactions are not.
875 pub is_throttled: bool,
876 /// Channel for sending metrics to the parent for aggregation.
877 pub metrics_channel: Option<flume::Sender<GooseMetric>>,
878 /// Channel for notifying the parent when thread shuts down.
879 pub shutdown_channel: Option<flume::Sender<usize>>,
880 /// An index into the internal [`GooseAttack`](../struct.GooseAttack.html)`.weighted_users`
881 /// vector, indicating which weighted `GooseUser` is running.
882 pub weighted_users_index: usize,
883 /// Load test hash.
884 pub load_test_hash: u64,
885 /// Tracks the cadence that this user is looping through all Transactions, used by Coordinated
886 /// Omission Mitigation.
887 request_cadence: GooseRequestCadence,
888 /// Tracks how much time is spent sleeping during a loop through all transactions.
889 pub(crate) slept: u64,
890 /// Optional per-user session data of a generic type implementing the
891 /// [`GooseUserData`] trait.
892 session_data: Option<Box<dyn GooseUserData>>,
893}
894
895impl Clone for GooseUser {
896 fn clone(&self) -> Self {
897 Self {
898 started: self.started,
899 iterations: self.iterations,
900 scenarios_index: self.scenarios_index,
901 scenario_name: self.scenario_name.clone(),
902 transaction_index: self.transaction_index.clone(),
903 transaction_name: self.transaction_name.clone(),
904 client: self.client.clone(),
905 base_url: self.base_url.clone(),
906 config: self.config.clone(),
907 logger: self.logger.clone(),
908 throttle: self.throttle.clone(),
909 is_throttled: self.is_throttled,
910 metrics_channel: self.metrics_channel.clone(),
911 shutdown_channel: self.shutdown_channel.clone(),
912 weighted_users_index: self.weighted_users_index,
913 load_test_hash: self.load_test_hash,
914 request_cadence: self.request_cadence.clone(),
915 slept: self.slept,
916 session_data: if self.session_data.is_some() {
917 Option::from(self.session_data.clone_goose_user_data())
918 } else {
919 None
920 },
921 }
922 }
923}
924
925impl Debug for dyn GooseUserData {
926 fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
927 write!(fmt, "GooseUserData")
928 }
929}
930
931impl GooseUser {
932 /// Create a new user state.
933 pub fn new(
934 scenarios_index: usize,
935 scenario_name: String,
936 base_url: Url,
937 configuration: &GooseConfiguration,
938 load_test_hash: u64,
939 reqwest_client: Option<Client>,
940 ) -> Result<Self, GooseError> {
941 trace!("new GooseUser");
942
943 let client = match reqwest_client {
944 Some(c) => c,
945 None => create_reqwest_client(configuration)?,
946 };
947
948 Ok(GooseUser {
949 started: Instant::now(),
950 iterations: 0,
951 scenarios_index,
952 scenario_name,
953 transaction_index: None,
954 transaction_name: None,
955 client,
956 base_url,
957 config: configuration.clone(),
958 logger: None,
959 throttle: None,
960 is_throttled: true,
961 metrics_channel: None,
962 shutdown_channel: None,
963 // A value of max_value() indicates this user isn't fully initialized yet.
964 weighted_users_index: usize::MAX,
965 load_test_hash,
966 request_cadence: GooseRequestCadence::new(),
967 slept: 0,
968 session_data: None,
969 })
970 }
971
972 /// Create a new single-use user.
973 pub fn single(base_url: Url, configuration: &GooseConfiguration) -> Result<Self, GooseError> {
974 let mut single_user = GooseUser::new(0, "".to_string(), base_url, configuration, 0, None)?;
975 // Only one user, so index is 0.
976 single_user.weighted_users_index = 0;
977 // Do not throttle [`test_start`](../struct.GooseAttack.html#method.test_start) (setup) and
978 // [`test_stop`](../struct.GooseAttack.html#method.test_stop) (teardown) transactions.
979 single_user.is_throttled = false;
980
981 Ok(single_user)
982 }
983
984 /// Returns the number of iterations this GooseUser has run through it's
985 /// assigned [`Scenario`].
986 pub fn get_iterations(&self) -> usize {
987 self.iterations
988 }
989
990 /// Returns an optional reference to per-[`GooseUser`] session data.
991 ///
992 /// Leaves the session data in-place, returning an optional reference to the
993 /// original session data if existing and of the correct type. Returns [`None`]
994 /// if no session data has been set or the session data set is not of type `T`.
995 ///
996 /// # Example
997 /// ```rust
998 /// use goose::prelude::*;
999 ///
1000 /// struct Foo(String);
1001 ///
1002 /// let mut transaction = transaction!(get_session_data_function);
1003 ///
1004 /// /// A very simple transaction that makes a GET request.
1005 /// async fn get_session_data_function(user: &mut GooseUser) -> TransactionResult {
1006 /// let foo = user.get_session_data::<Foo>().expect("Missing session data!");
1007 /// println!("Session data: {}", foo.0);
1008 ///
1009 /// Ok(())
1010 /// }
1011 /// ```
1012 pub fn get_session_data<T: GooseUserData>(&self) -> Option<&T> {
1013 match &self.session_data {
1014 Some(data) => data.downcast_ref::<T>(),
1015 None => None,
1016 }
1017 }
1018
1019 /// Returns a reference to per-[`GooseUser`] session data, without doing any
1020 /// validation that the session data exists and is of the correct type.
1021 ///
1022 /// Leaves the session data in-place, returning a reference to the original
1023 /// session data. Calling this method on a [`GooseUser`] object without
1024 /// session data or with a different type `T` will panic.
1025 ///
1026 /// For a safe alternative see [`GooseUser::get_session_data`].
1027 ///
1028 /// # Example
1029 /// ```rust
1030 /// use goose::prelude::*;
1031 ///
1032 /// struct Foo(String);
1033 ///
1034 /// let mut transaction = transaction!(get_session_data_unchecked_function);
1035 ///
1036 /// /// A very simple transaction that makes a GET request.
1037 /// async fn get_session_data_unchecked_function(user: &mut GooseUser) -> TransactionResult {
1038 /// let foo = user.get_session_data_unchecked::<Foo>();
1039 /// println!("Session data: {}", foo.0);
1040 ///
1041 /// Ok(())
1042 /// }
1043 /// ```
1044 pub fn get_session_data_unchecked<T: GooseUserData>(&self) -> &T {
1045 let session_data = self.session_data.as_deref().expect("Missing session data!");
1046
1047 session_data
1048 .downcast_ref::<T>()
1049 .expect("Invalid session data!")
1050 }
1051
1052 /// Returns an optional mutable reference to per-[`GooseUser`] session data.
1053 ///
1054 /// Leaves the session data in-place, returning an optional mutable reference
1055 /// to the original session data if existing and of the correct type. Returns
1056 /// [`None`] if no session data has been set or the session data set is not of
1057 /// type `T`.
1058 ///
1059 /// # Example
1060 /// ```rust
1061 /// use goose::prelude::*;
1062 ///
1063 /// struct Foo(String);
1064 ///
1065 /// let mut transaction = transaction!(get_session_data_mut_function);
1066 ///
1067 /// /// A very simple transaction that makes a GET request.
1068 /// async fn get_session_data_mut_function(user: &mut GooseUser) -> TransactionResult {
1069 /// let foo = user.get_session_data_mut::<Foo>().expect("Missing session data!");
1070 /// foo.0 = "Bar".to_owned();
1071 /// Ok(())
1072 /// }
1073 /// ```
1074 pub fn get_session_data_mut<T: GooseUserData>(&mut self) -> Option<&mut T> {
1075 match &mut self.session_data {
1076 Some(data) => data.downcast_mut::<T>(),
1077 None => None,
1078 }
1079 }
1080
1081 /// Returns a mutable reference to per-[`GooseUser`] session data, without
1082 /// doing any validation that the session data exists and is of the correct
1083 /// type.
1084 ///
1085 /// Leaves the session data in-place, returning a mutable reference to the
1086 /// original session data. Calling this method on a [`GooseUser`] object
1087 /// without session data or with a different type `T` will panic.
1088 ///
1089 /// For a safe alternative see [`GooseUser::get_session_data_mut`].
1090 ///
1091 /// # Example
1092 /// ```rust
1093 /// use goose::prelude::*;
1094 ///
1095 /// struct Foo(String);
1096 ///
1097 /// let mut transaction = transaction!(get_session_data_unchecked_mut_function);
1098 ///
1099 /// /// A very simple transaction that makes a GET request.
1100 /// async fn get_session_data_unchecked_mut_function(user: &mut GooseUser) -> TransactionResult {
1101 /// let foo = user.get_session_data_unchecked_mut::<Foo>();
1102 /// foo.0 = "Bar".to_owned();
1103 /// Ok(())
1104 /// }
1105 /// ```
1106 pub fn get_session_data_unchecked_mut<T: GooseUserData>(&mut self) -> &mut T {
1107 let session_data = self
1108 .session_data
1109 .as_deref_mut()
1110 .expect("Missing session data!");
1111 session_data
1112 .downcast_mut::<T>()
1113 .expect("Invalid session data!")
1114 }
1115
1116 /// Sets session data for the current [`GooseUser`].
1117 ///
1118 /// If session data already exists for the current [`GooseUser`], it will be
1119 /// replaced. Session data must be of a type implementing the
1120 /// [`GooseUserData`] trait.
1121 ///
1122 /// # Example
1123 /// ```rust
1124 /// use goose::prelude::*;
1125 ///
1126 /// struct Foo(String);
1127 ///
1128 /// let mut transaction = transaction!(set_session_data_function);
1129 ///
1130 /// /// A very simple transaction that makes a GET request.
1131 /// async fn set_session_data_function(user: &mut GooseUser) -> TransactionResult {
1132 /// user.set_session_data(Foo("Foo".to_string()));
1133 ///
1134 /// Ok(())
1135 /// }
1136 /// ```
1137 pub fn set_session_data<T: GooseUserData>(&mut self, data: T) {
1138 self.session_data.replace(Box::new(data));
1139 }
1140
1141 /// A helper that prepends a `base_url` to all relative paths.
1142 ///
1143 /// A `base_url` is determined per user thread, using the following order
1144 /// of precedence:
1145 /// 1. `--host` (host specified on the command line when running load test)
1146 /// 2. [`Scenario`](./struct.Scenario.html)`.host` (default host defined for the
1147 /// current scenario)
1148 /// 3. [`GooseDefault::Host`](../config/enum.GooseDefault.html#variant.Host) (default host
1149 /// defined for the current load test)
1150 pub fn build_url(&self, path: &str) -> Result<String, Box<TransactionError>> {
1151 // If URL includes a host, simply use it.
1152 if let Ok(parsed_path) = Url::parse(path) {
1153 if let Some(_host) = parsed_path.host() {
1154 return Ok(path.to_string());
1155 }
1156 }
1157
1158 // Otherwise use the `base_url`.
1159 match self.base_url.join(path) {
1160 Ok(u) => Ok(u.to_string()),
1161 Err(e) => Err(Box::new(e.into())),
1162 }
1163 }
1164
1165 /// A helper to make a `GET` request of a path and collect relevant metrics.
1166 /// Automatically prepends the correct host.
1167 ///
1168 /// Calls to `get()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1169 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1170 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1171 ///
1172 /// If you need to set headers, change timeouts, or otherwise make use of the
1173 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1174 /// object, refer to [`GooseUser::get_request_builder`].
1175 ///
1176 /// # Example
1177 /// GET a URL.
1178 /// ```rust
1179 /// use goose::prelude::*;
1180 ///
1181 /// let mut transaction = transaction!(get_function);
1182 ///
1183 /// /// A very simple transaction that makes a GET request.
1184 /// async fn get_function(user: &mut GooseUser) -> TransactionResult {
1185 /// let _goose = user.get("path/to/foo/").await?;
1186 ///
1187 /// Ok(())
1188 /// }
1189 /// ```
1190 pub async fn get(&mut self, path: &str) -> Result<GooseResponse, Box<TransactionError>> {
1191 // GET path.
1192 let goose_request = GooseRequest::builder()
1193 .method(GooseMethod::Get)
1194 .path(path)
1195 .build();
1196
1197 // Make the request and return the GooseResponse.
1198 self.request(goose_request).await
1199 }
1200
1201 /// A helper to make a named `GET` request of a path and collect relevant metrics.
1202 /// Automatically prepends the correct host.
1203 ///
1204 /// Calls to `get_named()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1205 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1206 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1207 ///
1208 /// If you need to set headers, change timeouts, or otherwise make use of the
1209 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1210 /// object, refer to [`GooseUser::get_request_builder`].
1211 ///
1212 /// # Example
1213 /// GET a URL and name the request in collected metrics.
1214 /// ```rust
1215 /// use goose::prelude::*;
1216 ///
1217 /// let mut transaction = transaction!(get_function);
1218 ///
1219 /// /// A very simple transaction that makes a GET request, naming it for metrics.
1220 /// async fn get_function(user: &mut GooseUser) -> TransactionResult {
1221 /// let _goose = user.get_named("path/to/foo/", "foo").await?;
1222 ///
1223 /// Ok(())
1224 /// }
1225 /// ```
1226 pub async fn get_named(
1227 &mut self,
1228 path: &str,
1229 name: &str,
1230 ) -> Result<GooseResponse, Box<TransactionError>> {
1231 // GET path named.
1232 let goose_request = GooseRequest::builder()
1233 .method(GooseMethod::Get)
1234 .path(path)
1235 .name(name)
1236 .build();
1237
1238 // Make the request and return the GooseResponse.
1239 self.request(goose_request).await
1240 }
1241
1242 /// A helper to make a `POST` request of a path and collect relevant metrics.
1243 /// Automatically prepends the correct host.
1244 ///
1245 /// Calls to `post()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1246 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1247 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1248 ///
1249 /// If you need to set headers, change timeouts, or otherwise make use of the
1250 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1251 /// object, refer to [`GooseUser::get_request_builder`].
1252 ///
1253 /// # Example
1254 /// POST an arbitrary body.
1255 /// ```rust
1256 /// use goose::prelude::*;
1257 ///
1258 /// let mut transaction = transaction!(post_function);
1259 ///
1260 /// /// A very simple transaction that makes a POST request.
1261 /// async fn post_function(user: &mut GooseUser) -> TransactionResult {
1262 /// let _goose = user.post("path/to/foo/", "BODY BEING POSTED").await?;
1263 ///
1264 /// Ok(())
1265 /// }
1266 /// ```
1267 pub async fn post<T: Into<reqwest::Body>>(
1268 &mut self,
1269 path: &str,
1270 body: T,
1271 ) -> Result<GooseResponse, Box<TransactionError>> {
1272 // Build a Reqwest RequestBuilder object.
1273 let url = self.build_url(path)?;
1274 let reqwest_request_builder = self.client.post(url);
1275
1276 // POST request.
1277 let goose_request = GooseRequest::builder()
1278 .method(GooseMethod::Post)
1279 .path(path)
1280 .set_request_builder(reqwest_request_builder.body(body))
1281 .build();
1282
1283 // Make the request and return the GooseResponse.
1284 self.request(goose_request).await
1285 }
1286
1287 /// A helper to make a `POST` request of a form on a path and collect relevant metrics.
1288 /// Automatically prepends the correct host.
1289 ///
1290 /// Calls to `post_form()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1291 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1292 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1293 ///
1294 /// If you need to set headers, change timeouts, or otherwise make use of the
1295 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1296 /// object, refer to [`GooseUser::get_request_builder`].
1297 ///
1298 /// # Example
1299 /// POST a form.
1300 /// ```rust
1301 /// use goose::prelude::*;
1302 ///
1303 /// let mut transaction = transaction!(post_function);
1304 ///
1305 /// /// A very simple transaction that POSTs form parameters.
1306 /// async fn post_function(user: &mut GooseUser) -> TransactionResult {
1307 /// let params = [("foo", "bar"), ("foo2", "bar2")];
1308 /// let _goose = user.post_form("path/to/foo/", ¶ms).await?;
1309 ///
1310 /// Ok(())
1311 /// }
1312 /// ```
1313 pub async fn post_form<T: Serialize + ?Sized>(
1314 &mut self,
1315 path: &str,
1316 form: &T,
1317 ) -> Result<GooseResponse, Box<TransactionError>> {
1318 // Build a Reqwest RequestBuilder object.
1319 let url = self.build_url(path)?;
1320 let reqwest_request_builder = self.client.post(url);
1321
1322 // POST form request.
1323 let goose_request = GooseRequest::builder()
1324 .method(GooseMethod::Post)
1325 .path(path)
1326 .set_request_builder(reqwest_request_builder.form(&form))
1327 .build();
1328
1329 // Make the request and return the GooseResponse.
1330 self.request(goose_request).await
1331 }
1332
1333 /// A helper to make a `POST` request of json on a path and collect relevant metrics.
1334 /// Automatically prepends the correct host.
1335 ///
1336 /// Calls to `post_json()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1337 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1338 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1339 ///
1340 /// If you need to set headers, change timeouts, or otherwise make use of the
1341 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1342 /// object, refer to [`GooseUser::get_request_builder`].
1343 ///
1344 /// # Example
1345 /// POST an arbitrary JSON object.
1346 /// ```rust
1347 /// use goose::prelude::*;
1348 ///
1349 /// let mut transaction = transaction!(post_function);
1350 ///
1351 /// /// A very simple transaction that POSTs an arbitrary json object.
1352 /// async fn post_function(user: &mut GooseUser) -> TransactionResult {
1353 /// let json = &serde_json::json!({
1354 /// "foo": "bar",
1355 /// "foo2": "bar2"
1356 /// });
1357 /// let _goose = user.post_json("path/to/foo/", &json).await?;
1358 ///
1359 /// Ok(())
1360 /// }
1361 /// ```
1362 pub async fn post_json<T: Serialize + ?Sized>(
1363 &mut self,
1364 path: &str,
1365 json: &T,
1366 ) -> Result<GooseResponse, Box<TransactionError>> {
1367 // Build a Reqwest RequestBuilder object.
1368 let url = self.build_url(path)?;
1369 let reqwest_request_builder = self.client.post(url);
1370
1371 // POST json request.
1372 let goose_request = GooseRequest::builder()
1373 .method(GooseMethod::Post)
1374 .path(path)
1375 .set_request_builder(reqwest_request_builder.json(&json))
1376 .build();
1377
1378 // Make the request and return the GooseResponse.
1379 self.request(goose_request).await
1380 }
1381
1382 /// A helper to make a `HEAD` request of a path and collect relevant metrics.
1383 /// Automatically prepends the correct host.
1384 ///
1385 /// Calls to `head()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1386 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1387 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1388 ///
1389 /// If you need to set headers, change timeouts, or otherwise make use of the
1390 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1391 /// object, refer to [`GooseUser::get_request_builder`].
1392 ///
1393 /// # Example
1394 /// Make a HEAD request.
1395 /// ```rust
1396 /// use goose::prelude::*;
1397 ///
1398 /// let mut transaction = transaction!(head_function);
1399 ///
1400 /// /// A very simple transaction that makes a HEAD request.
1401 /// async fn head_function(user: &mut GooseUser) -> TransactionResult {
1402 /// let _goose = user.head("path/to/foo/").await?;
1403 ///
1404 /// Ok(())
1405 /// }
1406 /// ```
1407 pub async fn head(&mut self, path: &str) -> Result<GooseResponse, Box<TransactionError>> {
1408 // HEAD request.
1409 let goose_request = GooseRequest::builder()
1410 .method(GooseMethod::Head)
1411 .path(path)
1412 .build();
1413
1414 // Make the request and return the GooseResponse.
1415 self.request(goose_request).await
1416 }
1417
1418 /// A helper to make a `DELETE` request of a path and collect relevant metrics.
1419 /// Automatically prepends the correct host.
1420 ///
1421 /// Calls to `delete()` return a [`GooseResponse`](./struct.GooseResponse.html) object which
1422 /// contains a copy of the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)),
1423 /// and the response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1424 ///
1425 /// If you need to set headers, change timeouts, or otherwise make use of the
1426 /// [`reqwest::RequestBuilder`](https://docs.rs/reqwest/*/reqwest/struct.RequestBuilder.html)
1427 /// object, refer to [`GooseUser::get_request_builder`].
1428 ///
1429 /// # Example
1430 /// Make a DELETE request.
1431 /// ```rust
1432 /// use goose::prelude::*;
1433 ///
1434 /// let mut transaction = transaction!(delete_function);
1435 ///
1436 /// /// A very simple transaction that makes a DELETE request.
1437 /// async fn delete_function(user: &mut GooseUser) -> TransactionResult {
1438 /// let _goose = user.delete("path/to/foo/").await?;
1439 ///
1440 /// Ok(())
1441 /// }
1442 /// ```
1443 pub async fn delete(&mut self, path: &str) -> Result<GooseResponse, Box<TransactionError>> {
1444 // DELETE request.
1445 let goose_request = GooseRequest::builder()
1446 .method(GooseMethod::Delete)
1447 .path(path)
1448 .build();
1449
1450 // Make the request and return the GooseResponse.
1451 self.request(goose_request).await
1452 }
1453
1454 /// Used to get a [`reqwest::RequestBuilder`] object. If no [`reqwest::RequestBuilder`] is
1455 /// already defined in the [`GooseRequest`] passed to [`GooseUser::request`] it will automatically
1456 /// invoke this function.
1457 ///
1458 /// The HTTP request method must be defined as a [`GooseMethod`], and the path that will be requested
1459 /// must be defined as a [`&str`].
1460 ///
1461 /// It is possible to use this function to directly interact with the [`reqwest::RequestBuilder`]
1462 /// object and the [`GooseRequest`] object during load tests. In the following example, we set a
1463 /// timeout on the Request, and tell Goose to expect a 404 HTTP response status code.
1464 ///
1465 /// # Example
1466 /// Request a non-existent page, timing out after 500 milliseconds.
1467 /// ```rust
1468 /// use goose::prelude::*;
1469 ///
1470 /// let mut transaction = transaction!(test_404);
1471 ///
1472 /// async fn test_404(user: &mut GooseUser) -> TransactionResult {
1473 /// use std::time::Duration;
1474 ///
1475 /// // Manually interact with the Reqwest RequestBuilder object.
1476 /// let request_builder = user.get_request_builder(&GooseMethod::Get, "no/such/path")?
1477 /// // Configure the request to timeout if it takes longer than 500 milliseconds.
1478 /// .timeout(Duration::from_millis(500));
1479 ///
1480 /// // Manually build a GooseRequest.
1481 /// let goose_request = GooseRequest::builder()
1482 /// // Manually add our custom RequestBuilder object.
1483 /// .set_request_builder(request_builder)
1484 /// // Tell Goose to expect a 404 status code.
1485 /// .expect_status_code(404)
1486 /// // Turn the GooseRequestBuilder object into a GooseRequest.
1487 /// .build();
1488 ///
1489 /// // Finally make the actual request with our custom GooseRequest object.
1490 /// let _goose = user.request(goose_request).await?;
1491 ///
1492 /// Ok(())
1493 /// }
1494 /// ```
1495 pub fn get_request_builder(
1496 &self,
1497 method: &GooseMethod,
1498 path: &str,
1499 ) -> Result<RequestBuilder, Box<TransactionError>> {
1500 // Prepend the `base_url` to all relative paths.
1501 let url = self.build_url(path)?;
1502
1503 // Invoke appropriate Reqwest convenience function to generate an
1504 // appropriate RequestBuilder.
1505 Ok(match method {
1506 GooseMethod::Delete => self.client.delete(&url),
1507 GooseMethod::Get => self.client.get(&url),
1508 GooseMethod::Head => self.client.head(&url),
1509 GooseMethod::Patch => self.client.patch(&url),
1510 GooseMethod::Post => self.client.post(&url),
1511 GooseMethod::Put => self.client.put(&url),
1512 })
1513 }
1514
1515 /// Makes a request for the provided [`GooseRequest`] object, and if metrics are enabled
1516 /// captures relevant metrics.
1517 ///
1518 /// Calls to `request()` return a [`Result`] containing a [`GooseResponse`] on success, and a
1519 /// [`flume::SendError`](https://docs.rs/flume/*/flume/struct.SendError.html)`<bool>`,
1520 /// on failure. Failure only happens when `--throttle-requests` is enabled and the load test
1521 /// completes. The [`GooseResponse`](./struct.GooseResponse.html) object contains a copy of
1522 /// the request you made ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)), and the
1523 /// response ([`reqwest::Response`](https://docs.rs/reqwest/*/reqwest/struct.Response.html)).
1524 ///
1525 /// # Example
1526 /// Make a GET request.
1527 /// ```rust
1528 /// use goose::prelude::*;
1529 ///
1530 /// let mut transaction = transaction!(get_function);
1531 ///
1532 /// /// A simple transaction that makes a GET request.
1533 /// async fn get_function(user: &mut GooseUser) -> TransactionResult {
1534 /// let goose_request = GooseRequest::builder()
1535 /// // Goose will prepend a host name to this path.
1536 /// .path("path/to/loadtest")
1537 /// // GET is the default method, this is not necessary.
1538 /// .method(GooseMethod::Get)
1539 /// // Assemble the `GooseRequestBuilder` into a `GooseRequest.
1540 /// .build();
1541 /// let goose = user.request(goose_request).await?;
1542 ///
1543 /// // Do stuff with goose.request and/or goose.response here.
1544 ///
1545 /// Ok(())
1546 /// }
1547 /// ```
1548 pub async fn request(
1549 &mut self,
1550 mut request: GooseRequest<'_>,
1551 ) -> Result<GooseResponse, Box<TransactionError>> {
1552 // If the RequestBuilder is already defined in the GooseRequest use it.
1553 let request_builder = if request.request_builder.is_some() {
1554 request.request_builder.take().unwrap()
1555 // Otherwise get a new RequestBuilder.
1556 } else {
1557 self.get_request_builder(&request.method, request.path)?
1558 };
1559
1560 // Determine the name for this request.
1561 let request_name = self.get_request_name(&request);
1562
1563 // If throttle-requests is enabled...
1564 if self.is_throttled && self.throttle.is_some() {
1565 // ...wait until there's room to add a token to the throttle channel before proceeding.
1566 debug!("GooseUser: waiting on throttle");
1567 // Will result in TransactionError::RequestCanceled if this fails.
1568 if let Err(e) = self.throttle.clone().unwrap().send_async(true).await {
1569 return Err(Box::new(e.into()));
1570 }
1571 };
1572
1573 // Once past the throttle, the request is officially started.
1574 let started = Instant::now();
1575
1576 // Create a Reqwest Request object from the RequestBuilder.
1577 let built_request = match request_builder.build() {
1578 Ok(r) => r,
1579 Err(e) => return Err(Box::new(e.into())),
1580 };
1581
1582 // Get a string version of request path for logging.
1583 let path = match Url::parse(built_request.url().as_ref()) {
1584 Ok(u) => u.path().to_string(),
1585 Err(e) => {
1586 error!("failed to parse url: {}", e);
1587 "".to_string()
1588 }
1589 };
1590
1591 // Grab a copy of any headers set by this request, included in the request log
1592 // and the debug log.
1593 let mut headers: Vec<String> = Vec::new();
1594 for header in built_request.headers() {
1595 headers.push(format!("{:?}", header));
1596 }
1597
1598 // If enabled, grab a copy of the request body, included in the request log and
1599 // the debug log.
1600 let body = if self.config.request_body {
1601 // Get a bytes representation of the body, if any.
1602 let body_bytes = match built_request.body() {
1603 Some(b) => b.as_bytes().unwrap_or(b""),
1604 None => b"",
1605 };
1606 // Convert the bytes into a &str if valid utf8.
1607 str::from_utf8(body_bytes).unwrap_or("")
1608 } else {
1609 ""
1610 };
1611
1612 // Record the complete client request, included in the request log and the debug log.
1613 let raw_request = GooseRawRequest::new(
1614 goose_method_from_method(built_request.method().clone())?,
1615 built_request.url().as_str(),
1616 headers,
1617 body,
1618 );
1619
1620 // Provide details about the current transaction for logging.
1621 let transaction_detail = TransactionDetail {
1622 scenario_index: self.scenarios_index,
1623 scenario_name: self.scenario_name.as_str(),
1624 transaction_index: self
1625 .transaction_index
1626 .as_ref()
1627 .map_or_else(|| "", |v| v.as_ref()),
1628 transaction_name: self
1629 .transaction_name
1630 .as_ref()
1631 .map_or_else(|| "", |v| v.as_ref()),
1632 };
1633
1634 // Record information about the request.
1635 let mut request_metric = GooseRequestMetric::new(
1636 raw_request,
1637 transaction_detail,
1638 request_name,
1639 self.started.elapsed().as_millis(),
1640 self.weighted_users_index,
1641 );
1642
1643 // Make the actual request.
1644 let response = self.client.execute(built_request).await;
1645 request_metric.set_response_time(started.elapsed().as_millis());
1646
1647 // Determine if the request suceeded or failed.
1648 match &response {
1649 Ok(r) => {
1650 let status_code = r.status();
1651 debug!("{:?}: status_code {}", &path, status_code);
1652
1653 // Update the request_metric object.
1654 request_metric.set_status_code(Some(status_code));
1655 request_metric.set_final_url(r.url().as_str());
1656
1657 // Check if we were expecting a specific status code.
1658 if let Some(expect_status_code) = request.expect_status_code {
1659 // Record a failure if the expected status code was not returned.
1660 if status_code != expect_status_code {
1661 request_metric.success = false;
1662 request_metric.error = format!("{}: {}", status_code, request_name);
1663 }
1664 // Otherwise record a failure if the returned status code was not a success.
1665 } else if !status_code.is_success() {
1666 request_metric.success = false;
1667 request_metric.error = format!("{}: {}", status_code, request_name);
1668 }
1669
1670 // Load test user was redirected.
1671 if self.config.sticky_follow && request_metric.raw.url != request_metric.final_url {
1672 let base_url = self.base_url.to_string();
1673 // Check if the URL redirected started with the load test base_url.
1674 if !request_metric.final_url.starts_with(&base_url) {
1675 let redirected_url = match Url::parse(&request_metric.final_url) {
1676 Ok(u) => u,
1677 Err(e) => return Err(Box::new(e.into())),
1678 };
1679 let redirected_base_url =
1680 redirected_url[..url::Position::BeforePath].to_string();
1681 info!(
1682 "base_url for user {} redirected from {} to {}",
1683 self.weighted_users_index + 1,
1684 &base_url,
1685 &redirected_base_url
1686 );
1687 let _ = self.set_base_url(&redirected_base_url);
1688 }
1689 }
1690 }
1691 Err(e) => {
1692 // @TODO: what can we learn from a reqwest error?
1693 warn!("{:?}: {}", &path, e);
1694 request_metric.success = false;
1695 request_metric.set_status_code(None);
1696 request_metric.error = clean_reqwest_error(e, request_name);
1697 }
1698 };
1699
1700 // If enabled, track the cadence between each time the same request is made while
1701 // this GooseUser is running. If requests are blocked by the upstream server, this
1702 // allows Goose to backfill the requests that should have been made based on
1703 // cadence statistics.
1704 request_metric.user_cadence = self
1705 .coordinated_omission_mitigation(&request_metric)
1706 .await?;
1707
1708 // Send a copy of the raw request object to the parent process if
1709 // we're tracking metrics.
1710 if !self.config.no_metrics {
1711 self.send_request_metric_to_parent(request_metric.clone())?;
1712 }
1713
1714 if request.error_on_fail && !request_metric.success {
1715 error!("{:?} {}", &path, &request_metric.error);
1716 return Err(Box::new(TransactionError::RequestFailed {
1717 raw_request: request_metric,
1718 }));
1719 }
1720
1721 Ok(GooseResponse::new(request_metric, response))
1722 }
1723
1724 /// Tracks the time it takes for the current GooseUser to loop through all Transactions
1725 /// if Coordinated Omission Mitigation is enabled.
1726 pub(crate) async fn update_request_cadence(&mut self, thread_number: usize) {
1727 if let Some(co_mitigation) = self.config.co_mitigation.as_ref() {
1728 // Return immediately if coordinated omission mitigation is disabled.
1729 if co_mitigation == &GooseCoordinatedOmissionMitigation::Disabled {
1730 return;
1731 }
1732
1733 // Grab the current timestamp to calculate the difference since the last
1734 // time through the loop.
1735 let now = std::time::Instant::now();
1736
1737 // Swap out the `slept` counter, which is the total time the GooseUser slept
1738 // between transactions, a potentially randomly changing value. Reset to 0 for the
1739 // next loop through all Transactions.
1740 self.request_cadence.delays_since_last_time = self.slept;
1741 self.slept = 0;
1742
1743 // How much time passed since the last time this GooseUser looped through all
1744 // transactions, accounting for time waiting between Transactions due to `set_wait_time`.
1745 let elapsed = (now - self.request_cadence.last_time).as_millis() as u64
1746 - self.request_cadence.delays_since_last_time;
1747
1748 // Update `minimum_cadence` if this was the fastest seen.
1749 if elapsed < self.request_cadence.minimum_cadence
1750 || self.request_cadence.minimum_cadence == 0
1751 {
1752 self.request_cadence.minimum_cadence = elapsed;
1753 // Update `maximum_cadence` if this was the slowest seen.
1754 } else if elapsed > self.request_cadence.maximum_cadence {
1755 self.request_cadence.maximum_cadence = elapsed;
1756 }
1757
1758 // Update request_cadence metrics based on the timing of the current request.
1759 self.request_cadence.counter += 1;
1760 self.request_cadence.total_elapsed += elapsed;
1761 self.request_cadence.last_time = now;
1762 self.request_cadence.average_cadence =
1763 self.request_cadence.total_elapsed / self.request_cadence.counter;
1764
1765 if self.request_cadence.counter > 3 {
1766 if self.request_cadence.coordinated_omission_counter < 0 {
1767 debug!(
1768 "user {} enabled coordinated omission mitigation",
1769 thread_number
1770 );
1771 self.request_cadence.coordinated_omission_counter += 1;
1772 }
1773 // Calculate the expected cadence for this Transaction request.
1774 let cadence = match co_mitigation {
1775 // Expected cadence is the average time between requests.
1776 GooseCoordinatedOmissionMitigation::Average => {
1777 self.request_cadence.average_cadence
1778 }
1779 // Expected cadence is the maximum time between requests.
1780 GooseCoordinatedOmissionMitigation::Maximum => {
1781 self.request_cadence.maximum_cadence
1782 }
1783 // Expected cadence is the minimum time between requests.
1784 GooseCoordinatedOmissionMitigation::Minimum => {
1785 self.request_cadence.minimum_cadence
1786 }
1787 // This is not possible as we would have exited already if coordinated
1788 // omission mitigation was disabled.
1789 GooseCoordinatedOmissionMitigation::Disabled => unreachable!(),
1790 };
1791 if elapsed > (cadence * 2) {
1792 debug!(
1793 "user {}: coordinated_omission_mitigation: elapsed({}) > cadence({})",
1794 thread_number, elapsed, cadence
1795 );
1796 self.request_cadence.coordinated_omission_counter += 1;
1797 self.request_cadence.coordinated_omission_mitigation = elapsed;
1798 } else {
1799 self.request_cadence.coordinated_omission_mitigation = 0;
1800 }
1801 // Always track the expected cadence.
1802 self.request_cadence.user_cadence = cadence;
1803 }
1804 } else {
1805 // Coordinated Omission Mitigation defaults to average.
1806 unreachable!();
1807 }
1808 }
1809
1810 /// If Coordinated Omission Mitigation is enabled, compares how long has passed since the last
1811 /// loop through all Transactions by the current GooseUser. Through this mechanism, Goose is
1812 /// able to detect stalls on the upstream server being load tested, backfilling requests based
1813 /// on what statistically should have happened. Can be disabled with `--co-mitigation disabled`.
1814 async fn coordinated_omission_mitigation(
1815 &self,
1816 request_metric: &GooseRequestMetric,
1817 ) -> Result<u64, Box<TransactionError>> {
1818 if let Some(co_mitigation) = self.config.co_mitigation.as_ref() {
1819 // Return immediately if coordinated omission mitigation is disabled.
1820 if co_mitigation == &GooseCoordinatedOmissionMitigation::Disabled {
1821 return Ok(0);
1822 }
1823
1824 // Generate an info level alert if this specific request took longer than the normal
1825 // cadence, as that means this specific request will likely trigger Coordinated
1826 // Omission Mitigation.
1827 if self.request_cadence.counter > 3
1828 && request_metric.response_time > self.request_cadence.user_cadence
1829 {
1830 let transaction_name = if let Some(transaction_name) = &self.transaction_name {
1831 format!(", transaction name: \"{}\"", transaction_name)
1832 } else {
1833 "".to_string()
1834 };
1835 info!(
1836 "{:.3}s into goose attack: \"{} {}\" [{}] took abnormally long ({} ms){}",
1837 request_metric.elapsed as f64 / 1_000.0,
1838 request_metric.raw.method,
1839 request_metric.raw.url,
1840 request_metric.status_code,
1841 request_metric.response_time,
1842 transaction_name,
1843 );
1844 }
1845
1846 // Check if Coordinated Omission Mitigation has been triggered.
1847 if self.request_cadence.coordinated_omission_mitigation > 0 {
1848 // Base our coordinated omission generated request metric on the actual
1849 // metric that triggered this logic.
1850 let mut coordinated_omission_request_metric = request_metric.clone();
1851 // Record data points specific to coordinated_omission.
1852 coordinated_omission_request_metric.coordinated_omission_elapsed =
1853 self.request_cadence.coordinated_omission_mitigation;
1854 // Record data points specific to coordinated_omission.
1855 coordinated_omission_request_metric.user_cadence =
1856 self.request_cadence.user_cadence;
1857 // Send the coordinated omission mitigation generated metrics to the parent.
1858 self.send_request_metric_to_parent(coordinated_omission_request_metric)?;
1859 }
1860 Ok(self.request_cadence.user_cadence)
1861 } else {
1862 // A setting for coordinated omission mitigation is required, defaults to Average.
1863 unreachable!();
1864 }
1865 }
1866
1867 fn send_request_metric_to_parent(
1868 &self,
1869 request_metric: GooseRequestMetric,
1870 ) -> TransactionResult {
1871 // If requests-file is enabled, send a copy of the raw request to the logger thread.
1872 if !self.config.request_log.is_empty() {
1873 if let Some(logger) = self.logger.as_ref() {
1874 if let Err(e) = logger.send(Some(GooseLog::Request(request_metric.clone()))) {
1875 return Err(Box::new(e.into()));
1876 }
1877 }
1878 }
1879
1880 // Parent is not defined when running
1881 // [`test_start`](../struct.GooseAttack.html#method.test_start),
1882 // [`test_stop`](../struct.GooseAttack.html#method.test_stop), and during testing.
1883 if let Some(metrics_channel) = self.metrics_channel.clone() {
1884 if let Err(e) = metrics_channel.send(GooseMetric::Request(Box::new(request_metric))) {
1885 return Err(Box::new(e.into()));
1886 }
1887 }
1888
1889 Ok(())
1890 }
1891
1892 /// If `request_name` is set, unwrap and use this. Otherwise, if the Transaction has a name
1893 /// set use it. Otherwise use the path.
1894 fn get_request_name<'a>(&'a self, request: &'a GooseRequest) -> &'a str {
1895 match request.name {
1896 // If a request.name is set, unwrap and return it.
1897 Some(rn) => rn,
1898 None => {
1899 // Otherwise determine if the current Transaction is named, and if so return it.
1900 if let Some(transaction_name) = &self.transaction_name {
1901 transaction_name
1902 } else {
1903 // Otherwise return a copy of the the path.
1904 request.path
1905 }
1906 }
1907 }
1908 }
1909
1910 /// Manually mark a request as a success.
1911 ///
1912 /// Goose determines if a request was successful based on the the HTTP response status
1913 /// code. By default, it uses [`reqwest::StatusCode::is_success`]. If an alternative
1914 /// HTTP response code is expected, use [`GooseRequestBuilder::expect_status_code`]. If
1915 /// validation requires additional logic, you can use set_success().
1916 ///
1917 /// A copy of your original request is returned with the response, and a mutable copy
1918 /// must be included when setting a request as a success.
1919 ///
1920 /// # Example
1921 /// ```rust
1922 /// use goose::prelude::*;
1923 ///
1924 /// let mut transaction = transaction!(get_function);
1925 ///
1926 /// /// A simple transaction that makes a GET request.
1927 /// async fn get_function(user: &mut GooseUser) -> TransactionResult {
1928 /// let mut goose = user.get("404").await?;
1929 ///
1930 /// if let Ok(response) = &goose.response {
1931 /// // We expect a 404 here.
1932 /// if response.status() == 404 {
1933 /// return user.set_success(&mut goose.request);
1934 /// }
1935 /// }
1936 ///
1937 /// Err(Box::new(TransactionError::RequestFailed {
1938 /// raw_request: goose.request.clone(),
1939 /// }))
1940 /// }
1941 /// ````
1942 pub fn set_success(&self, request: &mut GooseRequestMetric) -> TransactionResult {
1943 // Only send update if this was previously not a success.
1944 if !request.success {
1945 request.success = true;
1946 request.update = true;
1947 self.send_request_metric_to_parent(request.clone())?;
1948 }
1949
1950 Ok(())
1951 }
1952
1953 /// Manually mark a request as a failure.
1954 ///
1955 /// By default, Goose will consider any response with a 2xx status code as a success.
1956 /// You may require more advanced logic, in which a 2xx status code is actually a
1957 /// failure. A copy of your original request is returned with the response, and a
1958 /// mutable copy must be included when setting a request as a failure.
1959 ///
1960 /// Calls to `set_failure` must include four parameters. The first, `tag`, is an
1961 /// arbitrary string identifying the reason for the failure, used when logging. The
1962 /// second, `request`, is a mutable reference to the
1963 /// ([`GooseRequestMetric`](./struct.GooseRequestMetric.html)) object of the request being
1964 /// identified as a failure (the contained `success` field will be set to `false`,
1965 /// and the `update` field will be set to `true`). The last two parameters, `header`
1966 /// and `body`, are optional and used to provide more detail in logs.
1967 ///
1968 /// The value of `tag` will normally be collected into the errors summary table if
1969 /// metrics are being displayed. However, if `set_failure` is called multiple times,
1970 /// or is called on a request that was already an error, only the first error will
1971 /// be collected.
1972 ///
1973 /// This also calls [`GooseUser::log_debug`].
1974 ///
1975 /// # Example
1976 /// ```rust
1977 /// use goose::prelude::*;
1978 ///
1979 /// let mut transaction = transaction!(loadtest_index_page);
1980 ///
1981 /// async fn loadtest_index_page(user: &mut GooseUser) -> TransactionResult {
1982 /// let mut goose = user.get("").await?;
1983 ///
1984 /// if let Ok(response) = goose.response {
1985 /// // We only need to check pages that returned a success status code.
1986 /// if response.status().is_success() {
1987 /// match response.text().await {
1988 /// Ok(text) => {
1989 /// // If the expected string doesn't exist, this page load
1990 /// // was a failure.
1991 /// if !text.contains("this string must exist") {
1992 /// // As this is a named request, pass in the name not the URL
1993 /// return user.set_failure("string missing", &mut goose.request, None, None);
1994 /// }
1995 /// }
1996 /// // Empty page, this is a failure.
1997 /// Err(_) => {
1998 /// return user.set_failure("empty page", &mut goose.request, None, None);
1999 /// }
2000 /// }
2001 /// }
2002 /// };
2003 ///
2004 /// Ok(())
2005 /// }
2006 /// ````
2007 pub fn set_failure(
2008 &self,
2009 tag: &str,
2010 request: &mut GooseRequestMetric,
2011 headers: Option<&header::HeaderMap>,
2012 body: Option<&str>,
2013 ) -> TransactionResult {
2014 // Only send update if this was previously a success.
2015 if request.success {
2016 request.success = false;
2017 request.update = true;
2018 request.error = tag.to_string();
2019 self.send_request_metric_to_parent(request.clone())?;
2020 }
2021 // Write failure to log, converting `&mut request` to `&request` as needed by `log_debug()`.
2022 self.log_debug(tag, Some(&*request), headers, body)?;
2023
2024 // Print log to stdout.
2025 info!("set_failure: {}", tag);
2026
2027 Err(Box::new(TransactionError::RequestFailed {
2028 raw_request: request.clone(),
2029 }))
2030 }
2031
2032 /// Write to [`debug_file`](../struct.GooseConfiguration.html#structfield.debug_file)
2033 /// if enabled.
2034 ///
2035 /// This function provides a mechanism for optional debug logging when a load test
2036 /// is running. This can be especially helpful when writing a load test. Each entry
2037 /// must include a tag, which is an arbitrary string identifying the debug message.
2038 /// It may also optionally include references to the GooseRequestMetric made, the headers
2039 /// returned by the server, and the response body returned by the server,
2040 ///
2041 /// As the response body can be large, the `--no-debug-body` option (or
2042 /// [`GooseDefault::NoDebugBody`](../config/enum.GooseDefault.html#variant.NoDebugBody) default)
2043 /// can be set to prevent the debug log from including the response body. When this option
2044 /// is enabled, the body will always show up as `null` in the debug log.
2045 ///
2046 /// Calls to [`GooseUser::set_failure`] automatically invoke `log_debug`.
2047 ///
2048 /// To enable the debug log, a load test must be run with the `--debug-log-file=foo`
2049 /// option set, where `foo` is either a relative or an absolute path of the log file
2050 /// to create. Any existing file will be overwritten.
2051 ///
2052 /// In the following example, we are logging debug messages whenever there are errors.
2053 ///
2054 /// # Example
2055 /// ```rust
2056 /// use goose::prelude::*;
2057 ///
2058 /// let mut transaction = transaction!(loadtest_index_page);
2059 ///
2060 /// async fn loadtest_index_page(user: &mut GooseUser) -> TransactionResult {
2061 /// let mut goose = user.get("").await?;
2062 ///
2063 /// match goose.response {
2064 /// Ok(response) => {
2065 /// // Grab a copy of the headers so we can include them when logging errors.
2066 /// let headers = &response.headers().clone();
2067 /// // We only need to check pages that returned a success status code.
2068 /// if !response.status().is_success() {
2069 /// match response.text().await {
2070 /// Ok(html) => {
2071 /// // Server returned an error code, log everything.
2072 /// user.log_debug(
2073 /// "error loading /",
2074 /// Some(&goose.request),
2075 /// Some(headers),
2076 /// Some(&html),
2077 /// );
2078 /// },
2079 /// Err(e) => {
2080 /// // No body was returned, log everything else.
2081 /// user.log_debug(
2082 /// &format!("error loading /: {}", e),
2083 /// Some(&goose.request),
2084 /// Some(headers),
2085 /// None,
2086 /// );
2087 /// }
2088 /// }
2089 /// }
2090 /// },
2091 /// // No response from server.
2092 /// Err(e) => {
2093 /// user.log_debug(
2094 /// "no response from server when loading /",
2095 /// Some(&goose.request),
2096 /// None,
2097 /// None,
2098 /// );
2099 /// }
2100 /// }
2101 ///
2102 /// Ok(())
2103 /// }
2104 /// ````
2105 pub fn log_debug(
2106 &self,
2107 tag: &str,
2108 request: Option<&GooseRequestMetric>,
2109 headers: Option<&header::HeaderMap>,
2110 body: Option<&str>,
2111 ) -> TransactionResult {
2112 if !self.config.debug_log.is_empty() {
2113 // Logger is not defined when running
2114 // [`test_start`](../struct.GooseAttack.html#method.test_start),
2115 // [`test_stop`](../struct.GooseAttack.html#method.test_stop), and during testing.
2116 if let Some(logger) = self.logger.clone() {
2117 if self.config.no_debug_body {
2118 if let Err(e) = logger.send(Some(GooseLog::Debug(GooseDebug::new(
2119 tag, request, headers, None,
2120 )))) {
2121 return Err(Box::new(e.into()));
2122 }
2123 } else if let Err(e) = logger.send(Some(GooseLog::Debug(GooseDebug::new(
2124 tag, request, headers, body,
2125 )))) {
2126 return Err(Box::new(e.into()));
2127 }
2128 }
2129 }
2130
2131 Ok(())
2132 }
2133
2134 /// Manually build a
2135 /// [`reqwest::Client`](https://docs.rs/reqwest/*/reqwest/struct.Client.html).
2136 ///
2137 /// By default, Goose configures the following options when building a
2138 /// [`reqwest::Client`](https://docs.rs/reqwest/*/reqwest/struct.Client.html):
2139 /// - reports itself as the
2140 /// [`user_agent`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.user_agent)
2141 /// requesting web pages (ie `goose/0.15.2`);
2142 /// - [stores cookies](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.cookie_store),
2143 /// generally necessary if you aim to simulate logged in users;
2144 /// - enables
2145 /// [`gzip`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.gzip) compression;
2146 /// - sets a 60 second [`timeout`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.timeout) all
2147 /// on all requests.
2148 ///
2149 /// # Default configuration:
2150 ///
2151 /// ```rust
2152 /// use reqwest::Client;
2153 /// use core::time::Duration;
2154 ///
2155 /// static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
2156 ///
2157 /// let builder = Client::builder()
2158 /// .user_agent(APP_USER_AGENT)
2159 /// .cookie_store(true)
2160 /// .gzip(true)
2161 /// .timeout(Duration::from_secs(60));
2162 /// ```
2163 ///
2164 /// Alternatively, you can use this function to manually build a
2165 /// [`reqwest::Client`](https://docs.rs/reqwest/*/reqwest/struct.Client.html).
2166 /// with custom configuration. Available options are found in the
2167 /// [`reqwest::ClientBuilder`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html)
2168 /// documentation.
2169 ///
2170 /// When manually building a
2171 /// [`reqwest::Client`](https://docs.rs/reqwest/*/reqwest/struct.Client.html),
2172 /// there are a few things to be aware of:
2173 /// - Manually building a client in [`test_start`](../struct.GooseAttack.html#method.test_start)
2174 /// will only affect requests made during test setup;
2175 /// - Manually building a client in [`test_stop`](../struct.GooseAttack.html#method.test_stop)
2176 /// will only affect requests made during test teardown;
2177 /// - A manually built client is specific to a single Goose thread -- if you are
2178 /// generating a large load test with many users, each will need to manually build their
2179 /// own client (typically you'd do this in a Transaction that is registered with
2180 /// [`Transaction::set_on_start()`] in each Scenario requiring a custom client;
2181 /// - Manually building a client will completely replace the automatically built client
2182 /// with a brand new one, so any configuration, cookies or headers set in the previously
2183 /// built client will be gone;
2184 /// - You must include all desired configuration, as you are completely replacing Goose
2185 /// defaults. For example, if you want Goose clients to store cookies, you will have to
2186 /// include
2187 /// [`.cookie_store(true)`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.cookie_store).
2188 ///
2189 /// In the following example, the Goose client is configured with a different user agent,
2190 /// sets a default header on every request, stores cookies, supports gzip compression, and
2191 /// times out requests after 30 seconds.
2192 ///
2193 /// ## Example
2194 /// ```rust
2195 /// use goose::prelude::*;
2196 /// use core::time::Duration;
2197 ///
2198 /// transaction!(setup_custom_client).set_on_start();
2199 ///
2200 /// async fn setup_custom_client(user: &mut GooseUser) -> TransactionResult {
2201 /// use reqwest::{Client, header};
2202 ///
2203 /// // Build a custom HeaderMap to include with all requests made by this client.
2204 /// let mut headers = header::HeaderMap::new();
2205 /// headers.insert("X-Custom-Header", header::HeaderValue::from_str("custom value").unwrap());
2206 ///
2207 /// // Build a custom client.
2208 /// let builder = Client::builder()
2209 /// .default_headers(headers)
2210 /// .user_agent("custom user agent")
2211 /// .cookie_store(true)
2212 /// .gzip(true)
2213 /// .timeout(Duration::from_secs(30));
2214 ///
2215 /// // Assign the custom client to this GooseUser.
2216 /// user.set_client_builder(builder).await?;
2217 ///
2218 /// Ok(())
2219 /// }
2220 /// ```
2221 ///
2222 /// # Alternative Compression Algorithms
2223 /// Reqwest also supports
2224 /// [`brotli`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.brotli) and
2225 /// [`deflate`](https://docs.rs/reqwest/*/reqwest/struct.ClientBuilder.html#method.deflate) compression.
2226 ///
2227 /// To enable either, you must enable the features in your load test's `Cargo.toml`, for example:
2228 /// ```text
2229 /// reqwest = { version = "^0.11.4", default-features = false, features = [
2230 /// "brotli",
2231 /// "cookies",
2232 /// "deflate",
2233 /// "gzip",
2234 /// "json",
2235 /// ] }
2236 /// ```
2237 ///
2238 /// Once enabled, you can add `.brotli(true)` and/or `.deflate(true)` to your custom
2239 /// [`reqwest::Client::builder()`], following the documentation above.
2240 ///
2241 /// # Custom Cookies
2242 /// Custom cookies can also be manually set when building a custom [`reqwest::Client`]. This requires
2243 /// loading the [`GooseUser::base_url`] being load tested in order to properly build the cookie. Then
2244 /// a custom [`reqwest::cookie::Jar`] is created and the custom cookie is added with
2245 /// [`reqwest::cookie::Jar::add_cookie_str`]. Finally, the new cookie jar must be specified as the
2246 /// [`reqwest::ClientBuilder::cookie_provider`] for the custom client.
2247 ///
2248 /// ## Example
2249 /// ```rust
2250 /// use reqwest::{cookie::Jar, Client};
2251 /// use std::sync::Arc;
2252 ///
2253 /// use goose::prelude::*;
2254 ///
2255 /// transaction!(custom_cookie_with_custom_client).set_on_start();
2256 ///
2257 /// async fn custom_cookie_with_custom_client(user: &mut GooseUser) -> TransactionResult {
2258 /// // Prepare the contents of a custom cookie.
2259 /// let cookie = "my-custom-cookie=custom-value";
2260 ///
2261 /// // Pre-load one or more cookies into a custom cookie jar to use with this client.
2262 /// let jar = Jar::default();
2263 /// jar.add_cookie_str(
2264 /// cookie,
2265 /// &user.base_url,
2266 /// );
2267 ///
2268 /// // Build a custom client.
2269 /// let builder = Client::builder()
2270 /// .user_agent("example-loadtest")
2271 /// .cookie_store(true)
2272 /// .cookie_provider(Arc::new(jar))
2273 /// .gzip(true);
2274 ///
2275 /// // Assign the custom client to this GooseUser.
2276 /// user.set_client_builder(builder).await?;
2277 ///
2278 /// Ok(())
2279 /// }
2280 /// ```
2281 pub async fn set_client_builder(
2282 &mut self,
2283 builder: ClientBuilder,
2284 ) -> Result<(), TransactionError> {
2285 self.client = builder.build()?;
2286
2287 Ok(())
2288 }
2289
2290 /// Some websites use multiple domains to serve traffic, redirecting depending on
2291 /// the user's roll. For this reason, Goose needs to respect a redirect of the
2292 /// `base_url` and subsequent paths should be built from the redirect domain.
2293 ///
2294 /// For example, if the `base_url` (ie `--host`) is set to `foo.example.com` and the
2295 /// load test requests `/login`, thereby loading `http://foo.example.com/login` and
2296 /// this request gets redirected by the server to `http://foo-secure.example.com/`,
2297 /// subsequent requests made by this user need to be against the new
2298 /// `foo-secure.example.com domain`. (Further, if the `base_url` is again redirected,
2299 /// such as when loading `http://foo-secure.example.com/logout`, the user should
2300 /// again follow for subsequent requests, perhaps in this case back to
2301 /// `foo.example.com`.)
2302 ///
2303 /// Load tests can also request absolute URLs, and if these URLs are redirected
2304 /// it does not affect the `base_url` of the load test. For example, if
2305 /// `foo.example.com` is the base url, and the load test requests
2306 /// `http://bar.example.com` (a different domain) and this request gets redirected
2307 /// to `http://other.example.com`, subsequent relative requests would still be made
2308 /// against `foo.example.com`.
2309 ///
2310 /// This functionality is used internally by Goose to follow redirects of the
2311 /// `base_url` when `--sticky-follow` is specified at run time, or
2312 /// [`set_default`](../struct.GooseAttack.html#method.set_default)
2313 /// `(`[`GooseDefault::StickyFollow`](../config/enum.GooseDefault.html#variant.StickyFollow)
2314 /// `, true)` is enabled. It is also
2315 /// available to be manually invoked from a load test such as in the following
2316 /// example.
2317 ///
2318 /// # Example
2319 /// ```rust
2320 /// use goose::prelude::*;
2321 /// use std::time::Duration;
2322 ///
2323 /// #[tokio::main]
2324 /// async fn main() -> Result<(), GooseError> {
2325 /// let _goose_metrics = GooseAttack::initialize()?
2326 /// .register_scenario(scenario!("LoadtestTransactions")
2327 /// .set_host("http://foo.example.com/")
2328 /// .set_wait_time(Duration::from_secs(0), Duration::from_secs(3))?
2329 /// .register_transaction(transaction!(transaction_foo).set_weight(10)?)
2330 /// .register_transaction(transaction!(transaction_bar))
2331 /// )
2332 /// // Set a default run time so this test runs to completion.
2333 /// .set_default(GooseDefault::RunTime, 1)?
2334 /// .execute()
2335 /// .await?;
2336 ///
2337 /// Ok(())
2338 /// }
2339 ///
2340 /// async fn transaction_foo(user: &mut GooseUser) -> TransactionResult {
2341 /// let _goose = user.get("").await?;
2342 ///
2343 /// Ok(())
2344 /// }
2345 ///
2346 /// async fn transaction_bar(user: &mut GooseUser) -> TransactionResult {
2347 /// // Before this transaction runs, all requests are being made against
2348 /// // http://foo.example.com, after this transaction runs all subsequent
2349 /// // requests are made against http://bar.example.com/.
2350 /// user.set_base_url("http://bar.example.com/");
2351 /// let _goose = user.get("").await?;
2352 ///
2353 /// Ok(())
2354 /// }
2355 /// ```
2356 pub fn set_base_url(&mut self, host: &str) -> Result<(), Box<TransactionError>> {
2357 self.base_url = match Url::parse(host) {
2358 Ok(u) => u,
2359 Err(e) => return Err(Box::new(e.into())),
2360 };
2361 Ok(())
2362 }
2363}
2364
2365/// Internal helper function to create the default GooseUser reqwest client
2366pub(crate) fn create_reqwest_client(
2367 configuration: &GooseConfiguration,
2368) -> Result<Client, reqwest::Error> {
2369 // Either use manually configured timeout, or default.
2370 let timeout = if configuration.timeout.is_some() {
2371 match crate::util::get_float_from_string(configuration.timeout.clone()) {
2372 Some(f) => f as u64 * 1_000,
2373 None => GOOSE_REQUEST_TIMEOUT,
2374 }
2375 } else {
2376 GOOSE_REQUEST_TIMEOUT
2377 };
2378
2379 Client::builder()
2380 .user_agent(APP_USER_AGENT)
2381 .cookie_store(true)
2382 .timeout(Duration::from_millis(timeout))
2383 // Enable gzip unless `--no-gzip` flag is enabled.
2384 .gzip(!configuration.no_gzip)
2385 // Validate https certificates unless `--accept-invalid-certs` is enabled.
2386 .danger_accept_invalid_certs(configuration.accept_invalid_certs)
2387 .build()
2388}
2389
2390/// Defines the HTTP requests that Goose makes.
2391///
2392/// Can be manually created and configured with [`GooseRequest::builder`], but it's typically
2393/// more convenient to use the [`GooseUser::get`], [`GooseUser::get_named`], [`GooseUser::post`],
2394/// [`GooseUser::post_form`], [`GooseUser::post_json`], [`GooseUser::head`] and
2395/// [`GooseUser::delete`] helpers.
2396///
2397/// For complete instructions review [`GooseRequestBuilder`].
2398#[derive(Debug)]
2399pub struct GooseRequest<'a> {
2400 // Defaults to `""`.
2401 path: &'a str,
2402 // Defaults to [`GooseMethod::Get`].
2403 method: GooseMethod,
2404 // Defaults to [`None`].
2405 name: Option<&'a str>,
2406 // Defaults to [`None`].
2407 expect_status_code: Option<u16>,
2408 // Defaults to [`false`].
2409 error_on_fail: bool,
2410 // Defaults to [`None`].
2411 request_builder: Option<RequestBuilder>,
2412}
2413impl<'a> GooseRequest<'a> {
2414 /// Convenience function to bring [`GooseRequestBuilder`] into scope.
2415 pub fn builder() -> GooseRequestBuilder<'a> {
2416 GooseRequestBuilder::new()
2417 }
2418}
2419
2420/// Used to build a [`GooseRequest`] object, necessary to make a request with Goose.
2421///
2422/// It's only necessary to build manually if the [`GooseUser::get`], [`GooseUser::get_named`],
2423/// [`GooseUser::post`], [`GooseUser::post_form`], [`GooseUser::post_json`], [`GooseUser::head`]
2424/// and [`GooseUser::delete`] helpers don't provide you with enough flexibility.
2425///
2426/// # Example
2427/// ```rust
2428/// use goose::prelude::*;
2429///
2430/// let mut a_transaction = transaction!(transaction_function);
2431///
2432/// // A simple transaction that loads a relative path.
2433/// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2434/// // Manually create a GooseRequestBuilder object.
2435/// let goose_request = GooseRequest::builder()
2436/// // Set a relative path to request.
2437/// .path("about")
2438/// // Name the request in the metircs.
2439/// .name("About page")
2440/// // Build the GooseRequest object.
2441/// .build();
2442///
2443/// // Make the configured request.
2444/// let _goose = user.request(goose_request).await?;
2445///
2446/// Ok(())
2447/// }
2448/// ```
2449pub struct GooseRequestBuilder<'a> {
2450 path: &'a str,
2451 method: GooseMethod,
2452 name: Option<&'a str>,
2453 expect_status_code: Option<u16>,
2454 error_on_fail: bool,
2455 request_builder: Option<RequestBuilder>,
2456}
2457impl<'a> GooseRequestBuilder<'a> {
2458 // Internal method to build a [`GooseRequest`] from a [`GooseRequestBuilder`].
2459 fn new() -> Self {
2460 Self {
2461 path: "",
2462 method: GooseMethod::Get,
2463 name: None,
2464 expect_status_code: None,
2465 error_on_fail: false,
2466 request_builder: None,
2467 }
2468 }
2469
2470 /// Set the path to request.
2471 ///
2472 /// Typically is a relative path allowing Goose to append a configurable base_url.
2473 ///
2474 /// Defaults to `""` (the main index).
2475 ///
2476 /// # Example
2477 /// This can be implemented in a simpler way using the [`GooseUser::get`] helper function.
2478 /// ```rust
2479 /// use goose::prelude::*;
2480 ///
2481 /// let mut a_transaction = transaction!(transaction_function);
2482 ///
2483 /// // A simple transaction that loads a relative path.
2484 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2485 /// // Manually create a GooseRequestBuilder object.
2486 /// let goose_request = GooseRequest::builder()
2487 /// // Set a relative path to request.
2488 /// .path("a/relative/path")
2489 /// // Build the GooseRequest object.
2490 /// .build();
2491 ///
2492 /// // Make the configured request.
2493 /// let _goose = user.request(goose_request).await?;
2494 ///
2495 /// Ok(())
2496 /// }
2497 /// ```
2498 pub fn path(mut self, path: impl Into<&'a str>) -> Self {
2499 self.path = path.into();
2500 self
2501 }
2502
2503 /// Set the method of the request.
2504 ///
2505 /// Must be set to a [`GooseMethod`].
2506 ///
2507 /// Defaults to [`GooseMethod::Get`].
2508 ///
2509 /// # Example
2510 /// ```rust
2511 /// use goose::prelude::*;
2512 ///
2513 /// let mut a_transaction = transaction!(transaction_function);
2514 ///
2515 /// // Make a DELETE request.
2516 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2517 /// // Manually create a GooseRequestBuilder object.
2518 /// let goose_request = GooseRequest::builder()
2519 /// // Set a relative path to request.
2520 /// .path("path/to/delete")
2521 /// // Set the method to DELETE.
2522 /// .method(GooseMethod::Delete)
2523 /// // Build the GooseRequest object.
2524 /// .build();
2525 ///
2526 /// // Make the configured DELETE request.
2527 /// let _goose = user.request(goose_request).await?;
2528 ///
2529 /// Ok(())
2530 /// }
2531 /// ```
2532 pub fn method(mut self, method: GooseMethod) -> Self {
2533 self.method = method;
2534 self
2535 }
2536
2537 /// Set a name for the request, affecting how it shows up in metrics.
2538 ///
2539 /// Must be set to a [`GooseMethod`].
2540 ///
2541 /// Defaults to [`GooseMethod::Get`].
2542 ///
2543 /// # Example
2544 /// ```rust
2545 /// use goose::prelude::*;
2546 ///
2547 /// let mut a_transaction = transaction!(transaction_function);
2548 ///
2549 /// // Make a named request.
2550 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2551 /// // Manually create a GooseRequestBuilder object.
2552 /// let goose_request = GooseRequest::builder()
2553 /// // Set a relative path to request.
2554 /// .path("path/to/request")
2555 /// // Name the request in the metrics.
2556 /// .name("custom name")
2557 /// // Build the GooseRequest object.
2558 /// .build();
2559 ///
2560 /// // Make the configured request.
2561 /// let _goose = user.request(goose_request).await?;
2562 ///
2563 /// Ok(())
2564 /// }
2565 /// ```
2566 pub fn name(mut self, name: impl Into<&'a str>) -> Self {
2567 self.name = Some(name.into());
2568 self
2569 }
2570
2571 /// Manually configure the expected HTTP response status code.
2572 ///
2573 /// Defaults to [`reqwest::StatusCode::is_success`].
2574 ///
2575 /// # Example
2576 /// Intentionally request a 404 page, and do not trigger an error.
2577 /// ```rust
2578 /// use goose::prelude::*;
2579 ///
2580 /// let mut a_transaction = transaction!(transaction_function);
2581 ///
2582 /// // Make a named request.
2583 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2584 /// // Manually create a GooseRequestBuilder object.
2585 /// let goose_request = GooseRequest::builder()
2586 /// // Set a relative path to request.
2587 /// .path("no/such/path")
2588 /// // Tell Goose to expect a 404 HTTP response status code.
2589 /// .expect_status_code(404)
2590 /// // Build the GooseRequest object.
2591 /// .build();
2592 ///
2593 /// // Make the configured request.
2594 /// let _goose = user.request(goose_request).await?;
2595 ///
2596 /// Ok(())
2597 /// }
2598 /// ```
2599 pub fn expect_status_code(mut self, status_code: u16) -> Self {
2600 self.expect_status_code = Some(status_code);
2601 self
2602 }
2603
2604 /// Configure whether the request should return on error when it
2605 /// fails
2606 ///
2607 /// Defaults to [`false`].
2608 ///
2609 /// # Example
2610 /// Intentionally request a 404 page, and do not trigger an error.
2611 /// ```rust
2612 /// use goose::prelude::*;
2613 ///
2614 /// let mut a_transaction = transaction!(transaction_function);
2615 ///
2616 /// // Make a named request.
2617 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2618 /// // Manually create a GooseRequestBuilder object.
2619 /// let goose_request = GooseRequest::builder()
2620 /// // Set a relative path to request.
2621 /// .path("no/such/path")
2622 /// // Tell Goose to expect a 404 HTTP response status code.
2623 /// .expect_status_code(404)
2624 /// // Tell Goose to return an error if the status code is
2625 /// // not a 404
2626 /// .error_on_fail()
2627 /// // Build the GooseRequest object.
2628 /// .build();
2629 ///
2630 /// // Make the configured request.
2631 /// let _goose = user.request(goose_request).await?;
2632 ///
2633 /// Ok(())
2634 /// }
2635 /// ```
2636 pub fn error_on_fail(mut self) -> Self {
2637 self.error_on_fail = true;
2638 self
2639 }
2640
2641 /// Manually create the [`reqwest::RequestBuilder`] used to make a request.
2642 ///
2643 /// # Example
2644 /// Manually create a `RequestBuilder` in order to set a timeout.
2645 /// ```rust
2646 /// use goose::prelude::*;
2647 ///
2648 /// let mut a_transaction = transaction!(transaction_function);
2649 ///
2650 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2651 /// use std::time::Duration;
2652 ///
2653 /// // Manually interact with the Reqwest RequestBuilder object.
2654 /// let request_builder = user.get_request_builder(&GooseMethod::Get, "no/such/path")?
2655 /// // Configure the request to timeout if it takes longer than 500 milliseconds.
2656 /// .timeout(Duration::from_millis(500));
2657 ///
2658 /// // Manually build a GooseRequest in order to set our custom RequestBuilder.
2659 /// let goose_request = GooseRequest::builder()
2660 /// // Manually add our custom RequestBuilder object.
2661 /// .set_request_builder(request_builder)
2662 /// // Turn the GooseRequestBuilder object into a GooseRequest.
2663 /// .build();
2664 ///
2665 /// // Finally make the actual request with our custom GooseRequest object.
2666 /// let _goose = user.request(goose_request).await?;
2667 ///
2668 /// Ok(())
2669 /// }
2670 /// ```
2671 pub fn set_request_builder(mut self, request_builder: RequestBuilder) -> Self {
2672 self.request_builder = Some(request_builder);
2673 self
2674 }
2675
2676 /// Build the [`GooseRequest`] object which is then passed to [`GooseUser::request`].
2677 ///
2678 /// # Example
2679 /// ```rust
2680 /// use goose::prelude::*;
2681 ///
2682 /// // Build the default "GET /".
2683 /// let goose_request = GooseRequest::builder().build();
2684 /// ```
2685 pub fn build(self) -> GooseRequest<'a> {
2686 let Self {
2687 path,
2688 method,
2689 name,
2690 expect_status_code,
2691 error_on_fail,
2692 request_builder,
2693 } = self;
2694 GooseRequest {
2695 path,
2696 method,
2697 name,
2698 expect_status_code,
2699 error_on_fail,
2700 request_builder,
2701 }
2702 }
2703}
2704
2705/// Remove path from Reqwest error to avoid having a lot of distincts error
2706/// when path parameters are used.
2707fn clean_reqwest_error(e: &reqwest::Error, request_name: &str) -> String {
2708 let kind = if e.is_builder() {
2709 "builder error"
2710 } else if e.is_request() {
2711 "error sending request"
2712 } else if e.is_body() {
2713 "request or response body error"
2714 } else if e.is_decode() {
2715 "error decoding response body"
2716 } else if e.is_redirect() {
2717 "error following redirect"
2718 } else {
2719 "Http status"
2720 };
2721
2722 if let Some(ref e) = std::error::Error::source(e) {
2723 format!("{} {}: {}", kind, request_name, e)
2724 } else {
2725 format!("{} {}", kind, request_name)
2726 }
2727}
2728
2729/// A helper to determine which host should be prepended to relative load test
2730/// paths in this Scenario.
2731///
2732/// The first of these defined will be returned as the prepended host:
2733/// 1. `--host` (host specified on the command line when running load test)
2734/// 2. [`Scenario`](./struct.Scenario.html)`.host` (default host defined
2735/// for the current scenario)
2736/// 3. [`GooseDefault::Host`](../config/enum.GooseDefault.html#variant.Host) (default
2737/// host defined for the current load test)
2738pub fn get_base_url(
2739 config_host: Option<String>,
2740 scenario_host: Option<String>,
2741 default_host: Option<String>,
2742) -> Result<Url, GooseError> {
2743 // If the `--host` CLI option is set, build the URL with it.
2744 match config_host {
2745 Some(host) => Ok(
2746 Url::parse(&host).map_err(|parse_error| GooseError::InvalidHost {
2747 host,
2748 detail: "There was a failure parsing the host specified with --host.".to_string(),
2749 parse_error,
2750 })?,
2751 ),
2752 None => {
2753 match scenario_host {
2754 // Otherwise, if `Scenario.host` is defined, usee this
2755 Some(host) => {
2756 Ok(
2757 Url::parse(&host).map_err(|parse_error| GooseError::InvalidHost {
2758 host,
2759 detail: "There was a failure parsing the host specified with the Scenario.set_host() function.".to_string(),
2760 parse_error,
2761 })?,
2762 )
2763 }
2764 // Otherwise, use global `GooseAttack.host`. `unwrap` okay as host validation was done at startup.
2765 None => {
2766 // Host is required, if we get here it's safe to unwrap this variable.
2767 let default_host = default_host.unwrap();
2768 Ok(
2769 Url::parse(&default_host).map_err(|parse_error| GooseError::InvalidHost {
2770 host: default_host.to_string(),
2771 detail: "There was a failure parsing the host specified globally with the GooseAttack.set_default() function.".to_string(),
2772 parse_error,
2773 })?,
2774 )
2775 }
2776 }
2777 }
2778 }
2779}
2780
2781/// The function type of a goose transaction function.
2782pub type TransactionFunction = Arc<
2783 dyn for<'r> Fn(
2784 &'r mut GooseUser,
2785 ) -> Pin<Box<dyn Future<Output = TransactionResult> + Send + 'r>>
2786 + Send
2787 + Sync,
2788>;
2789
2790/// An individual transaction within a [`Scenario`](./struct.Scenario.html).
2791#[derive(Clone)]
2792pub struct Transaction {
2793 /// An index into [`Scenario`](./struct.Scenario.html)`.transaction`, indicating which
2794 /// transaction this is.
2795 pub transactions_index: usize,
2796 /// An optional name for the transaction, used when displaying metrics.
2797 pub name: String,
2798 /// An integer value that controls the frequency that this transaction will be run.
2799 pub weight: usize,
2800 /// An integer value that controls when this transaction runs compared to other transactions in the same
2801 /// [`Scenario`](./struct.Scenario.html).
2802 pub sequence: usize,
2803 /// A flag indicating that this transaction runs when the user starts.
2804 pub on_start: bool,
2805 /// A flag indicating that this transaction runs when the user stops.
2806 pub on_stop: bool,
2807 /// A required function that is executed each time this transaction runs.
2808 pub function: TransactionFunction,
2809}
2810impl Transaction {
2811 pub fn new(function: TransactionFunction) -> Self {
2812 trace!("new transaction");
2813 Transaction {
2814 transactions_index: usize::MAX,
2815 name: "".to_string(),
2816 weight: 1,
2817 sequence: 0,
2818 on_start: false,
2819 on_stop: false,
2820 function,
2821 }
2822 }
2823
2824 /// Set an optional name for the transaction, used when displaying metrics.
2825 ///
2826 /// Individual requests can also be named using [`GooseRequestBuilder`], or for GET
2827 /// requests with the [`GooseUser::get_named`] helper.
2828 ///
2829 /// # Example
2830 /// ```rust
2831 /// use goose::prelude::*;
2832 ///
2833 /// transaction!(my_transaction_function).set_name("foo");
2834 ///
2835 /// async fn my_transaction_function(user: &mut GooseUser) -> TransactionResult {
2836 /// let _goose = user.get("").await?;
2837 ///
2838 /// Ok(())
2839 /// }
2840 /// ```
2841 pub fn set_name(mut self, name: &str) -> Self {
2842 trace!("[{}] set_name: {}", self.transactions_index, self.name);
2843 self.name = name.to_string();
2844 self
2845 }
2846
2847 /// Set an optional flag indicating that this transaction should be run when
2848 /// a user first starts. This could be used to log the user in, and
2849 /// so all subsequent transaction are done as a logged in user. A transaction
2850 /// with this flag set will only run at start time (and optionally at stop
2851 /// time as well, if that flag is also set).
2852 ///
2853 /// On-start transactions can be sequenced and weighted. Sequences allow
2854 /// multiple on-start transactions to run in a controlled order. Weights allow
2855 /// on-start transactions to run multiple times when a user starts.
2856 ///
2857 /// # Example
2858 /// ```rust
2859 /// use goose::prelude::*;
2860 ///
2861 /// transaction!(my_on_start_function).set_on_start();
2862 ///
2863 /// async fn my_on_start_function(user: &mut GooseUser) -> TransactionResult {
2864 /// let _goose = user.get("").await?;
2865 ///
2866 /// Ok(())
2867 /// }
2868 /// ```
2869 pub fn set_on_start(mut self) -> Self {
2870 trace!(
2871 "{} [{}] set_on_start transaction",
2872 self.name,
2873 self.transactions_index
2874 );
2875 self.on_start = true;
2876 self
2877 }
2878
2879 /// Set an optional flag indicating that this transaction should be run when
2880 /// a user stops. This could be used to log a user out when the user
2881 /// finishes its load test. A transaction with this flag set will only run at
2882 /// stop time (and optionally at start time as well, if that flag is
2883 /// also set).
2884 ///
2885 /// On-stop transactions can be sequenced and weighted. Sequences allow
2886 /// multiple on-stop transactions to run in a controlled order. Weights allow
2887 /// on-stop transactions to run multiple times when a user stops.
2888 ///
2889 /// # Example
2890 /// ```rust
2891 /// use goose::prelude::*;
2892 ///
2893 /// transaction!(my_on_stop_function).set_on_stop();
2894 ///
2895 /// async fn my_on_stop_function(user: &mut GooseUser) -> TransactionResult {
2896 /// let _goose = user.get("").await?;
2897 ///
2898 /// Ok(())
2899 /// }
2900 /// ```
2901 pub fn set_on_stop(mut self) -> Self {
2902 trace!(
2903 "{} [{}] set_on_stop transaction",
2904 self.name,
2905 self.transactions_index
2906 );
2907 self.on_stop = true;
2908 self
2909 }
2910
2911 /// Sets a weight on an individual transaction. The larger the value of weight, the more often it will be run
2912 /// in the Scenario. For example, if one transaction has a weight of 3 and another transaction has a weight of
2913 /// 1, the first transaction will run 3 times as often.
2914 ///
2915 /// # Example
2916 /// ```rust
2917 /// use goose::prelude::*;
2918 ///
2919 /// #[tokio::main]
2920 /// async fn main() -> Result<(), GooseError> {
2921 /// transaction!(transaction_function).set_weight(3)?;
2922 ///
2923 /// Ok(())
2924 /// }
2925 ///
2926 /// async fn transaction_function(user: &mut GooseUser) -> TransactionResult {
2927 /// let _goose = user.get("").await?;
2928 ///
2929 /// Ok(())
2930 /// }
2931 /// ```
2932 pub fn set_weight(mut self, weight: usize) -> Result<Self, GooseError> {
2933 trace!(
2934 "{} [{}] set_weight: {}",
2935 self.name,
2936 self.transactions_index,
2937 weight
2938 );
2939 if weight == 0 {
2940 return Err(GooseError::InvalidWeight {
2941 weight,
2942 detail: "Weight must be set to at least 1.".to_string(),
2943 });
2944 }
2945 self.weight = weight;
2946
2947 Ok(self)
2948 }
2949
2950 /// Defines the sequence value of an individual transactions. Transactions are run in order of their
2951 /// sequence value, so a transaction with a sequence value of 1 will run before a transaction with a
2952 /// sequence value of 2. Transactions with no sequence value (or a sequence value of 0) will run last,
2953 /// after all transactions with positive sequence values.
2954 ///
2955 /// All transactions with the same sequence value will run in a random order. Transactions can be
2956 /// assigned both squence values and weights.
2957 ///
2958 /// # Examples
2959 /// In this first example, the variable names indicate the order the transactions will be run in:
2960 /// ```rust
2961 /// use goose::prelude::*;
2962 ///
2963 /// let runs_first = transaction!(first_transaction_function).set_sequence(3);
2964 /// let runs_second = transaction!(second_transaction_function).set_sequence(5835);
2965 /// let runs_last = transaction!(third_transaction_function);
2966 ///
2967 /// async fn first_transaction_function(user: &mut GooseUser) -> TransactionResult {
2968 /// let _goose = user.get("1").await?;
2969 ///
2970 /// Ok(())
2971 /// }
2972 ///
2973 /// async fn second_transaction_function(user: &mut GooseUser) -> TransactionResult {
2974 /// let _goose = user.get("2").await?;
2975 ///
2976 /// Ok(())
2977 /// }
2978 ///
2979 /// async fn third_transaction_function(user: &mut GooseUser) -> TransactionResult {
2980 /// let _goose = user.get("3").await?;
2981 ///
2982 /// Ok(())
2983 /// }
2984 /// ```
2985 ///
2986 /// In the following example, the `runs_first` transactions runs two times, then one instance of `runs_second`
2987 /// and two instances of `also_runs_second` are all three run. The user will do this over and over
2988 /// the entire time it runs, with `runs_first` always running first, then the other transactions being
2989 /// run in a random and weighted order:
2990 /// ```rust
2991 /// use goose::prelude::*;
2992 ///
2993 /// #[tokio::main]
2994 /// async fn main() -> Result<(), GooseError> {
2995 /// let runs_first = transaction!(first_transaction_function).set_sequence(1).set_weight(2)?;
2996 /// let runs_second = transaction!(second_transaction_function_a).set_sequence(2);
2997 /// let also_runs_second = transaction!(second_transaction_function_b).set_sequence(2).set_weight(2)?;
2998 ///
2999 /// Ok(())
3000 /// }
3001 ///
3002 /// async fn first_transaction_function(user: &mut GooseUser) -> TransactionResult {
3003 /// let _goose = user.get("1").await?;
3004 ///
3005 /// Ok(())
3006 /// }
3007 ///
3008 /// async fn second_transaction_function_a(user: &mut GooseUser) -> TransactionResult {
3009 /// let _goose = user.get("2a").await?;
3010 ///
3011 /// Ok(())
3012 /// }
3013 ///
3014 /// async fn second_transaction_function_b(user: &mut GooseUser) -> TransactionResult {
3015 /// let _goose = user.get("2b").await?;
3016 ///
3017 /// Ok(())
3018 /// }
3019 /// ```
3020 pub fn set_sequence(mut self, sequence: usize) -> Self {
3021 trace!(
3022 "{} [{}] set_sequence: {}",
3023 self.name,
3024 self.transactions_index,
3025 sequence
3026 );
3027 if sequence < 1 {
3028 info!(
3029 "setting sequence to 0 for transaction {} is unnecessary, sequence disabled",
3030 self.name
3031 );
3032 }
3033 self.sequence = sequence;
3034 self
3035 }
3036}
3037impl Hash for Transaction {
3038 fn hash<H: Hasher>(&self, state: &mut H) {
3039 self.transactions_index.hash(state);
3040 self.name.hash(state);
3041 self.weight.hash(state);
3042 self.sequence.hash(state);
3043 self.on_start.hash(state);
3044 self.on_stop.hash(state);
3045 }
3046}
3047
3048#[cfg(test)]
3049mod tests {
3050 use super::*;
3051
3052 use gumdrop::Options;
3053 use httpmock::{
3054 Method::{GET, POST},
3055 MockServer,
3056 };
3057
3058 const EMPTY_ARGS: Vec<&str> = vec![];
3059
3060 fn setup_user(server: &MockServer) -> Result<GooseUser, GooseError> {
3061 let mut configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
3062 configuration.co_mitigation = Some(GooseCoordinatedOmissionMitigation::Average);
3063 let base_url = get_base_url(Some(server.url("/")), None, None).unwrap();
3064 GooseUser::single(base_url, &configuration)
3065 }
3066
3067 #[test]
3068 fn goose_scenario() {
3069 // Simplistic test transaction functions.
3070 async fn test_function_a(user: &mut GooseUser) -> TransactionResult {
3071 let _goose = user.get("/a/").await?;
3072
3073 Ok(())
3074 }
3075
3076 async fn test_function_b(user: &mut GooseUser) -> TransactionResult {
3077 let _goose = user.get("/b/").await?;
3078
3079 Ok(())
3080 }
3081
3082 let mut scenario = scenario!("foo");
3083 assert_eq!(scenario.name, "foo");
3084 assert_eq!(scenario.scenarios_index, usize::MAX);
3085 assert_eq!(scenario.weight, 1);
3086 assert_eq!(scenario.transaction_wait, None);
3087 assert!(scenario.host.is_none());
3088 assert_eq!(scenario.transactions.len(), 0);
3089 assert_eq!(scenario.weighted_transactions.len(), 0);
3090 assert_eq!(scenario.weighted_on_start_transactions.len(), 0);
3091 assert_eq!(scenario.weighted_on_stop_transactions.len(), 0);
3092
3093 // Registering a transaction adds it to transactions, but doesn't update weighted_transactions.
3094 scenario = scenario.register_transaction(transaction!(test_function_a));
3095 assert_eq!(scenario.transactions.len(), 1);
3096 assert_eq!(scenario.weighted_transactions.len(), 0);
3097 assert_eq!(scenario.scenarios_index, usize::MAX);
3098 assert_eq!(scenario.weight, 1);
3099 assert_eq!(scenario.transaction_wait, None);
3100 assert!(scenario.host.is_none());
3101
3102 // Different transactions can be registered.
3103 scenario = scenario.register_transaction(transaction!(test_function_b));
3104 assert_eq!(scenario.transactions.len(), 2);
3105 assert_eq!(scenario.weighted_transactions.len(), 0);
3106 assert_eq!(scenario.scenarios_index, usize::MAX);
3107 assert_eq!(scenario.weight, 1);
3108 assert_eq!(scenario.transaction_wait, None);
3109 assert!(scenario.host.is_none());
3110
3111 // Same transactions can be registered again.
3112 scenario = scenario.register_transaction(transaction!(test_function_a));
3113 assert_eq!(scenario.transactions.len(), 3);
3114 assert_eq!(scenario.weighted_transactions.len(), 0);
3115 assert_eq!(scenario.scenarios_index, usize::MAX);
3116 assert_eq!(scenario.weight, 1);
3117 assert_eq!(scenario.transaction_wait, None);
3118 assert!(scenario.host.is_none());
3119
3120 // Setting weight only affects weight field.
3121 scenario = scenario.set_weight(50).unwrap();
3122 assert_eq!(scenario.weight, 50);
3123 assert_eq!(scenario.transactions.len(), 3);
3124 assert_eq!(scenario.weighted_transactions.len(), 0);
3125 assert_eq!(scenario.scenarios_index, usize::MAX);
3126 assert_eq!(scenario.transaction_wait, None);
3127 assert!(scenario.host.is_none());
3128
3129 // Weight can be changed.
3130 scenario = scenario.set_weight(5).unwrap();
3131 assert_eq!(scenario.weight, 5);
3132
3133 // Setting host only affects host field.
3134 scenario = scenario.set_host("http://foo.example.com/");
3135 assert_eq!(scenario.host, Some("http://foo.example.com/".to_string()));
3136 assert_eq!(scenario.weight, 5);
3137 assert_eq!(scenario.transactions.len(), 3);
3138 assert_eq!(scenario.weighted_transactions.len(), 0);
3139 assert_eq!(scenario.scenarios_index, usize::MAX);
3140 assert_eq!(scenario.transaction_wait, None);
3141
3142 // Host field can be changed.
3143 scenario = scenario.set_host("https://bar.example.com/");
3144 assert_eq!(scenario.host, Some("https://bar.example.com/".to_string()));
3145
3146 // Wait time only affects wait time fields.
3147 scenario = scenario
3148 .set_wait_time(Duration::from_secs(1), Duration::from_secs(10))
3149 .unwrap();
3150 assert_eq!(
3151 scenario.transaction_wait,
3152 Some((Duration::from_secs(1), Duration::from_secs(10)))
3153 );
3154 assert_eq!(scenario.host, Some("https://bar.example.com/".to_string()));
3155 assert_eq!(scenario.weight, 5);
3156 assert_eq!(scenario.transactions.len(), 3);
3157 assert_eq!(scenario.weighted_transactions.len(), 0);
3158 assert_eq!(scenario.scenarios_index, usize::MAX);
3159
3160 // Wait time can be changed.
3161 scenario = scenario
3162 .set_wait_time(Duration::from_secs(3), Duration::from_secs(9))
3163 .unwrap();
3164 assert_eq!(
3165 scenario.transaction_wait,
3166 Some((Duration::from_secs(3), Duration::from_secs(9)))
3167 );
3168 }
3169
3170 #[test]
3171 fn goose_transaction() {
3172 // Simplistic test transaction functions.
3173 async fn test_function_a(user: &mut GooseUser) -> TransactionResult {
3174 let _goose = user.get("/a/").await?;
3175
3176 Ok(())
3177 }
3178
3179 // Initialize scenario.
3180 let mut transaction = transaction!(test_function_a);
3181 assert_eq!(transaction.transactions_index, usize::MAX);
3182 assert_eq!(transaction.name, "".to_string());
3183 assert_eq!(transaction.weight, 1);
3184 assert_eq!(transaction.sequence, 0);
3185 assert!(!transaction.on_start);
3186 assert!(!transaction.on_stop);
3187
3188 // Name can be set, without affecting other fields.
3189 transaction = transaction.set_name("foo");
3190 assert_eq!(transaction.name, "foo".to_string());
3191 assert_eq!(transaction.weight, 1);
3192 assert_eq!(transaction.sequence, 0);
3193 assert!(!transaction.on_start);
3194 assert!(!transaction.on_stop);
3195
3196 // Name can be set multiple times.
3197 transaction = transaction.set_name("bar");
3198 assert_eq!(transaction.name, "bar".to_string());
3199
3200 // On start flag can be set, without affecting other fields.
3201 transaction = transaction.set_on_start();
3202 assert!(transaction.on_start);
3203 assert_eq!(transaction.name, "bar".to_string());
3204 assert_eq!(transaction.weight, 1);
3205 assert_eq!(transaction.sequence, 0);
3206 assert!(!transaction.on_stop);
3207
3208 // Setting on start flag twice doesn't change anything.
3209 transaction = transaction.set_on_start();
3210 assert!(transaction.on_start);
3211
3212 // On stop flag can be set, without affecting other fields.
3213 // It's possible to set both on_start and on_stop for same transaction.
3214 transaction = transaction.set_on_stop();
3215 assert!(transaction.on_stop);
3216 assert!(transaction.on_start);
3217 assert_eq!(transaction.name, "bar".to_string());
3218 assert_eq!(transaction.weight, 1);
3219 assert_eq!(transaction.sequence, 0);
3220
3221 // Setting on stop flag twice doesn't change anything.
3222 transaction = transaction.set_on_stop();
3223 assert!(transaction.on_stop);
3224
3225 // Setting weight doesn't change anything else.
3226 transaction = transaction.set_weight(2).unwrap();
3227 assert_eq!(transaction.weight, 2);
3228 assert!(transaction.on_stop);
3229 assert!(transaction.on_start);
3230 assert_eq!(transaction.name, "bar".to_string());
3231 assert_eq!(transaction.sequence, 0);
3232
3233 // Weight field can be changed multiple times.
3234 transaction = transaction.set_weight(3).unwrap();
3235 assert_eq!(transaction.weight, 3);
3236
3237 // Setting sequence doesn't change anything else.
3238 transaction = transaction.set_sequence(4);
3239 assert_eq!(transaction.sequence, 4);
3240 assert_eq!(transaction.weight, 3);
3241 assert!(transaction.on_stop);
3242 assert!(transaction.on_start);
3243 assert_eq!(transaction.name, "bar".to_string());
3244
3245 // Sequence field can be changed multiple times.
3246 transaction = transaction.set_sequence(8);
3247 assert_eq!(transaction.sequence, 8);
3248 }
3249
3250 #[tokio::test]
3251 async fn goose_user() {
3252 const HOST: &str = "http://example.com/";
3253 let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
3254 let base_url = get_base_url(Some(HOST.to_string()), None, None).unwrap();
3255 let user = GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
3256 assert_eq!(user.scenarios_index, 0);
3257 assert_eq!(user.weighted_users_index, usize::MAX);
3258
3259 // Confirm the URLs are correctly built using the default_host.
3260 let url = user.build_url("/foo").unwrap();
3261 assert_eq!(&url, &[HOST, "foo"].concat());
3262 let url = user.build_url("bar/").unwrap();
3263 assert_eq!(&url, &[HOST, "bar/"].concat());
3264 let url = user.build_url("/foo/bar").unwrap();
3265 assert_eq!(&url, &[HOST, "foo/bar"].concat());
3266
3267 // Confirm the URLs are built with their own specified host.
3268 let url = user.build_url("https://example.com/foo").unwrap();
3269 assert_eq!(url, "https://example.com/foo");
3270 let url = user
3271 .build_url("https://www.example.com/path/to/resource")
3272 .unwrap();
3273 assert_eq!(url, "https://www.example.com/path/to/resource");
3274
3275 // Create a second user, this time setting a scenario_host.
3276 let base_url = get_base_url(
3277 None,
3278 Some("http://www2.example.com/".to_string()),
3279 Some("http://www.example.com/".to_string()),
3280 )
3281 .unwrap();
3282 let user2 = GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
3283
3284 // Confirm the URLs are correctly built using the scenario_host.
3285 let url = user2.build_url("/foo").unwrap();
3286 assert_eq!(url, "http://www2.example.com/foo");
3287
3288 // Confirm URLs are still built with their own specified host.
3289 let url = user2.build_url("https://example.com/foo").unwrap();
3290 assert_eq!(url, "https://example.com/foo");
3291
3292 // Confirm Goose can build a base_url that includes a path.
3293 const HOST_WITH_PATH: &str = "http://example.com/with/path/";
3294 let base_url = get_base_url(Some(HOST_WITH_PATH.to_string()), None, None).unwrap();
3295 let user = GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
3296
3297 // Confirm the URLs are correctly built using the default_host that includes a path.
3298 let url = user.build_url("foo").unwrap();
3299 assert_eq!(&url, &[HOST_WITH_PATH, "foo"].concat());
3300 let url = user.build_url("bar/").unwrap();
3301 assert_eq!(&url, &[HOST_WITH_PATH, "bar/"].concat());
3302 let url = user.build_url("foo/bar").unwrap();
3303 assert_eq!(&url, &[HOST_WITH_PATH, "foo/bar"].concat());
3304
3305 // Confirm that URLs are correctly re-written if an absolute path is used.
3306 let url = user.build_url("/foo").unwrap();
3307 assert_eq!(&url, &[HOST, "foo"].concat());
3308 }
3309
3310 #[tokio::test]
3311 async fn manual_requests() {
3312 let server = MockServer::start();
3313
3314 let mut user = setup_user(&server).unwrap();
3315
3316 // Set up a mock http server endpoint.
3317 const INDEX_PATH: &str = "/";
3318 let index = server.mock(|when, then| {
3319 when.method(GET).path(INDEX_PATH);
3320 then.status(200);
3321 });
3322
3323 // Make a GET request to the mock http server and confirm we get a 200 response.
3324 assert_eq!(index.hits(), 0);
3325 let goose = user
3326 .get(INDEX_PATH)
3327 .await
3328 .expect("get returned unexpected error");
3329 let status = goose.response.unwrap().status();
3330 assert_eq!(status, 200);
3331 assert_eq!(goose.request.raw.method, GooseMethod::Get);
3332 assert_eq!(goose.request.name, INDEX_PATH);
3333 assert!(goose.request.success);
3334 assert!(!goose.request.update);
3335 assert_eq!(goose.request.status_code, 200);
3336 assert_eq!(index.hits(), 1);
3337
3338 const NO_SUCH_PATH: &str = "/no/such/path";
3339 // Set up a mock http server endpoint.
3340 let not_found = server.mock(|when, then| {
3341 when.method(GET).path(NO_SUCH_PATH);
3342 then.status(404);
3343 });
3344
3345 // Make an invalid GET request to the mock http server and confirm we get a 404 response.
3346 assert_eq!(not_found.hits(), 0);
3347 let goose = user
3348 .get(NO_SUCH_PATH)
3349 .await
3350 .expect("get returned unexpected error");
3351 let status = goose.response.unwrap().status();
3352 assert_eq!(status, 404);
3353 assert_eq!(goose.request.raw.method, GooseMethod::Get);
3354 assert_eq!(goose.request.name, NO_SUCH_PATH);
3355 assert!(!goose.request.success);
3356 assert!(!goose.request.update);
3357 assert_eq!(goose.request.status_code, 404,);
3358 not_found.assert_hits(1);
3359
3360 // Set up a mock http server endpoint.
3361 const COMMENT_PATH: &str = "/comment";
3362 let comment = server.mock(|when, then| {
3363 when.method(POST).path(COMMENT_PATH).body("foo");
3364 then.status(200).body("foo");
3365 });
3366
3367 // Make a POST request to the mock http server and confirm we get a 200 OK response.
3368 assert_eq!(comment.hits(), 0);
3369 let goose = user
3370 .post(COMMENT_PATH, "foo")
3371 .await
3372 .expect("post returned unexpected error");
3373 let unwrapped_response = goose.response.unwrap();
3374 let status = unwrapped_response.status();
3375 assert_eq!(status, 200);
3376 let body = unwrapped_response.text().await.unwrap();
3377 assert_eq!(body, "foo");
3378 assert_eq!(goose.request.raw.method, GooseMethod::Post);
3379 assert!(goose.request.success);
3380 assert!(!goose.request.update);
3381 assert_eq!(goose.request.status_code, 200);
3382 comment.assert_hits(1);
3383 }
3384
3385 #[test]
3386 fn test_set_session_data() {
3387 #[derive(Debug, PartialEq, Eq, Clone)]
3388 struct CustomSessionData {
3389 data: String,
3390 }
3391
3392 let session_data = CustomSessionData {
3393 data: "foo".to_owned(),
3394 };
3395
3396 let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
3397 let mut user =
3398 GooseUser::single("http://localhost:8080".parse().unwrap(), &configuration).unwrap();
3399
3400 user.set_session_data(session_data.clone());
3401
3402 let session = user.get_session_data::<CustomSessionData>();
3403 assert!(session.is_some());
3404 assert_eq!(session.unwrap(), &session_data);
3405
3406 let session = user.get_session_data_unchecked::<CustomSessionData>();
3407 assert_eq!(session, &session_data);
3408 }
3409
3410 #[test]
3411 fn test_get_mut_session_data() {
3412 #[derive(Debug, Clone)]
3413 struct CustomSessionData {
3414 data: String,
3415 }
3416
3417 let session_data = CustomSessionData {
3418 data: "foo".to_owned(),
3419 };
3420
3421 let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
3422 let mut user =
3423 GooseUser::single("http://localhost:8080".parse().unwrap(), &configuration).unwrap();
3424
3425 user.set_session_data(session_data);
3426
3427 if let Some(session) = user.get_session_data_mut::<CustomSessionData>() {
3428 "bar".clone_into(&mut session.data);
3429 }
3430
3431 let session = user.get_session_data_unchecked::<CustomSessionData>();
3432 assert_eq!(session.data, "bar".to_string());
3433
3434 let session = user.get_session_data_unchecked_mut::<CustomSessionData>();
3435 "foo".clone_into(&mut session.data);
3436 let session = user.get_session_data_unchecked::<CustomSessionData>();
3437 assert_eq!(session.data, "foo".to_string());
3438 }
3439
3440 #[test]
3441 fn test_set_session_data_override() {
3442 #[derive(Debug, Clone)]
3443 struct CustomSessionData {
3444 data: String,
3445 }
3446
3447 let mut session_data = CustomSessionData {
3448 data: "foo".to_owned(),
3449 };
3450
3451 let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
3452 let mut user =
3453 GooseUser::single("http://localhost:8080".parse().unwrap(), &configuration).unwrap();
3454
3455 user.set_session_data(session_data.clone());
3456
3457 "bar".clone_into(&mut session_data.data);
3458 user.set_session_data(session_data);
3459
3460 let session = user.get_session_data_unchecked::<CustomSessionData>();
3461 assert_eq!(session.data, "bar".to_string());
3462 }
3463}