#![cfg(all(feature = "sqlite", feature = "test-support"))]
use autumn_web::app::AppBuilder;
use autumn_web::config::{SchedulerBackend, SchedulerConfig};
use autumn_web::job;
use autumn_web::migrate::{EmbeddedMigrations, embed_migrations};
use autumn_web::plugin::Plugin;
use autumn_web::prelude::*;
use autumn_web::scheduler::coordinator_from_config;
use autumn_web::sim::substrate::SqliteSubstrate;
use autumn_web::task::TaskCoordination;
use autumn_web::test::TestApp;
use diesel_async::RunQueryDsl as _;
use serde::{Deserialize, Serialize};
use serde_json::json;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("tests/fixtures/sim_sqlite_substrate");
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct MarkArgs {
id: i64,
tag: String,
}
#[job(name = "sim_write_mark", max_attempts = 1, backoff_ms = 1)]
async fn sim_write_mark(state: AppState, args: MarkArgs) -> AutumnResult<()> {
let pool = state
.pool()
.expect("substrate pool must be wired into AppState")
.clone();
let mut conn = pool
.get()
.await
.map_err(|e| AutumnError::internal_server_error(std::io::Error::other(e.to_string())))?;
diesel::sql_query("INSERT INTO sim_job_marks (id, tag) VALUES (?, ?)")
.bind::<diesel::sql_types::BigInt, _>(args.id)
.bind::<diesel::sql_types::Text, _>(args.tag)
.execute(&mut *conn)
.await
.map_err(|e| AutumnError::internal_server_error(std::io::Error::other(e.to_string())))?;
Ok(())
}
struct SimJobsPlugin;
impl Plugin for SimJobsPlugin {
fn build(self, app: AppBuilder) -> AppBuilder {
app.jobs(jobs![sim_write_mark])
}
}
#[derive(diesel::QueryableByName)]
struct MarkRow {
#[diesel(sql_type = diesel::sql_types::Text)]
tag: String,
}
#[tokio::test]
async fn job_and_scheduler_drain_end_to_end_on_sqlite_substrate() {
let _guard = job::global_job_runtime_test_lock().lock().await;
job::clear_global_job_client();
let substrate =
SqliteSubstrate::with_migrations(&[&MIGRATIONS]).expect("migrated substrate builds");
{
let pool = substrate.pool();
let mut conn = pool.get().await.expect("checkout substrate connection");
let rows: Vec<MarkRow> = diesel::sql_query("SELECT tag FROM sim_job_marks WHERE 1 = 0")
.load(&mut *conn)
.await
.expect("migrated `sim_job_marks` table is visible to the substrate pool");
assert!(rows.is_empty(), "no rows before the drain");
}
let client = TestApp::new()
.plugin(SimJobsPlugin)
.with_db(substrate.pool())
.build();
let coordinator = coordinator_from_config(&SchedulerConfig::default(), client.state())
.expect("in-process scheduler resolves under the sqlite feature");
assert_eq!(
coordinator.backend(),
"in_process",
"the sim exercises the in-process scheduler, not the Postgres advisory-lock coordinator"
);
let lease = coordinator
.try_acquire("sim_tick", "sim_tick:0", TaskCoordination::Fleet)
.await
.expect("in-process acquisition does not fail")
.expect("in-process coordinator grants the tick locally");
assert_eq!(lease.backend(), "in_process");
let pg_config = SchedulerConfig {
backend: SchedulerBackend::Postgres,
..SchedulerConfig::default()
};
let Err(pg_err) = coordinator_from_config(&pg_config, client.state()) else {
panic!("the Postgres advisory-lock scheduler must be refused under the sqlite feature");
};
assert!(
pg_err.to_string().contains("postgres"),
"rejection names the postgres backend: {pg_err}"
);
job::enqueue("sim_write_mark", json!({ "id": 1, "tag": "drained" }))
.await
.expect("enqueue via the local job backend");
client.assert_job_enqueued("sim_write_mark");
let report = client.perform_enqueued_jobs().await;
report.assert_all_succeeded();
assert_eq!(report.len(), 1, "exactly the one enqueued job drained");
{
let pool = substrate.pool();
let mut conn = pool.get().await.expect("checkout substrate connection");
let rows: Vec<MarkRow> = diesel::sql_query("SELECT tag FROM sim_job_marks ORDER BY id")
.load(&mut *conn)
.await
.expect("read back the drained row");
assert_eq!(
rows.iter().map(|r| r.tag.as_str()).collect::<Vec<_>>(),
vec!["drained"],
"the local job drain wrote exactly one row to the migrated SQLite schema"
);
}
job::clear_global_job_client();
}
#[tokio::test]
async fn two_substrates_are_isolated_databases() {
let a = SqliteSubstrate::with_migrations(&[&MIGRATIONS]).expect("substrate A builds");
let b = SqliteSubstrate::with_migrations(&[&MIGRATIONS]).expect("substrate B builds");
assert_ne!(
a.url(),
b.url(),
"each substrate gets a distinct database name"
);
{
let pool = a.pool();
let mut conn = pool.get().await.expect("checkout A");
diesel::sql_query("INSERT INTO sim_job_marks (id, tag) VALUES (1, 'only-in-a')")
.execute(&mut *conn)
.await
.expect("write into A");
}
let pool_b = b.pool();
let mut conn_b = pool_b.get().await.expect("checkout B");
let rows: Vec<MarkRow> = diesel::sql_query("SELECT tag FROM sim_job_marks")
.load(&mut *conn_b)
.await
.expect("read B");
assert!(
rows.is_empty(),
"substrate B must not see substrate A's row — they are isolated databases"
);
}