use std::path::Path;
use anyhow::Result;
use vcs_runner::Cmd;
use super::{test_db_name, DbEngine, ProvisionCtx, Provisioner, run_step, Setup};
pub struct Rails;
impl Provisioner for Rails {
fn name(&self) -> &'static str {
"rails"
}
fn detect(&self, ws_dir: &Path) -> bool {
ws_dir.join("config/database.yml").is_file()
}
fn setup(&self, ctx: &ProvisionCtx<'_>) -> Result<Setup> {
let yml = std::fs::read_to_string(ctx.ws_dir.join("config/database.yml")).unwrap_or_default();
let engine = match adapter(&yml).as_deref() {
Some("postgresql") | None => DbEngine::Postgres,
Some("mysql2") | Some("mysql") | Some("trilogy") => DbEngine::Mysql,
Some("sqlite3") => return Ok(Setup::default()), Some(other) => {
eprintln!("Rails adapter `{other}` not supported; skipping DB setup");
return Ok(Setup::default());
}
};
let db = test_db_name(ctx.project_name, ctx.ws_id);
eprintln!("Creating test database {db}...");
if engine.create(&db).is_err() {
eprintln!("Warning: could not create test database {db}");
return Ok(Setup::default());
}
let resource = engine.resource(&db);
let url = engine.url(&db);
eprintln!("Loading schema...");
let mut cmd = Cmd::new("bundle")
.args(["exec", "rails", "db:schema:load"])
.env("RAILS_ENV", "test")
.env("DATABASE_URL", &url)
.in_dir(ctx.ws_dir);
for (k, v) in ctx.mise_vars {
cmd = cmd.env(k, v);
}
run_step(cmd, "rails db:schema:load");
Ok(Setup {
resources: vec![resource],
env: vec![("DATABASE_URL".to_string(), url)],
env_file: None, ..Setup::default()
})
}
}
pub(crate) fn adapter(database_yml: &str) -> Option<String> {
database_yml.lines().find_map(|l| {
let v = l.trim().strip_prefix("adapter:")?.trim();
(!v.is_empty()).then(|| v.to_string())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adapter_reads_first_adapter_line() {
assert_eq!(adapter("default: &default\n adapter: postgresql\n").as_deref(), Some("postgresql"));
assert_eq!(adapter("test:\n adapter: mysql2\n").as_deref(), Some("mysql2"));
assert_eq!(adapter("test:\n database: x\n"), None);
}
#[test]
fn detects_a_rails_app_by_database_yml() {
let tmp = tempfile::tempdir().unwrap();
assert!(!Rails.detect(tmp.path()), "no config/database.yml -> not detected");
std::fs::create_dir(tmp.path().join("config")).unwrap();
std::fs::write(tmp.path().join("config/database.yml"), "test:\n adapter: postgresql\n").unwrap();
assert!(Rails.detect(tmp.path()), "config/database.yml -> detected");
}
#[test]
fn setup_creates_db_and_returns_resource_and_env() {
use crate::provision::Resource;
use std::collections::HashMap;
let name = test_db_name("provtest", "ws-selftest");
Resource::PostgresDb { name: name.clone() }.teardown();
if DbEngine::Postgres.create(&name).is_err() {
return; }
Resource::PostgresDb { name: name.clone() }.teardown();
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("config")).unwrap();
std::fs::write(tmp.path().join("config/database.yml"), "test:\n adapter: postgresql\n").unwrap();
let mise = HashMap::new();
let ctx = ProvisionCtx {
project_dir: tmp.path(),
project_name: "provtest",
ws_id: "ws-selftest",
ws_dir: tmp.path(),
mise_vars: &mise,
};
let setup = Rails.setup(&ctx).unwrap();
Resource::PostgresDb { name: name.clone() }.teardown();
assert_eq!(setup.resources, vec![Resource::PostgresDb { name: name.clone() }]);
assert_eq!(setup.env.len(), 1);
assert_eq!(setup.env[0].0, "DATABASE_URL");
assert!(setup.env[0].1.contains(&name), "url targets the db: {}", setup.env[0].1);
}
#[test]
fn rails_cycle_loads_schema_into_isolated_db() {
use crate::provision::Resource;
use std::collections::HashMap;
use std::process::Command;
let name = test_db_name("railsfix", "ws-cycle");
Resource::PostgresDb { name: name.clone() }.teardown();
if DbEngine::Postgres.create(&name).is_err() {
return; }
Resource::PostgresDb { name: name.clone() }.teardown();
let fixture = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/rails");
let bundle_ready = Command::new("bundle")
.arg("check")
.current_dir(fixture)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !bundle_ready {
return; }
let tmp = tempfile::tempdir().unwrap();
let ws_dir = tmp.path().join("railsfix-ws-cycle");
assert!(
Command::new("cp").args(["-R", fixture, ws_dir.to_str().unwrap()]).status().unwrap().success(),
"copy fixture",
);
let mise = HashMap::new();
let ctx = ProvisionCtx {
project_dir: &ws_dir,
project_name: "railsfix",
ws_id: "ws-cycle",
ws_dir: &ws_dir,
mise_vars: &mise,
};
let setup = Rails.setup(&ctx).unwrap();
assert_eq!(setup.resources, vec![Resource::PostgresDb { name: name.clone() }]);
let out = Command::new("psql")
.args(["-tAc", "select to_regclass('public.widgets')", &name])
.output()
.unwrap();
let table = String::from_utf8_lossy(&out.stdout).trim().to_string();
Resource::PostgresDb { name: name.clone() }.teardown();
assert_eq!(table, "widgets", "db:schema:load should have created the widgets table");
}
}