use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use anyhow::{Context, Result};
use std::path::Path;
static FIXTURE_SCHEMA: &str = include_str!("schema/fixture.schema.json");
const FIXTURE_SCHEMA_MARKER: &str = "Auto-generated by alef -- do not edit by hand.";
pub fn sync_fixture_schema(fixtures_dir: &Path) -> Result<()> {
std::fs::create_dir_all(fixtures_dir)
.with_context(|| format!("failed to create fixtures dir: {}", fixtures_dir.display()))?;
let schema_path = fixtures_dir.join("schema.json");
if let Ok(existing) = std::fs::read_to_string(&schema_path)
&& !existing.contains(FIXTURE_SCHEMA_MARKER)
{
tracing::warn!(
"refusing to write {}: pre-existing file carries no alef ownership marker -- leaving it untouched",
schema_path.display()
);
return Ok(());
}
std::fs::write(&schema_path, FIXTURE_SCHEMA).with_context(|| format!("failed to write {}", schema_path.display()))
}
pub fn init_fixtures(e2e_config: &E2eConfig, _config: &ResolvedCrateConfig) -> Result<Vec<String>> {
let fixtures_dir = Path::new(&e2e_config.fixtures);
let mut created = Vec::new();
if !fixtures_dir.exists() {
std::fs::create_dir_all(fixtures_dir)
.with_context(|| format!("failed to create fixtures dir: {}", fixtures_dir.display()))?;
created.push(fixtures_dir.display().to_string());
}
let schema_path = fixtures_dir.join("schema.json");
sync_fixture_schema(fixtures_dir)?;
created.push(schema_path.display().to_string());
let smoke_dir = fixtures_dir.join("smoke");
if !smoke_dir.exists() {
std::fs::create_dir_all(&smoke_dir)
.with_context(|| format!("failed to create smoke dir: {}", smoke_dir.display()))?;
created.push(smoke_dir.display().to_string());
}
let basic_path = smoke_dir.join("basic.json");
if !basic_path.exists() {
let basic_fixture = build_example_fixture(e2e_config);
std::fs::write(&basic_path, basic_fixture)
.with_context(|| format!("failed to write {}", basic_path.display()))?;
created.push(basic_path.display().to_string());
}
Ok(created)
}
pub fn scaffold_fixture(
e2e_config: &E2eConfig,
_config: &ResolvedCrateConfig,
id: &str,
category: &str,
description: &str,
) -> Result<String> {
let fixtures_dir = Path::new(&e2e_config.fixtures);
let category_dir = fixtures_dir.join(category);
if !category_dir.exists() {
std::fs::create_dir_all(&category_dir)
.with_context(|| format!("failed to create category dir: {}", category_dir.display()))?;
}
let fixture_path = category_dir.join(format!("{id}.json"));
if fixture_path.exists() {
anyhow::bail!(
"refusing to overwrite existing fixture at {}; pick a different --id or edit it directly",
fixture_path.display()
);
}
let fixture = build_scaffold_fixture(e2e_config, id, description);
std::fs::write(&fixture_path, fixture).with_context(|| format!("failed to write {}", fixture_path.display()))?;
Ok(fixture_path.display().to_string())
}
fn build_example_fixture(e2e_config: &E2eConfig) -> String {
let mut input_fields = Vec::new();
for arg in &e2e_config.call.args {
let value = example_value_for_type(&arg.arg_type);
input_fields.push(format!(" \"{}\": {value}", arg.field));
}
let input_block = if input_fields.is_empty() {
"{}".to_string()
} else {
format!("{{\n{}\n }}", input_fields.join(",\n"))
};
format!(
r#"{{
"id": "basic_smoke",
"description": "Basic smoke test verifying the function returns without error",
"input": {input_block},
"assertions": [
{{ "type": "not_error" }}
]
}}
"#
)
}
fn build_scaffold_fixture(e2e_config: &E2eConfig, id: &str, description: &str) -> String {
let mut input_fields = Vec::new();
for arg in &e2e_config.call.args {
let value = empty_value_for_type(&arg.arg_type);
input_fields.push(format!(" \"{}\": {value}", arg.field));
}
let input_block = if input_fields.is_empty() {
"{}".to_string()
} else {
format!("{{\n{}\n }}", input_fields.join(",\n"))
};
format!(
r#"{{
"id": "{id}",
"description": "{description}",
"input": {input_block},
"assertions": [
{{ "type": "not_error" }}
]
}}
"#
)
}
fn example_value_for_type(arg_type: &str) -> &'static str {
match arg_type {
"string" => "\"example\"",
"int" | "integer" => "0",
"float" | "number" => "0.0",
"bool" | "boolean" => "true",
"json_object" => "{}",
"bytes" => "\"\"",
_ => "\"\"",
}
}
fn empty_value_for_type(arg_type: &str) -> &'static str {
match arg_type {
"string" => "\"\"",
"int" | "integer" => "0",
"float" | "number" => "0.0",
"bool" | "boolean" => "false",
"json_object" => "{}",
"bytes" => "\"\"",
_ => "\"\"",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn e2e_config_for(fixtures_dir: &Path) -> E2eConfig {
E2eConfig {
fixtures: fixtures_dir.to_string_lossy().into_owned(),
..E2eConfig::default()
}
}
#[test]
fn refreshes_a_prior_alef_owned_schema() {
let directory = tempfile::tempdir().expect("temporary fixture directory");
let schema = directory.path().join("schema.json");
std::fs::write(&schema, format!("{{\"$comment\": \"{FIXTURE_SCHEMA_MARKER}\"}}"))
.expect("write stale alef-owned schema");
super::sync_fixture_schema(directory.path()).expect("refresh fixture schema");
let refreshed = std::fs::read_to_string(schema).expect("read refreshed schema");
assert!(refreshed.contains("FixtureDocsPresentation") || refreshed.contains("docs_presentation"));
}
#[test]
fn refuses_to_overwrite_a_hand_written_schema() {
let directory = tempfile::tempdir().expect("temporary fixture directory");
let schema = directory.path().join("schema.json");
let hand_written = "{\"note\": \"we hand-rolled a stricter schema\"}";
std::fs::write(&schema, hand_written).expect("write hand-written schema");
super::sync_fixture_schema(directory.path()).expect("refusal is not an error");
let preserved = std::fs::read_to_string(schema).expect("read schema after refused sync");
assert_eq!(
preserved, hand_written,
"a schema with no alef marker must be left byte-for-byte untouched"
);
}
#[test]
fn init_fixtures_does_not_overwrite_an_edited_smoke_fixture() {
let directory = tempfile::tempdir().expect("temporary fixture directory");
let fixtures_dir = directory.path().join("fixtures");
let smoke_dir = fixtures_dir.join("smoke");
std::fs::create_dir_all(&smoke_dir).expect("create smoke dir");
let basic_path = smoke_dir.join("basic.json");
let hand_edited = "{\"id\": \"basic_smoke\", \"description\": \"hand-edited real assertions\"}";
std::fs::write(&basic_path, hand_edited).expect("seed hand-edited fixture");
let e2e_config = e2e_config_for(&fixtures_dir);
let created = init_fixtures(&e2e_config, &ResolvedCrateConfig::default()).expect("init fixtures");
let preserved = std::fs::read_to_string(&basic_path).expect("read fixture after init");
assert_eq!(
preserved, hand_edited,
"a pre-existing smoke/basic.json must survive `alef e2e init`"
);
assert!(
!created.iter().any(|path| path.contains("basic.json")),
"an untouched pre-existing file must not be reported as created: {created:?}"
);
}
#[test]
fn scaffold_fixture_refuses_to_overwrite_an_existing_id() {
let directory = tempfile::tempdir().expect("temporary fixture directory");
let fixtures_dir = directory.path().join("fixtures");
let e2e_config = e2e_config_for(&fixtures_dir);
let config = ResolvedCrateConfig::default();
scaffold_fixture(&e2e_config, &config, "checks_something", "unit", "first description")
.expect("first scaffold succeeds");
let fixture_path = fixtures_dir.join("unit").join("checks_something.json");
let first_content = std::fs::read_to_string(&fixture_path).expect("read first fixture");
let result = scaffold_fixture(
&e2e_config,
&config,
"checks_something",
"unit",
"clobbering description",
);
assert!(
result.is_err(),
"scaffolding the same id twice must refuse the second write"
);
let preserved = std::fs::read_to_string(&fixture_path).expect("read fixture after refused scaffold");
assert_eq!(
preserved, first_content,
"a refused scaffold must leave the existing fixture byte-for-byte untouched"
);
}
}