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