ci: add ci tests, checks (clippy, fmt)
Some checks failed
CI / Check, test, lint (push) Failing after 51s
Some checks failed
CI / Check, test, lint (push) Failing after 51s
This commit is contained in:
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
name: Check, test, lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
run: cargo fmt --check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test
|
||||||
@@ -44,8 +44,7 @@ pub fn execute() -> Result<()> {
|
|||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
for &i in indices {
|
for &i in indices {
|
||||||
let id = jobs[i].id.clone();
|
let id = jobs[i].id.clone();
|
||||||
if let Some(maybe_ec) = results.get(&id) {
|
if let Some(Some(ec)) = results.get(&id) {
|
||||||
if let Some(ec) = maybe_ec {
|
|
||||||
jobs[i].status = if *ec == 0 {
|
jobs[i].status = if *ec == 0 {
|
||||||
JobStatus::Done
|
JobStatus::Done
|
||||||
} else {
|
} else {
|
||||||
@@ -55,8 +54,6 @@ pub fn execute() -> Result<()> {
|
|||||||
jobs[i].finished_at = Some(now);
|
jobs[i].finished_at = Some(now);
|
||||||
db::save(&jobs[i])?;
|
db::save(&jobs[i])?;
|
||||||
}
|
}
|
||||||
// None means still running — no update needed.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|||||||
@@ -72,3 +72,68 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn two_worker_config() -> Config {
|
||||||
|
Config {
|
||||||
|
default_worker: Some("beefy".to_string()),
|
||||||
|
workers: vec![
|
||||||
|
WorkerConfig {
|
||||||
|
name: "beefy".to_string(),
|
||||||
|
connection: "user@192.168.1.50".to_string(),
|
||||||
|
},
|
||||||
|
WorkerConfig {
|
||||||
|
name: "cloud".to_string(),
|
||||||
|
connection: "user@cloud.example.com".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_worker_found() {
|
||||||
|
assert!(two_worker_config().get_worker("beefy").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_worker_not_found() {
|
||||||
|
assert!(two_worker_config().get_worker("unknown").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_worker_returns_correct_name() {
|
||||||
|
let cfg = two_worker_config();
|
||||||
|
assert_eq!(cfg.default_worker().unwrap().name, "beefy");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_worker_by_name() {
|
||||||
|
let cfg = two_worker_config();
|
||||||
|
assert_eq!(cfg.resolve_worker(Some("cloud")).unwrap().name, "cloud");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_worker_falls_back_to_default() {
|
||||||
|
let cfg = two_worker_config();
|
||||||
|
assert_eq!(cfg.resolve_worker(None).unwrap().name, "beefy");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_worker_unknown_name_errors() {
|
||||||
|
assert!(two_worker_config().resolve_worker(Some("unknown")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_worker_no_default_errors() {
|
||||||
|
let cfg = Config {
|
||||||
|
default_worker: None,
|
||||||
|
workers: vec![],
|
||||||
|
};
|
||||||
|
assert!(cfg.resolve_worker(None).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
68
p/src/db.rs
68
p/src/db.rs
@@ -31,19 +31,6 @@ pub fn save(job: &Job) -> Result<()> {
|
|||||||
.with_context(|| format!("failed to write job {}", job.id))
|
.with_context(|| format!("failed to write job {}", job.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a job by exact UUID.
|
|
||||||
pub fn load(id: &str) -> Result<Option<Job>> {
|
|
||||||
let path = job_path(id)?;
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let content =
|
|
||||||
std::fs::read_to_string(&path).with_context(|| format!("failed to read job {}", id))?;
|
|
||||||
serde_json::from_str(&content)
|
|
||||||
.with_context(|| format!("failed to parse job {}", id))
|
|
||||||
.map(Some)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a job by UUID prefix. Errors if the prefix is ambiguous.
|
/// Find a job by UUID prefix. Errors if the prefix is ambiguous.
|
||||||
pub fn find(prefix: &str) -> Result<Option<Job>> {
|
pub fn find(prefix: &str) -> Result<Option<Job>> {
|
||||||
let dir = jobs_dir()?;
|
let dir = jobs_dir()?;
|
||||||
@@ -111,3 +98,58 @@ pub fn delete(id: &str) -> Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::job::{Job, JobStatus};
|
||||||
|
|
||||||
|
fn sample_job() -> Job {
|
||||||
|
Job {
|
||||||
|
id: "a3f2b091-1234-5678-9abc-000000000000".to_string(),
|
||||||
|
worker: "beefy".to_string(),
|
||||||
|
cwd: "/home/user/projects".to_string(),
|
||||||
|
command: "make".to_string(),
|
||||||
|
started_at: 1_700_000_000,
|
||||||
|
finished_at: Some(1_700_000_060),
|
||||||
|
exit_code: Some(0),
|
||||||
|
status: JobStatus::Done,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn job_json_roundtrip() {
|
||||||
|
let job = sample_job();
|
||||||
|
let json = serde_json::to_string_pretty(&job).unwrap();
|
||||||
|
let decoded: Job = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(decoded.id, job.id);
|
||||||
|
assert_eq!(decoded.worker, job.worker);
|
||||||
|
assert_eq!(decoded.command, job.command);
|
||||||
|
assert_eq!(decoded.exit_code, job.exit_code);
|
||||||
|
assert_eq!(decoded.status, job.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_serialises_lowercase() {
|
||||||
|
let job = sample_job();
|
||||||
|
let json = serde_json::to_string(&job).unwrap();
|
||||||
|
assert!(
|
||||||
|
json.contains("\"done\""),
|
||||||
|
"status should serialise as lowercase \"done\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn running_job_has_no_exit_code_in_json() {
|
||||||
|
let job = Job {
|
||||||
|
status: JobStatus::Running,
|
||||||
|
finished_at: None,
|
||||||
|
exit_code: None,
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&job).unwrap();
|
||||||
|
assert!(json.contains("\"running\""));
|
||||||
|
assert!(json.contains("\"exit_code\":null"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
119
p/src/job.rs
119
p/src/job.rs
@@ -35,16 +35,6 @@ impl JobStatus {
|
|||||||
Self::Unknown => "unknown",
|
Self::Unknown => "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(s: &str) -> Self {
|
|
||||||
match s {
|
|
||||||
"running" => Self::Running,
|
|
||||||
"done" => Self::Done,
|
|
||||||
"failed" => Self::Failed,
|
|
||||||
"stopped" => Self::Stopped,
|
|
||||||
_ => Self::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Display helpers ───────────────────────────────────────────────────────────
|
// ── Display helpers ───────────────────────────────────────────────────────────
|
||||||
@@ -100,3 +90,112 @@ impl Job {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample_job() -> Job {
|
||||||
|
Job {
|
||||||
|
id: "a3f2b091-1234-5678-9abc-000000000000".to_string(),
|
||||||
|
worker: "beefy".to_string(),
|
||||||
|
cwd: "/home/user/projects/foo".to_string(),
|
||||||
|
command: "make".to_string(),
|
||||||
|
started_at: 1_700_000_000,
|
||||||
|
finished_at: None,
|
||||||
|
exit_code: None,
|
||||||
|
status: JobStatus::Running,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_id_is_first_8_chars() {
|
||||||
|
assert_eq!(sample_job().short_id(), "a3f2b091");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_display_running() {
|
||||||
|
assert_eq!(sample_job().status_display(), "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_display_done() {
|
||||||
|
let job = Job {
|
||||||
|
status: JobStatus::Done,
|
||||||
|
exit_code: Some(0),
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.status_display(), "done [0]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_display_failed() {
|
||||||
|
let job = Job {
|
||||||
|
status: JobStatus::Failed,
|
||||||
|
exit_code: Some(2),
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.status_display(), "failed [2]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_display_stopped() {
|
||||||
|
let job = Job {
|
||||||
|
status: JobStatus::Stopped,
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.status_display(), "stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duration_display_minutes_seconds() {
|
||||||
|
let job = Job {
|
||||||
|
started_at: 1_000,
|
||||||
|
finished_at: Some(1_065),
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.duration_display(), "1:05");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duration_display_hours() {
|
||||||
|
let job = Job {
|
||||||
|
started_at: 0,
|
||||||
|
finished_at: Some(3_661),
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.duration_display(), "1:01:01");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_display_no_truncation() {
|
||||||
|
assert_eq!(sample_job().command_display(50), "make");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_display_truncated() {
|
||||||
|
assert_eq!(sample_job().command_display(3), "ma…");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cwd_display_outside_home() {
|
||||||
|
let job = Job {
|
||||||
|
cwd: "/var/tmp/something".to_string(),
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.cwd_display(), "/var/tmp/something");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cwd_display_under_home() {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
let job = Job {
|
||||||
|
cwd: home.join("projects/foo").to_string_lossy().to_string(),
|
||||||
|
..sample_job()
|
||||||
|
};
|
||||||
|
assert_eq!(job.cwd_display(), "~/projects/foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
63
p/src/ssh.rs
63
p/src/ssh.rs
@@ -161,3 +161,66 @@ pub fn poll_jobs(worker: &WorkerConfig, job_ids: &[&str]) -> Result<HashMap<Stri
|
|||||||
}
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_user_at_host() {
|
||||||
|
let (uh, port) = parse_connection("user@host.local");
|
||||||
|
assert_eq!(uh, "user@host.local");
|
||||||
|
assert_eq!(port, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_user_at_host_with_port() {
|
||||||
|
let (uh, port) = parse_connection("user@host.local:2222");
|
||||||
|
assert_eq!(uh, "user@host.local");
|
||||||
|
assert_eq!(port, Some(2222));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_bare_host() {
|
||||||
|
let (uh, port) = parse_connection("myserver");
|
||||||
|
assert_eq!(uh, "myserver");
|
||||||
|
assert_eq!(port, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_host_with_port() {
|
||||||
|
let (uh, port) = parse_connection("192.168.1.1:22");
|
||||||
|
assert_eq!(uh, "192.168.1.1");
|
||||||
|
assert_eq!(port, Some(22));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ssh_alias_unchanged() {
|
||||||
|
// Plain SSH config aliases contain no '@' or ':port' — returned as-is.
|
||||||
|
let (uh, port) = parse_connection("my-dev-box");
|
||||||
|
assert_eq!(uh, "my-dev-box");
|
||||||
|
assert_eq!(port, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_strips_user_prefix() {
|
||||||
|
assert_eq!(hostname_from_connection("user@host.local"), "host.local");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_strips_user_and_port() {
|
||||||
|
assert_eq!(hostname_from_connection("user@host.local:22"), "host.local");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_from_bare_host() {
|
||||||
|
assert_eq!(hostname_from_connection("myserver"), "myserver");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_from_ssh_alias() {
|
||||||
|
assert_eq!(hostname_from_connection("my-dev-box"), "my-dev-box");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user