Add CI
This commit is contained in:
parent
133a55035a
commit
1fe6971fb2
8 changed files with 140 additions and 12 deletions
111
.github/workflows/ci.yml
vendored
Normal file
111
.github/workflows/ci.yml
vendored
Normal 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
|
||||||
|
|
@ -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
10
clippy.toml
Normal 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" },
|
||||||
|
]
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(×tamp_str)
|
let timestamp = DateTime::parse_from_rfc3339(×tamp_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();
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue