cairo_lang_test_plugin/
test_config.rs1use 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 literal.numeric_value(db).and_then(|v| v.to_i64()).and_then(|v| v.to_usize())
145 }
146 _ => None,
147 },
148 _ => None,
149 }
150 .on_none(|| {
151 diagnostics.push(PluginDiagnostic::error(
152 attr.args_stable_ptr.untyped(),
153 format!(
154 "Attribute should have a single non-negative literal in `i64` range or \
155 `{STATIC_GAS_ARG}`."
156 ),
157 ))
158 })
159}
160
161fn extract_panic_bytes(db: &dyn Database, attr: &Attribute<'_>) -> Option<Vec<Felt252>> {
164 let [AttributeArg { variant: AttributeArgVariant::Named { name, value, .. }, .. }] =
165 &attr.args[..]
166 else {
167 return None;
168 };
169 require(name.text.long(db) == "expected")?;
170
171 match value {
172 ast::Expr::Tuple(panic_exprs) => {
173 let mut panic_bytes = Vec::new();
174 for panic_expr in panic_exprs.expressions(db).elements(db) {
175 match panic_expr {
176 ast::Expr::Literal(panic_expr) => {
177 panic_bytes.push(panic_expr.numeric_value(db).unwrap_or_default().into())
178 }
179 ast::Expr::ShortString(panic_expr) => {
180 panic_bytes.push(panic_expr.numeric_value(db).unwrap_or_default().into())
181 }
182 ast::Expr::String(panic_expr) => {
183 panic_bytes.append(&mut extract_string_panic_bytes(&panic_expr, db))
184 }
185 _ => return None,
186 }
187 }
188 Some(panic_bytes)
189 }
190 ast::Expr::String(panic_string) => Some(extract_string_panic_bytes(panic_string, db)),
191 ast::Expr::Literal(panic_expr) => {
192 Some(vec![panic_expr.numeric_value(db).unwrap_or_default().into()])
193 }
194 ast::Expr::ShortString(panic_expr) => {
195 Some(vec![panic_expr.numeric_value(db).unwrap_or_default().into()])
196 }
197 _ => None,
198 }
199}
200
201fn extract_string_panic_bytes(
203 panic_string: &ast::TerminalString<'_>,
204 db: &dyn Database,
205) -> Vec<Felt252> {
206 let panic_string = panic_string.string_value(db).unwrap();
207 let chunks = panic_string.as_bytes().chunks_exact(BYTES_IN_WORD);
208 let num_full_words = chunks.len().into();
209 let remainder = chunks.remainder();
210 let pending_word_len = remainder.len().into();
211 let full_words = chunks.map(|chunk| BigInt::from_bytes_be(Sign::Plus, chunk).into());
212 let pending_word = BigInt::from_bytes_be(Sign::Plus, remainder).into();
213
214 chain!(
215 [Felt252::from_hex(BYTE_ARRAY_MAGIC).unwrap(), num_full_words],
216 full_words.into_iter(),
217 [pending_word, pending_word_len]
218 )
219 .collect()
220}