use std::{
io::{Seek, SeekFrom, Write},
path::PathBuf,
};
use xpct::{be_err, be_ok, equal, expect, match_pattern, pattern};
use crate::{
Connection, CreateOptions,
file_metadata::{FileKind, Owner},
settings::{Chunking, Settings},
testing::{Case, random_buf, random_string},
};
fn with_fixed_chunk_size(size: usize) -> crate::Result<Connection> {
let settings = Settings {
chunking: Chunking::Fixed { size },
..Default::default()
};
Connection::open_for_testing(&settings)
}
mod basic_behavior {
use super::*;
#[test]
fn identical_content_produces_identical_content_id() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(100, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("file1.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
let content_id1 = file.content_id()?;
drop(file);
let mut file2 = fs.create("file2.txt", FileKind::Regular, Owner::ROOT)?;
file2.write_all(content.as_bytes())?;
let content_id2 = file2.content_id()?;
expect!(content_id1).to(equal(content_id2));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn different_content_produces_different_content_id() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content1 = random_string(100, Case::Lower);
let content2 = random_string(100, Case::Lower);
conn.exec(|fs| {
let mut file1 = fs.create("file1.txt", FileKind::Regular, Owner::ROOT)?;
file1.write_all(content1.as_bytes())?;
let content_id1 = file1.content_id()?;
drop(file1);
let mut file2 = fs.create("file2.txt", FileKind::Regular, Owner::ROOT)?;
file2.write_all(content2.as_bytes())?;
let content_id2 = file2.content_id()?;
expect!(content_id1).to_not(equal(content_id2));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn content_id_is_stable() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(100, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
let id1 = file.content_id()?;
let id2 = file.content_id()?;
let id3 = file.content_id()?;
expect!(&id1).to(equal(&id2));
expect!(&id2).to(equal(&id3));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn content_id_unchanged_after_reopen() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(100, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
let original_id = file.content_id()?;
drop(file);
let mut file = fs.open("test.txt")?;
let reopened_id = file.content_id()?;
expect!(reopened_id).to(equal(original_id));
crate::Result::Ok(())
})?;
Ok(())
}
}
mod content_variations {
use super::*;
#[test]
fn empty_file_content_id() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
conn.exec(|fs| {
let mut file = fs.create("empty.txt", FileKind::Regular, Owner::ROOT)?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn two_empty_files_same_content_id() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
conn.exec(|fs| {
let mut file1 = fs.create("empty1.txt", FileKind::Regular, Owner::ROOT)?;
let id1 = file1.content_id()?;
drop(file1);
let mut file2 = fs.create("empty2.txt", FileKind::Regular, Owner::ROOT)?;
let id2 = file2.content_id()?;
expect!(id1).to(equal(id2));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn small_file_less_than_one_block() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(32, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("small.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn exactly_one_block() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(64, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("oneblock.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn large_file_multiple_blocks() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(300, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("large.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn large_file() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(1024)?;
let content = random_buf(1024 * 1024, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("verylarge.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(&content)?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn is_entirely_one_massive_hole() -> crate::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("large_hole.txt", FileKind::Regular, Owner::ROOT)?;
file.set_len(i64::MAX as u64)?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn is_entirely_one_tiny_hole() -> crate::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("small_hole.txt", FileKind::Regular, Owner::ROOT)?;
file.set_len(1)?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn is_entirely_one_hole_not_aligned_to_block_boundaries() -> crate::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("large_hole.txt", FileKind::Regular, Owner::ROOT)?;
file.set_len(1024u64.pow(3) + 1)?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn contains_a_massive_hole() -> crate::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file = fs.create("large_hole.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(b"some bytes")?;
file.set_len(i64::MAX as u64)?;
expect!(file.content_id()).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn hole_is_equivalent_to_null_bytes() -> crate::Result<()> {
let mut conn = Connection::open_in_memory(&CreateOptions::new())?;
conn.exec(|fs| {
let mut file1 = fs.create("file_with_hole.txt", FileKind::Regular, Owner::ROOT)?;
file1.write_all(b"some bytes")?;
file1.set_len(1024 * 1024)?;
let id1 = file1.content_id()?;
drop(file1);
let mut file2 = fs.create("file_with_nulls.txt", FileKind::Regular, Owner::ROOT)?;
file2.write_all(b"some bytes")?;
file2.write_all(&vec![0u8; 1024 * 1024 - b"some bytes".len()])?;
let id2 = file2.content_id()?;
expect!(id1).to(equal(id2));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn same_content_different_names() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(100, Case::Lower);
conn.exec(|fs| {
let mut file1 = fs.create("file1.txt", FileKind::Regular, Owner::ROOT)?;
file1.write_all(content.as_bytes())?;
let id1 = file1.content_id()?;
drop(file1);
let mut file2 = fs.create("file2.txt", FileKind::Regular, Owner::ROOT)?;
file2.write_all(content.as_bytes())?;
let id2 = file2.content_id()?;
expect!(&id1).to(equal(&id2));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn rewrite_and_recompute_content_id() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content1 = random_string(1024 * 4, Case::Lower);
let content2 = random_string(1024 * 4, Case::Lower);
let id1 = conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content1.as_bytes())?;
file.content_id()
})?;
conn.exec(|fs| {
let mut file = fs.open("test.txt")?;
file.seek(SeekFrom::Start(0))?;
file.write_all(content2.as_bytes())?;
crate::Result::Ok(())
})?;
let id2 = conn.exec(|fs| {
let mut file = fs.open("test.txt")?;
file.content_id()
})?;
expect!(id1).to_not(equal(id2));
Ok(())
}
}
mod cross_filesystem {
use super::*;
#[test]
fn same_content_different_filesystems_produces_different_ids() -> crate::Result<()> {
let settings1 = Settings {
chunking: Chunking::Fixed { size: 64 },
..Default::default()
};
let settings2 = Settings {
chunking: Chunking::Fixed { size: 64 },
..Default::default()
};
let mut conn1 = Connection::open_for_testing(&settings1)?;
let mut conn2 = Connection::open_for_testing(&settings2)?;
let content = random_string(100, Case::Lower);
let id1 = conn1.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
let id2 = conn2.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
expect!(id1).to_not(equal(id2));
Ok(())
}
#[test]
fn empty_files_different_filesystems() -> crate::Result<()> {
let settings1 = Settings {
chunking: Chunking::Fixed { size: 64 },
..Default::default()
};
let settings2 = Settings {
chunking: Chunking::Fixed { size: 64 },
..Default::default()
};
let mut conn1 = Connection::open_for_testing(&settings1)?;
let mut conn2 = Connection::open_for_testing(&settings2)?;
let id1 = conn1.exec(|fs| {
let mut file = fs.create("empty.txt", FileKind::Regular, Owner::ROOT)?;
file.content_id()
})?;
let id2 = conn2.exec(|fs| {
let mut file = fs.create("empty.txt", FileKind::Regular, Owner::ROOT)?;
file.content_id()
})?;
expect!(id1).to_not(equal(id2));
Ok(())
}
#[test]
fn large_identical_content_different_filesystems() -> crate::Result<()> {
let settings1 = Settings {
chunking: Chunking::Fixed { size: 1024 },
..Default::default()
};
let settings2 = Settings {
chunking: Chunking::Fixed { size: 1024 },
..Default::default()
};
let mut conn1 = Connection::open_for_testing(&settings1)?;
let mut conn2 = Connection::open_for_testing(&settings2)?;
let content = random_buf(100 * 1024, Case::Lower);
let id1 = conn1.exec(|fs| {
let mut file = fs.create("large.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(&content)?;
file.content_id()
})?;
let id2 = conn2.exec(|fs| {
let mut file = fs.create("large.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(&content)?;
file.content_id()
})?;
expect!(id1).to_not(equal(id2));
Ok(())
}
}
mod error_cases {
use super::*;
#[test]
fn nonexistent_file_returns_error() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
conn.exec(|fs| {
fs.create("nonexistent.txt", FileKind::Regular, Owner::ROOT)?;
fs.delete("nonexistent.txt")?;
expect!(fs.open("nonexistent.txt"))
.to(be_err())
.to(match_pattern(pattern!(crate::Error::FileNotFound { .. })));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn directory_returns_not_a_regular_file_error() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
conn.exec(|fs| {
let mut file = fs.create("dir", FileKind::Dir, Owner::ROOT)?;
let result = file.content_id();
expect!(result).to(be_err()).to(match_pattern(pattern!(
crate::Error::NotARegularFile { .. }
)));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_returns_not_a_regular_file_error() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
conn.exec(|fs| {
let mut file = fs.create(
"symlink",
FileKind::Symlink {
target: PathBuf::from("/target"),
},
Owner::ROOT,
)?;
let result = file.content_id();
expect!(result).to(be_err()).to(match_pattern(pattern!(
crate::Error::NotARegularFile { .. }
)));
crate::Result::Ok(())
})?;
Ok(())
}
}
mod edge_cases {
use super::*;
#[test]
fn content_id_after_modification() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content1 = random_string(100, Case::Lower);
let content2 = random_string(100, Case::Upper);
conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content1.as_bytes())?;
let original_id = file.content_id()?;
file.seek(SeekFrom::Start(0))?;
file.write_all(content2.as_bytes())?;
let new_id = file.content_id()?;
expect!(new_id).to_not(equal(original_id));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn content_id_after_append() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content1 = random_string(50, Case::Lower);
let content2 = random_string(50, Case::Upper);
conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content1.as_bytes())?;
let original_id = file.content_id()?;
file.seek(SeekFrom::End(0))?;
file.write_all(content2.as_bytes())?;
let new_id = file.content_id()?;
expect!(new_id).to_not(equal(original_id));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn content_id_unaffected_by_read_position() -> crate::Result<()> {
let mut conn = with_fixed_chunk_size(64)?;
let content = random_string(100, Case::Lower);
conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.seek(SeekFrom::Start(0))?;
let id_at_start = file.content_id()?;
file.seek(SeekFrom::Start(50))?;
let id_at_middle = file.content_id()?;
file.seek(SeekFrom::End(0))?;
let id_at_end = file.content_id()?;
expect!(&id_at_start).to(equal(&id_at_middle));
expect!(&id_at_middle).to(equal(&id_at_end));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn different_chunking_settings_same_content() -> crate::Result<()> {
let settings1 = Settings {
chunking: Chunking::Fixed { size: 64 },
..Default::default()
};
let mut conn1 = Connection::open_for_testing(&settings1)?;
let settings2 = Settings {
chunking: Chunking::Fixed { size: 128 },
..Default::default()
};
let mut conn2 = Connection::open_for_testing(&settings2)?;
let content = random_string(500, Case::Lower);
let id1 = conn1.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
let id2 = conn2.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
expect!(id1).to_not(equal(id2));
Ok(())
}
#[test]
fn content_id_after_truncate() -> crate::Result<()> {
let mut conn = Connection::open_for_testing(&Settings {
chunking: Chunking::Fixed { size: 32 },
logical_block_size: 64,
..Default::default()
})?;
let content = random_string(200, Case::Lower);
let original_id = conn.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
conn.exec(|fs| {
let mut file = fs.open("test.txt")?;
file.set_len(100)?;
crate::Result::Ok(())
})?;
let truncated_id = conn.exec(|fs| {
let mut file = fs.open("test.txt")?;
file.content_id()
})?;
expect!(truncated_id).to_not(equal(original_id));
Ok(())
}
#[test]
#[cfg(feature = "chunking")]
fn cdc_vs_fixed_chunking() -> crate::Result<()> {
let settings_fixed = Settings {
chunking: Chunking::Fixed { size: 64 },
..Default::default()
};
let mut conn_fixed = Connection::open_for_testing(&settings_fixed)?;
let settings_cdc = Settings {
chunking: Chunking::FastCdc {
min_size: 64,
avg_size: 256,
max_size: 512,
},
..Default::default()
};
let mut conn_cdc = Connection::open_for_testing(&settings_cdc)?;
let content = random_string(500, Case::Lower);
let id_fixed = conn_fixed.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
let id_cdc = conn_cdc.exec(|fs| {
let mut file = fs.create("test.txt", FileKind::Regular, Owner::ROOT)?;
file.write_all(content.as_bytes())?;
file.content_id()
})?;
expect!(id_fixed).to_not(equal(id_cdc));
Ok(())
}
}