Skip to content

Commit

Permalink
test(frontmatter-gen): ✅ Add new unit tests and code optimisations
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienrousseau committed Nov 24, 2024
1 parent de4f2b7 commit 5b19d84
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 94 deletions.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,11 @@ required-features = ["cli"]
[features]
# Optional features that can be enabled or disabled.
default = [] # SSG is not enabled by default
cli = ["dep:clap", "dep:tokio"] # CLI functionality only
cli = ["dep:clap"] # CLI functionality only
ssg = [ # Full SSG functionality
"cli", # Include CLI as part of SSG
"dep:tera",
"dep:pulldown-cmark",
"dep:tokio",
"dep:dtt",
"dep:url",
]
Expand All @@ -94,6 +93,7 @@ serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
serde_yml = "0.0.12"
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
tokio = { version = "1.41.1", features = ["full"]}

thiserror = "2.0.3"
toml = "0.8.19"
Expand All @@ -109,7 +109,6 @@ pulldown-cmark = { version = "0.12.2", optional = true }
simple_logger = "5.0.0"
tempfile = "3.14.0"
tera = { version = "1.20.0", optional = true }
tokio = { version = "1.41.1", features = ["full"], optional = true }
url = { version = "2.5.4", optional = true }

# Examples development dependencies
Expand Down
266 changes: 175 additions & 91 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ use std::fs::remove_file;
use std::io::{self};
use std::path::Path;

#[cfg(feature = "ssg")]
use std::path::PathBuf;

#[cfg(feature = "ssg")]
use std::sync::Arc;

use anyhow::{Context, Result};
use thiserror::Error;

#[cfg(feature = "ssg")]
use tokio::sync::RwLock;

Expand Down Expand Up @@ -78,6 +76,7 @@ pub enum UtilsError {
/// File system utilities module
pub mod fs {
use super::*;
use std::path::PathBuf;

/// Tracks temporary files for cleanup
#[cfg(feature = "ssg")]
Expand Down Expand Up @@ -123,17 +122,13 @@ pub mod fs {
#[cfg(feature = "ssg")]
pub async fn create_temp_file(
prefix: &str,
) -> Result<(PathBuf, File)> {
) -> Result<(PathBuf, File), UtilsError> {
let temp_dir = std::env::temp_dir();
let file_name = format!("{}-{}", prefix, Uuid::new_v4());
let path = temp_dir.join(file_name);

let file = File::create(&path).with_context(|| {
format!(
"Failed to create temporary file: {}",
path.display()
)
})?;
let file =
File::create(&path).map_err(UtilsError::FileSystem)?;

Ok((path, file))
}
Expand Down Expand Up @@ -199,15 +194,23 @@ pub mod fs {
// In test mode, allow absolute paths in the temporary directory
if cfg!(test) {
let temp_dir = std::env::temp_dir();
let path_canonicalized =
path.canonicalize().with_context(|| {
let path_canonicalized = path
.canonicalize()
.or_else(|_| {
Ok::<PathBuf, io::Error>(path.to_path_buf())
}) // Specify the type explicitly
.with_context(|| {

Check warning on line 202 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L202

Added line #L202 was not covered by tests
format!(
"Failed to canonicalize path: {}",
path.display()
)
})?;
let temp_dir_canonicalized =
temp_dir.canonicalize().with_context(|| {
let temp_dir_canonicalized = temp_dir
.canonicalize()
.or_else(|_| {
Ok::<PathBuf, io::Error>(temp_dir.clone())
}) // Specify the type explicitly
.with_context(|| {

Check warning on line 213 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L210-L213

Added lines #L210 - L213 were not covered by tests
format!(
"Failed to canonicalize temp_dir: {}",
temp_dir.display()
Expand Down Expand Up @@ -418,14 +421,40 @@ pub mod log {
}
}

impl From<anyhow::Error> for UtilsError {
fn from(err: anyhow::Error) -> Self {
UtilsError::InvalidOperation(err.to_string())

Check warning on line 426 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L425-L426

Added lines #L425 - L426 were not covered by tests
}
}

impl From<tokio::task::JoinError> for UtilsError {
fn from(err: tokio::task::JoinError) -> Self {
UtilsError::InvalidOperation(err.to_string())

Check warning on line 432 in src/utils.rs

View check run for this annotation

Codecov / codecov/patch

src/utils.rs#L431-L432

Added lines #L431 - L432 were not covered by tests
}
}

#[cfg(all(test, feature = "ssg"))]
mod tests {
use super::*;
use crate::utils::fs::copy_file;
use crate::utils::fs::create_directory;
use crate::utils::fs::create_temp_file;
use crate::utils::fs::validate_path_safety;
use crate::utils::fs::TempFileTracker;
use crate::utils::log::LogEntry;
use crate::utils::log::LogWriter;
use crate::utils::UtilsError;
use log::Level;
use log::Record;
use std::fs::read_to_string;
use std::fs::remove_file;
use std::path::Path;
use std::sync::Arc;

#[tokio::test]
async fn test_temp_file_creation_and_cleanup() -> Result<()> {
let tracker = fs::TempFileTracker::new();
let (path, _file) = fs::create_temp_file("test").await?;
async fn test_temp_file_creation_and_cleanup() -> anyhow::Result<()>
{
let tracker = TempFileTracker::new();
let (path, _file) = create_temp_file("test").await?;

tracker.register(path.clone()).await?;
assert!(path.exists());
Expand All @@ -435,104 +464,159 @@ mod tests {
Ok(())
}

#[test]
fn test_path_validation() {
// Valid relative paths
assert!(fs::validate_path_safety(Path::new(
"content/file.txt"
))
.is_ok());
assert!(fs::validate_path_safety(Path::new("templates/blog"))
.is_ok());

// Invalid paths
assert!(
fs::validate_path_safety(Path::new("../outside")).is_err()
);
assert!(fs::validate_path_safety(Path::new("/absolute/path"))
.is_err());
assert!(fs::validate_path_safety(Path::new("content\0hidden"))
.is_err());
assert!(fs::validate_path_safety(Path::new("CON")).is_err());
#[tokio::test]
async fn test_temp_file_concurrent_access() -> Result<(), UtilsError>
{
use tokio::task;

// Test temporary directory paths
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join("valid_temp.txt");
let tracker = Arc::new(TempFileTracker::new());
let mut handles = Vec::new();

// Ensure the file exists before validation
let _ = File::create(&temp_path).unwrap();
for i in 0..5 {
let tracker = Arc::clone(&tracker);
handles.push(task::spawn(async move {
let (path, _) =
create_temp_file(&format!("test{}", i)).await?;
tracker.register(path).await
}));
}

assert!(fs::validate_path_safety(&temp_path).is_ok());
for handle in handles {
handle.await??;
}

// Cleanup
remove_file(temp_path).unwrap();
tracker.cleanup().await?;
Ok(())
}

#[test]
fn test_temp_path_validation() {
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join("test_temp_file.txt");
#[tokio::test]
async fn test_create_directory_valid_path() -> anyhow::Result<()> {
let temp_dir = std::env::temp_dir().join("test_dir");

// Ensure the file exists before validation
let _ = File::create(&temp_path).unwrap();
// Ensure the directory does not exist beforehand
if temp_dir.exists() {
tokio::fs::remove_dir_all(&temp_dir).await?;
}

let temp_dir_canonicalized = temp_dir.canonicalize().unwrap();
let temp_path_canonicalized = temp_path.canonicalize().unwrap();
create_directory(&temp_dir).await?;
assert!(temp_dir.exists());
tokio::fs::remove_dir_all(temp_dir).await?;
Ok(())
}

println!(
"Canonicalized Temp dir: {}",
temp_dir_canonicalized.display()
);
println!(
"Canonicalized Temp path: {}",
temp_path_canonicalized.display()
#[tokio::test]
async fn test_copy_file_valid_paths() -> anyhow::Result<()> {
let src = std::env::temp_dir().join("src.txt");
let dst = std::env::temp_dir().join("dst.txt");

// Create the source file with content
tokio::fs::write(&src, "test content").await?;

copy_file(&src, &dst).await?;
assert_eq!(
tokio::fs::read_to_string(&dst).await?,
"test content"
);

assert!(fs::validate_path_safety(&temp_path).is_ok());
tokio::fs::remove_file(src).await?;
tokio::fs::remove_file(dst).await?;
Ok(())
}

// Cleanup
remove_file(temp_path).unwrap();
#[test]
fn test_validate_path_safety_valid_paths() {
assert!(
validate_path_safety(Path::new("content/file.txt")).is_ok()
);
assert!(
validate_path_safety(Path::new("templates/blog")).is_ok()
);
}

#[test]
fn test_path_validation_edge_cases() {
// Test Unicode paths
fn test_validate_path_safety_invalid_paths() {
assert!(validate_path_safety(Path::new("../outside")).is_err());
assert!(
fs::validate_path_safety(Path::new("content/📚")).is_ok()
validate_path_safety(Path::new("content\0file")).is_err()
);
assert!(validate_path_safety(Path::new("CON")).is_err());
}

// Test long paths
#[test]
fn test_validate_path_safety_edge_cases() {
// Test Unicode
assert!(validate_path_safety(Path::new("content/📚")).is_ok());

// Long paths
let long_name = "a".repeat(255);
assert!(fs::validate_path_safety(Path::new(&long_name)).is_ok());
assert!(validate_path_safety(Path::new(&long_name)).is_ok());

// Test special characters
assert!(
fs::validate_path_safety(Path::new("content/#$@!")).is_ok()
);
// Special characters
assert!(validate_path_safety(Path::new("content/#$@!")).is_ok());
}

#[tokio::test]
async fn test_concurrent_temp_file_access() -> Result<()> {
use tokio::task;
#[test]
fn test_log_entry_format() {
let record = Record::builder()
.args(format_args!("Test log message"))
.level(Level::Info)
.target("test")
.module_path_static(Some("test"))
.file_static(Some("test.rs"))
.line(Some(42))
.build();

let entry = LogEntry::new(&record);
assert!(entry.format().contains("Test log message"));
assert!(entry.format().contains("INFO"));
}

let tracker = Arc::new(fs::TempFileTracker::new());
let mut handles = Vec::new();
#[test]
fn test_log_entry_with_error() {
let record = Record::builder()
.args(format_args!("Test error message"))
.level(Level::Error)
.target("test")
.module_path_static(Some("test"))
.file_static(Some("test.rs"))
.line(Some(42))
.build();

let mut entry = LogEntry::new(&record);
entry.error = Some("Error details".to_string());

let formatted = entry.format();
assert!(formatted.contains("Error details"));
assert!(formatted.contains("ERROR"));
}

for i in 0..5 {
let tracker = Arc::clone(&tracker);
handles.push(task::spawn(async move {
let (path, _) =
fs::create_temp_file(&format!("concurrent{}", i))
.await?;
tracker.register(path).await
}));
}
#[test]
fn test_log_writer_creation() {
let temp_log_path = std::env::temp_dir().join("test_log.txt");
let writer = LogWriter::new(&temp_log_path).unwrap();

for handle in handles {
handle.await??;
}
assert!(temp_log_path.exists());
drop(writer); // Ensure file is closed
remove_file(temp_log_path).unwrap();
}

tracker.cleanup().await?;
Ok(())
#[test]
fn test_log_writer_write() {
let temp_log_path =
std::env::temp_dir().join("test_log_write.txt");
let mut writer = LogWriter::new(&temp_log_path).unwrap();

let record = Record::builder()
.args(format_args!("Write test message"))
.level(Level::Info)
.target("test")
.build();

let entry = LogEntry::new(&record);
writer.write(&entry).unwrap();

let content = read_to_string(&temp_log_path).unwrap();
assert!(content.contains("Write test message"));
remove_file(temp_log_path).unwrap();
}
}

0 comments on commit 5b19d84

Please sign in to comment.