|
1 | 1 | #[path = "node_checker.generated.rs"] |
2 | | -pub mod node_checker; |
| 2 | +mod node_checker; |
3 | 3 |
|
4 | | -use std::fmt::{Debug, Write}; |
5 | | -use std::path::Path; |
6 | | -use std::rc::Rc; |
7 | | - |
8 | | -use anyhow::Result; |
9 | | -use infra_utils::codegen::CodegenFileSystem; |
10 | | -use semver::Version; |
11 | | -use slang_solidity::cst::{Cursor, Node, TextRange}; |
12 | | -use slang_solidity::parser::ParseOutput; |
| 4 | +#[cfg(feature = "__private_testing_utils")] |
| 5 | +pub mod testing; |
| 6 | +use slang_solidity::cst::{Cursor, TextRange}; |
13 | 7 | use slang_solidity_v2_common::versions::LanguageVersion; |
14 | | -use slang_solidity_v2_cst::structured_cst::nodes::{ContractDefinition, Expression, SourceUnit}; |
| 8 | +#[cfg(feature = "__private_testing_utils")] |
| 9 | +pub use testing::V2TesterConstructor; |
15 | 10 |
|
16 | | -use crate::parser::{ContractDefinitionParser, ExpressionParser, SourceUnitParser}; |
| 11 | +use crate::parser::{Parser, SourceUnitParser}; |
17 | 12 | use crate::temp_testing::node_checker::{NodeChecker, NodeCheckerError}; |
18 | | -use crate::Parser as ParserV2; |
19 | | - |
20 | | -/// A Tester for V2 parser that compares against V1 outputs. |
21 | | -/// |
22 | | -/// It generates snapshots for V2 outputs and diffs against V1 outputs. |
23 | | -pub trait V2Tester { |
24 | | - fn test_next( |
25 | | - &mut self, |
26 | | - test_dir: &Path, |
27 | | - fs: &mut CodegenFileSystem, |
28 | | - source_id: &str, |
29 | | - source: &str, |
30 | | - version: &Version, |
31 | | - v1_output: &ParseOutput, |
32 | | - ) -> Result<()>; |
33 | | -} |
34 | | - |
35 | | -pub struct V2TesterConstructor; |
36 | | - |
37 | | -impl V2TesterConstructor { |
38 | | - /// Return a new `V2Tester` instance for the given parser name. |
39 | | - /// |
40 | | - /// Only `SourceUnit`, `Expression`, and `ContractDefinition` are supported for now. |
41 | | - pub fn new_tester(parser_name: &str) -> Box<dyn V2Tester> { |
42 | | - match parser_name { |
43 | | - "SourceUnit" => Box::new(V2TesterImpl::<SourceUnit, SourceUnitParser>::new()), |
44 | | - "Expression" => Box::new(V2TesterImpl::<Expression, ExpressionParser>::new()), |
45 | | - "ContractDefinition" => { |
46 | | - Box::new(V2TesterImpl::<ContractDefinition, ContractDefinitionParser>::new()) |
47 | | - } |
48 | | - // TODO(v2): We should consolidate all tests to the supported non terminals |
49 | | - _ => Box::new(NonSupportedParserTester), |
50 | | - } |
51 | | - } |
52 | | -} |
53 | | - |
54 | | -/// A dummy tester that does nothing, used for unsupported parsers. |
55 | | -struct NonSupportedParserTester; |
56 | | - |
57 | | -impl V2Tester for NonSupportedParserTester { |
58 | | - fn test_next( |
59 | | - &mut self, |
60 | | - _test_dir: &Path, |
61 | | - _fs: &mut CodegenFileSystem, |
62 | | - _source_id: &str, |
63 | | - _source: &str, |
64 | | - _version: &Version, |
65 | | - _v1_output: &ParseOutput, |
66 | | - ) -> Result<()> { |
67 | | - Ok(()) |
68 | | - } |
69 | | -} |
70 | | - |
71 | | -struct V2TesterImpl<NT, T: ParserV2<NonTerminal = NT>> { |
72 | | - last_output: Option<Result<NT, String>>, |
73 | | - last_diff: Option<(bool, Option<String>, String)>, |
74 | | - phantom: std::marker::PhantomData<T>, |
75 | | -} |
76 | | - |
77 | | -impl<NT, T: ParserV2<NonTerminal = NT>> V2TesterImpl<NT, T> { |
78 | | - pub fn new() -> Self { |
79 | | - Self { |
80 | | - last_output: None, |
81 | | - last_diff: None, |
82 | | - phantom: std::marker::PhantomData, |
83 | | - } |
84 | | - } |
85 | | -} |
86 | | - |
87 | | -impl<NT: NodeChecker + Debug + PartialEq, T: ParserV2<NonTerminal = NT>> V2Tester |
88 | | - for V2TesterImpl<NT, T> |
89 | | -{ |
90 | | - fn test_next( |
91 | | - &mut self, |
92 | | - test_dir: &Path, |
93 | | - fs: &mut CodegenFileSystem, |
94 | | - source_id: &str, |
95 | | - source: &str, |
96 | | - version: &Version, |
97 | | - v1_output: &ParseOutput, |
98 | | - ) -> Result<()> { |
99 | | - // We check version 0.8.30 |
100 | | - // TODO(v2) check all versions |
101 | | - // _SLANG_V2_PARSER_VERSION_ (keep in sync) |
102 | | - if *version != Version::new(0, 8, 30) { |
103 | | - return Ok(()); |
104 | | - } |
105 | | - |
106 | | - // Get the output for v2 |
107 | | - let lang_version = LanguageVersion::try_from(version.clone()) |
108 | | - .unwrap_or_else(|_| panic!("Unsupported version: {version}")); |
109 | | - |
110 | | - let v2_output: Result<NT, _> = T::parse(source, lang_version); |
111 | | - |
112 | | - let v2_output = match self.last_output { |
113 | | - // Skip this version if it produces the same output. |
114 | | - // Note: comparing objects cheaply before expensive serialization. |
115 | | - Some(ref last) if last == &v2_output => last, |
116 | | - _ => { |
117 | | - let status = if v2_output.is_ok() { |
118 | | - "success" |
119 | | - } else { |
120 | | - "failure" |
121 | | - }; |
122 | | - |
123 | | - let snapshot_path = test_dir |
124 | | - .join("v2/generated") |
125 | | - .join(format!("{version}-{status}.txt")); |
126 | | - |
127 | | - let mut s = String::new(); |
128 | | - |
129 | | - match &v2_output { |
130 | | - Ok(parsed_checker) => { |
131 | | - // Print AST |
132 | | - writeln!(s, "{parsed_checker:#?}")?; |
133 | | - } |
134 | | - Err(err) => { |
135 | | - // We don't care about the errors for now, we just write them |
136 | | - writeln!(s, "{err:#?}")?; |
137 | | - } |
138 | | - } |
139 | | - |
140 | | - fs.write_file_raw(&snapshot_path, s)?; |
141 | | - self.last_output.insert(v2_output) |
142 | | - } |
143 | | - }; |
144 | | - |
145 | | - // Now check the diff between V1 and V2 |
146 | | - |
147 | | - let diff = Self::diff_report(v1_output, v2_output, source_id, source); |
148 | | - |
149 | | - match &self.last_diff { |
150 | | - // Skip if the diff is the same as last time |
151 | | - Some(ref last) if last == &diff => (), |
152 | | - _ => { |
153 | | - let (is_same, should_panic, diff_report) = &diff; |
154 | | - |
155 | | - let diff_path = test_dir.join("diff/generated").join(format!( |
156 | | - "{version}-{diff}.txt", |
157 | | - diff = if *is_same { "same" } else { "diff" } |
158 | | - )); |
159 | | - |
160 | | - fs.write_file_raw(&diff_path, diff_report)?; |
161 | | - |
162 | | - if let Some(panic_message) = should_panic { |
163 | | - panic!("{panic_message}"); |
164 | | - } |
165 | | - self.last_diff = Some(diff); |
166 | | - } |
167 | | - } |
168 | | - |
169 | | - Ok(()) |
170 | | - } |
171 | | -} |
172 | | - |
173 | | -impl<NT: NodeChecker + Debug + PartialEq, T: ParserV2<NonTerminal = NT>> V2TesterImpl<NT, T> { |
174 | | - fn diff_report( |
175 | | - v1_output: &ParseOutput, |
176 | | - v2_output: &Result<NT, String>, |
177 | | - source_id: &str, |
178 | | - source: &str, |
179 | | - ) -> (bool, Option<String>, String) { |
180 | | - match v2_output { |
181 | | - Ok(parsed_checker) => { |
182 | | - // check V1 validity |
183 | | - if v1_output.is_valid() { |
184 | | - // Check for errors |
185 | | - let checked = |
186 | | - parsed_checker.check_node(&Node::Nonterminal(Rc::clone(v1_output.tree()))); |
187 | | - |
188 | | - if checked.is_empty() { |
189 | | - ( |
190 | | - true, |
191 | | - None, |
192 | | - "V2 parser produced the same output as V1 output.".to_string(), |
193 | | - ) |
194 | | - } else { |
195 | | - let errors = write_errors(&checked, source_id, source); |
196 | | - |
197 | | - // TODO(v2): This is forced not to panic since some tests in V1 produce different outputs, |
198 | | - // in particular `state_variable_function` |
199 | | - (false, None, errors) |
200 | | - } |
201 | | - } else { |
202 | | - // TODO(v2): This is forced not to panic, since V2 has no validation yet, but we |
203 | | - // do want to be aware of these cases, so we write them in the diff report and we can review them later. |
204 | | - ( |
205 | | - false, |
206 | | - None, |
207 | | - "V1 Parser: Invalid\nV2 Parser: Valid\n".to_string(), |
208 | | - ) |
209 | | - } |
210 | | - } |
211 | | - // TODO(v2): We force this not to panic, since we need lexical context switching to work for some |
212 | | - // tests to pass |
213 | | - Err(_) if v1_output.is_valid() => ( |
214 | | - false, |
215 | | - None, |
216 | | - "V1 Parser: Valid\nV2 Parser: Invalid\n".to_string(), |
217 | | - ), |
218 | | - Err(_) => { |
219 | | - // TODO(v2): Both are invalid, compare the errors |
220 | | - ( |
221 | | - true, |
222 | | - None, |
223 | | - "Both V1 and V2 produced invalid output.\n".to_string(), |
224 | | - ) |
225 | | - } |
226 | | - } |
227 | | - } |
228 | | -} |
229 | | - |
230 | | -fn write_errors(errors: &Vec<NodeCheckerError>, source_id: &str, source: &str) -> String { |
231 | | - if errors.is_empty() { |
232 | | - return String::new(); |
233 | | - } |
234 | | - |
235 | | - let mut s = String::new(); |
236 | | - writeln!(s, "Errors: # {count} total", count = errors.len()).unwrap(); |
237 | | - |
238 | | - for error in errors { |
239 | | - writeln!(s, " - >").unwrap(); |
240 | | - for line in slang_solidity::diagnostic::render(error, source_id, source, false).lines() { |
241 | | - writeln!(s, " {line}").unwrap(); |
242 | | - } |
243 | | - } |
244 | | - |
245 | | - s |
246 | | -} |
247 | 13 |
|
248 | 14 | pub fn compare_with_v1_cursor(source: &str, root_cursor: &Cursor) -> Vec<NodeCheckerError> { |
249 | 15 | let v2_output = SourceUnitParser::parse(source, LanguageVersion::V0_8_30); |
|
0 commit comments