Skip to content

Commit d008177

Browse files
authored
Add detailed rustdoc to reftests (#1232)
The reference tests for Mountpoint can be quite complex, especially for those unfamiliar both with the tests themselves or the idea of reference-based testing. This change adds more detailed rustdoc with the goal to ramp up new readers with the reftests, give an overview of what the tests are doing, and point the reader to resources for learning more. ### Does this change impact existing behavior? No, all code documentation changes. ### Does this change need a changelog entry? Does it require a version change? No, code doc changes only. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and I agree to the terms of the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). Signed-off-by: Daniel Carl Jones <[email protected]>
1 parent dd8b881 commit d008177

File tree

4 files changed

+113
-15
lines changed

4 files changed

+113
-15
lines changed

mountpoint-s3/tests/reftests/generators.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Provides [Strategy]s for [proptest]. Strategies are ways that [proptest] can generate values for tests from primitive types.
2+
13
use mountpoint_s3_client::mock_client::MockObject;
24
use mountpoint_s3_client::types::ETag;
35
use proptest::prelude::*;
@@ -9,13 +11,20 @@ use std::ops::Deref;
911

1012
use crate::reftests::reference::valid_inode_name;
1113

14+
/// [Strategy] for providing valid POSIX path names.
15+
///
16+
/// We intentionally limit to a small input space to avoid testing less useful inputs.
1217
pub fn valid_name_strategy() -> impl Strategy<Value = String> {
18+
// Literally the character `a` and `-` between 1 and 3 times.
1319
string_regex("[a-]{1,3}").unwrap()
1420
}
1521

22+
/// [Strategy] providing strings which may or may not be valid POSIX path names.
23+
///
24+
/// We intentionally limit to a small input space to avoid testing less useful inputs.
25+
/// We also give more weight to the generation of valid names.
1626
pub fn name_strategy() -> impl Strategy<Value = String> {
1727
prop_oneof![
18-
// Valid names
1928
5 => valid_name_strategy(),
2029
// Potentially invalid names
2130
1 => string_regex("[a\\-/.\u{1}]{1,3}").unwrap(),
@@ -57,6 +66,12 @@ impl From<&str> for ValidName {
5766
}
5867
}
5968

69+
/// Split file size into two groups.
70+
///
71+
/// This allows [proptest] to generate a set of values for each group,
72+
/// and we can apply a weight to each group (which is equal by default).
73+
/// It is in our interest to balance focus between smaller file sizes where we may hit more edge cases,
74+
/// but also cover some much larger file sizes.
6075
#[derive(Clone, Copy, Arbitrary)]
6176
pub enum FileSize {
6277
Small(u8),
@@ -81,6 +96,10 @@ impl Debug for FileSize {
8196
}
8297
}
8398

99+
/// Represents some file content for property-based testing.
100+
///
101+
/// The second value is the length of the file content,
102+
/// while the first is the byte that will be repeated to generate the content.
84103
#[derive(Clone, Debug, Arbitrary)]
85104
pub struct FileContent(pub u8, pub FileSize);
86105

mountpoint-s3/tests/reftests/harness.rs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
//! Provides the test harness along with operations that can be performed against
2+
//! the [Harness::reference] model and the system under test [Harness::fs].
3+
//!
4+
//! Note that the system under test is Mountpoint's file system type wrapped by [TestS3Filesystem],
5+
//! and does not include FUSE integration.
6+
//!
7+
//! - TODO: How can we test the new incremental upload mode?
8+
//! - TODO: How can we test directory bucket semantics?
9+
110
use std::collections::{BTreeMap, HashSet};
211
use std::fmt::Debug;
312
use std::path::{Component, Path, PathBuf};
@@ -23,14 +32,26 @@ use crate::reftests::reference::{File, Node, Reference};
2332
// TODO: "reboot" (forget all the local inodes and re-bootstrap)
2433
#[derive(Debug, Arbitrary)]
2534
pub enum Op {
26-
/// Do an entire write in one step
35+
/// Opens a new file, writes to it, and 'closes' it in one step.
2736
WriteFile(ValidName, DirectoryIndex, FileContent),
2837

2938
// Individual steps of a file write -- create, open, write, close
39+
/// Create a new file, but don't open it yet. (aka. `mknod`)
40+
///
41+
/// If other writing file operations are invoked before this step,
42+
/// the other file operations will be no-op (as there are no in-flight writes).
3043
CreateFile(ValidName, DirectoryIndex, FileContent),
44+
/// Open a file for writing that was created by `CreateFile`.
3145
StartWriting(InflightWriteIndex),
32-
// usize is the percentage of the file to write (taken modulo 101)
46+
/// Write some percentage of the desired file content to the open file.
47+
///
48+
/// Effectively applies `StartWriting` if not already done.
49+
/// The second value is the percentage of the file to write (taken modulo 101).
3350
WritePart(InflightWriteIndex, usize),
51+
/// Closes an open file using `release`.
52+
///
53+
/// Does not invoke `flush`.
54+
/// If `StartWriting` wasn't already applied, it is effectively applied in this operation.
3455
FinishWrite(InflightWriteIndex),
3556

3657
/// Remove a file
@@ -120,6 +141,7 @@ pub struct InflightWrite {
120141
inode: InodeNo,
121142
/// Initially None until the file is opened by [Op::StartWriting]
122143
file_handle: Option<u64>,
144+
/// Prepared bytes ready to be written to the file system by the test.
123145
object: MockObject,
124146
written: usize,
125147
}
@@ -161,9 +183,15 @@ impl InflightWrites {
161183

162184
#[derive(Debug)]
163185
pub struct Harness {
164-
readdir_limit: usize, // max number of entries that a readdir will return; 0 means no limit
186+
/// Max number of entries that a readdir operation will return.
187+
///
188+
/// 0 means no limit.
189+
readdir_limit: usize,
190+
/// Reference model for the system under test (SUT) to be compared against.
165191
reference: Reference,
192+
/// The system under test (SUT) that we compare against the [Self::reference].
166193
fs: TestS3Filesystem<Arc<MockClient>>,
194+
/// An S3 client, used for performing operations against the 'remote' S3.
167195
client: Arc<MockClient>,
168196
bucket: String,
169197
inflight_writes: InflightWrites,
@@ -242,14 +270,15 @@ impl Harness {
242270
}
243271
}
244272

245-
/// Walk the filesystem tree and check that at each level, contents match the reference
273+
/// Walk the filesystem tree using `readdir` and `lookup` and check that at each level, contents match the reference.
246274
pub async fn compare_contents(&self) {
247275
let root = self.reference.root();
248276
self.compare_contents_recursive(FUSE_ROOT_INODE, FUSE_ROOT_INODE, root)
249277
.await;
250278
}
251279

252280
/// Walk a single path through the filesystem tree and ensure each node matches the reference.
281+
///
253282
/// Compared to [compare_contents], this avoids doing a `readdir` before `lookup`, and so tests
254283
/// a different path through the inode code.
255284
pub async fn compare_single_path(&self, idx: usize) {
@@ -302,8 +331,15 @@ impl Harness {
302331
Ok(inode)
303332
}
304333

305-
/// Create a new file ready for writing. We don't allow overwrites of existing files by default, so this
306-
/// can return None if the name already exists in the chosen directory.
334+
/// Create a new file within the directory ready for writing.
335+
///
336+
/// The file is not yet opened.
337+
/// Contents are provided only to be stored as part of the test state in this step,
338+
/// and will be read from when performing writes against the file system.
339+
///
340+
/// We don't allow overwrites of existing files by default,
341+
/// so this returns [None] if the name already exists in the chosen directory.
342+
///
307343
/// TODO: Update this function to support overwrites
308344
async fn perform_create_file(
309345
&mut self,
@@ -347,7 +383,7 @@ impl Harness {
347383
}
348384
}
349385

350-
/// Open a previously created file (by `perform_create_file`) for writing
386+
/// Open a previously created file (using [Self::perform_create_file]) for writing.
351387
async fn perform_start_writing(&mut self, index: InflightWriteIndex) {
352388
let Some(inflight_write) = self.inflight_writes.get(index) else {
353389
return;
@@ -366,7 +402,7 @@ impl Harness {
366402
}
367403
}
368404

369-
/// Continue writing to an open file
405+
/// Continue writing to a file open for writing.
370406
async fn perform_write_part(&mut self, index: InflightWriteIndex, percent: usize) {
371407
let Some(inflight_write) = self.inflight_writes.get(index) else {
372408
// No inflight writes available
@@ -597,6 +633,8 @@ impl Harness {
597633
self.reference.remove_remote_key(&key);
598634
}
599635

636+
/// Recurse through the file systems using readdir,
637+
/// comparing the [Self::reference] expected state to the system under test ([Self::fs]).
600638
fn compare_contents_recursive<'a>(
601639
&'a self,
602640
fs_parent: InodeNo,
@@ -693,6 +731,8 @@ impl Harness {
693731
.boxed()
694732
}
695733

734+
/// Compare the contents of a given reference file to the system under test (SUT),
735+
/// by opening and reading the file from the SUT.
696736
async fn compare_file<'a>(&'a self, fs_file: InodeNo, ref_file: &'a MockObject) {
697737
let fh = match self.fs.open(fs_file, OpenFlags::empty(), 0).await {
698738
Ok(ret) => ret.fh,

mountpoint-s3/tests/reftests/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
//! This module uses [proptest] to perform property or reference testing of Mountpoint.
2+
//!
3+
//! The [Proptest Book](https://proptest-rs.github.io/) is recommended reading
4+
//! for understanding the purpose and functionality of tests in this module.
5+
//!
6+
//! The reference tests are broken down as follows:
7+
//! - [generators] provides strategies for generating test input to our reference tests.
8+
//! - [harness] configures and runs the tests comparing an expected FS and S3 bucket state (provided by [reference])
9+
//! to what occurs with MP
10+
//! - [fuse] is a harness configuring and running tests comparing a real local FS with MP,
11+
//! with the goal to identify divergences from POSIX semantics
12+
113
mod fuse;
214
mod generators;
315
mod harness;

mountpoint-s3/tests/reftests/reference.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1+
//! Provides an expected state for the file system and its S3 bucket.
2+
//!
3+
//! As part of the reference model testing,
4+
//! a [MaterializedReference] is generated representing the expected state of the file system and S3.
5+
//! We compare its state with that returned by Mountpoint's file system
6+
//! when traversing or visiting specific paths to assert correctness.
7+
18
use mountpoint_s3_client::mock_client::MockObject;
29
use std::cell::RefCell;
310
use std::collections::BTreeMap;
411
use std::path::{Component, Path, PathBuf};
512
use std::rc::Rc;
613

14+
/// A file node, which could be local or remote.
715
#[derive(Debug)]
816
pub enum File {
917
Local,
1018
Remote(Box<MockObject>),
1119
}
1220

21+
/// A node in the reference model. This node could be local or remote.
1322
#[derive(Debug)]
1423
pub enum Node {
1524
Directory {
@@ -52,12 +61,18 @@ impl Node {
5261
}
5362
}
5463

55-
/// The expected state of a file system. We track three pieces of state: the keys in an S3 bucket,
56-
/// plus lists of local files and local directories. Whenever we need the tree structure of the
57-
/// file system, we construct it from these inputs as a [MaterializedReference]. Building the
58-
/// reference in this indirect way allows us to have only one definition of correctness -- the
59-
/// implementation of [build_reference] -- and to test both mutations to the file system itself and
60-
/// "remote" mutations to the bucket (like adding or deleting a key using another client).
64+
/// The expected state of a file system and its S3 bucket.
65+
///
66+
/// We track three pieces of state:
67+
/// - The keys in an S3 bucket.
68+
/// - The list of local directories.
69+
/// - The list of local files.
70+
///
71+
/// Whenever we need the tree structure of the file system,
72+
/// we take this state and produce a [MaterializedReference].
73+
/// By producing the reference in this indirect way, it allows us to have only one definition of correctness
74+
/// -- the implementation of [build_reference] -- and to test both mutations to the file system itself
75+
/// and "remote" mutations to the bucket (like adding or deleting a key using another client).
6176
#[derive(Debug)]
6277
pub struct Reference {
6378
/// Contents of our S3 bucket
@@ -70,9 +85,17 @@ pub struct Reference {
7085
materialized: MaterializedReference,
7186
}
7287

88+
/// A [MaterializedReference] is a product of the [Reference],
89+
/// presenting the desired tree ([Self::root]) and list of directories ([Self::directories]).
90+
///
91+
/// This should be 'rematerialized' each time S3 or local state is mutated
92+
/// to show the new desired state of the reference using [Reference::rematerialize].
93+
/// This will use [build_reference] to construct the file system based on the remote state,
94+
/// and then mutate it based on local state.
7395
#[derive(Debug)]
7496
struct MaterializedReference {
7597
root: Node,
98+
/// A collection of all the directories in the [Reference], both local and remote.
7699
directories: Vec<PathBuf>,
77100
}
78101

@@ -160,6 +183,10 @@ impl Reference {
160183
}
161184
}
162185

186+
/// Create a new [MaterializedReference] from the [Reference].
187+
///
188+
/// This will reevaluate what is in S3 and what should be maintained locally,
189+
/// and produce a result which should maintain our promised semantics.
163190
fn rematerialize(&self) -> MaterializedReference {
164191
tracing::debug!(
165192
remote_keys=?self.remote_keys, local_files=?self.local_files, local_directories=?self.local_directories,

0 commit comments

Comments
 (0)