use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "assertion")]
pub enum GoldAssertion {
#[serde(rename = "no_diagnostics")]
NoDiagnostics,
#[serde(rename = "no_diagnostic")]
NoDiagnostic { code: String },
#[serde(rename = "diagnostic_present")]
DiagnosticPresent {
code: String,
#[serde(default)]
byte_offset: Option<usize>,
#[serde(default)]
message_contains: Option<String>,
},
#[serde(rename = "diagnostic_count")]
DiagnosticCount { code: String, count: usize },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoldExpected {
pub diagnostics: Vec<GoldAssertion>,
}
#[derive(Debug, Clone)]
pub struct GoldFixture {
pub name: String,
pub fixture_path: PathBuf,
pub expected: GoldExpected,
}
pub fn load_gold_fixture<P: AsRef<Path>>(
dir: P,
) -> Result<GoldFixture, Box<dyn std::error::Error>> {
let dir = dir.as_ref();
let name = dir.file_name().ok_or("No directory name")?.to_string_lossy().to_string();
let fixture_path = dir.join("fixture.pl");
let expected_path = dir.join("expected.json");
if !fixture_path.exists() {
return Err(format!("fixture.pl not found in {}", dir.display()).into());
}
if !expected_path.exists() {
return Err(format!("expected.json not found in {}", dir.display()).into());
}
let expected_json = fs::read_to_string(&expected_path)?;
let expected: GoldExpected = serde_json::from_str(&expected_json)?;
Ok(GoldFixture { name, fixture_path, expected })
}
pub fn load_gold_fixtures<P: AsRef<Path>>(
root: P,
) -> Result<Vec<GoldFixture>, Box<dyn std::error::Error>> {
load_gold_fixtures_from(root)
}
pub fn load_gold_fixtures_from<P: AsRef<Path>>(
root: P,
) -> Result<Vec<GoldFixture>, Box<dyn std::error::Error>> {
let root = root.as_ref();
let mut fixtures = Vec::new();
if !root.exists() {
return Err(format!("Gold fixtures directory not found: {}", root.display()).into());
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
match load_gold_fixture(&path) {
Ok(fixture) => fixtures.push(fixture),
Err(e) => {
tracing::warn!("Failed to load fixture from {}: {}", path.display(), e);
}
}
}
}
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
Ok(fixtures)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum HoverAssertionKind {
HoverNonNull,
HoverNull,
HoverContains { needle: String },
HoverAbsent { needle: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HoverAssertion {
#[serde(flatten)]
pub kind: HoverAssertionKind,
pub line: u32,
pub character: u32,
#[serde(default)]
pub rationale: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HoverGoldExpected {
pub version: u32,
pub fixture: String,
pub assertions: Vec<HoverAssertion>,
}
#[derive(Debug, Clone)]
pub struct HoverGoldFixture {
pub name: String,
pub fixture_path: PathBuf,
pub hover_assertions: Vec<HoverAssertion>,
}
pub fn load_hover_gold_fixtures<P: AsRef<Path>>(
root: P,
) -> Result<Vec<HoverGoldFixture>, Box<dyn std::error::Error>> {
let root = root.as_ref();
let mut fixtures = Vec::new();
if !root.exists() {
return Err(format!("Gold fixtures directory not found: {}", root.display()).into());
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let fixture_path = path.join("fixture.pl");
let hover_path = path.join("expected_hover.json");
if !fixture_path.exists() || !hover_path.exists() {
continue; }
let name = path.file_name().ok_or("No directory name")?.to_string_lossy().to_string();
let json = fs::read_to_string(&hover_path)?;
let expected: HoverGoldExpected = serde_json::from_str(&json)
.map_err(|e| format!("Parsing {}: {e}", hover_path.display()))?;
fixtures.push(HoverGoldFixture {
name,
fixture_path,
hover_assertions: expected.assertions,
});
}
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
Ok(fixtures)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum GotoAssertionKind {
GotoNonNull,
GotoNull,
GotoLine { expected_line: u32 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GotoAssertion {
#[serde(flatten)]
pub kind: GotoAssertionKind,
pub line: u32,
pub character: u32,
#[serde(default)]
pub rationale: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GotoGoldExpected {
pub version: u32,
pub fixture: String,
pub assertions: Vec<GotoAssertion>,
}
#[derive(Debug, Clone)]
pub struct GotoGoldFixture {
pub name: String,
pub fixture_path: PathBuf,
pub goto_assertions: Vec<GotoAssertion>,
}
pub fn load_goto_gold_fixtures<P: AsRef<Path>>(
root: P,
) -> Result<Vec<GotoGoldFixture>, Box<dyn std::error::Error>> {
let root = root.as_ref();
let mut fixtures = Vec::new();
if !root.exists() {
return Err(format!("Gold fixtures directory not found: {}", root.display()).into());
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let fixture_path = path.join("fixture.pl");
let goto_path = path.join("expected_goto.json");
if !fixture_path.exists() || !goto_path.exists() {
continue;
}
let name = path.file_name().ok_or("No directory name")?.to_string_lossy().to_string();
let json = fs::read_to_string(&goto_path)?;
let expected: GotoGoldExpected = serde_json::from_str(&json)
.map_err(|e| format!("Parsing {}: {e}", goto_path.display()))?;
fixtures.push(GotoGoldFixture { name, fixture_path, goto_assertions: expected.assertions });
}
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
Ok(fixtures)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum CompletionAssertionKind {
CompletionNonEmpty,
CompletionTop1 { expected_label: String },
CompletionTop5 { expected_label: String },
CompletionPresent { expected_label: String },
CompletionNoiseAbsent { forbidden_label: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionAssertion {
#[serde(flatten)]
pub kind: CompletionAssertionKind,
pub line: u32,
pub character: u32,
#[serde(default)]
pub rationale: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionGoldExpected {
pub version: u32,
pub fixture: String,
pub assertions: Vec<CompletionAssertion>,
}
#[derive(Debug, Clone)]
pub struct CompletionGoldFixture {
pub name: String,
pub fixture_path: PathBuf,
pub completion_assertions: Vec<CompletionAssertion>,
}
pub fn load_completion_gold_fixtures<P: AsRef<Path>>(
root: P,
) -> Result<Vec<CompletionGoldFixture>, Box<dyn std::error::Error>> {
let root = root.as_ref();
let mut fixtures = Vec::new();
if !root.exists() {
return Err(format!("Gold fixtures directory not found: {}", root.display()).into());
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let fixture_path = path.join("fixture.pl");
let completion_path = path.join("expected_completion.json");
if !fixture_path.exists() || !completion_path.exists() {
continue;
}
let name = path.file_name().ok_or("No directory name")?.to_string_lossy().to_string();
let json = fs::read_to_string(&completion_path)?;
let expected: CompletionGoldExpected = serde_json::from_str(&json)
.map_err(|e| format!("Parsing {}: {e}", completion_path.display()))?;
fixtures.push(CompletionGoldFixture {
name,
fixture_path,
completion_assertions: expected.assertions,
});
}
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
Ok(fixtures)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum DocumentSymbolAssertionKind {
SymbolNonEmpty,
SymbolPresent { name: String },
SymbolAbsent { name: String },
SymbolCount { count: usize },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentSymbolAssertion {
#[serde(flatten)]
pub kind: DocumentSymbolAssertionKind,
#[serde(default)]
pub rationale: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentSymbolGoldExpected {
pub version: u32,
pub fixture: String,
pub assertions: Vec<DocumentSymbolAssertion>,
}
#[derive(Debug, Clone)]
pub struct DocumentSymbolGoldFixture {
pub name: String,
pub fixture_path: PathBuf,
pub symbol_assertions: Vec<DocumentSymbolAssertion>,
}
pub fn load_document_symbol_gold_fixtures<P: AsRef<Path>>(
root: P,
) -> Result<Vec<DocumentSymbolGoldFixture>, Box<dyn std::error::Error>> {
let root = root.as_ref();
let mut fixtures = Vec::new();
if !root.exists() {
return Err(format!("Gold fixtures directory not found: {}", root.display()).into());
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let fixture_path = path.join("fixture.pl");
let symbols_path = path.join("expected_symbols.json");
if !fixture_path.exists() || !symbols_path.exists() {
continue;
}
let name = path.file_name().ok_or("No directory name")?.to_string_lossy().to_string();
let json = fs::read_to_string(&symbols_path)?;
let expected: DocumentSymbolGoldExpected = serde_json::from_str(&json)
.map_err(|e| format!("Parsing {}: {e}", symbols_path.display()))?;
fixtures.push(DocumentSymbolGoldFixture {
name,
fixture_path,
symbol_assertions: expected.assertions,
});
}
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
Ok(fixtures)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gold_assertion_deserialization() -> Result<(), serde_json::Error> {
let json = r#"{"assertion": "no_diagnostics"}"#;
let assertion: GoldAssertion = serde_json::from_str(json)?;
assert!(matches!(assertion, GoldAssertion::NoDiagnostics));
Ok(())
}
#[test]
fn test_gold_assertion_diagnostic_present() -> Result<(), serde_json::Error> {
let json = r#"{"assertion": "diagnostic_present", "code": "PL100", "byte_offset": 24}"#;
let assertion: GoldAssertion = serde_json::from_str(json)?;
assert!(
matches!(
&assertion,
GoldAssertion::DiagnosticPresent {
code,
byte_offset: Some(24),
..
} if code == "PL100"
),
"Expected DiagnosticPresent variant with code PL100 and byte_offset 24"
);
Ok(())
}
#[test]
fn test_gold_expected_deserialization() -> Result<(), serde_json::Error> {
let json = r#"{"diagnostics": [{"assertion": "no_diagnostics"}]}"#;
let expected: GoldExpected = serde_json::from_str(json)?;
assert_eq!(expected.diagnostics.len(), 1);
Ok(())
}
#[test]
fn test_malformed_json_returns_error() {
let bad_json = r#"{"diagnostics": [{"assertion": "unknown_variant"}]}"#;
let result: Result<GoldExpected, _> = serde_json::from_str(bad_json);
assert!(result.is_err(), "unknown assertion variant should fail to deserialize");
}
#[test]
fn test_empty_diagnostics_array_deserializes() -> Result<(), serde_json::Error> {
let json = r#"{"diagnostics": []}"#;
let expected: GoldExpected = serde_json::from_str(json)?;
assert_eq!(expected.diagnostics.len(), 0);
Ok(())
}
#[test]
fn test_no_diagnostic_deserialization() -> Result<(), serde_json::Error> {
let json = r#"{"assertion": "no_diagnostic", "code": "PL100"}"#;
let assertion: GoldAssertion = serde_json::from_str(json)?;
assert!(
matches!(&assertion, GoldAssertion::NoDiagnostic { code } if code == "PL100"),
"Expected NoDiagnostic variant with code PL100"
);
Ok(())
}
#[test]
fn test_diagnostic_count_deserialization() -> Result<(), serde_json::Error> {
let json = r#"{"assertion": "diagnostic_count", "code": "PL001", "count": 3}"#;
let assertion: GoldAssertion = serde_json::from_str(json)?;
assert!(
matches!(&assertion, GoldAssertion::DiagnosticCount { code, count: 3 } if code == "PL001"),
"Expected DiagnosticCount variant with code PL001 and count 3"
);
Ok(())
}
}