1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
// Property-based tests for the logging and verbosity system.
//
// Feature: s3rm-rs, Property 21: Verbosity Level Configuration
// For any verbosity flag (-v, -vv, -vvv, -q), the tool should accept the flag
// and configure the appropriate logging level.
// **Validates: Requirements 4.1**
//
// Feature: s3rm-rs, Property 22: JSON Logging Format
// For any log output with JSON logging enabled, all logs should be valid JSON objects.
// **Validates: Requirements 4.7, 13.3**
//
// Feature: s3rm-rs, Property 23: Color Output Control
// For any execution environment, color output should be enabled by default in TTY
// environments, disabled in non-TTY environments unless explicitly enabled, and
// disabled when the disable-color flag is set.
// **Validates: Requirements 4.8, 4.9, 7.5, 13.7**
//
// Feature: s3rm-rs, Property 24: Error Logging
// For any deletion failure, the tool should log the error message and error code
// at the current verbosity level.
// **Validates: Requirements 4.10**
#[cfg(test)]
mod tests {
use crate::config::Config;
use crate::config::args::parse_from_args;
use crate::types::DeletionError;
use proptest::prelude::*;
// -----------------------------------------------------------------------
// Generators
// -----------------------------------------------------------------------
/// Generate a random verbosity flag set for the CLI.
///
/// clap_verbosity_flag with WarnLevel default maps as follows:
/// -qq → log_level() = None (two below Warn → off)
/// -q → log_level() = Error (one below Warn)
/// (none) → log_level() = Warn (default)
/// -v → log_level() = Info
/// -vv → log_level() = Debug
/// -vvv → log_level() = Trace
fn arb_verbosity_flags() -> impl Strategy<Value = (Vec<&'static str>, Option<log::Level>)> {
prop_oneof![
Just((vec!["-qq"], None)),
Just((vec!["-q"], Some(log::Level::Error))),
Just((vec![], Some(log::Level::Warn))),
Just((vec!["-v"], Some(log::Level::Info))),
Just((vec!["-vv"], Some(log::Level::Debug))),
Just((vec!["-vvv"], Some(log::Level::Trace))),
]
}
/// Generate a random DeletionError variant.
fn arb_deletion_error() -> impl Strategy<Value = DeletionError> {
prop_oneof![
Just(DeletionError::NotFound),
Just(DeletionError::AccessDenied),
Just(DeletionError::PreconditionFailed),
Just(DeletionError::Throttled),
"[a-z]{1,20}".prop_map(DeletionError::NetworkError),
"[a-z]{1,20}".prop_map(DeletionError::ServiceError),
]
}
// -----------------------------------------------------------------------
// Feature: s3rm-rs, Property 21: Verbosity Level Configuration
// -----------------------------------------------------------------------
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Feature: s3rm-rs, Property 21: Verbosity Level Configuration
/// **Validates: Requirements 4.1**
///
/// For any verbosity flag combination, the parsed Config should have the
/// corresponding tracing level set. Silent mode (-qq) disables tracing
/// entirely (tracing_config = None).
#[test]
fn prop_verbosity_level_configuration(
(flags, expected_level) in arb_verbosity_flags(),
) {
let mut args: Vec<&str> = vec!["s3rm", "s3://bucket/prefix/"];
args.extend(flags);
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
match expected_level {
None => {
prop_assert!(
config.tracing_config.is_none(),
"Expected no tracing config for silent mode"
);
}
Some(level) => {
prop_assert!(
config.tracing_config.is_some(),
"Expected tracing config to be present"
);
let tracing_config = config.tracing_config.unwrap();
prop_assert_eq!(tracing_config.tracing_level, level);
}
}
}
/// Feature: s3rm-rs, Property 21: Verbosity Level Configuration (dry-run boost)
/// **Validates: Requirements 4.1, 3.1**
///
/// When dry-run mode is enabled, the default level (Warn) is boosted to Info
/// so dry-run output is visible. But explicit -q flags are respected — the
/// user's quiet preference takes priority over dry-run convenience.
#[test]
fn prop_verbosity_dry_run_boost(
(flags, expected_level) in arb_verbosity_flags(),
) {
let mut args: Vec<&str> = vec!["s3rm", "s3://bucket/prefix/", "--dry-run"];
args.extend(flags);
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
match expected_level {
// Silent (-qq): user explicitly requested no output, dry-run respects that
None => {
prop_assert!(
config.tracing_config.is_none(),
"Dry-run must respect explicit silent mode (-qq)"
);
}
// Error (-q): user explicitly asked for errors only, dry-run respects that
Some(log::Level::Error) => {
prop_assert!(config.tracing_config.is_some());
prop_assert_eq!(config.tracing_config.unwrap().tracing_level, log::Level::Error);
}
// Warn (default, no flags): dry-run boosts to Info for visibility
Some(log::Level::Warn) => {
prop_assert!(config.tracing_config.is_some());
prop_assert_eq!(config.tracing_config.unwrap().tracing_level, log::Level::Info);
}
// Info, Debug, Trace (-v/-vv/-vvv): already >= Info, kept as-is
Some(level) => {
prop_assert!(config.tracing_config.is_some());
prop_assert_eq!(config.tracing_config.unwrap().tracing_level, level);
}
}
}
}
// -----------------------------------------------------------------------
// Feature: s3rm-rs, Property 22: JSON Logging Format
// -----------------------------------------------------------------------
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Feature: s3rm-rs, Property 22: JSON Logging Format
/// **Validates: Requirements 4.7, 13.3**
///
/// When --json-tracing is enabled, the TracingConfig must have
/// json_tracing=true regardless of other flags.
#[test]
fn prop_json_logging_format(
(flags, _) in arb_verbosity_flags(),
disable_color in proptest::bool::ANY,
aws_sdk_tracing in proptest::bool::ANY,
) {
let mut args: Vec<&str> = vec!["s3rm", "s3://bucket/prefix/", "--json-tracing", "--force"];
args.extend(flags.clone());
if disable_color {
args.push("--disable-color-tracing");
}
if aws_sdk_tracing {
args.push("--aws-sdk-tracing");
}
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
if config.tracing_config.is_some() {
let tracing_config = config.tracing_config.unwrap();
prop_assert!(
tracing_config.json_tracing,
"json_tracing must be true when --json-tracing is passed"
);
}
// If tracing_config is None (silent mode -qq), JSON flag has no
// effect since there's no tracing at all — this is acceptable.
}
/// Feature: s3rm-rs, Property 22: JSON Logging Format (default off)
/// **Validates: Requirements 4.7**
///
/// Without --json-tracing, json_tracing must be false.
#[test]
fn prop_json_logging_default_off(
(flags, _) in arb_verbosity_flags(),
) {
let mut args: Vec<&str> = vec!["s3rm", "s3://bucket/prefix/"];
args.extend(flags);
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
if let Some(tracing_config) = config.tracing_config {
prop_assert!(
!tracing_config.json_tracing,
"json_tracing must default to false"
);
}
}
}
// -----------------------------------------------------------------------
// Feature: s3rm-rs, Property 23: Color Output Control
// -----------------------------------------------------------------------
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Feature: s3rm-rs, Property 23: Color Output Control
/// **Validates: Requirements 4.8, 4.9, 7.5, 13.7**
///
/// When --disable-color-tracing is set, the TracingConfig must have
/// disable_color_tracing=true. When not set, it must be false (default).
#[test]
fn prop_color_output_control(
(flags, _) in arb_verbosity_flags(),
disable_color in proptest::bool::ANY,
) {
let mut args: Vec<&str> = vec!["s3rm", "s3://bucket/prefix/"];
args.extend(flags);
if disable_color {
args.push("--disable-color-tracing");
}
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
if let Some(tracing_config) = config.tracing_config {
prop_assert_eq!(
tracing_config.disable_color_tracing,
disable_color,
"disable_color_tracing should match CLI flag"
);
}
}
/// Feature: s3rm-rs, Property 23: Color Output Control (disable-color flag with verbosity)
/// **Validates: Requirements 4.8, 4.9**
///
/// The --disable-color-tracing flag must propagate correctly through the
/// CLI parser for all verbosity levels.
#[test]
fn prop_color_config_propagation_with_verbosity(
(flags, _) in arb_verbosity_flags(),
json_tracing in proptest::bool::ANY,
) {
// With color disabled
let mut args_disabled: Vec<&str> = vec![
"s3rm", "s3://bucket/prefix/",
"--disable-color-tracing",
];
args_disabled.extend(flags.clone());
if json_tracing {
args_disabled.push("--json-tracing");
args_disabled.push("--force");
}
let cli_disabled = parse_from_args(args_disabled).unwrap();
let config_disabled = Config::try_from(cli_disabled).unwrap();
// With color enabled (default)
let mut args_enabled: Vec<&str> = vec!["s3rm", "s3://bucket/prefix/"];
args_enabled.extend(flags);
if json_tracing {
args_enabled.push("--json-tracing");
args_enabled.push("--force");
}
let cli_enabled = parse_from_args(args_enabled).unwrap();
let config_enabled = Config::try_from(cli_enabled).unwrap();
// Color disabled: disable_color_tracing must be true
if let Some(tc) = config_disabled.tracing_config {
prop_assert!(tc.disable_color_tracing, "disable_color_tracing must be true when --disable-color-tracing is set");
}
// Color enabled (default): disable_color_tracing must be false
if let Some(tc) = config_enabled.tracing_config {
prop_assert!(!tc.disable_color_tracing, "disable_color_tracing must be false by default");
}
}
}
// -----------------------------------------------------------------------
// Feature: s3rm-rs, Property 24: Error Logging
// -----------------------------------------------------------------------
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Feature: s3rm-rs, Property 24: Error Logging
/// **Validates: Requirements 4.10**
///
/// For any DeletionError variant, the Display impl produces a non-empty
/// error message. This message is what the tracing macros (error!, warn!)
/// emit when logging deletion failures.
#[test]
fn prop_deletion_error_has_display_message(
error in arb_deletion_error(),
) {
let message = error.to_string();
prop_assert!(!message.is_empty(), "Error message must not be empty");
}
/// Feature: s3rm-rs, Property 24: Error Logging (retryable classification)
/// **Validates: Requirements 4.10, 6.1**
///
/// For any DeletionError, the is_retryable() method returns consistent
/// results: only Throttled, NetworkError, and ServiceError are retryable.
#[test]
fn prop_deletion_error_retryable_consistency(
error in arb_deletion_error(),
) {
let retryable = error.is_retryable();
match &error {
DeletionError::Throttled
| DeletionError::NetworkError(_)
| DeletionError::ServiceError(_) => {
prop_assert!(retryable, "Transient errors must be retryable");
}
DeletionError::NotFound
| DeletionError::AccessDenied
| DeletionError::PreconditionFailed => {
prop_assert!(!retryable, "Permanent errors must not be retryable");
}
}
}
/// Feature: s3rm-rs, Property 24: Error Logging (error code in display)
/// **Validates: Requirements 4.10**
///
/// For DeletionError variants that wrap a message (NetworkError,
/// ServiceError), the Display output includes the original error message.
/// For fixed variants, the Display output contains a descriptive string.
#[test]
fn prop_deletion_error_includes_context(
error in arb_deletion_error(),
) {
let message = error.to_string();
match &error {
DeletionError::NotFound => {
prop_assert!(message.contains("not found") || message.contains("Not found"));
}
DeletionError::AccessDenied => {
prop_assert!(message.to_lowercase().contains("denied"));
}
DeletionError::PreconditionFailed => {
prop_assert!(message.to_lowercase().contains("precondition") || message.to_lowercase().contains("etag"));
}
DeletionError::Throttled => {
prop_assert!(message.to_lowercase().contains("throttl"));
}
DeletionError::NetworkError(msg) => {
prop_assert!(
message.contains(msg),
"NetworkError display should include the original message"
);
}
DeletionError::ServiceError(msg) => {
prop_assert!(
message.contains(msg),
"ServiceError display should include the original message"
);
}
}
}
}
}