1#![allow(
2 clippy::blanket_clippy_restriction_lints,
3 clippy::pub_use,
4 clippy::question_mark_used,
5 clippy::single_call_fn,
6 clippy::mod_module_files,
7 clippy::implicit_return,
8 clippy::separated_literal_suffix,
9 clippy::unseparated_literal_suffix,
10 clippy::pub_with_shorthand,
11 clippy::pub_without_shorthand,
12 clippy::allow_attributes,
13 clippy::arbitrary_source_item_ordering,
14 clippy::min_ident_chars,
15 clippy::exhaustive_structs,
16 clippy::exhaustive_enums,
17 clippy::indexing_slicing,
18 clippy::redundant_test_prefix,
19 clippy::assertions_on_result_states,
20 clippy::absolute_paths,
21 clippy::default_numeric_fallback,
22 clippy::integer_division,
23 clippy::integer_division_remainder_used,
24 clippy::float_arithmetic,
25 clippy::as_conversions,
26 clippy::shadow_unrelated,
27 clippy::std_instead_of_core,
28 clippy::unwrap_used,
29 clippy::expect_used,
30 clippy::panic,
31 clippy::todo,
32 clippy::unimplemented,
33 clippy::dbg_macro,
34 clippy::print_stdout,
35 clippy::print_stderr,
36 clippy::missing_docs_in_private_items,
37 clippy::multiple_inherent_impl,
38 clippy::missing_trait_methods,
39 clippy::pattern_type_mismatch,
40 clippy::ref_patterns,
41 clippy::self_named_constructors,
42 clippy::wildcard_enum_match_arm,
43 clippy::string_slice,
44 clippy::arithmetic_side_effects,
45 clippy::cast_possible_truncation,
46 clippy::cast_sign_loss,
47 clippy::cast_possible_wrap,
48 clippy::cast_precision_loss,
49 clippy::unreachable,
50 clippy::else_if_without_else,
51 clippy::match_wildcard_for_single_variants,
52 clippy::match_same_arms,
53 clippy::single_match_else,
54 clippy::match_bool,
55 clippy::std_instead_of_alloc,
56 clippy::unused_trait_names,
57 clippy::str_to_string,
58 clippy::string_to_string,
59 clippy::iter_over_hash_type,
60 clippy::infinite_loop,
61 clippy::little_endian_bytes,
62 clippy::big_endian_bytes,
63 clippy::host_endian_bytes,
64 clippy::partial_pub_fields,
65 clippy::pub_without_shorthand,
66 clippy::pub_with_shorthand,
67 clippy::error_impl_error,
68 clippy::lossy_float_literal,
69 clippy::float_cmp,
70 clippy::float_cmp_const,
71 clippy::redundant_type_annotations,
72 clippy::field_reassign_with_default,
73 clippy::clone_on_ref_ptr,
74 clippy::rc_buffer,
75 clippy::rc_mutex,
76 clippy::multiple_unsafe_ops_per_block,
77 clippy::undocumented_unsafe_blocks,
78 clippy::mixed_read_write_in_expression,
79 clippy::tuple_array_conversions,
80 clippy::format_push_string,
81 clippy::tests_outside_test_module,
82 clippy::as_underscore,
83 clippy::deref_by_slicing,
84 clippy::empty_line_after_outer_attr,
85 clippy::empty_structs_with_brackets,
86 clippy::enum_glob_use,
87 clippy::exit,
88 clippy::filetype_is_file,
89 clippy::fn_to_numeric_cast_any,
90 clippy::format_collect,
91 clippy::four_forward_slashes,
92 clippy::get_unwrap,
93 clippy::impl_trait_in_params,
94 clippy::let_underscore_must_use,
95 clippy::let_underscore_untyped,
96 clippy::map_err_ignore,
97 clippy::mem_forget,
98 clippy::missing_assert_message,
99 clippy::missing_asserts_for_indexing,
100 clippy::mixed_attributes_style,
101 clippy::mutex_atomic,
102 clippy::needless_raw_strings,
103 clippy::needless_raw_string_hashes,
104 clippy::non_ascii_literal,
105 clippy::panic_in_result_fn,
106 clippy::partial_pub_fields,
107 clippy::print_literal,
108 clippy::rest_pat_in_fully_bound_structs,
109 clippy::same_name_method,
110 clippy::semicolon_outside_block,
111 clippy::shadow_same,
112 clippy::shadow_reuse,
113 clippy::single_char_lifetime_names,
114 clippy::string_lit_chars_any,
115 clippy::string_lit_as_bytes,
116 clippy::try_err,
117 clippy::undocumented_unsafe_blocks,
118 clippy::unneeded_field_pattern,
119 clippy::unseparated_literal_suffix,
120 clippy::separated_literal_suffix,
121 clippy::use_debug,
122 clippy::verbose_file_reads,
123 clippy::wildcard_dependencies,
124 clippy::module_name_repetitions,
125 clippy::missing_inline_in_public_items,
126 clippy::missing_trait_methods,
127 clippy::missing_docs_in_private_items,
128 clippy::single_call_fn,
129 clippy::cognitive_complexity,
130 clippy::too_many_lines,
131 clippy::type_complexity,
132 clippy::similar_names,
133 clippy::many_single_char_names,
134 clippy::items_after_statements,
135 clippy::cast_lossless,
136 clippy::cast_possible_truncation,
137 clippy::cast_possible_wrap,
138 clippy::cast_precision_loss,
139 clippy::cast_sign_loss,
140 clippy::fn_params_excessive_bools,
141 clippy::struct_excessive_bools,
142 clippy::if_not_else,
143 clippy::inline_always,
144 clippy::must_use_candidate,
145 clippy::option_if_let_else,
146 clippy::redundant_closure_for_method_calls,
147 clippy::similar_names,
148 clippy::single_match_else,
149 clippy::too_many_arguments,
150 clippy::unreadable_literal,
151 clippy::unused_self,
152 clippy::used_underscore_binding,
153 clippy::wildcard_imports,
154 clippy::doc_markdown,
155 clippy::missing_errors_doc,
156 clippy::missing_panics_doc,
157 clippy::return_self_not_must_use,
158 clippy::semicolon_if_nothing_returned,
159 clippy::should_implement_trait,
160 clippy::trivially_copy_pass_by_ref,
161 clippy::unnecessary_wraps,
162 clippy::naive_bytecount,
163 clippy::needless_collect,
164 clippy::needless_pass_by_value,
165 clippy::option_option,
166 clippy::ptr_as_ptr,
167 clippy::range_minus_one,
168 clippy::range_plus_one,
169 clippy::redundant_else,
170 clippy::stable_sort_primitive,
171 clippy::string_add_assign,
172 clippy::unicode_not_nfc,
173 clippy::unnested_or_patterns,
174 clippy::unreadable_literal,
175 clippy::unused_async,
176 clippy::use_self,
177 clippy::used_underscore_binding,
178 clippy::verbose_bit_mask,
179 clippy::inconsistent_struct_constructor,
180 clippy::index_refutable_slice,
181 clippy::inefficient_to_string,
182 clippy::implicit_clone,
183 clippy::cloned_instead_of_copied,
184 clippy::copy_iterator,
185 clippy::default_trait_access,
186 clippy::empty_enum,
187 clippy::enum_variant_names,
188 clippy::expl_impl_clone_on_copy,
189 clippy::explicit_deref_methods,
190 clippy::explicit_into_iter_loop,
191 clippy::explicit_iter_loop,
192 clippy::fallible_impl_from,
193 clippy::filter_map_next,
194 clippy::flat_map_option,
195 clippy::float_cmp_const,
196 clippy::fn_params_excessive_bools,
197 clippy::from_iter_instead_of_collect,
198 clippy::if_let_mutex,
199 clippy::ignored_unit_patterns,
200 clippy::implicit_clone,
201 clippy::implicit_hasher,
202 clippy::imprecise_flops,
203 clippy::inconsistent_struct_constructor,
204 clippy::index_refutable_slice,
205 clippy::inefficient_to_string,
206 clippy::inline_fn_without_body,
207 clippy::into_iter_on_ref,
208 clippy::invalid_upcast_comparisons,
209 clippy::iter_not_returning_iterator,
210 clippy::large_digit_groups,
211 clippy::large_stack_arrays,
212 clippy::large_types_passed_by_value,
213 clippy::let_unit_value,
214 clippy::linkedlist,
215 clippy::lossy_float_literal,
216 clippy::macro_use_imports,
217 clippy::manual_ok_or,
218 clippy::map_flatten,
219 clippy::map_unwrap_or,
220 clippy::match_same_arms,
221 clippy::match_wild_err_arm,
222 clippy::match_wildcard_for_single_variants,
223 clippy::mem_forget,
224 unexpected_cfgs,
225 clippy::missing_enforced_import_renames,
226 clippy::missing_errors_doc,
227 clippy::missing_panics_doc,
228 clippy::module_name_repetitions,
229 clippy::must_use_candidate,
230 clippy::mut_mut,
231 clippy::mutex_integer,
232 clippy::needless_continue,
233 clippy::needless_for_each,
234 clippy::needless_pass_by_value,
235 clippy::negative_feature_names,
236 clippy::no_effect_underscore_binding,
237 clippy::option_option,
238 clippy::path_buf_push_overwrite,
239 clippy::ptr_as_ptr,
240 clippy::ptr_cast_constness,
241 clippy::range_minus_one,
242 clippy::range_plus_one,
243 clippy::rc_buffer,
244 clippy::rc_mutex,
245 clippy::redundant_feature_names,
246 clippy::ref_option_ref,
247 clippy::rest_pat_in_fully_bound_structs,
248 clippy::same_functions_in_if_condition,
249 clippy::semicolon_if_nothing_returned,
250 clippy::should_implement_trait,
251 clippy::single_match_else,
252 clippy::stable_sort_primitive,
253 clippy::string_add,
254 clippy::string_add_assign,
255 clippy::string_lit_as_bytes,
256 clippy::string_to_string,
257 clippy::struct_excessive_bools,
258 clippy::suboptimal_flops,
259 clippy::suspicious_operation_groupings,
260 clippy::trait_duplication_in_bounds,
261 clippy::transmute_ptr_to_ptr,
262 clippy::trivially_copy_pass_by_ref,
263 clippy::type_repetition_in_bounds,
264 clippy::unchecked_duration_subtraction,
265 clippy::unicode_not_nfc,
266 clippy::unimplemented,
267 clippy::unnested_or_patterns,
268 clippy::unreadable_literal,
269 clippy::unsafe_derive_deserialize,
270 clippy::unused_async,
271 clippy::unused_self,
272 clippy::use_debug,
273 clippy::use_self,
274 clippy::used_underscore_binding,
275 clippy::useless_transmute,
276 clippy::verbose_bit_mask,
277 clippy::wildcard_dependencies,
278 clippy::wildcard_enum_match_arm,
279 clippy::wildcard_imports,
280 clippy::zero_sized_map_values,
281 clippy::else_if_without_else,
282 clippy::non_ascii_literal,
283 clippy::question_mark_used,
284 unfulfilled_lint_expectations,
285 reason = "restriction lints contain contradictory rules and overly pedantic restrictions"
286)]
287
288pub mod async_traits;
459pub mod config;
460pub mod connection;
461pub mod error;
462pub mod iterators;
463#[macro_use]
464pub mod macros;
465pub mod presets;
466pub mod security;
467pub mod target;
468pub mod types;
469pub mod zero_cost;
470
471pub use async_traits::{
473 AsyncConnectionStrategy, AsyncRetryStrategy, AsyncTargetChecker, ConcurrentProgressStrategy,
474 DefaultTargetChecker, ExponentialBackoffStrategy, LinearBackoffStrategy, WaitForAllStrategy,
475 WaitForAnyStrategy,
476};
477pub use config::WaitConfigBuilder;
478pub use connection::{wait_for_connection, wait_for_single_target};
479pub use error::{Result, ResultExt, WaitForError};
480pub use iterators::{ResultSummary, TargetIterExt, TargetResultIterExt};
481pub use security::{RateLimiter, SecurityValidator};
482pub use target::{HttpTargetBuilder, TcpTargetBuilder};
483pub use types::{
484 ConnectionError, Hostname, HttpError, Port, Target, TargetResult, WaitConfig, WaitResult,
485};
486pub use zero_cost::{
487 ConstRetryStrategy, DynamicPort, LazyFormat, RegisteredPort, SmallString, StringBuilder,
488 TargetDisplay, ValidatedPort, WellKnownPort,
489};
490
491pub(crate) use error::error_messages;
493
494#[cfg(test)]
495#[expect(
496 clippy::unwrap_used,
497 clippy::panic,
498 clippy::match_wildcard_for_single_variants,
499 reason = "test code where panics are acceptable"
500)]
501mod tests {
502 use super::*;
503 use proptest::prelude::*;
504 use std::time::Duration;
505 use test_case::test_case;
506 use url::Url;
507
508 #[test]
509 fn test_target_parse_tcp() {
510 let target = Target::parse("localhost:8080", 200).unwrap();
511 match target {
512 Target::Tcp { host, port } => {
513 assert_eq!(host.as_str(), "localhost");
514 assert_eq!(port.get(), 8080);
515 }
516 _ => panic!("Expected TCP target"),
517 }
518 }
519
520 #[test]
521 fn test_target_parse_http() {
522 let target = Target::parse("https://example.com/health", 200).unwrap();
523 match target {
524 Target::Http {
525 url,
526 expected_status,
527 ..
528 } => {
529 assert_eq!(url.to_string(), "https://example.com/health");
530 assert_eq!(expected_status, 200);
531 }
532 _ => panic!("Expected HTTP target"),
533 }
534 }
535
536 #[test]
537 fn test_target_display() {
538 let tcp_target = Target::tcp("localhost", 8080).unwrap();
539 assert_eq!(tcp_target.display(), "localhost:8080");
540
541 let url = Url::parse("https://example.com/health").unwrap();
542 let http_target = Target::http(url, 200).unwrap();
543 assert_eq!(http_target.display(), "https://example.com/health");
544 }
545
546 #[test]
547 fn test_wait_config_builder() {
548 let config = WaitConfig::builder()
549 .timeout(Duration::from_secs(60))
550 .interval(Duration::from_secs(2))
551 .max_interval(Duration::from_secs(30))
552 .wait_for_any(true)
553 .max_retries(Some(10))
554 .build();
555
556 assert_eq!(config.timeout, Duration::from_secs(60));
557 assert_eq!(config.initial_interval, Duration::from_secs(2));
558 assert_eq!(config.max_interval, Duration::from_secs(30));
559 assert!(config.wait_for_any);
560 assert_eq!(config.max_retries, Some(10));
561 }
562
563 #[test]
564 fn test_calculate_next_interval() {
565 use connection::calculate_next_interval;
566
567 let current = Duration::from_secs(1);
568 let max = Duration::from_secs(30);
569
570 let next = calculate_next_interval(current, max);
571 assert_eq!(next, Duration::from_millis(1500));
572
573 let large_current = Duration::from_secs(25);
574 let next = calculate_next_interval(large_current, max);
575 assert_eq!(next, max);
576 }
577
578 proptest! {
580 #[test]
581 fn test_port_new_valid_range(port in 1u16..=65535) {
582 let result = Port::new(port);
583 assert!(result.is_some());
584 assert_eq!(result.unwrap().get(), port);
585 }
586
587 #[test]
588 fn test_port_new_zero_invalid(port in 0u16..=0) {
589 let result = Port::new(port);
590 assert!(result.is_none());
591 }
592
593 #[test]
594 fn test_port_well_known_valid_range(port in 1u16..=1023) {
595 let result = Port::well_known(port);
596 assert!(result.is_some());
597 assert_eq!(result.unwrap().get(), port);
598 }
599
600 #[test]
601 fn test_port_well_known_invalid_range(port in 1024u16..=65535) {
602 let result = Port::well_known(port);
603 assert!(result.is_none());
604 }
605
606 #[test]
607 fn test_port_registered_valid_range(port in 1024u16..=49151) {
608 let result = Port::registered(port);
609 assert!(result.is_some());
610 assert_eq!(result.unwrap().get(), port);
611 }
612
613 #[test]
614 fn test_port_registered_invalid_low_range(port in 1u16..=1023) {
615 let result = Port::registered(port);
616 assert!(result.is_none());
617 }
618
619 #[test]
620 fn test_port_registered_invalid_high_range(port in 49152u16..=65535) {
621 let result = Port::registered(port);
622 assert!(result.is_none());
623 }
624
625 #[test]
626 fn test_port_dynamic_valid_range(port in 49152u16..=65535) {
627 let result = Port::dynamic(port);
628 assert!(result.is_some());
629 assert_eq!(result.unwrap().get(), port);
630 }
631
632 #[test]
633 fn test_port_dynamic_invalid_range(port in 1u16..=49151) {
634 let result = Port::dynamic(port);
635 assert!(result.is_none());
636 }
637
638 #[test]
639 fn test_hostname_validation_alphanumeric(
640 hostname in "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]"
641 ) {
642 let result = Hostname::new(hostname);
643 assert!(result.is_ok());
644 }
645
646 #[test]
647 fn test_hostname_validation_too_long(
648 hostname in "[a-zA-Z]{254,300}"
649 ) {
650 let result = Hostname::new(hostname);
651 assert!(result.is_err());
652 }
653
654 #[test]
655 fn test_target_tcp_creation(
656 hostname in "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,30}[a-zA-Z0-9]",
657 port in 1u16..=65535
658 ) {
659 let result = Target::tcp(hostname, port);
660 assert!(result.is_ok());
661 }
662
663 #[test]
664 fn test_calculate_next_interval_property(
665 current_ms in 1u64..=60000,
666 max_ms in 60_000u64..=300_000
667 ) {
668 let current = Duration::from_millis(current_ms);
669 let max = Duration::from_millis(max_ms);
670
671 let next = connection::calculate_next_interval(current, max);
672
673 assert!(next >= current);
675 assert!(next <= max);
677 }
678 }
679
680 #[test_case("localhost", 80; "http port")]
682 #[test_case("example.com", 443; "https port")]
683 #[test_case("127.0.0.1", 22; "ssh port")]
684 #[test_case("db.example.com", 5432; "postgres port")]
685 fn test_tcp_target_creation(hostname: &str, port: u16) {
686 let target = Target::tcp(hostname, port).unwrap();
687 match target {
688 Target::Tcp { host, port: p } => {
689 assert_eq!(host.as_str(), hostname);
690 assert_eq!(p.get(), port);
691 }
692 _ => panic!("Expected TCP target"),
693 }
694 }
695
696 #[test_case(80, Port::http(); "http port constant")]
697 #[test_case(443, Port::https(); "https port constant")]
698 #[test_case(22, Port::ssh(); "ssh port constant")]
699 #[test_case(5432, Port::postgres(); "postgres port constant")]
700 #[test_case(3306, Port::mysql(); "mysql port constant")]
701 #[test_case(6379, Port::redis(); "redis port constant")]
702 fn test_port_constants(expected: u16, port: Port) {
703 assert_eq!(port.get(), expected);
704 }
705
706 #[test_case("http://example.com/", 200; "http url")]
707 #[test_case("https://api.example.com/health", 200; "https health endpoint")]
708 #[test_case("https://example.com:8080/status", 204; "custom port and status")]
709 fn test_http_target_parsing(url_str: &str, status: u16) {
710 let target = Target::parse(url_str, status).unwrap();
711 match target {
712 Target::Http {
713 url,
714 expected_status,
715 ..
716 } => {
717 assert_eq!(url.to_string(), url_str);
718 assert_eq!(expected_status, status);
719 }
720 _ => panic!("Expected HTTP target"),
721 }
722 }
723
724 #[test_case(""; "empty string")]
725 #[test_case("invalid-target"; "missing port")]
726 #[test_case("host:"; "empty port")]
727 #[test_case("host:abc"; "non-numeric port")]
728 #[test_case("host:0"; "zero port")]
729 #[test_case("host:65536"; "port too high")]
730 fn test_invalid_target_parsing(target_str: &str) {
731 let result = Target::parse(target_str, 200);
732 assert!(result.is_err());
733 }
734
735 #[test_case(""; "empty hostname")]
736 #[test_case("-example.com"; "starts with hyphen")]
737 #[test_case("example.com-"; "ends with hyphen")]
738 #[test_case("ex..ample.com"; "empty label")]
739 #[test_case(&"a".repeat(254); "too long")]
740 fn test_invalid_hostname_validation(hostname: &str) {
741 let result = Hostname::new(hostname);
742 assert!(result.is_err());
743 }
744
745 #[test_case("192.168.1.1"; "valid ipv4")]
746 #[test_case("10.0.0.1"; "valid private ip")]
747 #[test_case("255.255.255.255"; "max ipv4")]
748 fn test_valid_ipv4_hostname(ip: &str) {
749 let result = Hostname::ipv4(ip);
750 assert!(result.is_ok());
751 assert_eq!(result.unwrap().as_str(), ip);
752 }
753
754 #[test_case("192.168.1"; "incomplete ipv4")]
755 #[test_case("192.168.1.1.1"; "too many parts")]
756 #[test_case("192.168.256.1"; "octet too high")]
757 #[test_case("192.168.abc.1"; "invalid octet")]
758 fn test_invalid_ipv4_hostname(ip: &str) {
759 let result = Hostname::ipv4(ip);
760 assert!(result.is_err());
761 }
762
763 #[test]
764 fn test_hostname_const_constructors() {
765 let localhost = Hostname::localhost();
766 assert_eq!(localhost.as_str(), "localhost");
767
768 let loopback = Hostname::loopback();
769 assert_eq!(loopback.as_str(), "127.0.0.1");
770
771 let loopback_v6 = Hostname::loopback_v6();
772 assert_eq!(loopback_v6.as_str(), "::1");
773
774 let any = Hostname::any();
775 assert_eq!(any.as_str(), "0.0.0.0");
776 }
777
778 #[test]
779 fn test_target_convenience_constructors() {
780 let localhost_target = Target::localhost(8080).unwrap();
781 assert_eq!(localhost_target.hostname(), "localhost");
782 assert_eq!(localhost_target.port(), Some(8080));
783
784 let loopback_target = Target::loopback(3000).unwrap();
785 assert_eq!(loopback_target.hostname(), "127.0.0.1");
786 assert_eq!(loopback_target.port(), Some(3000));
787
788 let loopback_v6_target = Target::loopback_v6(9090).unwrap();
789 assert_eq!(loopback_v6_target.hostname(), "::1");
790 assert_eq!(loopback_v6_target.port(), Some(9090));
791 }
792
793 #[test]
794 fn test_tcp_builder_fluent_interface() {
795 let target = Target::tcp_builder("example.com")
797 .unwrap()
798 .registered_port(8080)
799 .build()
800 .unwrap();
801
802 assert_eq!(target.hostname(), "example.com");
803 assert_eq!(target.port(), Some(8080));
804 }
805
806 #[test]
807 fn test_tcp_builder_error_deferred() {
808 let result = Target::tcp_builder("example.com")
810 .unwrap()
811 .well_known_port(8080) .build();
813
814 assert!(result.is_err());
815 }
816
817 #[test]
818 fn safe_tcp_targets_macro() {
819 let result = tcp_targets![
821 "localhost" => 8080,
822 "example.com" => 443,
823 ];
824
825 assert!(result.is_ok());
826 let targets = result.unwrap();
827 assert_eq!(targets.len(), 2);
828 assert_eq!(targets[0].hostname(), "localhost");
829 assert_eq!(targets[0].port(), Some(8080));
830 }
831
832 #[test]
833 fn safe_tcp_targets_macro_error() {
834 let result = tcp_targets![
836 "localhost" => 8080,
837 "example.com" => 0, ];
839
840 assert!(result.is_err());
841 }
842
843 #[test]
844 fn safe_http_targets_macro() {
845 let result = http_targets![
847 "https://example.com" => 200,
848 "http://localhost:8080" => 204,
849 ];
850
851 assert!(result.is_ok());
852 let targets = result.unwrap();
853 assert_eq!(targets.len(), 2);
854 }
855
856 #[test]
857 fn safe_http_targets_macro_error() {
858 let result = http_targets![
860 "https://example.com" => 200,
861 "invalid-url" => 200, ];
863
864 result.unwrap_err();
865 }
866}