1/*
2 * SPDX-FileCopyrightText: 2026 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14//! Integration tests for the local filesystem storage backend.
15
16use cda_interfaces::storage_api::{
17 Collection as _, CollectionName, RandomAccessData as _, Storage, StorageError,
18};
19use cda_storage::LocalStorage;
20
21/// Create a fresh `LocalStorage` in a unique temp directory.
22fn create_test_storage() -> (LocalStorage, tempfile::TempDir) {
23 let dir = tempfile::tempdir().expect("Failed to create temp dir");
24 let storage = LocalStorage::new(dir.path()).expect("Failed to create LocalStorage");
25 (storage, dir)
26}
27
28#[tokio::test]
29async fn write_read_roundtrip() {
30 let (storage, _dir) = create_test_storage();
31 let name = CollectionName::DiagnosticDatabase;
32 let collection = storage.get_or_create_collection(&name).await.unwrap();
33
34 let mut tx = storage.begin_transaction().unwrap();
35 let mut data: &[u8] = b"hello world";
36 collection
37 .write(&mut tx, "greeting", &mut data)
38 .await
39 .unwrap();
40 tx.commit().await.unwrap();
41
42 let handle = collection.read("greeting").await.unwrap();
43 let mut buf = vec![0u8; 11];
44 let n = handle.read_at(0, &mut buf).unwrap();
45 assert_eq!(n, 11);
46 assert_eq!(&buf, b"hello world");
47}
48
49#[tokio::test]
50async fn read_nonexistent_key_returns_not_found() {
51 let (storage, _dir) = create_test_storage();
52 let name = CollectionName::DiagnosticDatabase;
53 let collection = storage.get_or_create_collection(&name).await.unwrap();
54
55 let result = collection.read("nonexistent").await;
56 assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
57}
58
59#[tokio::test]
60async fn delete_removes_key_after_commit() {
61 let (storage, _dir) = create_test_storage();
62 let name = CollectionName::DiagnosticDatabase;
63 let collection = storage.get_or_create_collection(&name).await.unwrap();
64
65 // Write a key.
66 let mut tx = storage.begin_transaction().unwrap();
67 let mut data: &[u8] = b"to be deleted";
68 collection
69 .write(&mut tx, "doomed", &mut data)
70 .await
71 .unwrap();
72 tx.commit().await.unwrap();
73
74 // Delete the key.
75 let mut tx = storage.begin_transaction().unwrap();
76 collection.delete(&mut tx, "doomed").await.unwrap();
77 tx.commit().await.unwrap();
78
79 let result = collection.read("doomed").await;
80 assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
81}
82
83#[tokio::test]
84async fn delete_all_removes_all_keys() {
85 let (storage, _dir) = create_test_storage();
86 let name = CollectionName::DiagnosticDatabase;
87 let collection = storage.get_or_create_collection(&name).await.unwrap();
88
89 // Write multiple keys.
90 let mut tx = storage.begin_transaction().unwrap();
91 let mut d1: &[u8] = b"one";
92 let mut d2: &[u8] = b"two";
93 collection.write(&mut tx, "key1", &mut d1).await.unwrap();
94 collection.write(&mut tx, "key2", &mut d2).await.unwrap();
95 tx.commit().await.unwrap();
96
97 // Delete all.
98 let mut tx = storage.begin_transaction().unwrap();
99 collection.delete_all(&mut tx).await.unwrap();
100 tx.commit().await.unwrap();
101
102 let is_empty = collection.is_empty().await.unwrap();
103 assert!(is_empty);
104}
105
106// Transaction semantics
107#[tokio::test]
108async fn rollback_discards_writes() {
109 let (storage, _dir) = create_test_storage();
110 let name = CollectionName::DiagnosticDatabase;
111 let collection = storage.get_or_create_collection(&name).await.unwrap();
112
113 let mut tx = storage.begin_transaction().unwrap();
114 let mut data: &[u8] = b"should not persist";
115 collection
116 .write(&mut tx, "ephemeral", &mut data)
117 .await
118 .unwrap();
119 tx.rollback();
120
121 let result = collection.read("ephemeral").await;
122 assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
123}
124
125#[tokio::test]
126async fn drop_without_commit_is_implicit_rollback() {
127 let (storage, _dir) = create_test_storage();
128 let name = CollectionName::DiagnosticDatabase;
129 let collection = storage.get_or_create_collection(&name).await.unwrap();
130
131 {
132 let mut tx = storage.begin_transaction().unwrap();
133 let mut data: &[u8] = b"dropped";
134 collection.write(&mut tx, "ghost", &mut data).await.unwrap();
135 // tx is dropped here without commit.
136 }
137
138 let result = collection.read("ghost").await;
139 assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
140}
141
142#[tokio::test]
143async fn no_read_your_writes() {
144 let (storage, _dir) = create_test_storage();
145 let name = CollectionName::DiagnosticDatabase;
146 let collection = storage.get_or_create_collection(&name).await.unwrap();
147
148 let mut tx = storage.begin_transaction().unwrap();
149 let mut data: &[u8] = b"uncommitted";
150 collection
151 .write(&mut tx, "pending", &mut data)
152 .await
153 .unwrap();
154
155 // Read should not see the uncommitted write.
156 let result = collection.read("pending").await;
157 assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
158
159 tx.commit().await.unwrap();
160
161 // Now it should be visible.
162 let handle = collection.read("pending").await.unwrap();
163 assert_eq!(handle.data_size().unwrap(), 11);
164}
165
166#[tokio::test]
167async fn single_transaction_enforcement() {
168 let (storage, _dir) = create_test_storage();
169
170 let _tx = storage.begin_transaction().unwrap();
171 let result = storage.begin_transaction();
172 assert!(matches!(result, Err(StorageError::TransactionBusy)));
173}
174
175#[tokio::test]
176async fn can_begin_transaction_after_previous_commits() {
177 let (storage, _dir) = create_test_storage();
178
179 let tx = storage.begin_transaction().unwrap();
180 tx.commit().await.unwrap();
181
182 // Should be able to begin a new transaction now.
183 let tx2 = storage.begin_transaction().unwrap();
184 tx2.commit().await.unwrap();
185}
186
187#[tokio::test]
188async fn can_begin_transaction_after_previous_rollback() {
189 let (storage, _dir) = create_test_storage();
190
191 let tx = storage.begin_transaction().unwrap();
192 tx.rollback();
193
194 let tx2 = storage.begin_transaction().unwrap();
195 tx2.commit().await.unwrap();
196}
197
198#[tokio::test]
199async fn opening_missing_root_fails_without_creating_it() {
200 let dir = tempfile::tempdir().unwrap();
201 let root = dir.path().join("not_mounted");
202
203 let result = LocalStorage::new(&root);
204
205 assert!(
206 matches!(result, Err(StorageError::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound)
207 );
208 assert!(!root.exists());
209}
210
211#[tokio::test]
212async fn writes_never_recreate_a_vanished_root() {
213 let dir = tempfile::tempdir().unwrap();
214 let root = dir.path().join("mounted");
215 std::fs::create_dir(&root).unwrap();
216 let storage = LocalStorage::new(&root).unwrap();
217
218 // E.g. the storage is unmounted while the CDA runs.
219 std::fs::remove_dir(&root).unwrap();
220
221 assert!(matches!(
222 storage.begin_transaction(),
223 Err(StorageError::Io(_))
224 ));
225 assert!(matches!(
226 storage
227 .get_or_create_collection(&CollectionName::DiagnosticDatabase)
228 .await,
229 Err(StorageError::Io(_))
230 ));
231 assert!(!root.exists());
232
233 // A failed start must not leave the storage busy.
234 std::fs::create_dir(&root).unwrap();
235 storage.begin_transaction().unwrap();
236}
237
238#[tokio::test]
239async fn case_insensitive_keys() {
240 let (storage, _dir) = create_test_storage();
241 let name = CollectionName::DiagnosticDatabase;
242 let collection = storage.get_or_create_collection(&name).await.unwrap();
243
244 let mut tx = storage.begin_transaction().unwrap();
245 let mut data: &[u8] = b"case test";
246 collection
247 .write(&mut tx, "FooBar", &mut data)
248 .await
249 .unwrap();
250 tx.commit().await.unwrap();
251
252 // Reading with different cases should find the same entry.
253 let handle = collection.read("foobar").await.unwrap();
254 assert_eq!(handle.data_size().unwrap(), 9);
255
256 let handle = collection.read("FOOBAR").await.unwrap();
257 assert_eq!(handle.data_size().unwrap(), 9);
258}
259
260// Collection management
261#[tokio::test]
262async fn get_nonexistent_collection_returns_not_found() {
263 let (storage, _dir) = create_test_storage();
264 let name = CollectionName::Custom("nonexistent".to_string());
265 let result = storage.get_collection(&name).await;
266 assert!(matches!(result, Err(StorageError::CollectionNotFound(_))));
267}
268
269#[tokio::test]
270async fn create_and_delete_collection() {
271 let (storage, _dir) = create_test_storage();
272 let name = CollectionName::Custom("temp_collection".to_string());
273
274 let mut tx = storage.begin_transaction().unwrap();
275 let collection = storage.create_collection(&mut tx, &name).await.unwrap();
276
277 // Write data into the new collection (before commit, the dir doesn't exist yet for reads
278 // but the write goes to staging).
279 let mut data: &[u8] = b"in new collection";
280 collection.write(&mut tx, "item", &mut data).await.unwrap();
281 tx.commit().await.unwrap();
282
283 // Should be readable now. Drop the handle before the next transaction to avoid deadlock.
284 let collection = storage.get_collection(&name).await.unwrap();
285 {
286 let handle = collection.read("item").await.unwrap();
287 assert_eq!(handle.data_size().unwrap(), 17);
288 }
289
290 // Delete the collection.
291 let mut tx = storage.begin_transaction().unwrap();
292 storage.delete_collection(&mut tx, &name).await.unwrap();
293 tx.commit().await.unwrap();
294
295 let result = storage.get_collection(&name).await;
296 assert!(matches!(result, Err(StorageError::CollectionNotFound(_))));
297}
298
299#[tokio::test]
300async fn copy_collection() {
301 let (storage, _dir) = create_test_storage();
302 let source = CollectionName::DiagnosticDatabase;
303 let dest = CollectionName::DiagnosticDatabaseBackup;
304
305 // Write some data to source.
306 let source_col = storage.get_or_create_collection(&source).await.unwrap();
307 let mut tx = storage.begin_transaction().unwrap();
308 let mut d1: &[u8] = b"alpha";
309 let mut d2: &[u8] = b"beta";
310 source_col.write(&mut tx, "a", &mut d1).await.unwrap();
311 source_col.write(&mut tx, "b", &mut d2).await.unwrap();
312 tx.commit().await.unwrap();
313
314 // Copy source to dest.
315 let mut tx = storage.begin_transaction().unwrap();
316 storage
317 .copy_collection(&mut tx, &source, &dest)
318 .await
319 .unwrap();
320 tx.commit().await.unwrap();
321
322 // Verify dest has the same data.
323 let dest_col = storage.get_collection(&dest).await.unwrap();
324 let handle_a = dest_col.read("a").await.unwrap();
325 let mut buf_a = vec![0u8; 5];
326 handle_a.read_at(0, &mut buf_a).unwrap();
327 assert_eq!(&buf_a, b"alpha");
328
329 let handle_b = dest_col.read("b").await.unwrap();
330 let mut buf_b = vec![0u8; 4];
331 handle_b.read_at(0, &mut buf_b).unwrap();
332 assert_eq!(&buf_b, b"beta");
333}
334
335#[tokio::test]
336async fn copy_collection_replaces_existing_dest() {
337 let (storage, _dir) = create_test_storage();
338 let source = CollectionName::DiagnosticDatabase;
339 let dest = CollectionName::DiagnosticDatabaseBackup;
340
341 // Write data to source.
342 let source_col = storage.get_or_create_collection(&source).await.unwrap();
343 let mut tx = storage.begin_transaction().unwrap();
344 let mut d1: &[u8] = b"alpha";
345 source_col.write(&mut tx, "a", &mut d1).await.unwrap();
346 tx.commit().await.unwrap();
347
348 // Write different data to dest (pre-existing content that should be replaced).
349 let dest_col = storage.get_or_create_collection(&dest).await.unwrap();
350 let mut tx = storage.begin_transaction().unwrap();
351 let mut d_old: &[u8] = b"old_stuff";
352 let mut d_extra: &[u8] = b"extra_file";
353 dest_col.write(&mut tx, "a", &mut d_old).await.unwrap();
354 dest_col
355 .write(&mut tx, "extra", &mut d_extra)
356 .await
357 .unwrap();
358 tx.commit().await.unwrap();
359
360 // Copy source -> dest. Dest should be fully replaced.
361 let mut tx = storage.begin_transaction().unwrap();
362 storage
363 .copy_collection(&mut tx, &source, &dest)
364 .await
365 .unwrap();
366 tx.commit().await.unwrap();
367
368 // Verify dest has exactly source's contents (key "a" with "alpha"), not the old data.
369 let dest_col = storage.get_collection(&dest).await.unwrap();
370 let handle_a = dest_col.read("a").await.unwrap();
371 let mut buf = vec![0u8; 5];
372 handle_a.read_at(0, &mut buf).unwrap();
373 assert_eq!(&buf, b"alpha");
374
375 // The "extra" key that was only in dest should be gone (replaced, not merged).
376 let result = dest_col.read("extra").await;
377 assert!(matches!(result, Err(StorageError::KeyNotFound(_))));
378}
379
380// Metadata, list, len
381#[tokio::test]
382async fn metadata_returns_correct_size() {
383 let (storage, _dir) = create_test_storage();
384 let name = CollectionName::DiagnosticDatabase;
385 let collection = storage.get_or_create_collection(&name).await.unwrap();
386
387 let mut tx = storage.begin_transaction().unwrap();
388 let mut data: &[u8] = b"twelve chars";
389 collection.write(&mut tx, "sized", &mut data).await.unwrap();
390 tx.commit().await.unwrap();
391
392 let meta = collection.metadata("sized").await.unwrap();
393 assert_eq!(meta.name, "sized");
394 assert_eq!(meta.data_size, 12);
395}
396
397#[tokio::test]
398async fn list_and_len_reflect_committed_state() {
399 let (storage, _dir) = create_test_storage();
400 let name = CollectionName::DiagnosticDatabase;
401 let collection = storage.get_or_create_collection(&name).await.unwrap();
402
403 assert!(collection.is_empty().await.unwrap());
404 assert_eq!(collection.len().await.unwrap(), 0);
405
406 let mut tx = storage.begin_transaction().unwrap();
407 let mut d1: &[u8] = b"x";
408 let mut d2: &[u8] = b"y";
409 let mut d3: &[u8] = b"z";
410 collection.write(&mut tx, "one", &mut d1).await.unwrap();
411 collection.write(&mut tx, "two", &mut d2).await.unwrap();
412 collection.write(&mut tx, "three", &mut d3).await.unwrap();
413 tx.commit().await.unwrap();
414
415 assert_eq!(collection.len().await.unwrap(), 3);
416 assert!(!collection.is_empty().await.unwrap());
417
418 let mut keys = collection.list().await.unwrap();
419 keys.sort();
420 assert_eq!(keys, vec!["one", "three", "two"]);
421}
422
423// Random access data
424#[tokio::test]
425async fn random_access_read_at_offset() {
426 let (storage, _dir) = create_test_storage();
427 let name = CollectionName::DiagnosticDatabase;
428 let collection = storage.get_or_create_collection(&name).await.unwrap();
429
430 let mut tx = storage.begin_transaction().unwrap();
431 let mut data: &[u8] = b"0123456789ABCDEF";
432 collection.write(&mut tx, "hex", &mut data).await.unwrap();
433 tx.commit().await.unwrap();
434
435 let handle = collection.read("hex").await.unwrap();
436 assert_eq!(handle.data_size().unwrap(), 16);
437
438 // Read from offset 10.
439 let mut buf = vec![0u8; 6];
440 let n = handle.read_at(10, &mut buf).unwrap();
441 assert_eq!(n, 6);
442 assert_eq!(&buf, b"ABCDEF");
443
444 // Read from offset 14..only 2 bytes remaining.
445 let mut buf = vec![0u8; 6];
446 let n = handle.read_at(14, &mut buf).unwrap();
447 assert_eq!(n, 2);
448 assert_eq!(buf.get(..2).unwrap(), b"EF");
449}
450
451#[tokio::test]
452async fn overwrite_existing_key() {
453 let (storage, _dir) = create_test_storage();
454 let name = CollectionName::DiagnosticDatabase;
455 let collection = storage.get_or_create_collection(&name).await.unwrap();
456
457 // Write initial value.
458 let mut tx = storage.begin_transaction().unwrap();
459 let mut data: &[u8] = b"version 1";
460 collection
461 .write(&mut tx, "config", &mut data)
462 .await
463 .unwrap();
464 tx.commit().await.unwrap();
465
466 // Overwrite with new value.
467 let mut tx = storage.begin_transaction().unwrap();
468 let mut data: &[u8] = b"version 2";
469 collection
470 .write(&mut tx, "config", &mut data)
471 .await
472 .unwrap();
473 tx.commit().await.unwrap();
474
475 let handle = collection.read("config").await.unwrap();
476 let mut buf = vec![0u8; 9];
477 handle.read_at(0, &mut buf).unwrap();
478 assert_eq!(&buf, b"version 2");
479}
480
481/// Verifies the WAL on-disk format at the byte level: appends a mix of `CreateCollection`,
482/// `Write`, and `Delete` operations, then reads them back and checks that each operation is
483/// decoded correctly, in order, with its checksum intact and `truncated` reported as `false`.
484/// This underpins the crash-recovery guarantees, which depend on being able to faithfully replay
485/// exactly the operations that were durably recorded before a crash.
[docs]486/// [[ test~storage-wal-checksum-round-trip, WAL entries round-trip through checksum-verified encode/decode, test ]]
487// WAL checksum round-trip
488#[tokio::test]
489async fn wal_round_trip_with_checksum_verification() {
490 let dir = tempfile::tempdir().unwrap();
491 let wal_path = dir.path().join("test.wal");
492 cda_storage::wal::create_wal(&wal_path).unwrap();
493
494 let ops = vec![
495 cda_interfaces::storage_api::Operation::CreateCollection {
496 name: CollectionName::Custom("col_a".to_string()),
497 },
498 cda_interfaces::storage_api::Operation::Write {
499 collection: CollectionName::DiagnosticDatabase,
500 key: "my_key".to_string(),
501 staged_path: "/tmp/fake.tmp".to_string(),
502 },
503 cda_interfaces::storage_api::Operation::Delete {
504 collection: CollectionName::DiagnosticDatabase,
505 key: "old_key".to_string(),
506 },
507 ];
508
509 for op in &ops {
510 cda_storage::wal::append_operation(&wal_path, op).unwrap();
511 }
512
513 let wal_data = std::fs::read(&wal_path).unwrap();
514 let wal_result = cda_storage::wal::read_wal(&wal_data).unwrap();
515 assert!(!wal_result.truncated);
516 assert_eq!(wal_result.operations.len(), 3);
517
518 // Verify the operations match using .get() to satisfy clippy::indexing_slicing.
519 let read_op_0 = wal_result
520 .operations
521 .first()
522 .expect("Expected 3 operations");
523 assert!(matches!(
524 read_op_0,
525 cda_interfaces::storage_api::Operation::CreateCollection { name }
526 if name.as_str() == "col_a"
527 ));
528 let read_op_1 = wal_result.operations.get(1).expect("Expected 3 operations");
529 assert!(matches!(
530 read_op_1,
531 cda_interfaces::storage_api::Operation::Write { key, .. }
532 if key == "my_key"
533 ));
534 let read_op_2 = wal_result.operations.get(2).expect("Expected 3 operations");
535 assert!(matches!(
536 read_op_2,
537 cda_interfaces::storage_api::Operation::Delete { key, .. }
538 if key == "old_key"
539 ));
540}
541
542/// Writes two valid WAL entries, then truncates the file mid-way through the second entry to
543/// simulate a torn write. The reader must return exactly the first (fully valid) operation and
544/// report `truncated: true`, rather than erroring out or fabricating data for the incomplete
545/// entry. This is the detection mechanism that lets recovery safely distinguish "crash mid-write
546/// to the WAL itself" from genuine filesystem corruption.
[docs]547/// [[ test~storage-wal-truncation-detection, WAL reader detects and stops at a truncated entry, test ]]
548#[tokio::test]
549async fn wal_stops_at_truncated_entry() {
550 let dir = tempfile::tempdir().unwrap();
551 let wal_path = dir.path().join("test.wal");
552 cda_storage::wal::create_wal(&wal_path).unwrap();
553
554 // Write two valid entries.
555 cda_storage::wal::append_operation(
556 &wal_path,
557 &cda_interfaces::storage_api::Operation::CreateCollection {
558 name: CollectionName::Custom("first".to_string()),
559 },
560 )
561 .unwrap();
562 cda_storage::wal::append_operation(
563 &wal_path,
564 &cda_interfaces::storage_api::Operation::CreateCollection {
565 name: CollectionName::Custom("second".to_string()),
566 },
567 )
568 .unwrap();
569
570 // Truncate the file to corrupt the second entry (keep first + partial second).
571 let data = std::fs::read(&wal_path).unwrap();
572 // Write only 80% of the data to truncate the second entry.
573 let truncated_len = data.len() * 4 / 5;
574 std::fs::write(&wal_path, data.get(..truncated_len).unwrap()).unwrap();
575
576 let wal_data = std::fs::read(&wal_path).unwrap();
577 let wal_result = cda_storage::wal::read_wal(&wal_data).unwrap();
578 assert!(wal_result.truncated);
579 // Should only have the first valid entry.
580 assert_eq!(wal_result.operations.len(), 1);
581 let read_op_0 = wal_result.operations.first().expect("Expected 1 operation");
582 assert!(matches!(
583 read_op_0,
584 cda_interfaces::storage_api::Operation::CreateCollection { name }
585 if name.as_str() == "first"
586 ));
587}
588
589/// Regression test for a bug where `list()` returned a file's on-disk (possibly mixed-case)
590/// name verbatim, while `metadata()`/`read()`/`file_path()` normalize keys to lowercase before
591/// resolving them to a path. On a case-sensitive filesystem this mismatch caused every
592/// mixed-case file to appear in `list()` but then fail with `KeyNotFound` as soon as any
593/// per-key lookup (`metadata`, `read`, `file_path`) was attempted - exactly the "key vanished
594/// during iteration" symptom, even with no concurrent writer at all.
595///
596/// Simulates a file that ended up on disk with a mixed-case name outside of `Collection::write`
597/// (e.g. placed there during initial provisioning), then verifies that `list()`, `metadata()`,
598/// and `read()` are all consistent with each other.
599#[tokio::test]
600async fn list_normalizes_mixed_case_filenames_to_match_metadata_and_read() {
601 let (storage, dir) = create_test_storage();
602 let name = CollectionName::DiagnosticDatabase;
603
604 // Create the collection directory and place a mixed-case file directly on disk, bypassing
605 // `Collection::write` (which would have normalized the name to lowercase).
606 let collection_dir = dir.path().join("collections").join(name.as_str());
607 std::fs::create_dir_all(&collection_dir).unwrap();
608 std::fs::write(
609 collection_dir.join("FLXC1000_06.18.13.mdd"),
610 b"mixed case data",
611 )
612 .unwrap();
613
614 let collection = storage.get_or_create_collection(&name).await.unwrap();
615
616 // `list()` must return the normalized (lowercase) key, matching what `metadata`/`read`/
617 // `file_path` expect.
618 let keys = collection.list().await.unwrap();
619 assert_eq!(keys, vec!["flxc1000_06.18.13.mdd".to_string()]);
620
621 // Every key returned by `list()` must be resolvable via `metadata()` without a
622 // `KeyNotFound` error.
623 for key in &keys {
624 let meta = collection.metadata(key).await.unwrap();
625 assert_eq!(meta.data_size, "mixed case data".len() as u64);
626 }
627
628 // Every key returned by `list()` must also be resolvable via `read()`.
629 let Some(key) = keys.first() else {
630 panic!("expected one key");
631 };
632 let handle = collection.read(key).await.unwrap();
633 let mut buf = vec![0u8; "mixed case data".len()];
634 handle.read_at(0, &mut buf).unwrap();
635 assert_eq!(&buf, b"mixed case data");
636}