1use cairo_lang_defs::plugin::PluginDiagnostic;
2use cairo_lang_syntax::attribute::structured::{Attribute, AttributeArg, AttributeArgVariant};
3use cairo_lang_syntax::node::{TypedStablePtr, TypedSyntaxNode, ast};
4use cairo_lang_utils::byte_array::{BYTE_ARRAY_MAGIC, BYTES_IN_WORD};
5use cairo_lang_utils::{OptionHelper, require};
6use itertools::chain;
7use num_bigint::{BigInt, Sign};
8use num_traits::ToPrimitive;
9use salsa::Database;
10use serde::{Deserialize, Serialize};
11use starknet_types_core::felt::Felt as Felt252;
12
13use super::{AVAILABLE_GAS_ATTR, IGNORE_ATTR, SHOULD_PANIC_ATTR, STATIC_GAS_ARG, TEST_ATTR};
14
15#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
17pub enum PanicExpectation {
18 Any,
20 Exact(Vec<Felt252>),
22}
23
24#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
26pub enum TestExpectation {
27 Success,
29 Panics(PanicExpectation),
31}
32
33#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
35pub struct TestConfig {
36 pub available_gas: Option<usize>,
38 pub expectation: TestExpectation,
40 pub ignored: bool,
42}
43
44pub fn try_extract_test_config<'db>(
47 db: &'db dyn Database,
48 attrs: &[Attribute<'db>],
49) -> Result<Option<TestConfig>, Vec<PluginDiagnostic<'db>>> {
50 let test_attr = attrs.iter().find(|attr| attr.id.long(db) == TEST_ATTR);
51 let ignore_attr = attrs.iter().find(|attr| attr.id.long(db) == IGNORE_ATTR);
52 let available_gas_attr = attrs.iter().find(|attr| attr.id.long(db) == AVAILABLE_GAS_ATTR);
53 let should_panic_attr = attrs.iter().find(|attr| attr.id.long(db) == SHOULD_PANIC_ATTR);
54 let mut diagnostics = vec![];
55 if let Some(attr) = test_attr {
56 if !attr.args.is_empty() {
57 diagnostics.push(PluginDiagnostic::error(
58 attr.id_stable_ptr.untyped(),
59 "Attribute should not have arguments.".into(),
60 ));
61 }
62 } else {
63 for attr in [ignore_attr, available_gas_attr, should_panic_attr].into_iter().flatten() {
64 diagnostics.push(PluginDiagnostic::error(
65 attr.id_stable_ptr.untyped(),
66 "Attribute should only appear on tests.".into(),
67 ));
68 }
69 }
70 let ignored = if let Some(attr) = ignore_attr {
71 if !attr.args.is_empty() {
72 diagnostics.push(PluginDiagnostic::error(
73 attr.id_stable_ptr.untyped(),
74 "Attribute should not have arguments.".into(),
75 ));
76 }
77 true
78 } else {
79 false
80 };
81 let available_gas = extract_available_gas(available_gas_attr, db, &mut diagnostics);
82 let (should_panic, expected_panic_felts) = if let Some(attr) = should_panic_attr {
83 if attr.args.is_empty() {
84 (true, None)
85 } else {
86 (
87 true,
88 extract_panic_bytes(db, attr).on_none(|| {
89 diagnostics.push(PluginDiagnostic::error(
90 attr.args_stable_ptr.untyped(),
91 "Expected panic must be of the form `expected: <tuple of felt252s and \
92 strings>` or `expected: \"some string\"` or `expected: <some felt252>`."
93 .into(),
94 ));
95 }),
96 )
97 }
98 } else {
99 (false, None)
100 };
101 if !diagnostics.is_empty() {
102 return Err(diagnostics);
103 }
104 Ok(if test_attr.is_none() {
105 None
106 } else {
107 Some(TestConfig {
108 available_gas,
109 expectation: if should_panic {
110 TestExpectation::Panics(if let Some(felts) = expected_panic_felts {
111 PanicExpectation::Exact(felts)
112 } else {
113 PanicExpectation::Any
114 })
115 } else {
116 TestExpectation::Success
117 },
118 ignored,
119 })
120 })
121}
122
123fn extract_available_gas<'db>(
127 available_gas_attr: Option<&Attribute<'db>>,
128 db: &'db dyn Database,
129 diagnostics: &mut Vec<PluginDiagnostic<'db>>,
130) -> Option<usize> {
131 let Some(attr) = available_gas_attr else {
132 return Some(u32::MAX as usize);
135 };
136 match &attr.args[..] {
137 [AttributeArg { variant: AttributeArgVariant::Unnamed(value), .. }] => match value {
138 ast::Expr::Path(path)
139 if path.as_syntax_node().get_text_without_trivia(db).long(db) == STATIC_GAS_ARG =>
140 {
141 return None;
142 }
143 ast::Expr::Literal(literal) => {
144 let (value, suffix) = literal.numeric_value_and_suffix(db);
145 if suffix.is_some() {
146 diagnostics.push(PluginDiagnostic::error(
147 literal.stable_ptr(db).untyped(),
148 "Literals with suffix are not supported as gas values".to_string(),
149 ));
150 return None;
151 }
152 value.to_i64().and_then(|v| v.to_usize())
153 }
154 _ => None,
155 },
156 _ => None,
157 }
158 .on_none(|| {
159 diagnostics.push(PluginDiagnostic::error(
160 attr.args_stable_ptr.untyped(),
161 format!(
162 "Attribute should have a single non-negative literal in `i64` range or \
163 `{STATIC_GAS_ARG}`."
164 ),
165 ))
166 })
167}
168
169fn extract_panic_bytes(db: &dyn Database, attr: &Attribute<'_>) -> Option<Vec<Felt252>> {
172 let [AttributeArg { variant: AttributeArgVariant::Named { name, value, .. }, .. }] =
173 &attr.args[..]
174 else {
175 return None;
176 };
177 require(name.text.long(db) == "expected")?;
178
179 match value {
180 ast::Expr::Tuple(panic_exprs) => {
181 let mut panic_bytes = Vec::new();
182 for panic_expr in panic_exprs.expressions(db).elements(db) {
183 match panic_expr {
184 ast::Expr::Literal(panic_expr) => {
185 let (value, suffix) = panic_expr.numeric_value_and_suffix(db);
186 if suffix.is_some() {
187 return None;
188 }
189 panic_bytes.push(value.into());
190 }
191 ast::Expr::ShortString(panic_expr) => {
192 panic_bytes.push(panic_expr.numeric_value(db).unwrap_or_default().into())
193 }
194 ast::Expr::String(panic_expr) => {
195 panic_bytes.append(&mut extract_string_panic_bytes(&panic_expr, db))
196 }
197 _ => return None,
198 }
199 }
200 Some(panic_bytes)
201 }
202 ast::Expr::String(panic_string) => Some(extract_string_panic_bytes(panic_string, db)),
203 ast::Expr::Literal(panic_expr) => {
204 let (value, suffix) = panic_expr.numeric_value_and_suffix(db);
205 if suffix.is_some() {
206 return None;
207 }
208 Some(vec![value.into()])
209 }
210 ast::Expr::ShortString(panic_expr) => {
211 Some(vec![panic_expr.numeric_value(db).unwrap_or_default().into()])
212 }
213 _ => None,
214 }
215}
216
217fn extract_string_panic_bytes(
219 panic_string: &ast::TerminalString<'_>,
220 db: &dyn Database,
221) -> Vec<Felt252> {
222 let panic_string = panic_string.string_value(db).unwrap();
223 let chunks = panic_string.as_bytes().chunks_exact(BYTES_IN_WORD);
224 let num_full_words = chunks.len().into();
225 let remainder = chunks.remainder();
226 let pending_word_len = remainder.len().into();
227 let full_words = chunks.map(|chunk| BigInt::from_bytes_be(Sign::Plus, chunk).into());
228 let pending_word = BigInt::from_bytes_be(Sign::Plus, remainder).into();
229
230 chain!(
231 [Felt252::from_hex(BYTE_ARRAY_MAGIC).unwrap(), num_full_words],
232 full_words.into_iter(),
233 [pending_word, pending_word_len]
234 )
235 .collect()
236}