cucumber_thirtyfour_worlder/
lib.rs

1//! [![Crates.io](https://img.shields.io/crates/v/cucumber-thirtyfour-worlder?logo=rust)](https://crates.io/crates/cucumber-thirtyfour-worlder)
2//! [![License](https://img.shields.io/crates/l/cucumber-thirtyfour-worlder?logo=mit)](https://github.com/mondeja/cucumber-thirtyfour-worlder/blob/master/LICENSE)
3//! [![Tests](https://img.shields.io/github/actions/workflow/status/mondeja/cucumber-thirtyfour-worlder/ci.yml?label=tests&logo=github)](https://github.com/mondeja/cucumber-thirtyfour-worlder/actions)
4//! [![macro docs.rs](https://img.shields.io/docsrs/cucumber-thirtyfour-worlder?logo=docs.rs)](https://docs.rs/cucumber-thirtyfour-worlder)
5//! [![reference docs.rs](https://img.shields.io/docsrs/cucumber-thirtyfour-worlder?logo=docs.rs&label=world%20reference)][appworld-reference]
6//! [![Crates.io downloads](https://img.shields.io/crates/d/cucumber-thirtyfour-worlder)](https://crates.io/crates/cucumber-thirtyfour-worlder)
7//!
8//! Do you need to reuse a bunch of logic between different projects testing
9//! apps with [cucumber-rs] and [thirtyfour]? This crate is for you.
10//!
11//! Provides a [`cucumber::World`] builder that can be used to create an
12//! `AppWorld` for thirtyfour tests, allowing to inject environment variables to
13//! parametrize them.
14//!
15//! - `BROWSER`: browser to use. Supported are `firefox`, `chrome`, and `edge`.
16//! - `HEADLESS`: by default, tests are executed in headless mode. Set this
17//!   to `false` to run them in a visible browser.
18//! - `WINDOW_SIZE`: size of the browser window. The default is `1920x1080`.
19//! - `HOST_URL`: base URL of the application under test. The default is
20//!   `http://localhost:8080`.
21//! - `DRIVER_URL`: the URL of the `WebDriver` server. The default is
22//!   `http://localhost:4444`.
23//! - `DOWNLOADS_DIR`: directory where files downloaded by the browser.
24//!   The default is a random temporary directory.
25//!
26//! # Usage
27//!
28//! Create a crate and add the following dependencies to your `Cargo.toml`.
29//!
30//! ```toml
31//! [dependencies]
32//! cucumber = "0.21"
33//! thirtyfour = "0.35"
34//! cucumber-thirtyfour-worlder = "0.2"
35//! ```
36//!
37//! Inside, create your [`AppWorld`][appworld-reference] struct and pass
38//! it the [`#[worlder]`][worlder] attribute.
39//!
40//! ```rust,ignore
41//! use cucumber_thirtyfour_worlder::worlder;
42//!
43//! #[worlder]
44//! pub struct AppWorld;
45//! ```
46//!
47//! > See the reference of the created world [here][appworld-reference].
48//!
49//! Then, create a crate for tests and run the world as you would do with
50//! [`cucumber::World`] directly.
51//!
52//! ```rust,ignore
53//! // tests/desktop.rs
54//! use your_crate::AppWorld;
55//! use cucumber::World;
56//!
57//! #[tokio::main]
58//! async fn main() {
59//!     AppWorld::cucumber()
60//!         .fail_on_skipped()
61//!         .run_and_exit("./features/desktop")
62//!         .await
63//! }
64//! ```
65//!
66//! Start a webdriver server before running the tests.
67//!
68//! ```sh
69//! chromedriver --port=4444
70//! # or `geckodriver --port=4444` (for Firefox)
71//! # or `msedgedriver --port=4444` (for MsEdge)
72//! ```
73//!
74//! And run your tests passing a browser in the `BROWSER` environment variable.
75//!
76//! ```sh
77//! BROWSER=chrome cargo test --package your-crate --test desktop -- --fail-fast
78//! ```
79//!
80//! Where `desktop` is the name of your test file and `your-crate` is the name of
81//! the crate that contains the [`AppWorld`][appworld-reference] struct.
82//!
83//! # Known issues
84//!
85//! ## Additional configuration for cargo-machete
86//!
87//! The [`cargo-machete`][cargo-machete] tool don't know that you're not using
88//! `cucumber` and `thirtyfour`, so it could complain about missing dependencies.
89//! To fix this, add the following to your _Cargo.toml_.
90//!
91//! ```toml
92//! [package.metadata.cargo-machete]
93//! ignored = ["thirtyfour", "cucumber"]
94//! ```
95//!
96//! [cucumber-rs]: https://cucumber-rs.github.io/cucumber/main/
97//! [thirtyfour]: https://docs.rs/thirtyfour/latest/thirtyfour/
98//! [`cucumber::World`]: https://docs.rs/cucumber/latest/cucumber/trait.World.html
99//! [appworld-reference]: https://docs.rs/cucumber-thirtyfour-worlder-docref/latest/cucumber_thirtyfour_worlder_docref/struct.AppWorld.html
100//! [worlder]: https://docs.rs/cucumber-thirtyfour-worlder/latest/cucumber_thirtyfour_worlder/attr.worlder.html
101//! [cargo-machete]: https://github.com/bnjbvr/cargo-machete
102
103#[cfg(test)]
104mod tests;
105
106use proc_macro2::{TokenStream, TokenTree};
107use quote::quote;
108use syn::{
109    parse::{Parse, ParseStream},
110    parse_macro_input,
111};
112
113/// Attribute macro to build [`cucumber::World`] struct for the app to test.
114///
115/// Accept the next named arguments:
116///
117/// - `check_concurrency_cli_option_when_firefox` (*bool*, default `true`): when enabled,
118///   the implementation will check if the `--concurrency` or `-c` CLI option is set
119///   to `1` invoking cucumber tests when using Firefox. Multiple sessions in parallel
120///   are not allowed by geckodriver and this limitation is easy to forget, hence this
121///   convenient argument.
122/// - ``cucumber`` (*path*, default `::cucumber`): path to the `cucumber` crate.
123/// - ``thirtyfour`` (*path*, default `::thirtyfour`): path to the `thirtyfour` crate.
124/// - ``serde_json`` (*path*, default `::serde_json`): path to the `serde_json` crate.
125///
126/// See the reference of the created world [here][appworld-reference].
127///
128/// [`cucumber::World`]: https://docs.rs/cucumber/latest/cucumber/trait.World.html
129/// [appworld-reference]: https://docs.rs/cucumber-thirtyfour-worlder-docref/latest/cucumber_thirtyfour_worlder_docref/struct.AppWorld.html
130#[proc_macro_attribute]
131pub fn worlder(
132    args: proc_macro::TokenStream,
133    stream: proc_macro::TokenStream,
134) -> proc_macro::TokenStream {
135    assert!(
136        !stream.is_empty(),
137        "#[worlder] macro requires a struct to be passed"
138    );
139
140    let args = parse_macro_input!(args as WorlderArgs);
141    let (check_concurrency_cli_option_when_firefox, check_concurrency_cli_option_when_firefox_fn) =
142        if args.check_concurrency_cli_option_when_firefox {
143            (
144                quote!(Self::__check_firefox_concurrency_cli_option()),
145                build_check_concurrency_cli_option_when_firefox_fn(),
146            )
147        } else {
148            (TokenStream::new(), TokenStream::new())
149        };
150    let cucumber = args.cucumber;
151    let thirtyfour = args.thirtyfour;
152    let serde_json = args.serde_json;
153
154    let mut before_struct = TokenStream::new();
155    let original_struct = TokenStream::from(stream.clone());
156
157    let mut token_stream_iter = original_struct.into_iter();
158    let length = token_stream_iter.clone().count();
159
160    // Allow whatever before the struct definition.
161    // This is needed if we want to add attributes to the struct,
162    // like documenting it.
163    let mut maybe_item_1 = None;
164    for _ in 0..length {
165        let item = token_stream_iter.next();
166        if let Some(item) = item {
167            let item_clone = item.clone();
168            if let TokenTree::Ident(ident) = { item_clone } {
169                let ident_str = ident.to_string();
170                if &ident_str == "pub" || ident_str.starts_with("pub(") || &ident_str == "struct" {
171                    maybe_item_1 = Some(TokenTree::Ident(ident));
172                    break;
173                }
174            }
175            before_struct.extend(std::iter::once(item));
176        }
177    }
178    let maybe_item_2 = token_stream_iter.next();
179    let maybe_item_3 = token_stream_iter.next();
180    let maybe_item_4 = token_stream_iter.next();
181
182    let mut struct_idents = vec![];
183
184    let item_1_string = match maybe_item_1 {
185        Some(TokenTree::Ident(ident)) => {
186            struct_idents.push(TokenTree::from(ident.clone()));
187            ident.to_string()
188        }
189        _ => String::new(),
190    };
191    let item_2_string = match maybe_item_2 {
192        Some(TokenTree::Ident(ident)) => {
193            struct_idents.push(TokenTree::Ident(ident.clone()));
194            ident.to_string()
195        }
196        _ => String::new(),
197    };
198    let item_3_string = match maybe_item_3 {
199        Some(TokenTree::Ident(ident)) => {
200            struct_idents.push(TokenTree::Ident(ident.clone()));
201            ident.to_string()
202        }
203        Some(TokenTree::Punct(punct)) => punct.to_string(),
204        _ => String::new(),
205    };
206    let item_4_string = match maybe_item_4 {
207        Some(TokenTree::Punct(punct)) => punct.to_string(),
208        _ => String::new(),
209    };
210
211    let item_1_str = item_1_string.as_str();
212    let item_2_str = item_2_string.as_str();
213    let item_3_str = item_3_string.as_str();
214    let item_4_str = item_4_string.as_str();
215
216    let (valid, with_vis) = if (item_1_str == "pub" || item_1_str.starts_with("pub("))
217        && item_2_str == "struct"
218        && item_4_str == ";"
219    {
220        (true, true)
221    } else if item_1_str == "struct" && item_3_str == ";" {
222        (true, false)
223    } else {
224        (false, false)
225    };
226
227    assert!(
228        valid,
229        "#[worlder] macro requires a token stream like `pub struct AppWorld;` or `struct AppWorld;`"
230    );
231
232    let (vis_ident, struct_ident, struct_name_ident) = if with_vis {
233        (
234            struct_idents[0].clone(),
235            struct_idents[1].clone(),
236            struct_idents[2].clone(),
237        )
238    } else {
239        (
240            TokenTree::Ident(proc_macro2::Ident::new("", proc_macro2::Span::call_site())),
241            struct_idents[0].clone(),
242            struct_idents[1].clone(),
243        )
244    };
245
246    let ret = quote! {
247        #before_struct
248        #[derive(Debug, #cucumber::World)]
249        #[world(init = Self::new)]
250        #vis_ident #struct_ident #struct_name_ident {
251            driver: #thirtyfour::WebDriver,
252            driver_url: String,
253            host_url: String,
254            headless: bool,
255            window_size: (u32, u32),
256            downloads_dir: String,
257        }
258
259        impl #struct_name_ident {
260            #[doc(hidden)]
261            pub async fn new() -> Self {
262                Self::__build_driver().await
263            }
264
265            #[doc = "Get the driver of the world."]
266            #[must_use]
267            pub fn driver(&self) -> &#thirtyfour::WebDriver {
268                &self.driver
269            }
270
271            #[doc = "Get the driver URL of the world."]
272            #[doc = ""]
273            #[doc = "It's defined by the `DRIVER_URL` environment variable, which defaults to `\"http://localhost:4444\"`."]
274            #[must_use]
275            pub fn driver_url(&self) -> &str {
276                &self.driver_url
277            }
278
279            #[doc = "Get the host URL of the world."]
280            #[doc = ""]
281            #[doc = "It's defined by the `HOST_URL` environment variable, which defaults to `\"http://localhost:8080\"`."]
282            #[must_use]
283            pub fn host_url(&self) -> &str {
284                &self.host_url
285            }
286
287            #[doc = "Get the headless mode of the world."]
288            #[doc = ""]
289            #[doc = "It's defined by the `HEADLESS` environment variable, which defaults to `true`."]
290            #[must_use]
291            pub fn headless(&self) -> bool {
292                self.headless
293            }
294
295            #[doc = "Get the window size of the world."]
296            #[doc = ""]
297            #[doc = "It's defined by the `WINDOW_SIZE` environment variable, which defaults to `\"1920x1080\"`."]
298            #[must_use]
299            pub fn window_size(&self) -> (u32, u32) {
300                self.window_size
301            }
302
303            #[doc = "Get the downloads directory of the world."]
304            #[doc = ""]
305            #[doc = "It's defined by the `DOWNLOADS_DIR` environment variable, which defaults to a random temporary directory."]
306            #[must_use]
307            pub fn downloads_dir(&self) -> &str {
308                &self.downloads_dir
309            }
310
311            #[doc = "Navigate to the given path inside the host."]
312            pub async fn goto_path(&self, path: &str) -> Result<&Self, #thirtyfour::error::WebDriverError> {
313                let url = format!("{}{}", self.host_url(), path);
314                if let Err(err) = self.driver().goto(&url).await {
315                    Err(err)
316                } else {
317                    Ok(self)
318                }
319            }
320
321            async fn __build_driver() -> Self {
322                let browser = Self::__discover_browser();
323                let driver_url = Self::__discover_driver_url();
324                let host_url = Self::__discover_host_url();
325                let headless = Self::__discover_headless();
326                let downloads_dir = Self::__discover_downloads_dir();
327                let (window_width, window_height) = Self::__discover_window_size();
328
329                let driver = if &browser == "chrome" {
330                    let mut caps = #thirtyfour::DesiredCapabilities::chrome();
331                    let mut prefs = ::std::collections::HashMap::<String, #serde_json::Value>::new();
332                    prefs.insert(
333                        "download.default_directory".to_string(),
334                        #serde_json::Value::String(
335                            downloads_dir.clone(),
336                        ),
337                    );
338                    <#thirtyfour::ChromeCapabilities
339                        as
340                    #thirtyfour::BrowserCapabilitiesHelper>::insert_browser_option(
341                        &mut caps, "prefs", prefs,
342                    )
343                        .unwrap_or_else(|err| {
344                            panic!("Failed to set Chrome prefs: {err}");
345                        });
346                    let window_size_opt = format!(
347                        "--window-size={window_width},{window_height}",
348                    );
349                    let mut opts = vec!["--no-sandbox", &window_size_opt];
350                    if headless {
351                        opts.push("--headless");
352                    }
353                    <#thirtyfour::ChromeCapabilities
354                        as
355                    #thirtyfour::BrowserCapabilitiesHelper>::insert_browser_option(
356                        &mut caps, "args", opts
357                    )
358                        .unwrap_or_else(|err| {
359                            panic!("Failed to set Chrome options: {err}");
360                        });
361                    #thirtyfour::WebDriver::new(&driver_url, caps)
362                        .await
363                        .unwrap_or_else(|err| {
364                            panic!(
365                                "Failed to create WebDriver for Chrome: {err}. \
366                                Make sure that chromedriver server is running at {driver_url}",
367                            )
368                        })
369                } else if &browser == "firefox" {
370                    #check_concurrency_cli_option_when_firefox;
371                    let mut caps = #thirtyfour::DesiredCapabilities::firefox();
372                    let mut prefs = ::std::collections::HashMap::<String, #serde_json::Value>::new();
373                    prefs.insert(
374                        "browser.download.folderList".to_string(),
375                        #serde_json::Value::Number(2.into())
376                    );
377                    prefs.insert(
378                        "browser.download.dir".to_string(),
379                        #serde_json::Value::String(
380                            downloads_dir.clone(),
381                        ),
382                    );
383                    prefs.insert(
384                        "browser.download.useDownloadDir".to_string(),
385                        #serde_json::Value::Bool(true),
386                    );
387                    prefs.insert(
388                        "browser.download.manager.showWhenStarting".to_string(),
389                        #serde_json::Value::Bool(false),
390                    );
391                    prefs.insert(
392                        "browser.helperApps.neverAsk.saveToDisk".to_string(),
393                        #serde_json::Value::String(
394                            "application/octet-stream,application/pdf,image/png,image/jpeg,image/svg+xml,text/plain,text/csv,application/zip".to_string(),
395                        ),
396                    );
397                    // disable the built-in PDF viewer
398                    prefs.insert(
399                        "pdfjs.disabled".to_string(),
400                        #serde_json::Value::Bool(true),
401                    );
402                    <#thirtyfour::FirefoxCapabilities
403                        as
404                    #thirtyfour::BrowserCapabilitiesHelper>::insert_browser_option(
405                        &mut caps, "prefs", prefs,
406                    )
407                        .unwrap_or_else(|err| {
408                            panic!("Failed to set Firefox prefs: {err}");
409                        });
410                    if headless {
411                        caps.set_headless().unwrap_or_else(|err| {
412                            panic!("Failed to set Firefox headless mode: {err}");
413                        });
414                    }
415                    let driver = #thirtyfour::WebDriver::new(&driver_url, caps).await.unwrap_or_else(|err| {
416                        panic!(
417                            "Failed to create WebDriver for Firefox: {err}. \
418                            Make sure that geckodriver server is running at {driver_url}",
419                        )
420                    });
421                    // Firefox loads the window dimensions of the last session,
422                    // so we need to set the window size explicitly.
423                    driver.set_window_rect(0, 0, window_width, window_height)
424                        .await
425                        .expect("Failed to set window size to {width}x{height}");
426                    driver
427                } else if &browser == "edge" {
428                    let mut caps = #thirtyfour::DesiredCapabilities::edge();
429                    let mut prefs = ::std::collections::HashMap::<String, #serde_json::Value>::new();
430                    prefs.insert(
431                        "download.default_directory".to_string(),
432                        #serde_json::Value::String(
433                            downloads_dir.clone(),
434                        ),
435                    );
436                    prefs.insert(
437                        "download.prompt_for_download".to_string(),
438                        #serde_json::Value::Bool(false),
439                    );
440                    prefs.insert(
441                        "download.directory_upgrade".to_string(),
442                        #serde_json::Value::Bool(true),
443                    );
444                    prefs.insert(
445                        "safebrowsing.enabled".to_string(),
446                        #serde_json::Value::Bool(true),
447                    );
448                    <#thirtyfour::EdgeCapabilities
449                        as
450                    #thirtyfour::BrowserCapabilitiesHelper>::insert_browser_option(
451                        &mut caps, "prefs", prefs,
452                    )
453                        .unwrap_or_else(|err| {
454                            panic!("Failed to set Edge prefs: {err}");
455                        });
456                    let window_size_opt = format!(
457                        "--window-size={window_width},{window_height}",
458                    );
459                    let mut opts = vec!["--no-sandbox", &window_size_opt];
460                    if headless {
461                        opts.push("--headless");
462                    }
463                    <#thirtyfour::EdgeCapabilities
464                        as
465                    #thirtyfour::BrowserCapabilitiesHelper>::insert_browser_option(&mut caps, "args", opts)
466                        .unwrap_or_else(|err| {
467                            panic!("Failed to set Edge options: {err}");
468                        });
469                    #thirtyfour::WebDriver::new(&driver_url, caps).await.unwrap_or_else(|err| {
470                        panic!(
471                            "Failed to create WebDriver for Edge: {err}. \
472                            Make sure that edgedriver server is running at {driver_url}",
473                        )
474                    })
475                } else {
476                    panic!(
477                        "Unsupported browser. BROWSER environment variable is: \
478                        {browser}. Supported browsers are: \"chrome\", \"firefox\" \
479                        and \"edge\"."
480                    );
481                };
482
483                Self {
484                    driver,
485                    driver_url,
486                    host_url,
487                    headless,
488                    window_size: (window_width, window_height),
489                    downloads_dir,
490                }
491            }
492
493            fn __discover_browser() -> String {
494                std::env::var("BROWSER").unwrap_or_else(|_| {
495                    panic!(
496                        "BROWSER environment variable is not set. \
497                         Supported browsers are: \"chrome\", \"firefox\" \
498                         and \"edge\"."
499                    )
500                })
501            }
502
503            fn __discover_driver_url() -> String {
504                std::env::var("DRIVER_URL").unwrap_or("http://localhost:4444".to_string())
505            }
506
507            fn __discover_host_url() -> String {
508                std::env::var("HOST_URL").unwrap_or("http://localhost:8080".to_string())
509            }
510
511            fn __discover_headless() -> bool {
512                std::env::var("HEADLESS").unwrap_or("true".to_string()) == "true"
513            }
514
515            fn __discover_window_size() -> (u32, u32) {
516                let window_size = std::env::var("WINDOW_SIZE").unwrap_or("1920x1080".to_string());
517                let mut parts = window_size.split('x');
518                let width = parts.next().unwrap_or_else(|| {
519                    panic!(
520                        "Invalid WINDOW_SIZE environment variable format. \
521                        Expected format: WIDTHxHEIGHT"
522                    );
523                }).parse::<u32>().unwrap_or_else(|_| {
524                    panic!(
525                        "Invalid WINDOW_SIZE environment variable format. \
526                        Expected format: WIDTHxHEIGHT"
527                    );
528                });
529                let height = parts.next().unwrap_or_else(|| {
530                    panic!(
531                        "Invalid WINDOW_SIZE environment variable format. \
532                        Expected format: WIDTHxHEIGHT"
533                    );
534                }).parse::<u32>().unwrap_or_else(|_| {
535                    panic!(
536                        "Invalid WINDOW_SIZE environment variable format. \
537                        Expected format: WIDTHxHEIGHT"
538                    );
539                });
540                (width, height)
541            }
542
543            fn __discover_downloads_dir() -> String {
544                if let Ok(dir) = std::env::var("DOWNLOADS_DIR") {
545                    let path = std::path::PathBuf::from(dir);
546                    if !path.exists() {
547                        std::fs::create_dir_all(&path).unwrap_or_else(|err| {
548                            panic!(
549                                "Failed to create downloads directory at {:?}: {err}",
550                                path,
551                            )
552                        });
553                    }
554                    if let Ok(canonical_path) = path.canonicalize() {
555                        return canonical_path.display().to_string();
556                    }
557                    panic!(
558                        "Failed to canonicalize downloads directory at {:?}",
559                        path,
560                    );
561                }
562
563                let generate_random_unique_directory = || -> String {
564                    let temp_dir = std::env::temp_dir();
565                    let base_path = std::path::PathBuf::from(std::env::temp_dir());
566
567                    for attempt in 0..u32::MAX {
568                        let nanos = std::time::SystemTime::now()
569                            .duration_since(std::time::UNIX_EPOCH)
570                            .unwrap()
571                            .as_nanos();
572
573                        let name = format!("dir_{nanos}_{attempt}");
574                        let path = base_path.join(name);
575
576                        if !path.exists() {
577                            let result = std::fs::create_dir_all(&path);
578                            if result.is_ok() {
579                                if let Ok(canonical_path) = path.canonicalize() {
580                                    return canonical_path.display().to_string();
581                                }
582                            }
583                        }
584                    }
585
586                    panic!(
587                        "Failed to generate a unique temporary directory to store downloads. \
588                        Set the environment variable DOWNLOADS_DIR to a valid directory path \
589                        to avoid this issue."
590                    );
591                };
592
593                generate_random_unique_directory()
594            }
595
596            #check_concurrency_cli_option_when_firefox_fn
597        }
598    };
599
600    proc_macro::TokenStream::from(ret)
601}
602
603fn build_check_concurrency_cli_option_when_firefox_fn() -> TokenStream {
604    quote! {
605        fn __check_firefox_concurrency_cli_option() {
606            let lets_panic = || {
607                panic!(
608                    "The driver geckodriver requires --concurrency or -c \
609                    option to be set to 1 because geckodriver does not allows \
610                    multiple sessions in parallel. Pass --concurrency=1 or -c 1 \
611                    to the test command, like \
612                    `cargo test --test <test-name> -- --concurrency=1`."
613                )
614            };
615
616            let mut reading_arg = false;
617            let mut found = false;
618            let args = std::env::args();
619            for arg in args {
620                if arg == "--concurrency" || arg == "-c" {
621                    reading_arg = true;
622                } else if arg.starts_with("--concurrency=")
623                    || arg.starts_with("-c=")
624                {
625                    let value = arg
626                        .split('=')
627                        .nth(1)
628                        .unwrap_or_else(|| panic!("Invalid argument: {arg}"));
629                    let value = value.parse::<u32>();
630                    if value.is_ok() && value.unwrap() != 1 {
631                        lets_panic();
632                    }
633                    found = true;
634                    break;
635                } else if reading_arg {
636                    let value = arg.parse::<u32>();
637                    if value.is_ok() && value.unwrap() != 1 {
638                        lets_panic();
639                    }
640                    found = true;
641                    break;
642                }
643            }
644
645            if !found {
646                lets_panic();
647            }
648        }
649    }
650}
651
652struct WorlderArgs {
653    check_concurrency_cli_option_when_firefox: bool,
654    cucumber: syn::Path,
655    thirtyfour: syn::Path,
656    serde_json: syn::Path,
657}
658
659impl Default for WorlderArgs {
660    fn default() -> Self {
661        Self {
662            check_concurrency_cli_option_when_firefox: true,
663            cucumber: syn::parse_str::<syn::Path>("::cucumber").unwrap(),
664            thirtyfour: syn::parse_str::<syn::Path>("::thirtyfour").unwrap(),
665            serde_json: syn::parse_str::<syn::Path>("::serde_json").unwrap(),
666        }
667    }
668}
669
670impl Parse for WorlderArgs {
671    fn parse(input: ParseStream) -> syn::parse::Result<Self> {
672        let mut args = WorlderArgs::default();
673        while !input.is_empty() {
674            let ident: syn::Ident = input.parse()?;
675            if ident == "check_concurrency_cli_option_when_firefox" {
676                input.parse::<syn::Token![=]>()?;
677                let value: syn::LitBool = input.parse()?;
678                args.check_concurrency_cli_option_when_firefox = value.value;
679            } else if ident == "cucumber" {
680                input.parse::<syn::Token![=]>()?;
681                args.cucumber = input.parse()?;
682            } else if ident == "thirtyfour" {
683                input.parse::<syn::Token![=]>()?;
684                args.thirtyfour = input.parse()?;
685            } else if ident == "serde_json" {
686                input.parse::<syn::Token![=]>()?;
687                args.serde_json = input.parse()?;
688            } else {
689                return Err(input.error(format!("Unknown argument: {ident}")));
690            }
691            if !input.is_empty() {
692                input.parse::<syn::Token![,]>()?;
693            }
694        }
695        Ok(args)
696    }
697}