Compare commits

...

5 commits

Author SHA1 Message Date
7bbb2bbc19 [wip] unit tests for file module
- finalize `Chunk` coverage
2025-07-10 02:48:11 +00:00
b2c032d846 [wip] unit tests for file module
- testing for `Checked`
2025-07-10 02:04:29 +00:00
cb5873b732 [wip] unit testing
- tooling for code coverage measurement
2025-07-10 01:50:46 +00:00
96ea0ddab9 [wip] unit testing
- tooling for code coverage measurement
2025-07-09 17:02:37 +00:00
4d47530326 [wip] unit testing
- doc for `file` module
2025-07-09 17:02:12 +00:00
7 changed files with 135 additions and 30 deletions

View file

@ -13,7 +13,10 @@
"configureZshAsDefaultShell": "true" "configureZshAsDefaultShell": "true"
}, },
"ghcr.io/devcontainers/features/rust:1": { "ghcr.io/devcontainers/features/rust:1": {
"targets": "x86_64-unknown-linux-musl" "targets": "x86_64-unknown-linux-gnu,x86_64-unknown-linux-musl"
},
"ghcr.io/lee-orr/rusty-dev-containers/cargo-binstall:0": {
"packages": "cargo-llvm-cov"
}, },
"ghcr.io/devcontainers-contrib/features/apt-get-packages:1": { "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": {
"packages": "git-flow, musl-tools" "packages": "git-flow, musl-tools"
@ -33,7 +36,7 @@
// "forwardPorts": [], // "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "rustup target install x86_64-unknown-linux-musl | :", // "postCreateCommand": "rustup target install x86_64-unknown-linux-musl | :",
// Configure tool-specific properties. // Configure tool-specific properties.
"customizations": { "customizations": {
@ -43,7 +46,8 @@
}, },
"extensions": [ "extensions": [
"mhutchie.git-graph", "mhutchie.git-graph",
"Gruntfuggly.todo-tree" "Gruntfuggly.todo-tree",
"ryanluker.vscode-coverage-gutters"
] ]
} }
}, },

4
.gitignore vendored
View file

@ -1,3 +1,7 @@
# code coverage reports
coverage/
# https://github.com/github/gitignore/raw/refs/heads/main/Rust.gitignore # https://github.com/github/gitignore/raw/refs/heads/main/Rust.gitignore
# Generated by Cargo # Generated by Cargo

54
.vscode/tasks.json vendored
View file

@ -5,9 +5,6 @@
"label": "Build Project", "label": "Build Project",
"type": "cargo", "type": "cargo",
"command": "build", "command": "build",
// "presentation": {
// "reveal": "silent"
// },
"problemMatcher": "$rustc", "problemMatcher": "$rustc",
"group": "build" "group": "build"
}, },
@ -73,13 +70,62 @@
// "problemMatcher": "$rustc", // "problemMatcher": "$rustc",
// "group": "test" // "group": "test"
// }, // },
{
"label": "Test Coverage",
"hide": true,
"type": "cargo",
"command": "llvm-cov",
"args": [],
"problemMatcher": "$rustc",
"group": "test",
},
{
"label": "Report Coverage (html)",
"hide": true,
"type": "cargo",
"command": "llvm-cov",
"args": [
"report",
"--html",
"--output-dir" ,
"coverage",
],
"problemMatcher": "$rustc",
"group": "test"
},
{
"label": "Report Coverage (lcov)",
"hide": true,
"type": "cargo",
"command": "llvm-cov",
"args": [
"report",
"--lcov",
"--output-path" ,
"coverage/lcov.info",
],
"problemMatcher": "$rustc",
"group": "test"
},
{
"label": "Run Coverage",
"type": "shell",
"dependsOn": [
"Test Coverage",
"Report Coverage (html)",
"Report Coverage (lcov)",
],
"dependsOrder": "sequence",
"group": "test"
},
{ {
"label": "Run All Tests", "label": "Run All Tests",
"type": "shell", "type": "shell",
"command": "echo All Tests successful!", "command": "echo All Tests successful!",
"dependsOn": [ "dependsOn": [
"Run Unit Tests", "Run Unit Tests",
// "Run Integration Tests" // "Run Integration Tests",
"Run Coverage",
], ],
"dependsOrder": "sequence", "dependsOrder": "sequence",
"group": "test" "group": "test"

View file

@ -181,7 +181,7 @@ impl CacheFile {
.take() .take()
.expect("abort_upload called while not uploading"); .expect("abort_upload called while not uploading");
self.files.push_front(upl.into()); self.files.push_front(upl.stop());
} }
pub fn share_notify(&self, client: &impl Client) -> crate::Result<()> { pub fn share_notify(&self, client: &impl Client) -> crate::Result<()> {

View file

@ -115,20 +115,27 @@ impl FileTrait for Checked {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use tempfile::TempDir; use tempfile::{NamedTempFile, TempDir};
use crate::test_util::{ use crate::test_util::{
MockClient, create_file, MockClient, check_trait, create_file,
data::{HASHES_STD_GOOD, cases, data}, data::{HASHES_STD_GOOD, cases, data},
}; };
use super::*; use super::*;
fn create_checked(content: &[u8]) -> (Checked, NamedTempFile) {
let file = create_file(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
// return both, so the `NamedTempFile` is not auto-deleted here
(chk, file)
}
#[test] #[test]
fn new_on_existing_file_works() { fn new_on_existing_file_works() {
for (content, size) in cases() { for (content, size) in cases() {
let file = create_file(content); let (chk, file) = create_checked(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
let path = file let path = file
.path() .path()
@ -145,6 +152,19 @@ mod tests {
file.path().file_name().expect("`file_name` should succeed") file.path().file_name().expect("`file_name` should succeed")
); );
assert_eq!(chk.get_size(), size); assert_eq!(chk.get_size(), size);
check_trait(
chk.as_ref(),
path.as_os_str().as_encoded_bytes(),
"AsRef<u8>",
"Checked",
);
// new_direct
let chk = Checked::new_direct(chk.path, chk.size, chk.hash);
assert_eq!(chk.path, path);
assert_eq!(chk.size, size);
assert!(chk.hash.is_none());
} }
} }
@ -179,8 +199,7 @@ mod tests {
#[test] #[test]
fn hashing_works() { fn hashing_works() {
for (content, hash) in data().zip(HASHES_STD_GOOD) { for (content, hash) in data().zip(HASHES_STD_GOOD) {
let file = create_file(content); let (mut chk, _file) = create_checked(content);
let mut chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
chk.hash(drop).expect("`hash` should succeed"); chk.hash(drop).expect("`hash` should succeed");
// `FileTrait` // `FileTrait`
@ -193,10 +212,10 @@ mod tests {
#[test] #[test]
fn hashing_again_errors() { fn hashing_again_errors() {
for content in data() { for content in data() {
let file = create_file(content); let (mut chk, _file) = create_checked(content);
let mut chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
chk.hash(drop).expect("`hash` should succeed"); // fake hash
chk.hash = Some(String::default());
let err = chk.hash(drop).expect_err("`hash` twice should fail"); let err = chk.hash(drop).expect_err("`hash` twice should fail");
assert!(err.is_mismatch("unhashed file", chk.path.display().to_string())); assert!(err.is_mismatch("unhashed file", chk.path.display().to_string()));
@ -209,8 +228,7 @@ mod tests {
let share_id = client.add_share(); let share_id = client.add_share();
for content in data() { for content in data() {
let file = create_file(content); let (chk, _file) = create_checked(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
assert!( assert!(
chk.start_upload( chk.start_upload(

View file

@ -1,14 +1,19 @@
use std::fmt; use std::{any, fmt};
use crate::sharry; use crate::sharry;
/// Chunk of binary data belonging to a currently uploading file
pub struct Chunk<'t> { pub struct Chunk<'t> {
/// id of the associated file
file_id: sharry::FileID, file_id: sharry::FileID,
/// offset of this chunk in bytes
offset: u64, offset: u64,
/// data inside this chunk
data: &'t [u8], data: &'t [u8],
} }
impl fmt::Debug for Chunk<'_> { impl fmt::Debug for Chunk<'_> {
// chunks are 1 MiB or more, we shouldn't print that
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Chunk") f.debug_struct("Chunk")
.field("file_id", &self.file_id) .field("file_id", &self.file_id)
@ -18,6 +23,22 @@ impl fmt::Debug for Chunk<'_> {
} }
} }
/// convert usize into other type
///
/// # Panics
///
/// - if the given value does not fit into the target type
fn from_usize_or_panic<I>(value: usize) -> I
where
I: TryFrom<usize>,
I::Error: std::error::Error,
{
value.try_into().unwrap_or_else(|e| {
let target_type = any::type_name::<I>();
panic!("usize={value:?} did not fit into {target_type:?}: {e}")
})
}
impl<'t> Chunk<'t> { impl<'t> Chunk<'t> {
pub(super) fn new_direct(file_id: sharry::FileID, offset: u64, data: &'t [u8]) -> Self { pub(super) fn new_direct(file_id: sharry::FileID, offset: u64, data: &'t [u8]) -> Self {
Self { Self {
@ -44,12 +65,10 @@ impl<'t> Chunk<'t> {
/// get the chunk's length /// get the chunk's length
pub fn get_length(&self) -> u64 { pub fn get_length(&self) -> u64 {
let len = self.data.len(); // BOOKMARK this might **panic** on (hypothetical) platforms where `usize` has more than 64 bit.
// BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit.
// Also, you've allocated more than 2 EiB ... in ONE chunk. // Also, you've allocated more than 2 EiB ... in ONE chunk.
// Whoa! Maybe just chill? // Whoa! Maybe just chill?
u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}")) from_usize_or_panic(self.data.len())
} }
} }
@ -85,4 +104,17 @@ mod tests {
assert_eq!(chunk.get_length(), len); assert_eq!(chunk.get_length(), len);
} }
} }
#[test]
#[should_panic(expected = "did not fit into \"u32\"")]
fn test_usize_overflow_panics() {
// works
assert_eq!(from_usize_or_panic::<u64>(usize::MAX), u64::MAX);
assert_eq!(from_usize_or_panic::<u32>(u32::MAX as usize), u32::MAX);
assert_eq!(from_usize_or_panic::<u16>(u16::MAX as usize), u16::MAX);
assert_eq!(from_usize_or_panic::<u8>(u8::MAX as usize), u8::MAX);
// panics
from_usize_or_panic::<u32>(usize::MAX);
}
} }

View file

@ -32,12 +32,6 @@ pub struct Uploading {
offset: u64, offset: u64,
} }
impl From<Uploading> for Checked {
fn from(value: Uploading) -> Self {
Self::new_direct(value.path, value.size, value.hash)
}
}
impl Uploading { impl Uploading {
pub(super) fn new_direct( pub(super) fn new_direct(
path: PathBuf, path: PathBuf,
@ -118,6 +112,13 @@ impl Uploading {
Err(self.path) Err(self.path)
} }
} }
/// stop uploading this file
///
/// - consume self, returning as a `file::Checked`
pub fn stop(self) -> Checked {
Checked::new_direct(self.path, self.size, self.hash)
}
} }
impl FileTrait for Uploading { impl FileTrait for Uploading {