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"
},
"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": {
"packages": "git-flow, musl-tools"
@ -33,7 +36,7 @@
// "forwardPorts": [],
// 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.
"customizations": {
@ -43,7 +46,8 @@
},
"extensions": [
"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
# Generated by Cargo

54
.vscode/tasks.json vendored
View file

@ -5,9 +5,6 @@
"label": "Build Project",
"type": "cargo",
"command": "build",
// "presentation": {
// "reveal": "silent"
// },
"problemMatcher": "$rustc",
"group": "build"
},
@ -73,13 +70,62 @@
// "problemMatcher": "$rustc",
// "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",
"type": "shell",
"command": "echo All Tests successful!",
"dependsOn": [
"Run Unit Tests",
// "Run Integration Tests"
// "Run Integration Tests",
"Run Coverage",
],
"dependsOrder": "sequence",
"group": "test"

View file

@ -181,7 +181,7 @@ impl CacheFile {
.take()
.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<()> {

View file

@ -115,20 +115,27 @@ impl FileTrait for Checked {
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use tempfile::{NamedTempFile, TempDir};
use crate::test_util::{
MockClient, create_file,
MockClient, check_trait, create_file,
data::{HASHES_STD_GOOD, cases, data},
};
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]
fn new_on_existing_file_works() {
for (content, size) in cases() {
let file = create_file(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
let (chk, file) = create_checked(content);
let path = file
.path()
@ -145,6 +152,19 @@ mod tests {
file.path().file_name().expect("`file_name` should succeed")
);
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]
fn hashing_works() {
for (content, hash) in data().zip(HASHES_STD_GOOD) {
let file = create_file(content);
let mut chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
let (mut chk, _file) = create_checked(content);
chk.hash(drop).expect("`hash` should succeed");
// `FileTrait`
@ -193,10 +212,10 @@ mod tests {
#[test]
fn hashing_again_errors() {
for content in data() {
let file = create_file(content);
let mut chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
let (mut chk, _file) = create_checked(content);
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");
assert!(err.is_mismatch("unhashed file", chk.path.display().to_string()));
@ -209,8 +228,7 @@ mod tests {
let share_id = client.add_share();
for content in data() {
let file = create_file(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
let (chk, _file) = create_checked(content);
assert!(
chk.start_upload(

View file

@ -1,14 +1,19 @@
use std::fmt;
use std::{any, fmt};
use crate::sharry;
/// Chunk of binary data belonging to a currently uploading file
pub struct Chunk<'t> {
/// id of the associated file
file_id: sharry::FileID,
/// offset of this chunk in bytes
offset: u64,
/// data inside this chunk
data: &'t [u8],
}
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 {
f.debug_struct("Chunk")
.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> {
pub(super) fn new_direct(file_id: sharry::FileID, offset: u64, data: &'t [u8]) -> Self {
Self {
@ -44,12 +65,10 @@ impl<'t> Chunk<'t> {
/// get the chunk's length
pub fn get_length(&self) -> u64 {
let len = self.data.len();
// BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit.
// BOOKMARK this might **panic** on (hypothetical) platforms where `usize` has more than 64 bit.
// Also, you've allocated more than 2 EiB ... in ONE chunk.
// 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);
}
}
#[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,
}
impl From<Uploading> for Checked {
fn from(value: Uploading) -> Self {
Self::new_direct(value.path, value.size, value.hash)
}
}
impl Uploading {
pub(super) fn new_direct(
path: PathBuf,
@ -118,6 +112,13 @@ impl Uploading {
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 {