python_proto_importer/commands/check.rs
1use crate::config::AppConfig;
2use crate::verification::import_test::verify;
3use anyhow::{Context, Result};
4use std::path::Path;
5
6/// Execute the check command to verify existing generated Python code.
7///
8/// This command runs only the verification phase without regenerating any code.
9/// It loads the configuration from pyproject.toml and performs the same validation
10/// checks that are run at the end of the build process.
11///
12/// # Arguments
13///
14/// * `pyproject` - Optional path to the pyproject.toml file. If None, uses "pyproject.toml"
15///
16/// # Returns
17///
18/// Returns `Ok(())` if all verification checks pass, or an error if:
19/// - Configuration cannot be loaded
20/// - Import tests fail
21/// - Type checking fails (if configured)
22///
23/// # Verification Steps
24///
25/// 1. **Configuration Loading**: Reads and validates pyproject.toml settings
26/// 2. **Import Validation**: Attempts to import every generated Python module
27/// 3. **Type Checking**: Runs configured type checkers (mypy/pyright) if specified
28///
29/// # Use Cases
30///
31/// - **CI Integration**: Verify generated code without regenerating
32/// - **Development**: Quick validation after manual changes
33/// - **Debugging**: Isolate verification issues from generation issues
34///
35/// # Example
36///
37/// ```no_run
38/// use python_proto_importer::commands::check;
39///
40/// // Check with default pyproject.toml
41/// check(None)?;
42///
43/// // Check with custom config file
44/// check(Some("custom.toml"))?;
45/// # Ok::<(), anyhow::Error>(())
46/// ```
47pub fn check(pyproject: Option<&str>) -> Result<()> {
48 let cfg = AppConfig::load(pyproject.map(Path::new)).context("failed to load config")?;
49 verify(&cfg)
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use std::fs;
56 use std::io::Write;
57 use tempfile::TempDir;
58
59 fn create_test_config_file(dir: &Path, out_dir: &str) -> Result<String> {
60 let config_file = dir.join("pyproject.toml");
61 let mut file = fs::File::create(&config_file)?;
62 writeln!(file, "[tool.python_proto_importer]")?;
63 writeln!(file, "out = \"{}\"", out_dir)?;
64 writeln!(file, "proto_path = [\"proto\"]")?;
65 writeln!(file, "python_exe = \"python3\"")?;
66 Ok(config_file.to_string_lossy().to_string())
67 }
68
69 #[test]
70 fn test_check_invalid_config() {
71 let result = check(Some("nonexistent_config.toml"));
72 assert!(result.is_err());
73 assert!(
74 result
75 .unwrap_err()
76 .to_string()
77 .contains("failed to load config")
78 );
79 }
80
81 #[test]
82 fn test_check_with_empty_output_directory() {
83 let temp_dir = TempDir::new().unwrap();
84 let out_dir = temp_dir.path().join("empty_output");
85 fs::create_dir(&out_dir).unwrap();
86
87 let config_file =
88 create_test_config_file(temp_dir.path(), &out_dir.to_string_lossy()).unwrap();
89
90 // This should succeed because verify() handles empty directories gracefully
91 let result = check(Some(&config_file));
92 assert!(result.is_ok());
93 }
94
95 #[test]
96 fn test_check_nonexistent_output_directory() {
97 let temp_dir = TempDir::new().unwrap();
98 let out_dir = temp_dir.path().join("nonexistent_output");
99
100 let config_file =
101 create_test_config_file(temp_dir.path(), &out_dir.to_string_lossy()).unwrap();
102
103 // verify() should handle nonexistent output directory gracefully
104 let result = check(Some(&config_file));
105 assert!(result.is_ok());
106 }
107}