Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ jobs:
env:
COVERAGE: ${{ steps.coverage.outputs.percentage }}
run: |
THRESHOLD=80
THRESHOLD=90

if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) ))
then
echo "Coverage ${COVERAGE}% is below ${THRESHOLD}%"
echo "Consider adding more tests."
echo "Coverage ${COVERAGE}% is below ${THRESHOLD}% — failing CI."
exit 1
else
echo "Coverage ${COVERAGE}% meets ${THRESHOLD}%"
fi
2 changes: 1 addition & 1 deletion .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ jobs:
run: |
set -euo pipefail
HASHES=$(sha256sum \
target/release/rust-template \
target/release/rust_template \
| base64 -w0)
echo "hashes=${HASHES}" >> "$GITHUB_OUTPUT"

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ USER nonroot:nonroot

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/usr/local/bin/rust_template", "--version"]
CMD ["/usr/local/bin/rust_template"]

# Run the binary
ENTRYPOINT ["/usr/local/bin/rust_template"]
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ use rust_template::{add, divide, Config};
fn main() -> Result<(), rust_template::Error> {
// Basic arithmetic
let sum = add(2, 3);
println!("2 + 3 = {}", sum);
println!("2 + 3 = {sum}");

// Safe division with error handling
let quotient = divide(10, 2)?;
println!("10 / 2 = {}", quotient);
println!("10 / 2 = {quotient}");

// Using configuration builder
let config = Config::new()
Expand All @@ -58,7 +58,7 @@ fn main() -> Result<(), rust_template::Error> {
.with_timeout(60);

println!("Config: verbose={}, retries={}, timeout={}s",
config.verbose, config.max_retries, config.timeout_secs);
config.verbose(), config.max_retries(), config.timeout_secs());

Ok(())
}
Expand Down
113 changes: 98 additions & 15 deletions crates/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use thiserror::Error;

/// Error type for `rust_template` operations.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// Invalid input was provided.
#[error("invalid input: {0}")]
Expand Down Expand Up @@ -55,11 +56,12 @@ pub const fn add(a: i64, b: i64) -> i64 {
///
/// # Returns
///
/// The quotient, or an error if `divisor` is zero.
/// The quotient, or an error if `divisor` is zero or the operation overflows.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] if `divisor` is zero.
/// Returns [`Error::OperationFailed`] if the division overflows.
///
/// # Examples
///
Expand All @@ -73,18 +75,60 @@ pub fn divide(dividend: i64, divisor: i64) -> Result<i64> {
if divisor == 0 {
return Err(Error::InvalidInput("divisor cannot be zero".to_string()));
}
Ok(dividend / divisor)

dividend.checked_div(divisor).ok_or_else(|| Error::OperationFailed {
operation: "divide".to_string(),
cause: "overflow when dividing i64 values".to_string(),
})
}

/// Parses `input` as a non-negative integer and returns it.
///
/// # Arguments
///
/// * `input` - A string slice to parse as an `i64`.
///
/// # Returns
///
/// The parsed non-negative integer value on success.
///
/// # Errors
///
/// - Returns [`Error::InvalidInput`] if `input` cannot be parsed as an `i64`.
/// - Returns [`Error::OperationFailed`] if the parsed value is negative.
///
/// # Examples
///
/// ```rust
/// use rust_template::process;
///
/// assert_eq!(process("42").unwrap(), 42);
/// assert_eq!(process("0").unwrap(), 0);
/// assert!(process("abc").is_err());
/// assert!(process("-1").is_err());
/// ```
pub fn process(input: &str) -> Result<i64> {
let value = input
.parse::<i64>()
.map_err(|e| Error::InvalidInput(format!("not a valid integer: {e}")))?;

if value < 0 {
return Err(Error::OperationFailed {
operation: "process".to_string(),
cause: format!("value {value} is negative"),
});
}

Ok(value)
}

/// Configuration for the crate.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Config {
/// Enable verbose logging.
pub verbose: bool,
/// Maximum number of retries.
pub max_retries: u32,
/// Timeout in seconds.
pub timeout_secs: u64,
verbose: bool,
max_retries: u32,
timeout_secs: u64,
}

impl Default for Config {
Expand All @@ -104,6 +148,24 @@ impl Config {
}
}

/// Returns whether verbose logging is enabled.
#[must_use]
pub const fn verbose(&self) -> bool {
self.verbose
}

/// Returns the maximum number of retries.
#[must_use]
pub const fn max_retries(&self) -> u32 {
self.max_retries
}

/// Returns the timeout in seconds.
#[must_use]
pub const fn timeout_secs(&self) -> u64 {
self.timeout_secs
}

/// Sets the verbose flag.
#[must_use]
pub const fn with_verbose(mut self, verbose: bool) -> Self {
Expand Down Expand Up @@ -148,7 +210,6 @@ mod tests {
#[test]
fn test_divide_by_zero() {
let result = divide(10, 0);
assert!(result.is_err());
assert!(matches!(result, Err(Error::InvalidInput(ref msg)) if msg.contains("zero")));
}

Expand All @@ -159,17 +220,39 @@ mod tests {
.with_max_retries(5)
.with_timeout(60);

assert!(config.verbose);
assert_eq!(config.max_retries, 5);
assert_eq!(config.timeout_secs, 60);
assert!(config.verbose());
assert_eq!(config.max_retries(), 5);
assert_eq!(config.timeout_secs(), 60);
}

#[test]
fn test_config_default() {
let config = Config::default();
assert!(!config.verbose);
assert_eq!(config.max_retries, 3);
assert_eq!(config.timeout_secs, 30);
assert!(!config.verbose());
assert_eq!(config.max_retries(), 3);
assert_eq!(config.timeout_secs(), 30);
}

#[test]
fn test_process_valid() {
assert_eq!(process("42").unwrap(), 42);
assert_eq!(process("0").unwrap(), 0);
assert_eq!(process("100").unwrap(), 100);
}

#[test]
fn test_process_invalid_input() {
let result = process("abc");
assert!(matches!(result, Err(Error::InvalidInput(_))));
}

#[test]
fn test_process_negative() {
let result = process("-1");
assert!(matches!(
result,
Err(Error::OperationFailed { ref operation, .. }) if operation == "process"
));
}

#[test]
Expand Down
22 changes: 21 additions & 1 deletion crates/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rust_template::{Config, add, divide};
fn run() -> Result<(), rust_template::Error> {
let config = Config::new().with_verbose(true);

if config.verbose {
if config.verbose() {
eprintln!("Running rust_template with verbose mode enabled");
}

Expand All @@ -33,3 +33,23 @@ fn main() -> ExitCode {
},
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_run_succeeds() {
let result = run();
assert!(
result.is_ok(),
"run() should succeed with the default implementation"
);
}

#[test]
fn test_main_returns_success() {
let exit_code = main();
assert_eq!(exit_code, ExitCode::SUCCESS);
}
}
Loading
Loading