pub mod admin;
pub mod auth;
pub mod channel;
pub mod commentable;
pub mod config;
pub mod controller;
pub mod counter_cache;
pub mod dsl;
pub mod emit;
pub mod inbound_mail;
pub mod introspect;
pub mod job;
pub mod mailer;
pub mod migration;
pub mod model;
pub mod naming;
mod nested;
pub mod notifications;
pub mod plugin;
pub mod policy;
pub mod prior_index;
pub mod provenance;
pub mod pwa;
pub mod scaffold;
mod scaffold_i18n;
pub mod schema_edit;
mod sweep;
pub mod system_test;
pub mod task;
pub mod tauri;
pub mod tauri_mobile;
pub mod teams;
pub mod webhook;
pub mod wizard;
use provenance::RevertAnchor;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum GenerateError {
#[error("would overwrite existing file(s):\n{}", format_collisions(.0))]
Collisions(Vec<PathBuf>),
#[error("invalid resource name '{0}': {1}")]
InvalidName(String, String),
#[error("invalid field '{token}': {reason}")]
InvalidField {
token: String,
reason: String,
},
#[error("not inside an Autumn project (no Cargo.toml found in current directory)")]
NotInProject,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Config(String),
#[error(
"refusing to destroy — file(s) match neither the current generator output nor the \
digest recorded in {}; edit them back, or pass --force to delete them anyway:\n{}",
crate::generate::provenance::MANIFEST_PATH,
format_collisions(.0)
)]
Diverged(Vec<PathBuf>),
#[error(
"refusing to destroy — it would leave edits behind. `generate` made the edit(s) \
below. A project setting has changed since, so this destroy does not revert \
them. Undo the setting change and run destroy again, or check the files by \
hand and pass --force:\n{}",
format_anchors(.0)
)]
RevertsDropped(Vec<RevertAnchor>),
}
fn format_anchors(anchors: &[RevertAnchor]) -> String {
anchors
.iter()
.map(|a| format!(" {a}"))
.collect::<Vec<_>>()
.join("\n")
}
fn format_collisions(paths: &[PathBuf]) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(paths.len() * 60);
for (i, p) in paths.iter().enumerate() {
if i > 0 {
out.push('\n');
}
write!(out, " {}", p.display().to_string().replace('\\', "/")).unwrap();
}
out
}
#[must_use]
pub fn uuid_pk_commentable_unsupported_error() -> GenerateError {
GenerateError::Config(
"`comments:commentable` needs an integer primary key, so it cannot be combined with `--id uuid`. The shared `comments` table keys its rows on `commentable_id BIGINT` — one column serving every commentable model — and the generated helpers take `parent_id: i64`, so a UUID-keyed parent has no representation there. Re-run without `--id uuid` to use the default BIGINT primary key, or without `comments:commentable` and attach comments to a model that has one."
.to_owned(),
)
}
#[must_use]
pub fn sqlite_sharded_unsupported_error() -> GenerateError {
GenerateError::Config(
"sharded models (--sharded) require the Postgres backend: SQLite is single-host, \
single-writer, so horizontal sharding is Postgres-only and permanently out of scope \
for SQLite. A sharded resource needs a `[[database.shards]]` topology, which config \
validation rejects for a SQLite primary — so no SQLite app could use the generated \
resource. Drop `--sharded`, or target a Postgres database. See \
docs/guide/sqlite-in-production.md for SQLite's supported single-host deployment model."
.to_owned(),
)
}
#[must_use]
pub fn sqlite_add_not_null_without_default_error(table: &str, column: &str) -> GenerateError {
GenerateError::Config(format!(
"cannot add NOT NULL column `{column}` to table `{table}` on a SQLite app: SQLite \
rejects `ALTER TABLE {table} ADD COLUMN {column} … NOT NULL` without a DEFAULT once \
the table has rows (\"Cannot add a NOT NULL column with default value NULL\"). Make \
the field nullable (e.g. `{column}:Option<…>`) or give it a default so the column \
can be added safely. (A NOT NULL column is fine inside CREATE TABLE — this limit is \
specific to ALTER TABLE ADD COLUMN.)"
))
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Flags {
pub dry_run: bool,
pub force: bool,
}
pub fn ensure_project_root(dir: &Path) -> Result<(), GenerateError> {
if dir.join("Cargo.toml").is_file() {
Ok(())
} else {
Err(GenerateError::NotInProject)
}
}
pub fn timestamp_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
timestamp_from_unix(secs)
}
#[must_use]
pub fn timestamp_from_unix(unix_secs: u64) -> String {
let days = unix_secs / 86_400;
let rem = unix_secs % 86_400;
let hour = rem / 3600;
let minute = (rem % 3600) / 60;
let second = rem % 60;
let (y, m, d) = ymd_from_days(days);
format!("{y:04}{m:02}{d:02}{hour:02}{minute:02}{second:02}")
}
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "the input range (Unix seconds within Diesel's reasonable lifetime) is far\
below i64::MAX/2, so the i64↔u64 round-trip stays within bounds."
)]
const fn ymd_from_days(days_since_epoch: u64) -> (u64, u64, u64) {
let z = days_since_epoch as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u64, m, d)
}
pub fn read_or_empty(path: &Path) -> String {
std::fs::read_to_string(path).unwrap_or_default()
}
#[must_use]
pub fn detect_backend(project_root: &Path) -> autumn_web::config::DatabaseBackend {
detect_backend_for_profile(project_root, None)
}
#[must_use]
pub fn detect_backend_for_profile(
project_root: &Path,
profile: Option<&str>,
) -> autumn_web::config::DatabaseBackend {
let effective = crate::migrate::effective_profile(profile);
detect_backend_with(project_root, Some(&effective), |k| std::env::var(k))
}
#[must_use]
pub fn detect_backend_offline(
project_root: &Path,
profile: Option<&str>,
) -> autumn_web::config::DatabaseBackend {
use autumn_web::config::DatabaseBackend;
let effective = crate::migrate::effective_profile(profile);
let table = crate::migrate::read_autumn_toml_table_with_profile_in_using(
project_root,
Some(&effective),
crate::migrate::read_optional_toml_table,
);
let base = FnEnv(&|k: &str| std::env::var(k));
let dotenv: std::collections::BTreeMap<String, String> =
autumn_web::dotenv::resolve_dotenv_vars_in(project_root, &effective, &base)
.unwrap_or_default()
.into_iter()
.collect();
let env = |key: &str| {
std::env::var(key).or_else(|_| {
dotenv
.get(key)
.cloned()
.ok_or(std::env::VarError::NotPresent)
})
};
crate::migrate::resolve_primary_database_url_from_sources(env, table.as_ref())
.as_deref()
.and_then(DatabaseBackend::detect)
.unwrap_or(DatabaseBackend::Postgres)
}
fn detect_backend_with<F>(
project_root: &Path,
profile: Option<&str>,
env_var: F,
) -> autumn_web::config::DatabaseBackend
where
F: Fn(&str) -> Result<String, std::env::VarError>,
{
use autumn_web::config::DatabaseBackend;
let base = FnEnv(&env_var);
let dotenv_overlay: std::collections::BTreeMap<String, String> =
autumn_web::dotenv::resolve_dotenv_vars_in(project_root, profile.unwrap_or("dev"), &base)
.unwrap_or_default()
.into_iter()
.collect();
let env_with_dotenv = |key: &str| {
env_var(key).or_else(|_| {
dotenv_overlay
.get(key)
.cloned()
.ok_or(std::env::VarError::NotPresent)
})
};
let table = crate::migrate::read_autumn_toml_table_with_profile_in(project_root, profile);
crate::migrate::resolve_primary_database_url_from_sources(env_with_dotenv, table.as_ref())
.as_deref()
.and_then(DatabaseBackend::detect)
.unwrap_or(DatabaseBackend::Postgres)
}
struct FnEnv<F>(F);
impl<F> autumn_web::config::Env for FnEnv<F>
where
F: Fn(&str) -> Result<String, std::env::VarError>,
{
fn var(&self, key: &str) -> Result<String, std::env::VarError> {
(self.0)(key)
}
}
#[must_use]
pub fn sqlite_field_kind_unsupported_error(field: &str, rust_type: &str) -> GenerateError {
GenerateError::Config(format!(
"field `{field}` has no working diesel SQLite conversion for its Rust type \
`{rust_type}`: diesel implements no FromSql/ToSql for that type on its SQLite \
backend in a generated app's feature set, so a generated SQLite app using this \
field would fail to compile. Supported SQLite field kinds are: {kinds}. \
Use a supported field kind, or target a Postgres database.",
kinds = dsl::SQLITE_SUPPORTED_KINDS,
))
}
pub fn reject_sqlite_unsupported_field_kinds(fields: &[dsl::Field]) -> Result<(), GenerateError> {
for f in fields {
if !f.kind.sqlite_has_diesel_conversion() {
return Err(sqlite_field_kind_unsupported_error(&f.name, &f.rust_type()));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn postgres_only_leak(sql: &str) -> Option<&'static str> {
const NOT_SQLITE: &[&str] = &[
"BIGSERIAL",
"SERIAL",
"TIMESTAMPTZ",
"TIMESTAMP",
"JSONB",
"BYTEA",
"DOUBLE PRECISION",
"NOW()",
"GEN_RANDOM_UUID",
"TSVECTOR",
"USING GIN",
"USING GIST",
];
let statements = sql
.lines()
.map(|line| line.split("--").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n")
.to_uppercase();
NOT_SQLITE
.iter()
.copied()
.find(|token| contains_word(&statements, token))
}
#[test]
fn postgres_only_leak_flags_leaks_and_spares_lookalikes() {
for bad in [
"id BIGSERIAL PRIMARY KEY",
"id SERIAL PRIMARY KEY",
"at TIMESTAMPTZ NOT NULL",
"at TIMESTAMP NOT NULL",
"meta JSONB NOT NULL",
"blob BYTEA",
"x DOUBLE PRECISION",
"at TEXT NOT NULL DEFAULT NOW()",
"id TEXT DEFAULT gen_random_uuid()",
"CREATE INDEX i ON t USING GIN (v)",
] {
assert!(
postgres_only_leak(bad).is_some(),
"must be flagged as non-SQLite: {bad}"
);
}
for good in [
"serial_number TEXT NOT NULL",
"bigserial_legacy_id INTEGER",
"timestamp_source TEXT",
"comment_count BIGINT NOT NULL DEFAULT 0",
"id INTEGER PRIMARY KEY AUTOINCREMENT",
"created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
"-- was BIGSERIAL and DEFAULT NOW() on Postgres",
"id INTEGER PRIMARY KEY, -- BIGSERIAL on Postgres",
] {
assert_eq!(
postgres_only_leak(good),
None,
"must be accepted as valid SQLite DDL: {good}"
);
}
}
fn assert_no_postgres_only_sql(label: &str, dir: &std::path::Path, sql: &str) {
if let Some(token) = postgres_only_leak(sql) {
panic!(
"`generate {label}` leaked `{token}`, which is not valid SQLite DDL, into \
the SQLite migration at {}:\n{sql}",
dir.display()
);
}
}
fn contains_word(haystack: &str, needle: &str) -> bool {
let ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
let bounded_right = !needle.ends_with(ident);
haystack.match_indices(needle).any(|(at, _)| {
let before_ok = haystack[..at].chars().next_back().is_none_or(|c| !ident(c));
let after_ok = bounded_right
|| haystack[at + needle.len()..]
.chars()
.next()
.is_none_or(|c| !ident(c));
before_ok && after_ok
})
}
fn sqlite_project() -> tempfile::TempDir {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname=\"x\"\n\n[dependencies]\nautumn-web = \"0.7\"\n",
)
.unwrap();
std::fs::write(
root.join("autumn.toml"),
"[database]\nprimary_url = \"sqlite://app.db\"\n",
)
.unwrap();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("src/main.rs"),
"use autumn_web::prelude::*;\n\n\
pub fn layout(title: &str, current_path: &str, flash: maud::Markup, \
content: maud::Markup) -> maud::Markup {\n\
\x20 let _ = (current_path, flash);\n\
\x20 maud::html! { title { (title) } (content) }\n\
}\n\n\
#[autumn_web::main]\n\
async fn main() {\n\
\x20 autumn_web::app().routes(routes![]).run().await;\n\
}\n",
)
.unwrap();
tmp
}
fn assert_no_postgres_only_rust(label: &str, root: &std::path::Path) {
const POSTGRES_ONLY: &[(&str, &str)] = &[
(
".for_update()",
"use `::autumn_web::maybe_for_update!` instead",
),
(".skip_locked()", "SQLite has no locking clause"),
(".no_wait()", "SQLite has no locking clause"),
("AsyncPgConnection", "use `::autumn_web::RuntimeConnection`"),
("diesel::pg::", "use `::autumn_web::RuntimeBackend`"),
(
"on_constraint(",
"SQLite has no `ON CONFLICT ON CONSTRAINT`",
),
];
let mut stack = vec![root.join("src")];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().is_some_and(|e| e == "rs") {
let source = std::fs::read_to_string(&path).unwrap();
for (token, remedy) in POSTGRES_ONLY {
assert!(
!source.contains(token),
"`generate {label}` emitted `{token}`, which has no SQLite \
implementation, into {} — {remedy}",
path.display()
);
}
}
}
}
}
fn audit_one_generator(
label: &str,
plan: fn(&std::path::Path) -> Result<crate::generate::emit::Plan, GenerateError>,
root: &std::path::Path,
) {
use diesel::connection::SimpleConnection as _;
use diesel::prelude::*;
plan(root)
.unwrap_or_else(|e| panic!("`generate {label}` must plan on a SQLite app: {e}"))
.execute(Flags::default())
.unwrap_or_else(|e| panic!("`generate {label}` plan must execute: {e}"));
assert_no_postgres_only_rust(label, root);
let mut dirs: Vec<_> = std::fs::read_dir(root.join("migrations"))
.unwrap_or_else(|e| panic!("`generate {label}` must emit a migrations dir: {e}"))
.flatten()
.map(|e| e.path())
.filter(|p| p.join("up.sql").is_file())
.collect();
dirs.sort();
assert!(
!dirs.is_empty(),
"`generate {label}` emitted no migration to audit"
);
let mut conn = diesel::SqliteConnection::establish(":memory:").expect("in-memory sqlite");
for dir in &dirs {
let up = std::fs::read_to_string(dir.join("up.sql")).unwrap();
conn.batch_execute(&up).unwrap_or_else(|e| {
panic!(
"`generate {label}` emitted up.sql that SQLite refuses ({}): {e}\n{up}",
dir.display()
)
});
assert_no_postgres_only_sql(label, dir, &up);
}
for dir in dirs.iter().rev() {
let down = std::fs::read_to_string(dir.join("down.sql")).unwrap();
assert_no_postgres_only_sql(label, dir, &down);
conn.batch_execute(&down).unwrap_or_else(|e| {
panic!(
"`generate {label}` emitted down.sql that SQLite refuses ({}): {e}\n{down}",
dir.display()
)
});
}
}
#[test]
fn hand_written_ddl_generators_emit_applicable_sqlite_ddl() {
const TS: &str = "20260101000000";
const AUDITED: &[&str] = &[
"auth",
"mailer --list-unsubscribe",
"notifications",
"pwa",
"teams",
"model … comments:commentable",
];
type Planner = fn(&std::path::Path) -> Result<crate::generate::emit::Plan, GenerateError>;
let generators: &[(&str, Planner)] = &[
("auth", |root| {
crate::generate::auth::plan_auth_full_ex2(
root,
"User",
TS,
&crate::generate::auth::AuthOAuthOptions {
providers: vec!["github".to_owned()],
},
true,
true,
true,
)
}),
("mailer --list-unsubscribe", |root| {
crate::generate::mailer::plan_mailer(root, "Welcome", Some("news"), false)
}),
(
"notifications",
crate::generate::notifications::plan_notifications,
),
("pwa", crate::generate::pwa::plan_pwa),
("teams", |root| {
crate::generate::teams::plan_teams(root, TS, false)
}),
("model … comments:commentable", |root| {
crate::generate::model::plan_model(
root,
"Post",
&["title:String".to_owned(), "comments:commentable".to_owned()],
TS,
)
}),
];
assert_eq!(
generators
.iter()
.map(|(label, _)| *label)
.collect::<Vec<_>>(),
AUDITED,
"the audited-generator roster changed; update AUDITED alongside it"
);
for (label, plan) in generators {
temp_env::with_vars(
[
("AUTUMN_DATABASE__PRIMARY_URL", None::<&str>),
("AUTUMN_DATABASE__URL", None::<&str>),
("DATABASE_URL", None::<&str>),
],
|| {
let tmp = sqlite_project();
audit_one_generator(label, *plan, tmp.path());
},
);
}
}
#[test]
fn timestamp_format_is_14_digits() {
let ts = timestamp_now();
assert_eq!(ts.len(), 14);
assert!(ts.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn timestamp_2026_04_27_known_value() {
let ts = timestamp_from_unix(1_777_248_000);
assert_eq!(ts, "20260427000000");
}
#[test]
fn timestamp_1970_epoch() {
assert_eq!(timestamp_from_unix(0), "19700101000000");
}
#[test]
fn timestamp_handles_leap_year() {
assert_eq!(timestamp_from_unix(1_709_210_096), "20240229123456");
}
#[test]
fn ensure_project_root_succeeds_with_cargo_toml() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
assert!(ensure_project_root(tmp.path()).is_ok());
}
#[test]
fn ensure_project_root_fails_without_cargo_toml() {
let tmp = tempfile::TempDir::new().unwrap();
assert!(matches!(
ensure_project_root(tmp.path()).unwrap_err(),
GenerateError::NotInProject
));
}
fn no_env(_key: &str) -> Result<String, std::env::VarError> {
Err(std::env::VarError::NotPresent)
}
#[test]
fn detect_backend_honors_sqlite_url_in_active_profile_overlay() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join("autumn.toml"),
"[database]\nprimary_url = \"postgres://localhost/app\"\n\n\
[profile.prod.database]\nprimary_url = \"sqlite://prod.db\"\n",
)
.unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("prod"), no_env),
DatabaseBackend::Sqlite
);
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), no_env),
DatabaseBackend::Postgres
);
}
#[test]
fn detect_backend_honors_sqlite_url_in_profile_overlay_file() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join("autumn.toml"),
"[database]\nprimary_url = \"postgres://localhost/app\"\n",
)
.unwrap();
std::fs::write(
tmp.path().join("autumn-prod.toml"),
"[database]\nprimary_url = \"sqlite://prod.db\"\n",
)
.unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("prod"), no_env),
DatabaseBackend::Sqlite
);
}
#[test]
fn detect_backend_base_file_postgres_stays_postgres() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join("autumn.toml"),
"[database]\nprimary_url = \"postgres://localhost/app\"\n",
)
.unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), no_env),
DatabaseBackend::Postgres
);
}
#[test]
fn detect_backend_no_config_defaults_to_postgres() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), no_env),
DatabaseBackend::Postgres
);
}
#[test]
fn detect_backend_env_var_overrides_config() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join("autumn.toml"),
"[database]\nprimary_url = \"postgres://localhost/app\"\n",
)
.unwrap();
let env = |key: &str| {
if key == "DATABASE_URL" {
Ok("sqlite://override.db".to_owned())
} else {
Err(std::env::VarError::NotPresent)
}
};
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), env),
DatabaseBackend::Sqlite
);
}
#[test]
fn detect_backend_honors_sqlite_url_in_dotenv() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env"), "DATABASE_URL=sqlite://app.db\n").unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), no_env),
DatabaseBackend::Sqlite
);
}
#[test]
fn detect_backend_process_env_wins_over_dotenv() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env"), "DATABASE_URL=sqlite://app.db\n").unwrap();
let env = |key: &str| {
if key == "DATABASE_URL" {
Ok("postgres://localhost/app".to_owned())
} else {
Err(std::env::VarError::NotPresent)
}
};
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), env),
DatabaseBackend::Postgres
);
}
#[test]
fn detect_backend_dotenv_overlays_above_autumn_toml() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join("autumn.toml"),
"[database]\nprimary_url = \"postgres://localhost/app\"\n",
)
.unwrap();
std::fs::write(tmp.path().join(".env"), "DATABASE_URL=sqlite://app.db\n").unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), no_env),
DatabaseBackend::Sqlite
);
}
#[test]
fn detect_backend_no_dotenv_stays_postgres() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(
tmp.path().join("autumn.toml"),
"[database]\nprimary_url = \"postgres://localhost/app\"\n",
)
.unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("dev"), no_env),
DatabaseBackend::Postgres
);
}
#[test]
fn detect_backend_dotenv_gated_off_for_prod_profile() {
use autumn_web::config::DatabaseBackend;
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env"), "DATABASE_URL=sqlite://app.db\n").unwrap();
assert_eq!(
detect_backend_with(tmp.path(), Some("prod"), no_env),
DatabaseBackend::Postgres
);
}
#[test]
fn placeholder_icon_assets_match_between_desktop_and_mobile_tauri_generators() {
assert_eq!(
tauri::PLACEHOLDER_PNG,
tauri_mobile::PLACEHOLDER_PNG,
"tauri's and tauri_mobile's placeholder PNG bytes have drifted"
);
assert_eq!(
tauri::PLACEHOLDER_ICO,
tauri_mobile::PLACEHOLDER_ICO,
"tauri's and tauri_mobile's placeholder ICO bytes have drifted"
);
assert_eq!(
tauri::PLACEHOLDER_ICNS,
tauri_mobile::PLACEHOLDER_ICNS,
"tauri's and tauri_mobile's placeholder ICNS bytes have drifted"
);
assert_eq!(
tauri::render_placeholder_icon_svg(),
tauri_mobile::render_placeholder_icon_svg(),
"tauri's and tauri_mobile's placeholder SVG have drifted"
);
}
}