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
|
||||
|
||||
TODO: `./run-dev`, options
|
||||
tl;dr:
|
||||
* Run in development: `./run-dev`
|
||||
* Set the `SHEPHERD_MOCK_TIME` environment variable to mock the time,
|
||||
such as `SHEPHERD_MOCK_TIME="2025-12-25 15:30:00" ./run-dev`
|
||||
* Run tests: `cargo test`
|
||||
* Lint: `cargo clippy`
|
||||
|
||||
## 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(),
|
||||
entry_id: EntryId::new("game-1"),
|
||||
label: "Test Game".into(),
|
||||
deadline: Some(Local::now()),
|
||||
deadline: Some(shepherd_util::now()),
|
||||
});
|
||||
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ mod tests {
|
|||
always: true,
|
||||
};
|
||||
|
||||
let dt = Local::now();
|
||||
let dt = shepherd_util::now();
|
||||
assert!(policy.is_available(&dt));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -591,7 +591,7 @@ mod tests {
|
|||
let caps = HostCapabilities::minimal();
|
||||
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!(entries[0].enabled);
|
||||
}
|
||||
|
|
@ -604,7 +604,7 @@ mod tests {
|
|||
let engine = CoreEngine::new(policy, store, caps);
|
||||
|
||||
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(_)));
|
||||
}
|
||||
|
|
@ -617,7 +617,7 @@ mod tests {
|
|||
let mut engine = CoreEngine::new(policy, store, caps);
|
||||
|
||||
let entry_id = EntryId::new("test-game");
|
||||
let now = Local::now();
|
||||
let now = shepherd_util::now();
|
||||
let now_mono = MonotonicInstant::now();
|
||||
|
||||
// Launch first session
|
||||
|
|
@ -672,7 +672,7 @@ mod tests {
|
|||
let mut engine = CoreEngine::new(policy, store, caps);
|
||||
|
||||
let entry_id = EntryId::new("test");
|
||||
let now = Local::now();
|
||||
let now = shepherd_util::now();
|
||||
let now_mono = MonotonicInstant::now();
|
||||
|
||||
// Start session
|
||||
|
|
@ -733,7 +733,7 @@ mod tests {
|
|||
let mut engine = CoreEngine::new(policy, store, caps);
|
||||
|
||||
let entry_id = EntryId::new("test");
|
||||
let now = Local::now();
|
||||
let now = shepherd_util::now();
|
||||
let now_mono = MonotonicInstant::now();
|
||||
|
||||
// Start session
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ impl Store for SqliteStore {
|
|||
let (id, timestamp_str, event_json) = row?;
|
||||
let timestamp = DateTime::parse_from_rfc3339(×tamp_str)
|
||||
.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)?;
|
||||
|
||||
events.push(AuditEvent {
|
||||
|
|
@ -284,7 +284,7 @@ mod tests {
|
|||
fn test_usage_accounting() {
|
||||
let store = SqliteStore::in_memory().unwrap();
|
||||
let entry_id = EntryId::new("game-1");
|
||||
let today = Local::now().date_naive();
|
||||
let today = shepherd_util::now().date_naive();
|
||||
|
||||
// Initially zero
|
||||
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());
|
||||
|
||||
// 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();
|
||||
|
||||
let stored = store.get_cooldown_until(&entry_id).unwrap().unwrap();
|
||||
|
|
@ -334,7 +334,7 @@ mod tests {
|
|||
|
||||
// Save snapshot
|
||||
let snapshot = StateSnapshot {
|
||||
timestamp: Local::now(),
|
||||
timestamp: shepherd_util::now(),
|
||||
active_session: None,
|
||||
};
|
||||
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.
|
||||
/// 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> {
|
||||
*MOCK_TIME_OFFSET.get_or_init(|| {
|
||||
#[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 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.
|
||||
#[allow(clippy::disallowed_methods)] // This is the wrapper that provides mock time support
|
||||
pub fn now() -> DateTime<Local> {
|
||||
let real_now = chrono::Local::now();
|
||||
|
||||
|
|
@ -464,6 +466,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::disallowed_methods)] // Testing the offset calculation requires real time
|
||||
fn test_mock_time_offset_calculation() {
|
||||
// Test that the offset calculation works correctly
|
||||
let mock_time_str = "2025-12-25 14:30:00";
|
||||
|
|
@ -487,6 +490,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::disallowed_methods)] // Testing time advancement requires real time
|
||||
fn test_mock_time_advances_with_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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue