This commit is contained in:
Albert Armea 2025-12-28 21:48:25 -05:00
parent 133a55035a
commit 1fe6971fb2
8 changed files with 140 additions and 12 deletions

111
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,111 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
env:
CARGO_TERM_COLOR: always
SYSTEM_DEPS: >-
build-essential
pkg-config
libglib2.0-dev
libgtk-4-dev
libadwaita-1-dev
libcairo2-dev
libpango1.0-dev
libgdk-pixbuf-xlib-2.0-dev
libwayland-dev
libx11-dev
libxkbcommon-dev
libgirepository1.0-dev
libgtk4-layer-shell-dev
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y ${{ env.SYSTEM_DEPS }}
- name: Setup Rust toolchain
uses: dtolnay/rust-action@stable
- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build
run: cargo build --all-targets
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y ${{ env.SYSTEM_DEPS }}
- name: Setup Rust toolchain
uses: dtolnay/rust-action@stable
- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run tests
run: cargo test --all-targets
lint:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y ${{ env.SYSTEM_DEPS }}
- name: Setup Rust toolchain
uses: dtolnay/rust-action@stable
with:
components: clippy
- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run Clippy
run: cargo clippy --all-targets -- -D warnings

View file

@ -81,9 +81,12 @@ TODO
## Development ## Development
TODO: `./run-dev`, options tl;dr:
* Run in development: `./run-dev`
* Set the `SHEPHERD_MOCK_TIME` environment variable to mock the time, * Set the `SHEPHERD_MOCK_TIME` environment variable to mock the time,
such as `SHEPHERD_MOCK_TIME="2025-12-25 15:30:00" ./run-dev` such as `SHEPHERD_MOCK_TIME="2025-12-25 15:30:00" ./run-dev`
* Run tests: `cargo test`
* Lint: `cargo clippy`
## Contributing ## Contributing

10
clippy.toml Normal file
View file

@ -0,0 +1,10 @@
# Clippy configuration for shepherd-launcher
#
# This file configures clippy lints for the workspace.
# Disallow direct usage of Local::now() to ensure mock time is used consistently.
# Use shepherd_util::now() instead, which respects the SHEPHERD_MOCK_TIME env var
# in debug builds.
disallowed-methods = [
{ path = "chrono::Local::now", reason = "Use shepherd_util::now() instead to support mock time in debug builds" },
]

View file

@ -100,7 +100,7 @@ mod tests {
session_id: SessionId::new(), session_id: SessionId::new(),
entry_id: EntryId::new("game-1"), entry_id: EntryId::new("game-1"),
label: "Test Game".into(), label: "Test Game".into(),
deadline: Some(Local::now()), deadline: Some(shepherd_util::now()),
}); });
let json = serde_json::to_string(&event).unwrap(); let json = serde_json::to_string(&event).unwrap();

View file

@ -349,7 +349,7 @@ mod tests {
always: true, always: true,
}; };
let dt = Local::now(); let dt = shepherd_util::now();
assert!(policy.is_available(&dt)); assert!(policy.is_available(&dt));
} }

View file

@ -591,7 +591,7 @@ mod tests {
let caps = HostCapabilities::minimal(); let caps = HostCapabilities::minimal();
let engine = CoreEngine::new(policy, store, caps); let engine = CoreEngine::new(policy, store, caps);
let entries = engine.list_entries(Local::now()); let entries = engine.list_entries(shepherd_util::now());
assert_eq!(entries.len(), 1); assert_eq!(entries.len(), 1);
assert!(entries[0].enabled); assert!(entries[0].enabled);
} }
@ -604,7 +604,7 @@ mod tests {
let engine = CoreEngine::new(policy, store, caps); let engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let decision = engine.request_launch(&entry_id, Local::now()); let decision = engine.request_launch(&entry_id, shepherd_util::now());
assert!(matches!(decision, LaunchDecision::Approved(_))); assert!(matches!(decision, LaunchDecision::Approved(_)));
} }
@ -617,7 +617,7 @@ mod tests {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Launch first session // Launch first session
@ -672,7 +672,7 @@ mod tests {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test"); let entry_id = EntryId::new("test");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Start session // Start session
@ -733,7 +733,7 @@ mod tests {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test"); let entry_id = EntryId::new("test");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Start session // Start session

View file

@ -114,7 +114,7 @@ impl Store for SqliteStore {
let (id, timestamp_str, event_json) = row?; let (id, timestamp_str, event_json) = row?;
let timestamp = DateTime::parse_from_rfc3339(&timestamp_str) let timestamp = DateTime::parse_from_rfc3339(&timestamp_str)
.map(|dt| dt.with_timezone(&Local)) .map(|dt| dt.with_timezone(&Local))
.unwrap_or_else(|_| Local::now()); .unwrap_or_else(|_| shepherd_util::now());
let event: crate::AuditEventType = serde_json::from_str(&event_json)?; let event: crate::AuditEventType = serde_json::from_str(&event_json)?;
events.push(AuditEvent { events.push(AuditEvent {
@ -284,7 +284,7 @@ mod tests {
fn test_usage_accounting() { fn test_usage_accounting() {
let store = SqliteStore::in_memory().unwrap(); let store = SqliteStore::in_memory().unwrap();
let entry_id = EntryId::new("game-1"); let entry_id = EntryId::new("game-1");
let today = Local::now().date_naive(); let today = shepherd_util::now().date_naive();
// Initially zero // Initially zero
let usage = store.get_usage(&entry_id, today).unwrap(); let usage = store.get_usage(&entry_id, today).unwrap();
@ -314,7 +314,7 @@ mod tests {
assert!(store.get_cooldown_until(&entry_id).unwrap().is_none()); assert!(store.get_cooldown_until(&entry_id).unwrap().is_none());
// Set cooldown // Set cooldown
let until = Local::now() + chrono::Duration::hours(1); let until = shepherd_util::now() + chrono::Duration::hours(1);
store.set_cooldown_until(&entry_id, until).unwrap(); store.set_cooldown_until(&entry_id, until).unwrap();
let stored = store.get_cooldown_until(&entry_id).unwrap().unwrap(); let stored = store.get_cooldown_until(&entry_id).unwrap().unwrap();
@ -334,7 +334,7 @@ mod tests {
// Save snapshot // Save snapshot
let snapshot = StateSnapshot { let snapshot = StateSnapshot {
timestamp: Local::now(), timestamp: shepherd_util::now(),
active_session: None, active_session: None,
}; };
store.save_snapshot(&snapshot).unwrap(); store.save_snapshot(&snapshot).unwrap();

View file

@ -30,6 +30,7 @@ static MOCK_TIME_OFFSET: OnceLock<Option<chrono::Duration>> = OnceLock::new();
/// Initialize the mock time offset based on the environment variable. /// Initialize the mock time offset based on the environment variable.
/// Returns the offset between mock time and real time at process start. /// Returns the offset between mock time and real time at process start.
#[allow(clippy::disallowed_methods)] // This is the internal implementation that wraps Local::now()
fn get_mock_time_offset() -> Option<chrono::Duration> { fn get_mock_time_offset() -> Option<chrono::Duration> {
*MOCK_TIME_OFFSET.get_or_init(|| { *MOCK_TIME_OFFSET.get_or_init(|| {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -79,6 +80,7 @@ pub fn is_mock_time_active() -> bool {
/// In release builds, this always returns the real system time. /// In release builds, this always returns the real system time.
/// In debug builds, if `SHEPHERD_MOCK_TIME` is set, this returns a time /// In debug builds, if `SHEPHERD_MOCK_TIME` is set, this returns a time
/// that advances from the mock time at the same rate as real time. /// that advances from the mock time at the same rate as real time.
#[allow(clippy::disallowed_methods)] // This is the wrapper that provides mock time support
pub fn now() -> DateTime<Local> { pub fn now() -> DateTime<Local> {
let real_now = chrono::Local::now(); let real_now = chrono::Local::now();
@ -464,6 +466,7 @@ mod tests {
} }
#[test] #[test]
#[allow(clippy::disallowed_methods)] // Testing the offset calculation requires real time
fn test_mock_time_offset_calculation() { fn test_mock_time_offset_calculation() {
// Test that the offset calculation works correctly // Test that the offset calculation works correctly
let mock_time_str = "2025-12-25 14:30:00"; let mock_time_str = "2025-12-25 14:30:00";
@ -487,6 +490,7 @@ mod tests {
} }
#[test] #[test]
#[allow(clippy::disallowed_methods)] // Testing time advancement requires real time
fn test_mock_time_advances_with_real_time() { fn test_mock_time_advances_with_real_time() {
// Test that mock time advances at the same rate as real time // Test that mock time advances at the same rate as real time
// This tests the concept, not the actual implementation (since OnceLock is static) // This tests the concept, not the actual implementation (since OnceLock is static)