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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo fetch
- name: Build tests
run: cargo build --tests --all-features
run: cargo build --tests --all-features --workspace
- name: Run tests
run: cargo test --all-features
run: cargo test --all-features --workspace

deny:
name: Security & Licenses
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Code Coverage

on:
push:
branches:
- main
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2

- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin

- name: Run tests with coverage
run: cargo tarpaulin --all-features --workspace --out xml --out html --skip-clean

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
cobertura.xml
tarpaulin-report.html
retention-days: 14
48 changes: 48 additions & 0 deletions buswatch-adapters/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,51 @@ impl From<reqwest::Error> for AdapterError {
}
}
}

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

#[test]
fn error_display_http() {
let err = AdapterError::Http("404 Not Found".to_string());
assert_eq!(err.to_string(), "HTTP request failed: 404 Not Found");
}

#[test]
fn error_display_parse() {
let err = AdapterError::Parse("invalid JSON".to_string());
assert_eq!(err.to_string(), "Failed to parse response: invalid JSON");
}

#[test]
fn error_display_auth() {
let err = AdapterError::Auth("bad credentials".to_string());
assert_eq!(err.to_string(), "Authentication failed: bad credentials");
}

#[test]
fn error_display_connection() {
let err = AdapterError::Connection("connection refused".to_string());
assert_eq!(err.to_string(), "Connection failed: connection refused");
}

#[test]
fn error_display_timeout() {
let err = AdapterError::Timeout;
assert_eq!(err.to_string(), "Request timed out");
}

#[test]
fn error_display_unsupported() {
let err = AdapterError::Unsupported("feature X".to_string());
assert_eq!(err.to_string(), "Feature not supported: feature X");
}

#[test]
fn error_is_debug() {
let err = AdapterError::Timeout;
let debug = format!("{:?}", err);
assert!(debug.contains("Timeout"));
}
}
48 changes: 47 additions & 1 deletion buswatch-adapters/src/kafka.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,6 @@ mod tests {

#[test]
fn test_builder_defaults() {
// Just test that builder compiles correctly
let builder = KafkaAdapter::builder()
.brokers("localhost:9092")
.group_id("test-group")
Expand All @@ -260,4 +259,51 @@ mod tests {
assert!(builder.brokers.is_some());
assert!(builder.group_id.is_some());
}

#[test]
fn builder_stores_brokers() {
let builder = KafkaAdapter::builder().brokers("broker1:9092,broker2:9092");
assert_eq!(builder.brokers.unwrap(), "broker1:9092,broker2:9092");
}

#[test]
fn builder_stores_group_id() {
let builder = KafkaAdapter::builder().group_id("my-consumer-group");
assert_eq!(builder.group_id.unwrap(), "my-consumer-group");
}

#[test]
fn builder_stores_group_filter() {
let builder = KafkaAdapter::builder().group_filter("prefix-");
assert_eq!(builder.group_filter.unwrap(), "prefix-");
}

#[test]
fn builder_stores_timeout() {
let builder = KafkaAdapter::builder().timeout(Duration::from_secs(30));
assert_eq!(builder.timeout.unwrap(), Duration::from_secs(30));
}

#[test]
fn builder_chains_all_options() {
let builder = KafkaAdapter::builder()
.brokers("localhost:9092")
.group_id("test")
.group_filter("app-")
.timeout(Duration::from_secs(15));

assert_eq!(builder.brokers.unwrap(), "localhost:9092");
assert_eq!(builder.group_id.unwrap(), "test");
assert_eq!(builder.group_filter.unwrap(), "app-");
assert_eq!(builder.timeout.unwrap(), Duration::from_secs(15));
}

#[test]
fn builder_default_is_empty() {
let builder = KafkaAdapterBuilder::default();
assert!(builder.brokers.is_none());
assert!(builder.group_id.is_none());
assert!(builder.group_filter.is_none());
assert!(builder.timeout.is_none());
}
}
29 changes: 29 additions & 0 deletions buswatch-adapters/src/nats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,33 @@ mod tests {
assert!(builder.url.is_some());
assert_eq!(builder.url.unwrap(), "nats://localhost:4222");
}

#[test]
fn builder_stores_url() {
let builder = NatsAdapter::builder().url("nats://nats.example.com:4222");
assert_eq!(builder.url.unwrap(), "nats://nats.example.com:4222");
}

#[test]
fn builder_stores_credentials_file() {
let builder = NatsAdapter::builder().credentials_file("/path/to/creds.txt");
assert_eq!(builder.credentials.unwrap(), "/path/to/creds.txt");
}

#[test]
fn builder_chains_options() {
let builder = NatsAdapter::builder()
.url("nats://localhost:4222")
.credentials_file("/etc/nats/creds");

assert_eq!(builder.url.unwrap(), "nats://localhost:4222");
assert_eq!(builder.credentials.unwrap(), "/etc/nats/creds");
}

#[test]
fn builder_default_is_empty() {
let builder = NatsAdapterBuilder::default();
assert!(builder.url.is_none());
assert!(builder.credentials.is_none());
}
}
118 changes: 118 additions & 0 deletions buswatch-adapters/src/rabbitmq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,122 @@ mod tests {
assert_eq!(write.count, 600);
assert_eq!(write.rate, Some(10.5));
}

#[test]
fn queue_with_no_consumers_shows_backlog_only() {
let adapter = RabbitMqAdapter::builder().build();

let queue = QueueInfo {
name: "idle-queue".to_string(),
messages_ready: 50,
messages_unacknowledged: 0,
consumers: 0, // No consumers
messages_delivered: None,
messages_published: Some(50),
message_stats: None,
};

let metrics = adapter.queue_to_metrics(&queue);

let read = metrics.reads.get("messages").unwrap();
assert_eq!(read.count, 0); // No delivered messages
assert_eq!(read.backlog, Some(50)); // But backlog is shown
}

#[test]
fn queue_with_no_message_stats_has_no_rates() {
let adapter = RabbitMqAdapter::builder().build();

let queue = QueueInfo {
name: "new-queue".to_string(),
messages_ready: 0,
messages_unacknowledged: 0,
consumers: 1,
messages_delivered: Some(10),
messages_published: Some(10),
message_stats: None,
};

let metrics = adapter.queue_to_metrics(&queue);

assert!(metrics.reads.get("messages").unwrap().rate.is_none());
assert!(metrics.writes.get("messages").unwrap().rate.is_none());
}

#[test]
fn builder_with_timeout() {
let adapter = RabbitMqAdapter::builder()
.timeout(Duration::from_secs(30))
.build();

// Can't directly test timeout, but verify it doesn't panic
assert_eq!(adapter.endpoint, "http://localhost:15672");
}

#[test]
fn urlencoded_handles_multiple_slashes() {
assert_eq!(urlencoded("/a/b/c"), "%2Fa%2Fb%2Fc");
}

#[test]
fn urlencoded_preserves_other_special_chars() {
// Only slashes are encoded by this function
assert_eq!(urlencoded("name:with:colons"), "name:with:colons");
assert_eq!(urlencoded("name.with.dots"), "name.with.dots");
}

#[test]
fn message_stats_rate_extraction() {
let stats = MessageStats {
publish_details: Some(RateDetails { rate: 100.5 }),
deliver_get_details: Some(RateDetails { rate: 99.2 }),
};

assert_eq!(stats.publish_rate(), Some(100.5));
assert_eq!(stats.deliver_get_rate(), Some(99.2));
}

#[test]
fn message_stats_missing_details_returns_none() {
let stats = MessageStats {
publish_details: None,
deliver_get_details: None,
};

assert_eq!(stats.publish_rate(), None);
assert_eq!(stats.deliver_get_rate(), None);
}

#[test]
fn queue_info_deserializes_from_json() {
let json = r#"{
"name": "orders",
"messages_ready": 42,
"messages_unacknowledged": 3,
"consumers": 2,
"message_stats": {
"publish_details": { "rate": 10.0 },
"deliver_get_details": { "rate": 9.5 }
}
}"#;

let queue: QueueInfo = serde_json::from_str(json).unwrap();
assert_eq!(queue.name, "orders");
assert_eq!(queue.messages_ready, 42);
assert_eq!(queue.consumers, 2);
assert_eq!(queue.message_stats.unwrap().publish_rate(), Some(10.0));
}

#[test]
fn queue_info_deserializes_with_missing_optional_fields() {
let json = r#"{
"name": "simple-queue"
}"#;

let queue: QueueInfo = serde_json::from_str(json).unwrap();
assert_eq!(queue.name, "simple-queue");
assert_eq!(queue.messages_ready, 0);
assert_eq!(queue.consumers, 0);
assert!(queue.message_stats.is_none());
}
}
Loading