1use std::convert::identity;
4use std::thread::Builder;
5use std::time::{Duration, Instant};
6use std::{cell::RefCell, fs::read_to_string, panic::AssertUnwindSafe, path::PathBuf};
7
8use hir::{ChangeWithProcMacros, Crate};
9use ide::{AnalysisHost, DiagnosticCode, DiagnosticsConfig};
10use ide_db::base_db;
11use itertools::Either;
12use paths::Utf8PathBuf;
13use profile::StopWatch;
14use project_model::toolchain_info::{QueryConfig, target_data_layout};
15use project_model::{
16 CargoConfig, ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, RustLibSource,
17 RustSourceWorkspaceConfig, Sysroot,
18};
19
20use load_cargo::{LoadCargoConfig, ProcMacroServerChoice, load_workspace};
21use rustc_hash::FxHashMap;
22use triomphe::Arc;
23use vfs::{AbsPathBuf, FileId};
24use walkdir::WalkDir;
25
26use crate::cli::{Result, flags, report_metric};
27
28struct Tester {
29 host: AnalysisHost,
30 root_file: FileId,
31 pass_count: u64,
32 ignore_count: u64,
33 fail_count: u64,
34 stopwatch: StopWatch,
35}
36
37fn string_to_diagnostic_code_leaky(code: &str) -> DiagnosticCode {
38 thread_local! {
39 static LEAK_STORE: RefCell<FxHashMap<String, DiagnosticCode>> = RefCell::new(FxHashMap::default());
40 }
41 LEAK_STORE.with_borrow_mut(|s| match s.get(code) {
42 Some(c) => *c,
43 None => {
44 let v = DiagnosticCode::RustcHardError(format!("E{code}").leak());
45 s.insert(code.to_owned(), v);
46 v
47 }
48 })
49}
50
51fn detect_errors_from_rustc_stderr_file(p: PathBuf) -> FxHashMap<DiagnosticCode, usize> {
52 let text = read_to_string(p).unwrap();
53 let mut result = FxHashMap::default();
54 {
55 let mut text = &*text;
56 while let Some(p) = text.find("error[E") {
57 text = &text[p + 7..];
58 let code = string_to_diagnostic_code_leaky(&text[..4]);
59 *result.entry(code).or_insert(0) += 1;
60 }
61 }
62 result
63}
64
65impl Tester {
66 fn new() -> Result<Self> {
67 let mut path = AbsPathBuf::assert_utf8(std::env::temp_dir());
68 path.push("ra-rustc-test");
69 let tmp_file = path.join("ra-rustc-test.rs");
70 std::fs::write(&tmp_file, "")?;
71 let cargo_config = CargoConfig {
72 sysroot: Some(RustLibSource::Discover),
73 all_targets: true,
74 set_test: true,
75 ..Default::default()
76 };
77
78 let mut sysroot = Sysroot::discover(tmp_file.parent().unwrap(), &cargo_config.extra_env);
79 let loaded_sysroot = sysroot.load_workspace(
80 &RustSourceWorkspaceConfig::default_cargo(),
81 false,
82 &path,
83 &Utf8PathBuf::default(),
84 &|_| (),
85 );
86 if let Some(loaded_sysroot) = loaded_sysroot {
87 sysroot.set_workspace(loaded_sysroot);
88 }
89
90 let data_layout = target_data_layout::get(
91 QueryConfig::Rustc(&sysroot, tmp_file.parent().unwrap().as_ref()),
92 None,
93 &cargo_config.extra_env,
94 );
95
96 let workspace = ProjectWorkspace {
97 kind: ProjectWorkspaceKind::DetachedFile {
98 file: ManifestPath::try_from(tmp_file).unwrap(),
99 cargo: None,
100 },
101 sysroot,
102 rustc_cfg: vec![],
103 toolchain: None,
104 target_layout: data_layout.map(Arc::from).map_err(|it| Arc::from(it.to_string())),
105 cfg_overrides: Default::default(),
106 extra_includes: vec![],
107 set_test: true,
108 };
109 let load_cargo_config = LoadCargoConfig {
110 load_out_dirs_from_check: false,
111 with_proc_macro_server: ProcMacroServerChoice::Sysroot,
112 prefill_caches: false,
113 };
114 let (db, _vfs, _proc_macro) =
115 load_workspace(workspace, &cargo_config.extra_env, &load_cargo_config)?;
116 let host = AnalysisHost::with_database(db);
117 let db = host.raw_database();
118 let krates = Crate::all(db);
119 let root_crate = krates.iter().cloned().find(|krate| krate.origin(db).is_local()).unwrap();
120 let root_file = root_crate.root_file(db);
121 Ok(Self {
122 host,
123 root_file,
124 pass_count: 0,
125 ignore_count: 0,
126 fail_count: 0,
127 stopwatch: StopWatch::start(),
128 })
129 }
130
131 fn test(&mut self, p: PathBuf) {
132 println!("{}", p.display());
133 if p.parent().unwrap().file_name().unwrap() == "auxiliary" {
134 return;
136 }
137 if IGNORED_TESTS.iter().any(|ig| p.file_name().is_some_and(|x| x == *ig)) {
138 println!("{p:?} IGNORE");
139 self.ignore_count += 1;
140 return;
141 }
142 let stderr_path = p.with_extension("stderr");
143 let expected = if stderr_path.exists() {
144 detect_errors_from_rustc_stderr_file(stderr_path)
145 } else {
146 FxHashMap::default()
147 };
148 let text = read_to_string(&p).unwrap();
149 let mut change = ChangeWithProcMacros::default();
150 let mut ignore_test = text.contains("#![feature");
152 ignore_test |= text.contains("// aux-build:") || text.contains("// aux-crate:");
154 ignore_test |= text.contains("mod ");
156 ignore_test |= text.contains("extern crate proc_macro");
158 let should_have_no_error = text.contains("// check-pass")
159 || text.contains("// build-pass")
160 || text.contains("// run-pass");
161 change.change_file(self.root_file, Some(text));
162 self.host.apply_change(change);
163 let diagnostic_config = DiagnosticsConfig::test_sample();
164
165 let res = std::thread::scope(|s| {
166 let worker = Builder::new()
167 .stack_size(40 * 1024 * 1024)
168 .spawn_scoped(s, {
169 let diagnostic_config = &diagnostic_config;
170 let main = std::thread::current();
171 let analysis = self.host.analysis();
172 let root_file = self.root_file;
173 move || {
174 let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
175 analysis.full_diagnostics(
176 diagnostic_config,
177 ide::AssistResolveStrategy::None,
178 root_file,
179 )
180 }));
181 main.unpark();
182 res
183 }
184 })
185 .unwrap();
186
187 let timeout = Duration::from_secs(5);
188 let now = Instant::now();
189 while now.elapsed() <= timeout && !worker.is_finished() {
190 std::thread::park_timeout(timeout - now.elapsed());
191 }
192
193 if !worker.is_finished() {
194 self.host.request_cancellation();
196 }
197 worker.join().and_then(identity)
198 });
199 let mut actual = FxHashMap::default();
200 let panicked = match res {
201 Err(e) => Some(Either::Left(e)),
202 Ok(Ok(diags)) => {
203 for diag in diags {
204 if !matches!(diag.code, DiagnosticCode::RustcHardError(_)) {
205 continue;
206 }
207 if !should_have_no_error && !SUPPORTED_DIAGNOSTICS.contains(&diag.code) {
208 continue;
209 }
210 *actual.entry(diag.code).or_insert(0) += 1;
211 }
212 None
213 }
214 Ok(Err(e)) => Some(Either::Right(e)),
215 };
216 ignore_test |= expected.keys().any(|k| !SUPPORTED_DIAGNOSTICS.contains(k));
218 if ignore_test {
219 println!("{p:?} IGNORE");
220 self.ignore_count += 1;
221 } else if let Some(panic) = panicked {
222 match panic {
223 Either::Left(panic) => {
224 if let Some(msg) = panic
225 .downcast_ref::<String>()
226 .map(String::as_str)
227 .or_else(|| panic.downcast_ref::<&str>().copied())
228 {
229 println!("{msg:?} ")
230 }
231 println!("{p:?} PANIC");
232 }
233 Either::Right(_) => println!("{p:?} CANCELLED"),
234 }
235 self.fail_count += 1;
236 } else if actual == expected {
237 println!("{p:?} PASS");
238 self.pass_count += 1;
239 } else {
240 println!("{p:?} FAIL");
241 println!("actual (r-a) = {actual:?}");
242 println!("expected (rustc) = {expected:?}");
243 self.fail_count += 1;
244 }
245 }
246
247 fn report(&mut self) {
248 println!(
249 "Pass count = {}, Fail count = {}, Ignore count = {}",
250 self.pass_count, self.fail_count, self.ignore_count
251 );
252 println!("Testing time and memory = {}", self.stopwatch.elapsed());
253 report_metric("rustc failed tests", self.fail_count, "#");
254 report_metric("rustc testing time", self.stopwatch.elapsed().time.as_millis() as u64, "ms");
255 }
256}
257
258const IGNORED_TESTS: &[&str] = &[
260 "trait-with-missing-associated-type-restriction.rs", "trait-with-missing-associated-type-restriction-fixable.rs", "resolve-self-in-impl.rs",
263 "basic.rs", "issue-26056.rs",
265 "float-field.rs",
266 "invalid_operator_trait.rs",
267 "type-alias-impl-trait-assoc-dyn.rs",
268 "deeply-nested_closures.rs", "hang-on-deeply-nested-dyn.rs", "dyn-rpit-and-let.rs", "issue-16098.rs", "issue-83471.rs", ];
274
275const SUPPORTED_DIAGNOSTICS: &[DiagnosticCode] = &[
276 DiagnosticCode::RustcHardError("E0023"),
277 DiagnosticCode::RustcHardError("E0046"),
278 DiagnosticCode::RustcHardError("E0063"),
279 DiagnosticCode::RustcHardError("E0107"),
280 DiagnosticCode::RustcHardError("E0117"),
281 DiagnosticCode::RustcHardError("E0133"),
282 DiagnosticCode::RustcHardError("E0210"),
283 DiagnosticCode::RustcHardError("E0268"),
284 DiagnosticCode::RustcHardError("E0308"),
285 DiagnosticCode::RustcHardError("E0384"),
286 DiagnosticCode::RustcHardError("E0407"),
287 DiagnosticCode::RustcHardError("E0432"),
288 DiagnosticCode::RustcHardError("E0451"),
289 DiagnosticCode::RustcHardError("E0507"),
290 DiagnosticCode::RustcHardError("E0583"),
291 DiagnosticCode::RustcHardError("E0559"),
292 DiagnosticCode::RustcHardError("E0616"),
293 DiagnosticCode::RustcHardError("E0618"),
294 DiagnosticCode::RustcHardError("E0624"),
295 DiagnosticCode::RustcHardError("E0774"),
296 DiagnosticCode::RustcHardError("E0767"),
297 DiagnosticCode::RustcHardError("E0777"),
298];
299
300impl flags::RustcTests {
301 pub fn run(self) -> Result<()> {
302 let mut tester = Tester::new()?;
303 let walk_dir = WalkDir::new(self.rustc_repo.join("tests/ui"));
304 eprintln!("Running tests for tests/ui");
305 for i in walk_dir {
306 let i = i?;
307 let p = i.into_path();
308 if let Some(f) = &self.filter
309 && !p.as_os_str().to_string_lossy().contains(f)
310 {
311 continue;
312 }
313 if p.extension().is_none_or(|x| x != "rs") {
314 continue;
315 }
316 if let Err(e) = std::panic::catch_unwind({
317 let tester = AssertUnwindSafe(&mut tester);
318 let p = p.clone();
319 move || {
320 let _guard = base_db::DbPanicContext::enter(p.display().to_string());
321 { tester }.0.test(p);
322 }
323 }) {
324 std::panic::resume_unwind(e);
325 }
326 }
327 tester.report();
328 Ok(())
329 }
330}