Skip to main content

max / makenotwork

22.9 KB · 676 lines History Blame Raw
1 //! Every `S3Client` method, against a real object store.
2 //!
3 //! WHY THIS FILE EXISTS. The crate had no integration test of any kind, and a
4 //! mutation run said what that costs: 114 of 206 mutants survived, 104 of them
5 //! inside `S3Client` (infra `1498fe2a`). `upload -> Ok(())` passed the whole
6 //! suite, because nothing in this repo ever called `upload`. The two worth
7 //! naming: `attempt < 3` can become `true` in all three multipart drivers,
8 //! which is an infinite retry on a permanent failure, and the part-offset
9 //! arithmetic in the copy driver can be wrong in either direction, which is
10 //! silent corruption of creator media -- the object completes and the bytes are
11 //! the wrong ones. Nothing short of a real round trip observes that.
12 //!
13 //! HOW TO RUN IT. The tier is astra, where MinIO is a local service:
14 //!
15 //! set -a; . ~/.config/s3-storage-tests.env; set +a
16 //! cargo nextest run --run-ignored all
17 //!
18 //! Every test is `#[ignore]`d so a developer machine reports them as SKIPPED
19 //! rather than passing silently. A test that did not run must never read as one
20 //! that did, which is the same rule the sweep applies to a cell it could not
21 //! evaluate.
22 //!
23 //! ISOLATION. `S3_TEST_*` names a bucket the credentials can reach and nothing
24 //! else: the key is scoped to it and MinIO refuses a write to the media buckets
25 //! (infra `6f7c7b46`, measured). Belt and braces anyway, every test works under
26 //! its own unique prefix and deletes it on the way out, so two runs cannot
27 //! collide and a leaked object is traceable to a run.
28 //!
29 //! MINIO IS NOT S3, and where they differ these tests assert the property that
30 //! matters rather than the exact bytes of a header. Multipart ETags are the
31 //! usual example: their format is not contractual, so nothing here reads one.
32
33 use std::io::{Read, Write};
34 use std::net::TcpStream;
35 use std::sync::atomic::{AtomicU32, Ordering};
36 use std::time::{SystemTime, UNIX_EPOCH};
37
38 use s3_storage::{S3Client, S3Config};
39
40 const SKIP: &str = "live S3: set S3_TEST_* and run with --run-ignored (see the module docs)";
41
42 /// The bucket and credentials, or a panic naming what is missing.
43 ///
44 /// A panic rather than a skip: these tests are `#[ignore]`d, so the only way to
45 /// reach this code is to have asked for them, and silently passing an asked-for
46 /// test because a variable was unset is the failure this whole file is about.
47 fn config() -> S3Config {
48 fn var(name: &str) -> String {
49 std::env::var(name)
50 .unwrap_or_else(|_| panic!("{name} is not set; source ~/.config/s3-storage-tests.env"))
51 }
52 S3Config {
53 endpoint: var("S3_TEST_ENDPOINT"),
54 bucket: var("S3_TEST_BUCKET"),
55 access_key: var("S3_TEST_ACCESS_KEY"),
56 secret_key: var("S3_TEST_SECRET_KEY"),
57 region: std::env::var("S3_TEST_REGION").unwrap_or_else(|_| "us-east-1".into()),
58 }
59 }
60
61 async fn client() -> S3Client {
62 S3Client::new(&config())
63 .await
64 .expect("building the client should not need the network")
65 }
66
67 /// A prefix no other run will use: the clock, the process, and a counter.
68 ///
69 /// All three are needed. The counter because two tests in one process start
70 /// inside the same millisecond, and the pid because a mutation run has a dozen
71 /// copies of this suite against one bucket at once (infra `1498fe2a`), where
72 /// two runs sharing a prefix would delete each other's objects and report it as
73 /// a killed mutant.
74 fn prefix(what: &str) -> String {
75 static SEQ: AtomicU32 = AtomicU32::new(0);
76 let millis = SystemTime::now()
77 .duration_since(UNIX_EPOCH)
78 .expect("the clock is after 1970")
79 .as_millis();
80 let n = SEQ.fetch_add(1, Ordering::Relaxed);
81 format!("run-{millis}-{}-{n}/{what}", std::process::id())
82 }
83
84 /// Bytes that are not all the same, so a wrong offset shows up as wrong content
85 /// rather than as an identical-looking block.
86 fn pattern(len: usize) -> Vec<u8> {
87 (0..len).map(|i| (i % 251) as u8).collect()
88 }
89
90 /// Compare without printing megabytes on failure: say where they diverge.
91 fn assert_same_bytes(expected: &[u8], actual: &[u8], what: &str) {
92 assert_eq!(
93 expected.len(),
94 actual.len(),
95 "{what}: length differs, expected {} bytes and got {}",
96 expected.len(),
97 actual.len()
98 );
99 if let Some(at) = expected.iter().zip(actual).position(|(a, b)| a != b) {
100 panic!(
101 "{what}: first difference at byte {at} of {}, expected {:#04x} and got {:#04x}",
102 expected.len(),
103 expected[at],
104 actual[at]
105 );
106 }
107 }
108
109 /// Best-effort teardown. A failed cleanup must not turn a passing test red, and
110 /// must not hide a failing one, so it reports and moves on.
111 async fn cleanup(s3: &S3Client, prefix: &str) {
112 if let Err(e) = s3.delete_prefix(prefix).await {
113 eprintln!("cleanup of {prefix} failed, leaving objects behind: {e}");
114 }
115 }
116
117 #[tokio::test]
118 #[ignore = "live S3"]
119 async fn an_object_round_trips_with_its_content_type() {
120 let s3 = client().await;
121 let key = prefix("round-trip/a.bin");
122 let body = pattern(4096);
123
124 s3.upload(
125 &key,
126 "application/octet-stream",
127 body.clone(),
128 Some("no-store"),
129 )
130 .await
131 .expect("upload");
132
133 let (got, content_type) = s3.download(&key).await.expect("download");
134 assert_same_bytes(&body, &got, "round trip");
135 assert_eq!(content_type, "application/octet-stream");
136
137 assert!(s3.object_exists(&key).await.expect("exists"));
138 assert_eq!(
139 s3.object_size(&key).await.expect("size"),
140 Some(body.len() as i64)
141 );
142
143 s3.delete(&key).await.expect("delete");
144 assert!(
145 !s3.object_exists(&key).await.expect("exists after delete"),
146 "the object survived its own deletion"
147 );
148 assert_eq!(s3.object_size(&key).await.expect("size after delete"), None);
149
150 cleanup(&s3, &key).await;
151 }
152
153 #[tokio::test]
154 #[ignore = "live S3"]
155 async fn download_head_reads_a_prefix_and_nothing_more() {
156 let s3 = client().await;
157 let key = prefix("head/a.bin");
158 let body = pattern(10_000);
159 s3.upload(&key, "application/octet-stream", body.clone(), None)
160 .await
161 .expect("upload");
162
163 let head = s3.download_head(&key, 512).await.expect("ranged read");
164 assert_same_bytes(&body[..512], &head, "ranged read");
165
166 // Asking for more than the object holds is not an error, and the answer is
167 // the object.
168 let all = s3
169 .download_head(&key, body.len() * 2)
170 .await
171 .expect("over-read");
172 assert_same_bytes(&body, &all, "over-read");
173
174 // Zero is answered without a request at all. It has to be: the range header
175 // is built as `bytes=0-{len-1}`, which underflows at zero.
176 assert!(
177 s3.download_head(&key, 0)
178 .await
179 .expect("zero-length")
180 .is_empty()
181 );
182
183 cleanup(&s3, &key).await;
184 }
185
186 #[tokio::test]
187 #[ignore = "live S3"]
188 async fn the_streaming_download_carries_the_same_bytes() {
189 // `download` is `download_buf` plus a copy, and `download_buf` is the
190 // aggregating form of this one, so the three share a path and only this one
191 // hands the caller an unread stream.
192 let s3 = client().await;
193 let key = prefix("stream/a.bin");
194 let body = pattern(200_000);
195 s3.upload(&key, "application/octet-stream", body.clone(), None)
196 .await
197 .expect("upload");
198
199 let stream = s3.download_stream(&key).await.expect("download_stream");
200 let collected = stream.collect().await.expect("draining the stream");
201 assert_same_bytes(&body, &collected.to_vec(), "streamed download");
202
203 let (buffered, content_type) = s3.download_buf(&key).await.expect("download_buf");
204 assert_same_bytes(&body, &buffered, "buffered download");
205 assert_eq!(content_type, "application/octet-stream");
206
207 cleanup(&s3, &key).await;
208 }
209
210 #[tokio::test]
211 #[ignore = "live S3"]
212 async fn a_missing_object_is_absent_rather_than_an_error() {
213 let s3 = client().await;
214 let key = prefix("missing/never-written.bin");
215 assert!(!s3.object_exists(&key).await.expect("exists"));
216 assert_eq!(s3.object_size(&key).await.expect("size"), None);
217 assert!(
218 s3.download(&key).await.is_err(),
219 "downloading nothing should fail rather than return empty"
220 );
221 }
222
223 #[tokio::test]
224 #[ignore = "live S3"]
225 async fn a_copy_carries_the_bytes_not_just_a_status() {
226 let s3 = client().await;
227 let root = prefix("copy");
228 let src = format!("{root}/src.bin");
229 let dst = format!("{root}/dst.bin");
230 let from = format!("{root}/from.bin");
231 let body = pattern(65_536);
232
233 s3.upload(&src, "application/octet-stream", body.clone(), None)
234 .await
235 .expect("upload");
236
237 s3.copy_object(&src, &dst).await.expect("copy_object");
238 let (got, _) = s3.download(&dst).await.expect("download copy");
239 assert_same_bytes(&body, &got, "copy_object");
240
241 // Same bucket, named explicitly: the cross-bucket form with the only bucket
242 // these credentials can reach.
243 s3.copy_object_from(s3.bucket(), &src, &from)
244 .await
245 .expect("copy_object_from");
246 let (got, _) = s3.download(&from).await.expect("download copy_from");
247 assert_same_bytes(&body, &got, "copy_object_from");
248
249 cleanup(&s3, &root).await;
250 }
251
252 #[tokio::test]
253 #[ignore = "live S3"]
254 async fn deleting_a_batch_reports_no_failures_and_removes_every_key() {
255 let s3 = client().await;
256 let root = prefix("batch");
257 let keys: Vec<String> = (0..5).map(|i| format!("{root}/{i}.bin")).collect();
258 for key in &keys {
259 s3.upload(key, "application/octet-stream", pattern(32), None)
260 .await
261 .expect("upload");
262 }
263 // A key that was never written, because a batch in the wild has them and S3
264 // treats deleting an absent key as success.
265 let mut with_absent = keys.clone();
266 with_absent.push(format!("{root}/never-written.bin"));
267
268 let failures = s3
269 .delete_objects(&with_absent)
270 .await
271 .expect("delete_objects");
272 assert!(failures.is_empty(), "delete_objects reported {failures:?}");
273 for key in &keys {
274 assert!(
275 !s3.object_exists(key).await.expect("exists"),
276 "{key} survived the batch delete"
277 );
278 }
279
280 cleanup(&s3, &root).await;
281 }
282
283 #[tokio::test]
284 #[ignore = "live S3"]
285 async fn delete_prefix_stops_at_the_prefix() {
286 let s3 = client().await;
287 let root = prefix("wipe");
288 let doomed = format!("{root}/doomed");
289 let kept = format!("{root}/kept");
290 for i in 0..3 {
291 s3.upload(
292 &format!("{doomed}/{i}.bin"),
293 "application/octet-stream",
294 pattern(16),
295 None,
296 )
297 .await
298 .expect("upload");
299 }
300 s3.upload(
301 &format!("{kept}/survivor.bin"),
302 "application/octet-stream",
303 pattern(16),
304 None,
305 )
306 .await
307 .expect("upload");
308
309 s3.delete_prefix(&doomed).await.expect("delete_prefix");
310
311 for i in 0..3 {
312 assert!(
313 !s3.object_exists(&format!("{doomed}/{i}.bin"))
314 .await
315 .expect("exists"),
316 "object {i} survived delete_prefix"
317 );
318 }
319 assert!(
320 s3.object_exists(&format!("{kept}/survivor.bin"))
321 .await
322 .expect("exists"),
323 "delete_prefix reached past its prefix, which is how a wipe becomes an incident"
324 );
325
326 cleanup(&s3, &root).await;
327 }
328
329 #[tokio::test]
330 #[ignore = "live S3"]
331 async fn a_multipart_upload_reads_back_byte_identical() {
332 let s3 = client().await;
333 let key = prefix("multipart/big.bin");
334 // Three parts at the 5 MiB floor, the last one deliberately short: a driver
335 // that gets the final part's length wrong passes on an exact multiple.
336 let part_size = 5 * 1024 * 1024;
337 let body = pattern(part_size * 2 + 1_234_567);
338
339 let file = std::env::temp_dir().join(format!("s3-live-{}.bin", std::process::id()));
340 std::fs::write(&file, &body).expect("writing the source file");
341
342 let uploaded = s3
343 .upload_multipart(&key, "application/octet-stream", &file, Some(part_size))
344 .await;
345 std::fs::remove_file(&file).ok();
346 uploaded.expect("upload_multipart");
347
348 assert_eq!(
349 s3.object_size(&key).await.expect("size"),
350 Some(body.len() as i64),
351 "the assembled object is the wrong length"
352 );
353 let (got, _) = s3.download(&key).await.expect("download");
354 assert_same_bytes(&body, &got, "multipart round trip");
355
356 cleanup(&s3, &key).await;
357 }
358
359 #[tokio::test]
360 #[ignore = "live S3"]
361 async fn the_default_part_size_uploads_a_file_larger_than_one_part() {
362 // `part_size: None` takes the 10 MiB default, which no other test exercises
363 // and which nothing asserts: the default is written as an arithmetic
364 // expression, and a wrong one is either a refusal at the 5 MiB floor or an
365 // upload in more parts than intended. 12 MiB is two parts at the real
366 // default and one at any smaller one.
367 let s3 = client().await;
368 let key = prefix("default-part/big.bin");
369 let body = pattern(12 * 1024 * 1024);
370
371 let file = std::env::temp_dir().join(format!("s3-live-default-{}.bin", std::process::id()));
372 std::fs::write(&file, &body).expect("writing the source file");
373 let uploaded = s3
374 .upload_multipart(&key, "application/octet-stream", &file, None)
375 .await;
376 std::fs::remove_file(&file).ok();
377 uploaded.expect("upload_multipart with the default part size");
378
379 let (got, _) = s3.download(&key).await.expect("download");
380 assert_same_bytes(&body, &got, "default part size round trip");
381
382 cleanup(&s3, &key).await;
383 }
384
385 #[tokio::test]
386 #[ignore = "live S3"]
387 async fn a_part_size_below_the_s3_floor_is_refused_before_anything_is_created() {
388 // The floor is 5 MiB and the check is `<`, so one byte under is refused and
389 // exactly 5 MiB is not. Refused BEFORE the upload is created, which is why
390 // this asserts on the pending-upload list as well as on the error: a
391 // pre-flight that creates first and validates second strands an upload
392 // nobody will ever complete or be billed for noticing.
393 let s3 = client().await;
394 let key = prefix("floor/rejected.bin");
395 let file = std::env::temp_dir().join(format!("s3-live-floor-{}.bin", std::process::id()));
396 std::fs::write(&file, pattern(1024)).expect("writing the source file");
397
398 let err = s3
399 .upload_multipart(
400 &key,
401 "application/octet-stream",
402 &file,
403 Some(5 * 1024 * 1024 - 1),
404 )
405 .await
406 .expect_err("a part below the floor must be refused");
407 assert!(
408 err.contains("at least 5 MB"),
409 "refused for the wrong reason: {err}"
410 );
411 std::fs::remove_file(&file).ok();
412
413 assert!(
414 s3.list_multipart_uploads_for_key(&key)
415 .await
416 .expect("list")
417 .is_empty(),
418 "the refusal created an upload and left it pending"
419 );
420 }
421
422 #[tokio::test]
423 #[ignore = "live S3"]
424 async fn pending_uploads_are_listed_for_their_own_key_only() {
425 // The listing filters a bucket-wide response down to one key. A filter that
426 // widened would hand a caller somebody else's upload id, and
427 // abort_multipart_upload would then cancel an upload that was going fine.
428 let s3 = client().await;
429 let root = prefix("listing");
430 let mine = format!("{root}/mine.bin");
431 let theirs = format!("{root}/theirs.bin");
432
433 let mine_id = s3
434 .create_multipart_upload(&mine, "application/octet-stream")
435 .await
436 .expect("create mine");
437 let theirs_id = s3
438 .create_multipart_upload(&theirs, "application/octet-stream")
439 .await
440 .expect("create theirs");
441
442 let listed = s3
443 .list_multipart_uploads_for_key(&mine)
444 .await
445 .expect("list mine");
446 assert!(
447 listed.contains(&mine_id),
448 "our own upload is missing: {listed:?}"
449 );
450 assert!(
451 !listed.contains(&theirs_id),
452 "the listing reached past its key: {listed:?}"
453 );
454
455 s3.abort_multipart_upload(&mine, &mine_id)
456 .await
457 .expect("abort mine");
458 s3.abort_multipart_upload(&theirs, &theirs_id)
459 .await
460 .expect("abort theirs");
461 }
462
463 #[tokio::test]
464 #[ignore = "live S3"]
465 async fn a_multipart_copy_preserves_every_byte() {
466 let s3 = client().await;
467 let root = prefix("multipart-copy");
468 let src = format!("{root}/src.bin");
469 let dst = format!("{root}/dst.bin");
470 let part_size = 5 * 1024 * 1024;
471 let body = pattern(part_size * 2 + 999_983);
472
473 let file = std::env::temp_dir().join(format!("s3-live-copy-{}.bin", std::process::id()));
474 std::fs::write(&file, &body).expect("writing the source file");
475 let uploaded = s3
476 .upload_multipart(&src, "application/octet-stream", &file, Some(part_size))
477 .await;
478 std::fs::remove_file(&file).ok();
479 uploaded.expect("upload_multipart");
480
481 s3.copy_object_multipart(
482 s3.bucket(),
483 &src,
484 &dst,
485 "application/octet-stream",
486 body.len() as u64,
487 Some(part_size),
488 )
489 .await
490 .expect("copy_object_multipart");
491
492 let (got, _) = s3.download(&dst).await.expect("download");
493 // THE ASSERTION THIS FILE WAS WRITTEN FOR. A copy driver whose part offsets
494 // are off by one part duplicates or drops a range, the object still
495 // completes, and nothing but the bytes says so.
496 assert_same_bytes(&body, &got, "multipart copy");
497
498 cleanup(&s3, &root).await;
499 }
500
501 #[tokio::test]
502 #[ignore = "live S3"]
503 async fn an_aborted_upload_leaves_nothing_behind() {
504 let s3 = client().await;
505 let key = prefix("abort/pending.bin");
506
507 let upload_id = s3
508 .create_multipart_upload(&key, "application/octet-stream")
509 .await
510 .expect("create_multipart_upload");
511 let pending = s3
512 .list_multipart_uploads_for_key(&key)
513 .await
514 .expect("list_multipart_uploads_for_key");
515 assert!(
516 pending.contains(&upload_id),
517 "the upload we just created is not listed as pending: {pending:?}"
518 );
519
520 s3.abort_multipart_upload(&key, &upload_id)
521 .await
522 .expect("abort_multipart_upload");
523 let after = s3
524 .list_multipart_uploads_for_key(&key)
525 .await
526 .expect("list after abort");
527 assert!(
528 !after.contains(&upload_id),
529 "the aborted upload is still pending, which is a bill nobody sees: {after:?}"
530 );
531 assert!(
532 !s3.object_exists(&key).await.expect("exists"),
533 "an aborted upload produced an object"
534 );
535 }
536
537 #[tokio::test]
538 #[ignore = "live S3"]
539 async fn a_presigned_url_actually_fetches_and_actually_uploads() {
540 let s3 = client().await;
541 let root = prefix("presign");
542 let download_key = format!("{root}/download.bin");
543 let upload_key = format!("{root}/upload.bin");
544 let body = pattern(2048);
545
546 s3.upload(
547 &download_key,
548 "application/octet-stream",
549 body.clone(),
550 None,
551 )
552 .await
553 .expect("upload");
554
555 let url = s3
556 .presign_download(&download_key, 300)
557 .await
558 .expect("presign_download");
559 let fetched = http_get(&url).expect("fetching the presigned URL");
560 assert_same_bytes(&body, &fetched, "presigned download");
561
562 let put_url = s3
563 .presign_upload(
564 &upload_key,
565 "application/octet-stream",
566 300,
567 None,
568 Some(body.len() as i64),
569 )
570 .await
571 .expect("presign_upload");
572 http_put(&put_url, "application/octet-stream", &body).expect("PUT to the presigned URL");
573 let (got, _) = s3
574 .download(&upload_key)
575 .await
576 .expect("download what was PUT");
577 assert_same_bytes(&body, &got, "presigned upload");
578
579 cleanup(&s3, &root).await;
580 }
581
582 #[tokio::test]
583 #[ignore = "live S3"]
584 async fn connectivity_answers_for_the_configured_bucket() {
585 let s3 = client().await;
586 s3.check_connectivity().await.expect("check_connectivity");
587
588 let mut wrong = config();
589 wrong.bucket = format!("{}-does-not-exist", wrong.bucket);
590 let s3 = S3Client::new(&wrong).await.expect("client");
591 assert!(
592 s3.check_connectivity().await.is_err(),
593 "connectivity passed against a bucket that does not exist, so it is not checking the bucket"
594 );
595 }
596
597 // ---------------------------------------------------------------------------
598 // A minimal HTTP client, because a presigned URL is only worth anything if
599 // something outside this crate can use it.
600 //
601 // Raw TCP rather than a dependency: the tier's endpoint is MinIO on localhost
602 // over plain HTTP, and adding an HTTP stack to dev-dependencies to issue two
603 // requests would pull a TLS backend into a crate that deliberately pins its own
604 // (see Cargo.toml on `rustls-ring`). If the tier ever points at an https
605 // endpoint, these two helpers are what has to change, and they will fail loudly
606 // rather than quietly skip.
607 // ---------------------------------------------------------------------------
608
609 fn split_url(url: &str) -> (String, String) {
610 let rest = url
611 .strip_prefix("http://")
612 .unwrap_or_else(|| panic!("this helper speaks plain HTTP only, got: {url}"));
613 match rest.split_once('/') {
614 Some((host, path)) => (host.to_string(), format!("/{path}")),
615 None => (rest.to_string(), "/".to_string()),
616 }
617 }
618
619 fn read_response(mut stream: TcpStream) -> Result<Vec<u8>, String> {
620 let mut raw = Vec::new();
621 stream
622 .read_to_end(&mut raw)
623 .map_err(|e| format!("reading the response: {e}"))?;
624 let split = raw
625 .windows(4)
626 .position(|w| w == b"\r\n\r\n")
627 .ok_or_else(|| "no header terminator in the response".to_string())?;
628 let headers = String::from_utf8_lossy(&raw[..split]).to_string();
629 let status = headers
630 .lines()
631 .next()
632 .unwrap_or_default()
633 .split_whitespace()
634 .nth(1)
635 .unwrap_or_default()
636 .to_string();
637 if !status.starts_with('2') {
638 return Err(format!("HTTP {status}: {headers}"));
639 }
640 Ok(raw[split + 4..].to_vec())
641 }
642
643 fn http_get(url: &str) -> Result<Vec<u8>, String> {
644 let (host, path) = split_url(url);
645 let mut stream = TcpStream::connect(&host).map_err(|e| format!("connecting to {host}: {e}"))?;
646 let req = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n");
647 stream
648 .write_all(req.as_bytes())
649 .map_err(|e| format!("writing the request: {e}"))?;
650 read_response(stream)
651 }
652
653 fn http_put(url: &str, content_type: &str, body: &[u8]) -> Result<Vec<u8>, String> {
654 let (host, path) = split_url(url);
655 let mut stream = TcpStream::connect(&host).map_err(|e| format!("connecting to {host}: {e}"))?;
656 let head = format!(
657 "PUT {path} HTTP/1.1\r\nHost: {host}\r\nContent-Type: {content_type}\r\n\
658 Content-Length: {}\r\nConnection: close\r\n\r\n",
659 body.len()
660 );
661 stream
662 .write_all(head.as_bytes())
663 .map_err(|e| format!("writing the request head: {e}"))?;
664 stream
665 .write_all(body)
666 .map_err(|e| format!("writing the body: {e}"))?;
667 read_response(stream)
668 }
669
670 /// Named so `cargo nextest list` shows why the file looks empty on a laptop.
671 #[test]
672 #[ignore = "live S3"]
673 fn these_tests_need_a_live_object_store() {
674 println!("{SKIP}");
675 }
676