use std::path::Path;
use anyhow::Result;
use vcs_runner::Cmd;
use super::{run_step, test_db_name, DbEngine, ProvisionCtx, Provisioner, Setup};
pub struct Laravel;
impl Provisioner for Laravel {
fn name(&self) -> &'static str {
"laravel"
}
fn detect(&self, ws_dir: &Path) -> bool {
ws_dir.join("artisan").is_file()
}
fn setup(&self, ctx: &ProvisionCtx<'_>) -> Result<Setup> {
let xml = std::fs::read_to_string(ctx.ws_dir.join("phpunit.xml")).unwrap_or_default();
let Some(connection) = phpunit_db_connection(&xml) else { return Ok(Setup::default()) };
let engine = match connection.as_str() {
"pgsql" => DbEngine::Postgres,
"mysql" | "mariadb" => DbEngine::Mysql,
_ => return Ok(Setup::default()), };
let db = test_db_name(ctx.project_name, ctx.ws_id);
eprintln!("Creating test database {db}...");
if let Err(e) = engine.create(&db) {
return Ok(Setup::database_not_created(&db, &e));
}
let resource = engine.resource(&db);
let url = engine.url(&db);
eprintln!("Migrating (php artisan migrate)...");
let mut cmd = Cmd::new("php")
.args(["artisan", "migrate", "--force"])
.env("DB_CONNECTION", &connection)
.env("DB_URL", &url)
.in_dir(ctx.ws_dir);
for (k, v) in ctx.mise_vars {
cmd = cmd.env(k, v);
}
let failed_steps = run_step(cmd, "php artisan migrate").into_iter().collect();
Ok(Setup {
resources: vec![resource],
env: vec![("DB_CONNECTION".to_string(), connection), ("DB_URL".to_string(), url)],
env_file: Some(".env.testing".to_string()),
failed_steps,
..Setup::default()
})
}
}
pub(crate) fn phpunit_db_connection(xml: &str) -> Option<String> {
let line = xml.lines().find(|l| l.contains("DB_CONNECTION"))?;
let value = line.split("value=").nth(1)?.trim();
let value = value.strip_prefix('"').unwrap_or(value);
value.split('"').next().map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_by_artisan() {
let tmp = tempfile::tempdir().unwrap();
assert!(!Laravel.detect(tmp.path()));
std::fs::write(tmp.path().join("artisan"), "#!/usr/bin/env php\n").unwrap();
assert!(Laravel.detect(tmp.path()));
}
#[test]
fn phpunit_db_connection_parsing() {
assert_eq!(phpunit_db_connection("<env name=\"DB_CONNECTION\" value=\"pgsql\"/>").as_deref(), Some("pgsql"));
assert_eq!(phpunit_db_connection("<phpunit></phpunit>"), None);
assert_eq!(phpunit_db_connection("<env name=\"DB_CONNECTION\" value=\"\"/>").as_deref(), Some(""));
}
#[test]
fn sqlite_default_is_a_noop() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("artisan"), "#!/usr/bin/env php\n").unwrap();
std::fs::write(tmp.path().join("phpunit.xml"), "<env name=\"DB_CONNECTION\" value=\"sqlite\"/>").unwrap();
let mise = std::collections::HashMap::new();
let ctx = ProvisionCtx {
project_dir: tmp.path(),
project_name: "p",
ws_id: "ws-x",
ws_dir: tmp.path(),
mise_vars: &mise,
};
assert!(Laravel.setup(&ctx).unwrap().resources.is_empty());
}
#[test]
fn laravel_cycle_migrates_isolated_db() {
use crate::provision::Resource;
use std::collections::HashMap;
use std::process::Command;
let name = test_db_name("laravelfix", "ws-cycle");
Resource::PostgresDb { name: name.clone() }.teardown();
if DbEngine::Postgres.create(&name).is_err() {
return;
}
Resource::PostgresDb { name: name.clone() }.teardown();
let fixture = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/laravel"));
if !fixture.join("vendor/autoload.php").exists() || !fixture.join(".env").exists() {
return; }
let mise = HashMap::new();
let ctx = ProvisionCtx {
project_dir: fixture,
project_name: "laravelfix",
ws_id: "ws-cycle",
ws_dir: fixture,
mise_vars: &mise,
};
let setup = Laravel.setup(&ctx).unwrap();
assert_eq!(setup.resources, vec![Resource::PostgresDb { name: name.clone() }]);
assert_eq!(setup.env_file.as_deref(), Some(".env.testing"));
let out =
Command::new("psql").args(["-tAc", "select to_regclass('public.migrations')", &name]).output().unwrap();
let table = String::from_utf8_lossy(&out.stdout).trim().to_string();
Resource::PostgresDb { name: name.clone() }.teardown();
assert_eq!(table, "migrations", "artisan migrate should have created the migrations table");
}
#[test]
fn laravel_cycle_migrates_isolated_mysql_db() {
use crate::provision::Resource;
use std::collections::HashMap;
use std::process::Command;
let name = test_db_name("laravelmysql", "ws-cycle");
Resource::MysqlDb { name: name.clone() }.teardown();
if DbEngine::Mysql.create(&name).is_err() {
return; }
Resource::MysqlDb { name: name.clone() }.teardown();
let fixture = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/laravel"));
if !fixture.join("vendor/autoload.php").exists() || !fixture.join(".env").exists() {
return; }
let tmp = tempfile::tempdir().unwrap();
let ws_dir = tmp.path().join("laravelmysql-ws-cycle");
assert!(Command::new("cp")
.args(["-R", fixture.to_str().unwrap(), ws_dir.to_str().unwrap()])
.status()
.unwrap()
.success());
let phpunit = ws_dir.join("phpunit.xml");
let xml = std::fs::read_to_string(&phpunit)
.unwrap()
.replace("<env name=\"DB_CONNECTION\" value=\"pgsql\"/>", "<env name=\"DB_CONNECTION\" value=\"mysql\"/>");
std::fs::write(&phpunit, xml).unwrap();
let mise = HashMap::new();
let ctx = ProvisionCtx {
project_dir: &ws_dir,
project_name: "laravelmysql",
ws_id: "ws-cycle",
ws_dir: &ws_dir,
mise_vars: &mise,
};
let setup = Laravel.setup(&ctx).unwrap();
assert_eq!(setup.resources, vec![Resource::MysqlDb { name: name.clone() }]);
let user =
std::env::var("MYSQL_USER").ok().or_else(|| std::env::var("USER").ok()).unwrap_or_else(|| "root".into());
let query = format!(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='{name}' AND table_name='migrations'"
);
let out = Command::new("mysql").args(["-u", &user, "-N", "-e", &query]).output().unwrap();
let found = String::from_utf8_lossy(&out.stdout).trim() == "1";
Resource::MysqlDb { name: name.clone() }.teardown();
assert!(found, "artisan migrate should have created the migrations table in mysql");
}
proptest::proptest! {
#[test]
fn the_db_connection_value_reads_back(
value in "[a-z0-9_]{0,10}",
attrs in "( force=\"true\")?",
others in proptest::collection::vec(" <env name=\"[A-Z_]{1,8}\" value=\"[a-z]{0,5}\"/>", 0..3),
) {
let xml = format!(
"<phpunit>\n <php>\n{}\n <env name=\"DB_CONNECTION\" value=\"{value}\"{attrs}/>\n </php>\n</phpunit>\n",
others.join("\n")
);
proptest::prop_assert_eq!(phpunit_db_connection(&xml), Some(value));
}
}
}