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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
tags:
# Match semantic version tags (e.g. v1.2.3, v10.11.12, v12.3.7-beta7)
- 'v*.*.*'
- 'v[0-9]*.[0-9]*.[0-9]*'

jobs:
goreleaser:
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ thiserror = "1.0"
ortho_config = { git = "https://github.com/leynos/ortho-config.git", tag = "v0.4.0" }
serde_yaml = "0.9"
tempfile = "3.10"
rstest = "0.18.0"

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
Expand Down
4 changes: 4 additions & 0 deletions crates/comenq/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ serde_json = { workspace = true }
comenq-lib = { path = "../.." }
thiserror = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
rstest = { workspace = true }
tempfile = { workspace = true }
1 change: 0 additions & 1 deletion crates/comenq/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ mod tests {
use super::{ClientError, parse_slug, run};
use crate::Args;
use comenq_lib::CommentRequest;
use rstest::rstest;
use tempfile::tempdir;
use tokio::io::AsyncReadExt;
use tokio::net::UnixListener;
Expand Down
7 changes: 3 additions & 4 deletions crates/comenqd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ ortho_config = { workspace = true }
figment = { version = "0.10", default-features = false, features = ["env", "toml"] }

[dev-dependencies]
rstest = "0.18.0"
tempfile = "3.10" # latest 3.x at time of writing; update as new patch versions release
rstest = { workspace = true }
tempfile = { workspace = true } # latest 3.x at time of writing; update as new patch versions release
serial_test = "2"
test-support = { path = "../../test-support" }
test-utils = { path = "../test-utils" }
wiremock = "0.6"
53 changes: 44 additions & 9 deletions crates/comenqd/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const DEFAULT_QUEUE_PATH: &str = "/var/lib/comenq/queue";
const DEFAULT_COOLDOWN: u64 = 960;

/// Runtime configuration for the daemon.
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Config {
/// GitHub Personal Access Token.
pub github_token: String,
Expand Down Expand Up @@ -119,17 +119,52 @@ mod tests {
use tempfile::tempdir;

mod env_guard {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/support/env_guard.rs"
));
}
//! Test helpers for managing environment variables.

#[derive(Debug)]
pub struct EnvVarGuard {
key: String,
original: Option<String>,
}

impl EnvVarGuard {
/// Set an environment variable for the lifetime of the returned guard.
pub fn set(key: &str, value: &str) -> Self {
let original = std::env::var(key).ok();
set_env_var(key, value);
Self {
key: key.to_string(),
original,
}
}
}

pub mod support {
pub use super::env_guard::{EnvVarGuard, remove_env_var, set_env_var};
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.original {
Some(v) => set_env_var(&self.key, v),
None => remove_env_var(&self.key),
}
}
}

/// Set an environment variable for tests.
///
/// The nightly compiler marks `std::env::set_var` as `unsafe`.
/// Tests run serially so using it is acceptable here.
pub fn set_env_var(key: &str, value: &str) {
unsafe { std::env::set_var(key, value) };
}

/// Remove an environment variable for tests.
///
/// `std::env::remove_var` is also `unsafe` on nightly.
pub fn remove_env_var(key: &str) {
unsafe { std::env::remove_var(key) };
}
}

use support::{EnvVarGuard, remove_env_var};
use env_guard::{EnvVarGuard, remove_env_var};

#[rstest]
#[serial_test::serial]
Expand Down
46 changes: 40 additions & 6 deletions crates/comenqd/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,29 +281,63 @@ pub async fn run_worker(
mod tests {
//! Tests for the daemon tasks.
use super::*;
use tempfile::tempdir;
use test_support::wait_for_file;
use test_utils::{octocrab_for, temp_config};
use octocrab::Octocrab;
use std::fs as stdfs;
use std::path::Path;
use std::sync::Arc;
use tempfile::{TempDir, tempdir};
use tokio::io::AsyncWriteExt;
use tokio::net::{UnixListener, UnixStream};
use tokio::net::UnixStream;
use tokio::sync::{mpsc, watch};
use tokio::time::{Duration, sleep};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use yaque::Receiver;
fn temp_config(tmp: &TempDir) -> Config {
Config {
github_token: String::from("t"),
socket_path: tmp.path().join("sock"),
queue_path: tmp.path().join("q"),
cooldown_period_seconds: 1,
}
}

fn cfg_with_cooldown(dir: &TempDir, secs: u64) -> Config {
Config {
cooldown_period_seconds: secs,
..temp_config(dir)
}
}

#[expect(clippy::expect_used, reason = "simplify test helper setup")]
fn octocrab_for(server: &MockServer) -> Arc<Octocrab> {
Arc::new(
Octocrab::builder()
.personal_token("t".to_string())
.base_uri(server.uri())
.expect("base_uri")
.build()
.expect("build octocrab"),
)
}

async fn wait_for_file(path: &Path, tries: u32, delay: Duration) -> bool {
for _ in 0..tries {
if path.exists() {
return true;
}
sleep(delay).await;
}
path.exists()
}

async fn setup_run_worker(status: u16) -> (MockServer, Arc<Config>, Receiver, Arc<Octocrab>) {
let dir = tempdir().expect("tempdir");
let cfg = Arc::new(Config {
cooldown_period_seconds: 0,
..temp_config(&dir)
});
let (sender, rx) = channel(&cfg.queue_path).expect("channel");
let (mut sender, rx) = channel(&cfg.queue_path).expect("channel");
let req = CommentRequest {
owner: "o".into(),
repo: "r".into(),
Expand Down Expand Up @@ -364,7 +398,7 @@ mod tests {
let dir = tempdir().expect("tempdir");
let queue_path = dir.path().join("q");
let (sender, mut receiver) = channel(&queue_path).expect("channel");
let (client_tx, mut writer_rx) = mpsc::unbounded_channel();
let (client_tx, writer_rx) = mpsc::unbounded_channel();
let writer = tokio::spawn(queue_writer(sender, writer_rx));

let (mut client, server) = UnixStream::pair().expect("pair");
Expand Down
3 changes: 1 addition & 2 deletions crates/comenqd/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ where
fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(writer)
.json()
.init();
}

Expand Down Expand Up @@ -66,7 +65,7 @@ mod tests {
#[test]
fn init_logging() {
let buf = Arc::new(Mutex::new(Vec::new()));
std::env::set_var("RUST_LOG", "info");
unsafe { std::env::set_var("RUST_LOG", "info") };
init_with_writer(BufMakeWriter { buf: buf.clone() });
info!("captured");
let output = String::from_utf8(buf.lock().expect("Failed to lock log buffer").clone())
Expand Down
36 changes: 36 additions & 0 deletions test-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,41 @@
pub mod daemon;
pub mod util;

// Re-exports from daemon module (added in main)
pub use daemon::{octocrab_for, temp_config};

// Re-exports from util module with documentation (from your branch)
/// Maximum number of times to poll for an expected file.
pub use util::SOCKET_RETRY_COUNT;

/// Delay between polls when waiting for a file to appear.
///
/// Multiply by [`SOCKET_RETRY_COUNT`] for the worst-case wait duration.
pub use util::SOCKET_RETRY_DELAY;

/// Wait for a file to appear, retrying with a fixed delay.
///
/// This is re-exported from [`util`] for convenience in tests.
///
/// # Arguments
/// * `path` – Path to the file that is expected to be created.
/// * `tries` – Maximum number of polling attempts.
/// * `delay` – Pause between attempts as a [`std::time::Duration`].
/// The total wait time is `tries * delay`.
///
/// # Returns
/// `true` if the file appears within `tries` attempts, otherwise `false`.
///
/// # Examples
/// ```rust,no_run
/// use std::path::Path;
/// use test_support::{wait_for_file, SOCKET_RETRY_COUNT, SOCKET_RETRY_DELAY};
///
/// #[tokio::main]
/// async fn main() {
/// let path = Path::new("/tmp/example.sock");
/// let found = wait_for_file(path, SOCKET_RETRY_COUNT, SOCKET_RETRY_DELAY).await;
/// assert!(found);
/// }
/// ```
pub use util::wait_for_file;
10 changes: 10 additions & 0 deletions test-support/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ use std::path::Path;
use std::time::Duration;
use tokio::time::sleep;

/// Maximum number of times to poll for an expected file.
pub const SOCKET_RETRY_COUNT: u32 = 10;

/// Delay between polls when waiting for a file to appear.
///
/// Each attempt sleeps for this duration; multiply by
/// [`SOCKET_RETRY_COUNT`] to obtain the worst-case total wait.
/// The value is ten milliseconds.
pub const SOCKET_RETRY_DELAY: Duration = Duration::from_millis(10);

/// Wait for a file to appear within the given number of tries.
///
/// # Examples
Expand Down
7 changes: 4 additions & 3 deletions tests/steps/listener_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
use std::sync::Arc;
use std::time::Duration;

use cucumber::{World, given, then, when};
use cucumber::World;
use cucumber::{given, then, when};
use tempfile::TempDir;
use test_support::wait_for_file;
use test_support::{SOCKET_RETRY_COUNT, SOCKET_RETRY_DELAY, wait_for_file};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
use tokio::sync::{mpsc, watch};
Expand Down Expand Up @@ -63,7 +64,7 @@ async fn running_listener(world: &mut ListenerWorld) {
.expect("config not initialised in ListenerWorld")
.socket_path;
assert!(
wait_for_file(socket_path, 10, Duration::from_millis(10)).await,
wait_for_file(socket_path, SOCKET_RETRY_COUNT, SOCKET_RETRY_DELAY).await,
"socket file {} not created within timeout",
socket_path.display()
);
Expand Down
Loading