use std::{fmt::Display, iter::zip, ops::Not, path::PathBuf, sync::LazyLock};
use cairo_lang_utils::iterators::zip_eq3;
use cairo_language_server::lsp::ext::ExpandMacro;
use itertools::Itertools;
use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams};
use serde::Serialize;
use serde_json::json;
use crate::support::{
MockClient,
cursor::peek_caret,
cursors,
diagnostics::{DiagnosticAndRelatedInfo, DiagnosticsWithUrl, get_related_diagnostic_code},
fixture::Fixture,
itertools::IteratorExtension,
normalize::normalize_diagnostics,
sandbox,
};
mod builtin;
mod fixtures;
mod procedural;
mod user_inline;
pub const SCARB_TEST_MACROS_PACKAGE_NAME: &str = "scarb_procedural_macros";
pub static SCARB_TEST_MACROS_PACKAGE: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join(SCARB_TEST_MACROS_PACKAGE_NAME)
.canonicalize()
.expect("should be able to obtain an absolute path to `scarb_procedural_macros`")
});
pub const SCARB_TEST_MACROS_V2_PACKAGE_NAME: &str = "scarb_procedural_macros_v2";
pub static SCARB_TEST_MACROS_V2_PACKAGE: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join(SCARB_TEST_MACROS_V2_PACKAGE_NAME)
.canonicalize()
.expect("should be able to obtain an absolute path to `scarb_procedural_macros_v2`")
});
macro_rules! test_macro_expansion_and_diagnostics {
(
project = $project:ident,
cwd = $cwd:expr,
files { $($path:expr => $content:expr),* $(,)? }
) => {
let files = vec![$(($path.to_string(), $content.to_string()),)*];
let (report, ls) =
<$project as $crate::macros::MacroTest>::test_many($cwd, files);
::insta::assert_snapshot!(report);
drop(ls);
};
($test_setup:ident, $code_with_cursors:expr) => {
let (report, ls) =
<$test_setup as $crate::macros::MacroTest>::test(::indoc::indoc!($code_with_cursors));
::insta::assert_snapshot!(report);
drop(ls); };
}
pub(crate) use test_macro_expansion_and_diagnostics;
#[derive(Debug, Serialize)]
pub struct Report {
pub expansions: Option<ExpansionsReport>,
pub mapped_diagnostics: Option<DiagnosticsReport>,
}
#[derive(Debug, Serialize)]
pub struct ExpansionGroup {
analyzed_lines: String,
generated_code: String,
}
#[derive(Debug, Serialize)]
pub struct ExpansionsReport {
pub expansions: Vec<ExpansionGroup>,
}
#[derive(Debug, Serialize)]
pub struct DiagnosticsReport {
pub mapped_diagnostics: Vec<DiagnosticsWithUrl>,
}
impl Report {
fn new(expansions: Vec<ExpansionGroup>, mapped_diagnostics: Vec<DiagnosticsWithUrl>) -> Self {
let expansions = expansions.is_empty().not().then_some(ExpansionsReport { expansions });
let mapped_diagnostics =
mapped_diagnostics.is_empty().not().then_some(DiagnosticsReport { mapped_diagnostics });
Self { mapped_diagnostics, expansions }
}
}
impl Display for Report {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref expansions) = self.expansions {
writeln!(
f,
"{}",
toml::to_string_pretty(expansions)
.expect("ExpansionsReport should be serializable to TOML.")
)?;
}
if let Some(ref diagnostics) = self.mapped_diagnostics {
write!(
f,
"{}",
serde_yaml::to_string(diagnostics)
.expect("DiagnosticsReport should be serializable to JSON.")
)?;
}
Ok(())
}
}
pub trait MacroTest {
fn fixture() -> Fixture;
fn workspace_configuration() -> serde_json::Value {
json!({
"cairo1": {
"enableProcMacros": true,
"traceMacroDiagnostics": true,
}
})
}
fn test_many(
cwd: &str,
cairo_codes_with_carets: Vec<(String, String)>,
) -> (Report, MockClient) {
let (paths, codes_with_carets): (Vec<_>, Vec<_>) =
cairo_codes_with_carets.into_iter().unzip();
let (codes, cursors): (Vec<_>, Vec<_>) =
codes_with_carets.iter().map(|code| cursors(code)).unzip();
let mut fixture = Self::fixture();
for (path, code) in zip(&paths, &codes) {
fixture.add_file(path, code);
}
let mut ls = sandbox! {
fixture = fixture;
cwd = cwd;
workspace_configuration = Self::workspace_configuration();
};
let diagnostics = ls.open_and_wait_for_diagnostics_generation(&paths[0]);
let mapped_diagnostics = normalize_diagnostics(&ls, diagnostics)
.into_iter()
.filter_map(|(original_url, normalized_url, diagnostics)| {
if diagnostics.is_empty() {
return None;
}
Some(DiagnosticsWithUrl {
url: normalized_url,
diagnostics: diagnostics
.into_iter()
.map(|diag| DiagnosticAndRelatedInfo {
related_code: get_related_diagnostic_code(
&mut ls,
&diag,
&original_url,
),
diagnostic: diag,
})
.collect(),
})
})
.collect();
let expansions = zip_eq3(paths, codes, cursors)
.flat_map(|(path, code, cursors)| {
cursors
.carets()
.into_iter()
.map(|position| {
let expansion = get_expansion_at(&mut ls, &path, position) + "\n";
(expansion, position)
})
.into_ordered_group_map()
.into_iter()
.map(|(generated_code, positions)| {
let analyzed_lines = positions
.into_iter()
.map(|position| peek_caret(&code, position))
.join("");
ExpansionGroup { generated_code, analyzed_lines }
})
.collect::<Vec<_>>() })
.collect();
let report = Report::new(expansions, mapped_diagnostics);
(report, ls)
}
}
fn get_expansion_at(ls: &mut MockClient, source_path: &str, position: Position) -> String {
let macro_expansion = ls.send_request::<ExpandMacro>(TextDocumentPositionParams {
position,
text_document: TextDocumentIdentifier { uri: ls.doc_id(source_path).uri },
});
macro_expansion.unwrap_or_else(|| String::from("No expansion information."))
}