html2pdf_api/config.rs
1//! Configuration for browser pool behavior and limits.
2//!
3//! This module provides [`BrowserPoolConfig`] and [`BrowserPoolConfigBuilder`]
4//! for configuring pool size, browser lifecycle, and health monitoring parameters.
5//!
6//! # Example
7//!
8//! ```rust
9//! use std::time::Duration;
10//! use html2pdf_api::BrowserPoolConfigBuilder;
11//!
12//! let config = BrowserPoolConfigBuilder::new()
13//! .max_pool_size(10)
14//! .warmup_count(5)
15//! .browser_ttl(Duration::from_secs(7200))
16//! .build()
17//! .expect("Invalid configuration");
18//!
19//! assert_eq!(config.max_pool_size, 10);
20//! assert_eq!(config.warmup_count, 5);
21//! ```
22//!
23//! # Environment Configuration
24//!
25//! When the `env-config` feature is enabled, you can load configuration
26//! from environment variables and an optional `app.env` file:
27//!
28//! ```rust,ignore
29//! use html2pdf_api::config::env::from_env;
30//!
31//! let config = from_env()?;
32//! ```
33//!
34//! See [`mod@env`] module for available environment variables.
35
36use std::time::Duration;
37
38/// Configuration for browser pool behavior and limits.
39///
40/// Controls pool size, browser lifecycle, and health monitoring parameters.
41/// Use [`BrowserPoolConfigBuilder`] for validation and convenience.
42///
43/// # Fields Overview
44///
45/// | Field | Default | Description |
46/// |-------|---------|-------------|
47/// | `max_pool_size` | 5 | Maximum browsers in pool |
48/// | `warmup_count` | 3 | Browsers to pre-create |
49/// | `ping_interval` | 15s | Health check frequency |
50/// | `browser_ttl` | 1 hour | Browser lifetime |
51/// | `max_ping_failures` | 3 | Failures before removal |
52/// | `warmup_timeout` | 60s | Warmup time limit |
53///
54/// # Example
55///
56/// ```rust
57/// use html2pdf_api::BrowserPoolConfig;
58///
59/// // Use defaults
60/// let config = BrowserPoolConfig::default();
61/// assert_eq!(config.max_pool_size, 5);
62/// ```
63#[derive(Debug, Clone)]
64pub struct BrowserPoolConfig {
65 /// Maximum number of browsers to keep in the pool (idle + active).
66 ///
67 /// This is a soft limit - active browsers may temporarily exceed this during high load.
68 ///
69 /// # Default
70 ///
71 /// 5 browsers
72 ///
73 /// # Considerations
74 ///
75 /// - Higher values = more memory usage, better concurrency
76 /// - Lower values = less memory, potential queuing under load
77 pub max_pool_size: usize,
78
79 /// Number of browsers to pre-create during warmup phase.
80 ///
81 /// Must be d `max_pool_size`. Reduces first-request latency.
82 ///
83 /// # Default
84 ///
85 /// 3 browsers
86 ///
87 /// # Considerations
88 ///
89 /// - Set to `max_pool_size` for fastest first requests
90 /// - Set to 0 for lazy initialization (browsers created on demand)
91 pub warmup_count: usize,
92
93 /// Interval between health check pings for active browsers.
94 ///
95 /// Shorter intervals = faster failure detection, higher overhead.
96 ///
97 /// # Default
98 ///
99 /// 15 seconds
100 ///
101 /// # Considerations
102 ///
103 /// - Too short: Unnecessary CPU/memory overhead
104 /// - Too long: Slow detection of crashed browsers
105 pub ping_interval: Duration,
106
107 /// Time-to-live for each browser instance before forced retirement.
108 ///
109 /// Prevents memory leaks from long-running browser processes.
110 ///
111 /// # Default
112 ///
113 /// 1 hour (3600 seconds)
114 ///
115 /// # Considerations
116 ///
117 /// - Chrome can accumulate memory over time
118 /// - Shorter TTL = more browser restarts, fresher instances
119 /// - Longer TTL = fewer restarts, potential memory growth
120 pub browser_ttl: Duration,
121
122 /// Maximum consecutive ping failures before removing a browser.
123 ///
124 /// Higher values = more tolerance for transient failures.
125 ///
126 /// # Default
127 ///
128 /// 3 consecutive failures
129 ///
130 /// # Considerations
131 ///
132 /// - Set to 1 for aggressive failure detection
133 /// - Set higher if experiencing transient network issues
134 pub max_ping_failures: u32,
135
136 /// Maximum time allowed for warmup process to complete.
137 ///
138 /// If warmup doesn't complete in this time, it fails with timeout error.
139 ///
140 /// # Default
141 ///
142 /// 60 seconds
143 ///
144 /// # Considerations
145 ///
146 /// - Should be at least `warmup_count * ~5 seconds` per browser
147 /// - Increase if running on slow hardware or with many warmup browsers
148 pub warmup_timeout: Duration,
149
150 /// Interval between pre-created browser startups during warmup.
151 ///
152 /// Distributes expiration intervals so the entire pool doesn't crash simultaneously.
153 /// Default: 30 seconds
154 pub warmup_stagger: Duration,
155}
156
157impl Default for BrowserPoolConfig {
158 /// Production-ready default configuration.
159 ///
160 /// - Pool size: 5 browsers
161 /// - Warmup: 3 browsers
162 /// - Health checks: Every 15 seconds
163 /// - TTL: 1 hour
164 /// - Failure tolerance: 3 consecutive failures
165 /// - Warmup timeout: 60 seconds
166 ///
167 /// # Example
168 ///
169 /// ```rust
170 /// use html2pdf_api::BrowserPoolConfig;
171 /// use std::time::Duration;
172 ///
173 /// let config = BrowserPoolConfig::default();
174 ///
175 /// assert_eq!(config.max_pool_size, 5);
176 /// assert_eq!(config.warmup_count, 3);
177 /// assert_eq!(config.ping_interval, Duration::from_secs(15));
178 /// assert_eq!(config.browser_ttl, Duration::from_secs(3600));
179 /// assert_eq!(config.max_ping_failures, 3);
180 /// assert_eq!(config.warmup_timeout, Duration::from_secs(60));
181 /// ```
182 fn default() -> Self {
183 Self {
184 max_pool_size: 5,
185 warmup_count: 3,
186 ping_interval: Duration::from_secs(15),
187 browser_ttl: Duration::from_secs(3600), // 1 hour
188 max_ping_failures: 3,
189 warmup_timeout: Duration::from_secs(60),
190 warmup_stagger: Duration::from_secs(30),
191 }
192 }
193}
194
195/// Builder for [`BrowserPoolConfig`] with validation.
196///
197/// Provides a fluent API for constructing validated configurations.
198/// All setter methods can be chained together.
199///
200/// # Example
201///
202/// ```rust
203/// use std::time::Duration;
204/// use html2pdf_api::BrowserPoolConfigBuilder;
205///
206/// let config = BrowserPoolConfigBuilder::new()
207/// .max_pool_size(10)
208/// .warmup_count(5)
209/// .browser_ttl(Duration::from_secs(7200))
210/// .build()
211/// .expect("Invalid configuration");
212/// ```
213///
214/// # Validation
215///
216/// The [`build()`](Self::build) method validates:
217/// - `max_pool_size` must be greater than 0
218/// - `warmup_count` must be d `max_pool_size`
219pub struct BrowserPoolConfigBuilder {
220 config: BrowserPoolConfig,
221}
222
223impl BrowserPoolConfigBuilder {
224 /// Create a new builder with default values.
225 ///
226 /// # Example
227 ///
228 /// ```rust
229 /// use html2pdf_api::BrowserPoolConfigBuilder;
230 ///
231 /// let builder = BrowserPoolConfigBuilder::new();
232 /// let config = builder.build().unwrap();
233 ///
234 /// // Has default values
235 /// assert_eq!(config.max_pool_size, 5);
236 /// ```
237 pub fn new() -> Self {
238 Self {
239 config: BrowserPoolConfig::default(),
240 }
241 }
242
243 /// Set maximum pool size (must be > 0).
244 ///
245 /// # Parameters
246 ///
247 /// * `size` - Maximum number of browsers in the pool.
248 ///
249 /// # Example
250 ///
251 /// ```rust
252 /// use html2pdf_api::BrowserPoolConfigBuilder;
253 ///
254 /// let config = BrowserPoolConfigBuilder::new()
255 /// .max_pool_size(10)
256 /// .build()
257 /// .unwrap();
258 ///
259 /// assert_eq!(config.max_pool_size, 10);
260 /// ```
261 pub fn max_pool_size(mut self, size: usize) -> Self {
262 self.config.max_pool_size = size;
263 self
264 }
265
266 /// Set warmup count (must be d max_pool_size).
267 ///
268 /// # Parameters
269 ///
270 /// * `count` - Number of browsers to pre-create during warmup.
271 ///
272 /// # Example
273 ///
274 /// ```rust
275 /// use html2pdf_api::BrowserPoolConfigBuilder;
276 ///
277 /// let config = BrowserPoolConfigBuilder::new()
278 /// .max_pool_size(10)
279 /// .warmup_count(5)
280 /// .build()
281 /// .unwrap();
282 ///
283 /// assert_eq!(config.warmup_count, 5);
284 /// ```
285 pub fn warmup_count(mut self, count: usize) -> Self {
286 self.config.warmup_count = count;
287 self
288 }
289
290 /// Set health check interval.
291 ///
292 /// # Parameters
293 ///
294 /// * `interval` - Duration between health check pings.
295 ///
296 /// # Example
297 ///
298 /// ```rust
299 /// use std::time::Duration;
300 /// use html2pdf_api::BrowserPoolConfigBuilder;
301 ///
302 /// let config = BrowserPoolConfigBuilder::new()
303 /// .ping_interval(Duration::from_secs(30))
304 /// .build()
305 /// .unwrap();
306 ///
307 /// assert_eq!(config.ping_interval, Duration::from_secs(30));
308 /// ```
309 pub fn ping_interval(mut self, interval: Duration) -> Self {
310 self.config.ping_interval = interval;
311 self
312 }
313
314 /// Set browser time-to-live before forced retirement.
315 ///
316 /// # Parameters
317 ///
318 /// * `ttl` - Maximum lifetime for each browser instance.
319 ///
320 /// # Example
321 ///
322 /// ```rust
323 /// use std::time::Duration;
324 /// use html2pdf_api::BrowserPoolConfigBuilder;
325 ///
326 /// let config = BrowserPoolConfigBuilder::new()
327 /// .browser_ttl(Duration::from_secs(7200)) // 2 hours
328 /// .build()
329 /// .unwrap();
330 ///
331 /// assert_eq!(config.browser_ttl, Duration::from_secs(7200));
332 /// ```
333 pub fn browser_ttl(mut self, ttl: Duration) -> Self {
334 self.config.browser_ttl = ttl;
335 self
336 }
337
338 /// Set maximum consecutive ping failures before removal.
339 ///
340 /// # Parameters
341 ///
342 /// * `failures` - Number of consecutive failures tolerated.
343 ///
344 /// # Example
345 ///
346 /// ```rust
347 /// use html2pdf_api::BrowserPoolConfigBuilder;
348 ///
349 /// let config = BrowserPoolConfigBuilder::new()
350 /// .max_ping_failures(5)
351 /// .build()
352 /// .unwrap();
353 ///
354 /// assert_eq!(config.max_ping_failures, 5);
355 /// ```
356 pub fn max_ping_failures(mut self, failures: u32) -> Self {
357 self.config.max_ping_failures = failures;
358 self
359 }
360
361 /// Set warmup timeout.
362 ///
363 /// # Parameters
364 ///
365 /// * `timeout` - Maximum time allowed for warmup to complete.
366 ///
367 /// # Example
368 ///
369 /// ```rust
370 /// use std::time::Duration;
371 /// use html2pdf_api::BrowserPoolConfigBuilder;
372 ///
373 /// let config = BrowserPoolConfigBuilder::new()
374 /// .warmup_timeout(Duration::from_secs(120))
375 /// .build()
376 /// .unwrap();
377 ///
378 /// assert_eq!(config.warmup_timeout, Duration::from_secs(120));
379 /// ```
380 pub fn warmup_timeout(mut self, timeout: Duration) -> Self {
381 self.config.warmup_timeout = timeout;
382 self
383 }
384
385 /// Set warmup stagger interval.
386 ///
387 /// # Parameters
388 ///
389 /// * `stagger` - Delay between browser spawns during initial warmup.
390 pub fn warmup_stagger(mut self, stagger: Duration) -> Self {
391 self.config.warmup_stagger = stagger;
392 self
393 }
394
395 /// Build and validate the configuration.
396 ///
397 /// # Errors
398 ///
399 /// - Returns error if `max_pool_size` is 0
400 /// - Returns error if `warmup_count` > `max_pool_size`
401 ///
402 /// # Example
403 ///
404 /// ```rust
405 /// use html2pdf_api::BrowserPoolConfigBuilder;
406 ///
407 /// // Valid configuration
408 /// let config = BrowserPoolConfigBuilder::new()
409 /// .max_pool_size(10)
410 /// .warmup_count(5)
411 /// .build();
412 /// assert!(config.is_ok());
413 ///
414 /// // Invalid: pool size is 0
415 /// let config = BrowserPoolConfigBuilder::new()
416 /// .max_pool_size(0)
417 /// .build();
418 /// assert!(config.is_err());
419 ///
420 /// // Invalid: warmup exceeds pool size
421 /// let config = BrowserPoolConfigBuilder::new()
422 /// .max_pool_size(5)
423 /// .warmup_count(10)
424 /// .build();
425 /// assert!(config.is_err());
426 /// ```
427 pub fn build(self) -> std::result::Result<BrowserPoolConfig, String> {
428 // Validation: Pool size must be positive
429 if self.config.max_pool_size == 0 {
430 return Err("max_pool_size must be greater than 0".to_string());
431 }
432
433 // Validation: Can't warmup more browsers than pool can hold
434 if self.config.warmup_count > self.config.max_pool_size {
435 return Err("warmup_count cannot exceed max_pool_size".to_string());
436 }
437
438 Ok(self.config)
439 }
440}
441
442impl Default for BrowserPoolConfigBuilder {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448// ============================================================================
449// Environment Configuration (feature-gated)
450// ============================================================================
451
452/// Environment-based configuration loading.
453///
454/// This module is only available when the `env-config` feature is enabled.
455///
456/// # Environment File
457///
458/// This module uses `dotenvy` to load environment variables from an `app.env`
459/// file in the current directory. The file is optional - if not found,
460/// environment variables and defaults are used.
461///
462/// # Environment Variables
463///
464/// | Variable | Type | Default | Description |
465/// |----------|------|---------|-------------|
466/// | `BROWSER_POOL_SIZE` | usize | 5 | Maximum pool size |
467/// | `BROWSER_WARMUP_COUNT` | usize | 3 | Warmup browser count |
468/// | `BROWSER_TTL_SECONDS` | u64 | 3600 | Browser TTL in seconds |
469/// | `BROWSER_WARMUP_TIMEOUT_SECONDS` | u64 | 60 | Warmup timeout |
470/// | `BROWSER_PING_INTERVAL_SECONDS` | u64 | 15 | Health check interval |
471/// | `BROWSER_MAX_PING_FAILURES` | u32 | 3 | Max ping failures |
472/// | `CHROME_PATH` | String | auto | Custom Chrome binary path |
473///
474/// # Example `app.env` File
475///
476/// ```text
477/// # Browser Pool Configuration
478/// BROWSER_POOL_SIZE=5
479/// BROWSER_WARMUP_COUNT=3
480/// BROWSER_TTL_SECONDS=3600
481/// BROWSER_WARMUP_TIMEOUT_SECONDS=60
482/// BROWSER_PING_INTERVAL_SECONDS=15
483/// BROWSER_MAX_PING_FAILURES=3
484///
485/// # Chrome Configuration (optional)
486/// # CHROME_PATH=/usr/bin/google-chrome
487/// ```
488#[cfg(feature = "env-config")]
489pub mod env {
490 use super::*;
491 use crate::error::BrowserPoolError;
492
493 /// Default environment file name.
494 pub const ENV_FILE_NAME: &str = "app.env";
495
496 /// Load environment variables from `app.env` file.
497 ///
498 /// Call this early in your application startup to ensure environment
499 /// variables are loaded before any configuration functions are called.
500 ///
501 /// This function is automatically called by [`from_env`], but you can
502 /// call it explicitly if you need to load the file earlier or check
503 /// for errors.
504 ///
505 /// # Returns
506 ///
507 /// - `Ok(PathBuf)` if the file was found and loaded successfully
508 /// - `Err(dotenvy::Error)` if the file was not found or couldn't be parsed
509 ///
510 /// # Example
511 ///
512 /// ```rust,ignore
513 /// use html2pdf_api::config::env::load_env_file;
514 ///
515 /// // Load at application startup
516 /// match load_env_file() {
517 /// Ok(path) => println!("Loaded environment from: {:?}", path),
518 /// Err(e) => println!("No app.env file found: {}", e),
519 /// }
520 /// ```
521 pub fn load_env_file() -> Result<std::path::PathBuf, dotenvy::Error> {
522 dotenvy::from_filename(ENV_FILE_NAME)
523 }
524
525 /// Load configuration from environment variables.
526 ///
527 /// Reads configuration from environment variables with sensible defaults.
528 /// Also loads `app.env` file if present (via `dotenvy`).
529 ///
530 /// # Environment File
531 ///
532 /// This function looks for an `app.env` file in the current directory
533 /// and loads it if present. The file is optional - if not found,
534 /// environment variables and defaults are used.
535 ///
536 /// # Environment Variables
537 ///
538 /// - `BROWSER_POOL_SIZE`: Maximum pool size (default: 5)
539 /// - `BROWSER_WARMUP_COUNT`: Warmup browser count (default: 3)
540 /// - `BROWSER_TTL_SECONDS`: Browser TTL in seconds (default: 3600)
541 /// - `BROWSER_WARMUP_TIMEOUT_SECONDS`: Warmup timeout (default: 60)
542 /// - `BROWSER_PING_INTERVAL_SECONDS`: Health check interval (default: 15)
543 /// - `BROWSER_MAX_PING_FAILURES`: Max ping failures (default: 3)
544 ///
545 /// # Errors
546 ///
547 /// Returns [`BrowserPoolError::Configuration`] if configuration values are invalid.
548 ///
549 /// # Example
550 ///
551 /// ```rust,ignore
552 /// use html2pdf_api::config::env::from_env;
553 ///
554 /// // Set environment variables before calling
555 /// std::env::set_var("BROWSER_POOL_SIZE", "10");
556 ///
557 /// let config = from_env()?;
558 /// assert_eq!(config.max_pool_size, 10);
559 /// ```
560 pub fn from_env() -> Result<BrowserPoolConfig, BrowserPoolError> {
561 // Load app.env file if present (ignore errors if not found)
562 match load_env_file() {
563 Ok(path) => {
564 log::info!("⚙️ Loaded configuration from: {:?}", path);
565 }
566 Err(e) => {
567 log::debug!(
568 "⚠️ No {} file found or failed to load: {} (using environment variables and defaults)",
569 ENV_FILE_NAME,
570 e
571 );
572 }
573 }
574
575 let max_pool_size = std::env::var("BROWSER_POOL_SIZE")
576 .ok()
577 .and_then(|s| s.parse().ok())
578 .unwrap_or(5);
579
580 let warmup_count = std::env::var("BROWSER_WARMUP_COUNT")
581 .ok()
582 .and_then(|s| s.parse().ok())
583 .unwrap_or(3);
584
585 let ttl_seconds = std::env::var("BROWSER_TTL_SECONDS")
586 .ok()
587 .and_then(|s| s.parse().ok())
588 .unwrap_or(3600u64);
589
590 let warmup_timeout_seconds = std::env::var("BROWSER_WARMUP_TIMEOUT_SECONDS")
591 .ok()
592 .and_then(|s| s.parse().ok())
593 .unwrap_or(60u64);
594
595 let ping_interval_seconds = std::env::var("BROWSER_PING_INTERVAL_SECONDS")
596 .ok()
597 .and_then(|s| s.parse().ok())
598 .unwrap_or(15u64);
599
600 let max_ping_failures = std::env::var("BROWSER_MAX_PING_FAILURES")
601 .ok()
602 .and_then(|s| s.parse().ok())
603 .unwrap_or(3);
604
605 log::info!("' Loading pool configuration from environment:");
606 log::info!(" - Max pool size: {}", max_pool_size);
607 log::info!(" - Warmup count: {}", warmup_count);
608 log::info!(
609 " - Browser TTL: {}s ({}min)",
610 ttl_seconds,
611 ttl_seconds / 60
612 );
613 log::info!(" - Warmup timeout: {}s", warmup_timeout_seconds);
614 log::info!(" - Ping interval: {}s", ping_interval_seconds);
615 log::info!(" - Max ping failures: {}", max_ping_failures);
616
617 BrowserPoolConfigBuilder::new()
618 .max_pool_size(max_pool_size)
619 .warmup_count(warmup_count)
620 .browser_ttl(Duration::from_secs(ttl_seconds))
621 .warmup_timeout(Duration::from_secs(warmup_timeout_seconds))
622 .ping_interval(Duration::from_secs(ping_interval_seconds))
623 .max_ping_failures(max_ping_failures)
624 .build()
625 .map_err(BrowserPoolError::Configuration)
626 }
627
628 /// Get Chrome path from environment.
629 ///
630 /// Reads `CHROME_PATH` environment variable.
631 ///
632 /// **Note:** Call [`from_env`] or [`load_env_file`] first to ensure
633 /// `app.env` is loaded if you're using a configuration file.
634 ///
635 /// # Returns
636 ///
637 /// - `Some(path)` if `CHROME_PATH` is set
638 /// - `None` if not set (will use auto-detection)
639 ///
640 /// # Example
641 ///
642 /// ```rust,ignore
643 /// use html2pdf_api::config::env::{load_env_file, chrome_path_from_env};
644 ///
645 /// // Ensure app.env is loaded first
646 /// let _ = load_env_file();
647 ///
648 /// let path = chrome_path_from_env();
649 /// if let Some(p) = path {
650 /// println!("Using Chrome at: {}", p);
651 /// }
652 /// ```
653 pub fn chrome_path_from_env() -> Option<String> {
654 std::env::var("CHROME_PATH").ok()
655 }
656}
657
658// ============================================================================
659// Unit Tests
660// ============================================================================
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 /// Verifies that BrowserPoolConfigBuilder correctly sets all configuration values.
667 ///
668 /// Tests the happy path where all values are valid and within constraints.
669 #[test]
670 fn test_config_builder() {
671 let config = BrowserPoolConfigBuilder::new()
672 .max_pool_size(10)
673 .warmup_count(5)
674 .browser_ttl(Duration::from_secs(7200))
675 .warmup_timeout(Duration::from_secs(120))
676 .build()
677 .unwrap();
678
679 assert_eq!(config.max_pool_size, 10);
680 assert_eq!(config.warmup_count, 5);
681 assert_eq!(config.browser_ttl.as_secs(), 7200);
682 assert_eq!(config.warmup_timeout.as_secs(), 120);
683 }
684
685 /// Verifies that config builder rejects invalid pool size (zero).
686 ///
687 /// Pool size must be at least 1 to be useful. This test ensures
688 /// the validation catches this error at build time.
689 #[test]
690 fn test_config_validation() {
691 let result = BrowserPoolConfigBuilder::new().max_pool_size(0).build();
692
693 assert!(result.is_err());
694 let err_msg = result.unwrap_err();
695 assert!(
696 err_msg.contains("max_pool_size must be greater than 0"),
697 "Expected validation error message, got: {}",
698 err_msg
699 );
700 }
701
702 /// Verifies that warmup count cannot exceed pool size.
703 ///
704 /// It's illogical to warmup more browsers than the pool can hold.
705 /// This test ensures the configuration builder catches this mistake.
706 #[test]
707 fn test_config_warmup_exceeds_pool() {
708 let result = BrowserPoolConfigBuilder::new()
709 .max_pool_size(5)
710 .warmup_count(10)
711 .build();
712
713 assert!(result.is_err());
714 let err_msg = result.unwrap_err();
715 assert!(
716 err_msg.contains("warmup_count cannot exceed max_pool_size"),
717 "Expected validation error message, got: {}",
718 err_msg
719 );
720 }
721
722 /// Verifies that default configuration values are production-ready.
723 ///
724 /// These defaults are used when no explicit configuration is provided.
725 /// They should be safe and reasonable for most use cases.
726 #[test]
727 fn test_config_defaults() {
728 let config = BrowserPoolConfig::default();
729
730 // Verify production-ready defaults
731 assert_eq!(config.max_pool_size, 5, "Default pool size should be 5");
732 assert_eq!(config.warmup_count, 3, "Default warmup should be 3");
733 assert_eq!(
734 config.ping_interval,
735 Duration::from_secs(15),
736 "Default ping interval should be 15s"
737 );
738 assert_eq!(
739 config.browser_ttl,
740 Duration::from_secs(3600),
741 "Default TTL should be 1 hour"
742 );
743 assert_eq!(
744 config.max_ping_failures, 3,
745 "Default max failures should be 3"
746 );
747 assert_eq!(
748 config.warmup_timeout,
749 Duration::from_secs(60),
750 "Default warmup timeout should be 60s"
751 );
752 }
753
754 /// Verifies that config builder supports method chaining.
755 ///
756 /// The builder pattern should allow fluent API usage where all
757 /// setters can be chained together.
758 #[test]
759 fn test_config_builder_chaining() {
760 let config = BrowserPoolConfigBuilder::new()
761 .max_pool_size(8)
762 .warmup_count(4)
763 .ping_interval(Duration::from_secs(30))
764 .browser_ttl(Duration::from_secs(1800))
765 .max_ping_failures(5)
766 .warmup_timeout(Duration::from_secs(90))
767 .build()
768 .unwrap();
769
770 // Verify all chained values were set correctly
771 assert_eq!(config.max_pool_size, 8);
772 assert_eq!(config.warmup_count, 4);
773 assert_eq!(config.ping_interval.as_secs(), 30);
774 assert_eq!(config.browser_ttl.as_secs(), 1800);
775 assert_eq!(config.max_ping_failures, 5);
776 assert_eq!(config.warmup_timeout.as_secs(), 90);
777 }
778
779 /// Verifies that BrowserPoolConfigBuilder implements Default.
780 #[test]
781 fn test_builder_default() {
782 let builder: BrowserPoolConfigBuilder = Default::default();
783 let config = builder.build().unwrap();
784
785 // Should have same values as BrowserPoolConfig::default()
786 assert_eq!(config.max_pool_size, 5);
787 assert_eq!(config.warmup_count, 3);
788 }
789}