python_proto_importer/verification/
import_test.rs1use crate::config::AppConfig;
2use crate::utils::run_cmd;
3use crate::verification::{
4 create_import_test_script, determine_package_structure, determine_package_structure_legacy,
5};
6use anyhow::{Context, Result};
7use std::ffi::OsStr;
8use std::path::Path;
9use walkdir::WalkDir;
10
11pub fn verify(cfg: &AppConfig) -> Result<()> {
13 let out_abs = cfg.out.canonicalize().unwrap_or_else(|_| cfg.out.clone());
14 let mut modules: Vec<String> = Vec::new();
15 let py_suffixes: Vec<&str> = cfg
16 .postprocess
17 .module_suffixes
18 .iter()
19 .filter_map(|s| {
20 if s.ends_with(".py") {
21 Some(s.as_str())
22 } else {
23 None
24 }
25 })
26 .collect();
27
28 for entry in WalkDir::new(&out_abs).into_iter().filter_map(Result::ok) {
29 let path = entry.path();
30 if path.is_file()
31 && path.extension() == Some(OsStr::new("py"))
32 && path.file_name() != Some(OsStr::new("__init__.py"))
33 {
34 let rel = path.strip_prefix(&out_abs).unwrap_or(path);
35 let rel_str = rel.to_string_lossy();
36 if !py_suffixes.is_empty() && !py_suffixes.iter().any(|s| rel_str.ends_with(s)) {
37 continue;
38 }
39 let rel_no_ext = rel.with_extension("");
40 let mut parts: Vec<String> = Vec::new();
41 for comp in rel_no_ext.components() {
42 if let std::path::Component::Normal(os) = comp {
43 parts.push(os.to_string_lossy().to_string());
44 }
45 }
46 if !parts.is_empty() {
47 modules.push(parts.join("."));
48 }
49 }
50 }
51
52 modules.sort();
53
54 if modules.is_empty() {
55 tracing::info!("no python modules found for verification");
56 } else {
57 let (parent_path, package_name) = determine_package_structure(&out_abs)?;
58
59 tracing::debug!(
60 "using parent_path={}, package_name={}",
61 parent_path.display(),
62 package_name
63 );
64
65 let test_script = create_import_test_script(&package_name, &modules);
66
67 if tracing::enabled!(tracing::Level::DEBUG)
69 && let Ok(temp_dir) = std::env::temp_dir().canonicalize()
70 {
71 let script_path = temp_dir.join(format!(
72 "python_proto_importer_test_{}.py",
73 std::process::id()
74 ));
75 if let Err(e) = std::fs::write(&script_path, &test_script) {
76 tracing::debug!(
77 "failed to write debug script to {}: {}",
78 script_path.display(),
79 e
80 );
81 } else {
82 tracing::debug!(
83 "comprehensive test script saved to: {}",
84 script_path.display()
85 );
86 }
87 }
88
89 let mut cmd = std::process::Command::new(&cfg.python_exe);
90 if cfg.python_exe == "uv" {
91 cmd.arg("run").arg("python").arg("-c").arg(&test_script);
92 } else {
93 cmd.arg("-c").arg(&test_script);
94 }
95
96 let output = cmd
97 .env("PYTHONPATH", &parent_path)
98 .output()
99 .with_context(|| {
100 format!(
101 "failed running {} for package-aware import dry-run",
102 cfg.python_exe
103 )
104 })?;
105
106 let stderr_output = String::from_utf8_lossy(&output.stderr);
107 for line in stderr_output.lines() {
108 if line.starts_with("IMPORT_TEST_SUMMARY:") {
109 tracing::debug!(
110 "{}",
111 line.strip_prefix("IMPORT_TEST_SUMMARY:").unwrap_or(line)
112 );
113 } else if line.starts_with("IMPORT_TEST_SUCCESS:") {
114 tracing::debug!(
115 "comprehensive import test: {}",
116 line.strip_prefix("IMPORT_TEST_SUCCESS:")
117 .unwrap_or("success")
118 );
119 } else if line.starts_with("IMPORT_ERROR:") {
120 tracing::warn!(
121 "import issue detected: {}",
122 line.strip_prefix("IMPORT_ERROR:").unwrap_or(line)
123 );
124 }
125 }
126
127 if !output.status.success() {
128 tracing::warn!(
129 "comprehensive import test failed, running individual fallback tests for detailed diagnosis"
130 );
131 let failed_modules =
132 run_individual_fallback_tests(cfg, &parent_path, &package_name, &modules)?;
133 if !failed_modules.is_empty() {
134 tracing::warn!("retrying with legacy package structure determination...");
136 let (legacy_parent_path, legacy_package_name) =
137 determine_package_structure_legacy(&out_abs)?;
138
139 if legacy_parent_path != parent_path || legacy_package_name != package_name {
140 tracing::debug!(
141 "legacy fallback: parent_path={}, package_name={}",
142 legacy_parent_path.display(),
143 legacy_package_name
144 );
145 let legacy_failed_modules = run_individual_fallback_tests(
146 cfg,
147 &legacy_parent_path,
148 &legacy_package_name,
149 &modules,
150 )?;
151
152 if legacy_failed_modules.is_empty() {
153 tracing::info!(
154 "import dry-run passed with legacy package structure ({} modules)",
155 modules.len()
156 );
157 } else if legacy_failed_modules.len() < failed_modules.len() {
158 tracing::warn!(
159 "legacy fallback reduced failures from {} to {} modules",
160 failed_modules.len(),
161 legacy_failed_modules.len()
162 );
163 for (m, error) in &legacy_failed_modules {
164 tracing::error!(module=%m, "import failed (legacy fallback): {}", error);
165 }
166 anyhow::bail!(
167 "import dry-run failed for {} modules (out of {}) even with legacy fallback. Use -v for more details.",
168 legacy_failed_modules.len(),
169 modules.len()
170 );
171 } else {
172 tracing::warn!(
173 "legacy fallback did not improve results, showing original errors"
174 );
175 for (m, error) in &failed_modules {
176 tracing::error!(module=%m, "import failed: {}", error);
177 }
178 anyhow::bail!(
179 "import dry-run failed for {} modules (out of {}). Use -v for more details.",
180 failed_modules.len(),
181 modules.len()
182 );
183 }
184 } else {
185 tracing::debug!("legacy fallback would use same configuration, skipping");
186 for (m, error) in &failed_modules {
187 tracing::error!(module=%m, "import failed: {}", error);
188 }
189 anyhow::bail!(
190 "import dry-run failed for {} modules (out of {}). Use -v for more details.",
191 failed_modules.len(),
192 modules.len()
193 );
194 }
195 }
196 tracing::warn!(
197 "comprehensive test failed but individual tests passed - this may indicate a package structure issue"
198 );
199 }
200
201 tracing::info!("import dry-run passed ({} modules)", modules.len());
202 }
203
204 if let Some(v) = &cfg.verify {
205 if let Some(cmd) = v.mypy_cmd.as_deref().filter(|cmd| !cmd.is_empty()) {
206 run_cmd(cmd).context("mypy_cmd failed")?;
207 }
208 if let Some(cmd) = v.pyright_cmd.as_deref().filter(|cmd| !cmd.is_empty()) {
209 run_cmd(cmd).context("pyright_cmd failed")?;
210 }
211 }
212 Ok(())
213}
214
215fn run_individual_fallback_tests(
217 cfg: &AppConfig,
218 parent_path: &Path,
219 package_name: &str,
220 modules: &[String],
221) -> Result<Vec<(String, String)>> {
222 let mut failed = Vec::new();
223
224 tracing::debug!(
225 "running individual fallback tests for {} modules",
226 modules.len()
227 );
228 tracing::debug!(
229 "environment: PYTHONPATH={}, package_name={}, python_exe={}",
230 parent_path.display(),
231 package_name,
232 cfg.python_exe
233 );
234
235 for (idx, module) in modules.iter().enumerate() {
236 let full_module = if package_name.is_empty() {
237 module.clone()
238 } else {
239 format!("{}.{}", package_name, module)
240 };
241
242 tracing::trace!(
243 "testing individual module ({}/{}): {}",
244 idx + 1,
245 modules.len(),
246 full_module
247 );
248
249 let test_script = format!(
250 r#"
251import sys
252import importlib
253import traceback
254
255module_name = '{}'
256full_module_name = '{}'
257
258try:
259 mod = importlib.import_module(full_module_name)
260 print('SUCCESS:' + module_name, file=sys.stderr)
261except ImportError as e:
262 error_msg = str(e)
263 if "relative import" in error_msg.lower():
264 print('RELATIVE_IMPORT_ERROR:' + module_name + ':' + error_msg, file=sys.stderr)
265 else:
266 print('IMPORT_ERROR:' + module_name + ':' + error_msg, file=sys.stderr)
267except ModuleNotFoundError as e:
268 print('MODULE_NOT_FOUND_ERROR:' + module_name + ':' + str(e), file=sys.stderr)
269except SyntaxError as e:
270 print('SYNTAX_ERROR:' + module_name + ':line ' + str(e.lineno or '?') + ': ' + str(e), file=sys.stderr)
271except Exception as e:
272 print('GENERAL_ERROR:' + module_name + ':' + type(e).__name__ + ': ' + str(e), file=sys.stderr)
273 traceback.print_exc(file=sys.stderr)
274"#,
275 module, full_module
276 );
277
278 if tracing::enabled!(tracing::Level::TRACE)
280 && let Ok(temp_dir) = std::env::temp_dir().canonicalize()
281 {
282 let script_path = temp_dir.join(format!(
283 "python_proto_importer_individual_{}_{}.py",
284 std::process::id(),
285 idx
286 ));
287 if let Err(e) = std::fs::write(&script_path, &test_script) {
288 tracing::trace!(
289 "failed to write debug script to {}: {}",
290 script_path.display(),
291 e
292 );
293 } else {
294 tracing::trace!("individual test script saved to: {}", script_path.display());
295 }
296 }
297
298 let mut cmd = std::process::Command::new(&cfg.python_exe);
299 if cfg.python_exe == "uv" {
300 cmd.arg("run").arg("python").arg("-c").arg(&test_script);
301 } else {
302 cmd.arg("-c").arg(&test_script);
303 }
304
305 let output = cmd
306 .env("PYTHONPATH", parent_path)
307 .output()
308 .with_context(|| {
309 format!(
310 "failed running {} for individual fallback test",
311 cfg.python_exe
312 )
313 })?;
314
315 if !output.status.success() {
316 let stderr_output = String::from_utf8_lossy(&output.stderr);
317 let stdout_output = String::from_utf8_lossy(&output.stdout);
318 let mut error_msg = String::new();
319
320 if tracing::enabled!(tracing::Level::DEBUG) {
322 tracing::debug!("individual test failed for module {}", module);
323 tracing::debug!("exit code: {:?}", output.status.code());
324 if !stderr_output.trim().is_empty() {
325 tracing::debug!("stderr:\n{}", stderr_output);
326 }
327 if !stdout_output.trim().is_empty() {
328 tracing::debug!("stdout:\n{}", stdout_output);
329 }
330 }
331
332 for line in stderr_output.lines() {
334 if line.starts_with("RELATIVE_IMPORT_ERROR:") {
335 error_msg = format!(
336 "Relative import issue: {}",
337 line.strip_prefix("RELATIVE_IMPORT_ERROR:").unwrap_or(line)
338 );
339 break;
340 } else if line.starts_with("IMPORT_ERROR:") {
341 error_msg = format!(
342 "Import error: {}",
343 line.strip_prefix("IMPORT_ERROR:").unwrap_or(line)
344 );
345 break;
346 } else if line.starts_with("MODULE_NOT_FOUND_ERROR:") {
347 error_msg = format!(
348 "Module not found: {}",
349 line.strip_prefix("MODULE_NOT_FOUND_ERROR:").unwrap_or(line)
350 );
351 break;
352 } else if line.starts_with("SYNTAX_ERROR:") {
353 error_msg = format!(
354 "Syntax error: {}",
355 line.strip_prefix("SYNTAX_ERROR:").unwrap_or(line)
356 );
357 break;
358 } else if line.starts_with("GENERAL_ERROR:") {
359 error_msg = format!(
360 "General error: {}",
361 line.strip_prefix("GENERAL_ERROR:").unwrap_or(line)
362 );
363 break;
364 }
365 else if line.contains("ImportError:") {
367 error_msg = format!("ImportError found in stderr: {}", line.trim());
368 break;
369 } else if line.contains("ModuleNotFoundError:") {
370 error_msg = format!("ModuleNotFoundError found in stderr: {}", line.trim());
371 break;
372 } else if line.contains("SyntaxError:") {
373 error_msg = format!("SyntaxError found in stderr: {}", line.trim());
374 break;
375 } else if line.contains("NameError:") {
376 error_msg = format!("NameError found in stderr: {}", line.trim());
377 break;
378 }
379 }
380
381 if error_msg.is_empty() {
383 for line in stdout_output.lines() {
384 if line.contains("ImportError:") {
385 error_msg = format!("ImportError found in stdout: {}", line.trim());
386 break;
387 } else if line.contains("ModuleNotFoundError:") {
388 error_msg = format!("ModuleNotFoundError found in stdout: {}", line.trim());
389 break;
390 } else if line.contains("SyntaxError:") {
391 error_msg = format!("SyntaxError found in stdout: {}", line.trim());
392 break;
393 } else if line.contains("Traceback (most recent call last):") {
394 error_msg = format!("Python traceback found in stdout: {}", line.trim());
395 break;
396 }
397 }
398 }
399
400 if error_msg.is_empty() {
402 let detailed_info =
403 if !stderr_output.trim().is_empty() || !stdout_output.trim().is_empty() {
404 let stderr_preview =
405 stderr_output.lines().take(2).collect::<Vec<_>>().join("; ");
406 let stdout_preview =
407 stdout_output.lines().take(2).collect::<Vec<_>>().join("; ");
408 format!(
409 "Unknown error (exit code: {}) - stderr: '{}' - stdout: '{}'",
410 output.status.code().unwrap_or(-1),
411 stderr_preview.trim(),
412 stdout_preview.trim()
413 )
414 } else {
415 format!(
416 "Unknown error (exit code: {}) - no output",
417 output.status.code().unwrap_or(-1)
418 )
419 };
420 error_msg = detailed_info;
421 }
422
423 failed.push((module.clone(), error_msg));
424 } else {
425 tracing::trace!("individual test passed: {}", module);
426 }
427 }
428
429 tracing::debug!(
430 "individual fallback tests completed: {}/{} failed",
431 failed.len(),
432 modules.len()
433 );
434 Ok(failed)
435}