Skip to main content

simics_python_utils/
discovery.rs

1// Copyright (C) 2024 Intel Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{environment::PackageSource, PythonEnvironment, PythonVersion};
5use anyhow::{anyhow, Result};
6use ispm_wrapper::{
7    data::InstalledPackage,
8    ispm::{self, GlobalOptions},
9};
10use std::{
11    env,
12    fs::read_dir,
13    path::{Path, PathBuf},
14};
15use versions::Versioning;
16
17#[cfg(not(windows))]
18/// The name of the binary/library/object subdirectory on linux systems
19pub const HOST_DIRNAME: &str = "linux64";
20
21#[cfg(windows)]
22/// The name of the binary/library/object subdirectory on Windows systems
23pub const HOST_DIRNAME: &str = "win64";
24
25/// Environment variable name for Python include path override
26pub const PYTHON3_INCLUDE_ENV: &str = "PYTHON3_INCLUDE";
27/// Environment variable name for Python library path override
28pub const PYTHON3_LDFLAGS_ENV: &str = "PYTHON3_LDFLAGS";
29
30// ============================================================================
31// Platform Configuration - All platform differences centralized here
32// ============================================================================
33
34/// Platform-specific configuration for Python library discovery
35#[derive(Debug, Clone)]
36struct PlatformConfig {
37    /// Subdirectory components from base_path to library directory
38    /// Unix: ["sys", "lib"], Windows: ["bin"]
39    lib_subdir: &'static [&'static str],
40    /// Prefix for Python library files (e.g., "libpython" or "python3")
41    lib_prefix: &'static str,
42    /// Extension for Python library files (e.g., ".so" or ".dll")
43    lib_extension: &'static str,
44    /// Generic library name to filter out in favor of versioned one
45    generic_lib_name: &'static str,
46}
47
48#[cfg(unix)]
49const PLATFORM_CONFIG: PlatformConfig = PlatformConfig {
50    lib_subdir: &["sys", "lib"],
51    lib_prefix: "libpython",
52    lib_extension: ".so",
53    generic_lib_name: "libpython3.so",
54};
55
56#[cfg(windows)]
57const PLATFORM_CONFIG: PlatformConfig = PlatformConfig {
58    lib_subdir: &["bin"],
59    lib_prefix: "python3",
60    lib_extension: ".dll",
61    generic_lib_name: "python3.dll",
62};
63
64// ============================================================================
65// Core Discovery Logic - Platform agnostic, testable
66// ============================================================================
67
68/// Check if a filename matches the Python library pattern for a given config
69fn is_python_library_name(name: &str, config: &PlatformConfig) -> bool {
70    name.starts_with(config.lib_prefix) && name.contains(config.lib_extension)
71}
72
73/// Filter library files to prefer versioned ones over generic
74fn filter_python_libraries_with_config(files: &mut Vec<PathBuf>, config: &PlatformConfig) {
75    // Remove generic library in favor of versioned one
76    if files.len() > 1 {
77        files.retain(|path| {
78            path.file_name()
79                .and_then(|name| name.to_str())
80                .map(|name| name != config.generic_lib_name)
81                .unwrap_or(false)
82        });
83    }
84
85    // On Unix, also prefer .so.X.Y over plain .so (e.g., libpython3.9.so.1.0 over libpython3.9.so)
86    if config.lib_extension == ".so" && files.len() > 1 {
87        files.retain(|path| {
88            path.file_name()
89                .and_then(|name| name.to_str())
90                .map(|name| !name.ends_with(".so"))
91                .unwrap_or(false)
92        });
93    }
94}
95
96/// Build library directory path from base path and config
97fn get_lib_dir(base_path: &Path, config: &PlatformConfig) -> PathBuf {
98    config
99        .lib_subdir
100        .iter()
101        .fold(base_path.to_path_buf(), |p, component| p.join(component))
102}
103
104/// Find Python library in a directory using the given config
105fn find_libpython_in_dir_with_config(
106    lib_dir: &Path,
107    config: &PlatformConfig,
108) -> Result<(PathBuf, PathBuf)> {
109    if !lib_dir.exists() {
110        return Err(anyhow!(
111            "Library directory does not exist: {}",
112            lib_dir.display()
113        ));
114    }
115
116    let entries = read_dir(lib_dir).map_err(|e| {
117        anyhow!(
118            "Failed to read library directory {}: {}",
119            lib_dir.display(),
120            e
121        )
122    })?;
123
124    let mut libpython_files: Vec<PathBuf> = entries
125        .filter_map(|entry| entry.ok())
126        .map(|entry| entry.path())
127        .filter(|path| path.is_file())
128        .filter(|path| {
129            path.file_name()
130                .and_then(|name| name.to_str())
131                .map(|name| is_python_library_name(name, config))
132                .unwrap_or(false)
133        })
134        .collect();
135
136    filter_python_libraries_with_config(&mut libpython_files, config);
137
138    match libpython_files.len() {
139        0 => Err(anyhow!(
140            "No Python library file found in {}",
141            lib_dir.display()
142        )),
143        1 => {
144            let lib_path = libpython_files
145                .into_iter()
146                .next()
147                .expect("exactly one element guaranteed by match arm");
148            Ok((lib_dir.to_path_buf(), lib_path))
149        }
150        _ => Err(anyhow!(
151            "Multiple Python library files found in {}, expected exactly one",
152            lib_dir.display()
153        )),
154    }
155}
156
157// ============================================================================
158// ISPM Package Discovery
159// ============================================================================
160
161/// Find the latest Python package (1033) for a given Simics major version using ISPM API
162fn find_latest_python_package(simics_major_version: u32) -> Result<InstalledPackage> {
163    let packages = ispm::packages::list(&GlobalOptions::default())
164        .map_err(|e| anyhow!("Failed to query ISPM for installed packages: {}", e))?;
165
166    let installed = packages
167        .installed_packages
168        .as_ref()
169        .ok_or_else(|| anyhow!("No installed packages found"))?;
170
171    // Filter for Python packages (1033) matching the major version
172    let python_packages: Vec<_> = installed
173        .iter()
174        .filter(|pkg| pkg.package_number == 1033)
175        .filter(|pkg| pkg.name == "Python")
176        .filter(|pkg| {
177            // Check if the version starts with the expected major version
178            pkg.version
179                .starts_with(&format!("{}.", simics_major_version))
180        })
181        .collect();
182
183    if python_packages.is_empty() {
184        return Err(anyhow!(
185            "No Python packages found for Simics major version {}",
186            simics_major_version
187        ));
188    }
189
190    // Find the package with the highest version
191    let latest_package = python_packages
192        .iter()
193        .max_by(|a, b| {
194            let version_a = Versioning::new(&a.version).unwrap_or_default();
195            let version_b = Versioning::new(&b.version).unwrap_or_default();
196            version_a.cmp(&version_b)
197        })
198        .ok_or_else(|| anyhow!("Failed to find latest Python package"))?;
199
200    Ok((*latest_package).clone())
201}
202
203/// Extract Simics major version using hints and fallbacks
204fn detect_simics_major_version_from_base(simics_base: &Path) -> Result<u32> {
205    if let Some(dir_name) = simics_base.file_name().and_then(|n| n.to_str()) {
206        // Look for patterns like "simics-7.57.0" or "simics-6.0.191"
207        if let Some(version_part) = dir_name.strip_prefix("simics-") {
208            if let Some(major_str) = version_part.split('.').next() {
209                if let Ok(major) = major_str.parse::<u32>() {
210                    return Ok(major);
211                }
212            }
213        }
214    }
215
216    Err(anyhow!(
217        "Unable to determine Simics major version from SIMICS base path {}; expected directory name like simics-x.x.x",
218        simics_base.display()
219    ))
220}
221
222// ============================================================================
223// Public API
224// ============================================================================
225
226/// Auto-discover Python environment from SIMICS_BASE environment variable
227pub fn discover_python_environment() -> Result<PythonEnvironment> {
228    let simics_base =
229        env::var("SIMICS_BASE").map_err(|_| anyhow!("SIMICS_BASE environment variable not set"))?;
230
231    discover_python_environment_from_base(simics_base)
232}
233
234/// Discover Python environment from a specific Simics base path
235pub fn discover_python_environment_from_base<P: AsRef<Path>>(
236    simics_base: P,
237) -> Result<PythonEnvironment> {
238    let simics_base = simics_base.as_ref();
239
240    // Try traditional paths first (Simics 1000)
241    let traditional_err = match try_traditional_paths(simics_base) {
242        Ok(env) => return Ok(env.with_source(PackageSource::Traditional)),
243        Err(err) => err.context("Traditional discovery failed"),
244    };
245
246    // Try dynamic discovery of separate Python package (Simics 1033)
247    let dynamic_err = match try_dynamic_python_package_discovery(simics_base) {
248        Ok(env) => return Ok(env.with_source(PackageSource::SeparatePackage)),
249        Err(err) => err.context("Dynamic discovery failed"),
250    };
251
252    Err(anyhow!(
253        "Python environment not found in traditional location ({}) or through dynamic package discovery\nTraditional error: {:#}\nDynamic error: {:#}",
254        simics_base.join(HOST_DIRNAME).display(),
255        traditional_err,
256        dynamic_err
257    ))
258}
259
260/// Try to discover Python environment from traditional Simics base package paths
261fn try_traditional_paths(simics_base: &Path) -> Result<PythonEnvironment> {
262    let base_path = simics_base.join(HOST_DIRNAME);
263    discover_from_base_path(base_path)
264}
265
266/// Try to discover Python environment using dynamic ISPM-based package discovery
267fn try_dynamic_python_package_discovery(simics_base: &Path) -> Result<PythonEnvironment> {
268    // Detect the Simics major version using multiple strategies
269    let major_version = detect_simics_major_version_from_base(simics_base)?;
270
271    // Find the latest Python package for this specific major version
272    let python_package = find_latest_python_package(major_version)?;
273
274    println!(
275        "cargo:warning=Using dynamically discovered Python package: {} (version {})",
276        python_package.name, python_package.version
277    );
278
279    // Get the first path from the installed package
280    let package_path = python_package.paths.first().ok_or_else(|| {
281        anyhow!(
282            "No installation paths found for Python package {}",
283            python_package.name
284        )
285    })?;
286
287    // Construct the path to the host-specific directory
288    let python_package_path = package_path.join(HOST_DIRNAME);
289
290    discover_from_base_path(python_package_path)
291}
292
293/// Common discovery logic for both package types
294fn discover_from_base_path(base_path: PathBuf) -> Result<PythonEnvironment> {
295    // SIMICS_BASE/HOST_DIRNAME/bin/mini-python
296    let mini_python = find_mini_python(&base_path)?;
297    // SIMICS_BASE/HOST_DIRNAME/include/python3.X
298    let include_dir = find_python_include(&base_path)?;
299    // Unix: SIMICS_BASE/HOST_DIRNAME/sys/lib/libpython3.X.so.Y.Z
300    // Windows: SIMICS_BASE/HOST_DIRNAME/bin/python3.X.dll
301    let (lib_dir, lib_path) = find_python_library(&base_path)?;
302    let version = PythonVersion::parse_from_include_dir(&include_dir)?;
303
304    let env = PythonEnvironment::new(
305        mini_python,
306        include_dir,
307        lib_dir,
308        lib_path,
309        version,
310        PackageSource::Traditional, // Will be updated by caller
311    );
312
313    // Validate the environment before returning
314    env.validate()?;
315
316    Ok(env)
317}
318
319/// Find mini-python executable in the given base path
320fn find_mini_python(base_path: &Path) -> Result<PathBuf> {
321    #[cfg(unix)]
322    let executable_name = "mini-python";
323
324    #[cfg(windows)]
325    let executable_name = "mini-python.exe";
326
327    let mini_python_path = base_path.join("bin").join(executable_name);
328
329    if mini_python_path.exists() {
330        Ok(mini_python_path)
331    } else {
332        Err(anyhow!(
333            "Mini-python executable not found at {}",
334            mini_python_path.display()
335        ))
336    }
337}
338
339/// Find Python include directory with python3.X subdirectory
340fn find_python_include(base_path: &Path) -> Result<PathBuf> {
341    // Check environment variable first for compatibility
342    if let Ok(include_env) = env::var(PYTHON3_INCLUDE_ENV) {
343        // Extract path from -I flag if present
344        let include_path = if let Some(path) = include_env.strip_prefix("-I") {
345            PathBuf::from(path)
346        } else {
347            PathBuf::from(include_env)
348        };
349
350        if include_path.exists() {
351            return Ok(include_path);
352        }
353    }
354
355    let include_base = base_path.join("include");
356    find_python_subdir(&include_base)
357}
358
359/// Find the python3.X subdirectory in the include directory
360fn find_python_subdir(include_dir: &Path) -> Result<PathBuf> {
361    if !include_dir.exists() {
362        return Err(anyhow!(
363            "Include directory does not exist: {}",
364            include_dir.display()
365        ));
366    }
367
368    let entries = read_dir(include_dir).map_err(|e| {
369        anyhow!(
370            "Failed to read include directory {}: {}",
371            include_dir.display(),
372            e
373        )
374    })?;
375
376    let python_dirs: Vec<PathBuf> = entries
377        .filter_map(|entry| entry.ok())
378        .map(|entry| entry.path())
379        .filter(|path| path.is_dir())
380        .filter(|path| {
381            path.file_name()
382                .and_then(|name| name.to_str())
383                .map(|name| name.starts_with("python"))
384                .unwrap_or(false)
385        })
386        .collect();
387
388    match python_dirs.len() {
389        0 => Err(anyhow!(
390            "No python* subdirectory found in {}",
391            include_dir.display()
392        )),
393        1 => Ok(python_dirs
394            .into_iter()
395            .next()
396            .expect("exactly one element guaranteed by match arm")),
397        _ => Err(anyhow!(
398            "Multiple python* subdirectories found in {}, expected exactly one",
399            include_dir.display()
400        )),
401    }
402}
403
404/// Find Python library directory and specific library file
405fn find_python_library(base_path: &Path) -> Result<(PathBuf, PathBuf)> {
406    // Check environment variable first for compatibility
407    if let Ok(lib_env) = env::var(PYTHON3_LDFLAGS_ENV) {
408        let lib_path = PathBuf::from(lib_env);
409        if lib_path.exists() {
410            let lib_dir = lib_path
411                .parent()
412                .ok_or_else(|| anyhow!("Library path has no parent directory"))?
413                .to_path_buf();
414            return Ok((lib_dir, lib_path));
415        }
416    }
417
418    let lib_dir = get_lib_dir(base_path, &PLATFORM_CONFIG);
419    find_libpython_in_dir_with_config(&lib_dir, &PLATFORM_CONFIG)
420}
421
422// ============================================================================
423// Tests
424// ============================================================================
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use std::fs;
430    use tempfile::TempDir;
431
432    // Test configs - can test BOTH platforms on ANY platform
433    const UNIX_CONFIG: PlatformConfig = PlatformConfig {
434        lib_subdir: &["sys", "lib"],
435        lib_prefix: "libpython",
436        lib_extension: ".so",
437        generic_lib_name: "libpython3.so",
438    };
439
440    const WINDOWS_CONFIG: PlatformConfig = PlatformConfig {
441        lib_subdir: &["bin"],
442        lib_prefix: "python3",
443        lib_extension: ".dll",
444        generic_lib_name: "python3.dll",
445    };
446
447    // ========================================================================
448    // Cross-platform unit tests for core logic
449    // ========================================================================
450
451    #[test]
452    fn test_is_python_library_name_unix() {
453        assert!(is_python_library_name("libpython3.9.so.1.0", &UNIX_CONFIG));
454        assert!(is_python_library_name("libpython3.so", &UNIX_CONFIG));
455        assert!(is_python_library_name("libpython3.9.so", &UNIX_CONFIG));
456        assert!(!is_python_library_name("python3.9.dll", &UNIX_CONFIG));
457        assert!(!is_python_library_name("libfoo.so", &UNIX_CONFIG));
458    }
459
460    #[test]
461    fn test_is_python_library_name_windows() {
462        assert!(is_python_library_name("python3.10.dll", &WINDOWS_CONFIG));
463        assert!(is_python_library_name("python3.dll", &WINDOWS_CONFIG));
464        assert!(!is_python_library_name("libpython3.9.so", &WINDOWS_CONFIG));
465        assert!(!is_python_library_name("python3.10.lib", &WINDOWS_CONFIG));
466        assert!(!is_python_library_name("python2.7.dll", &WINDOWS_CONFIG));
467    }
468
469    #[test]
470    fn test_get_lib_dir_unix() {
471        let base = PathBuf::from("/opt/simics/linux64");
472        let lib_dir = get_lib_dir(&base, &UNIX_CONFIG);
473        assert_eq!(lib_dir, PathBuf::from("/opt/simics/linux64/sys/lib"));
474    }
475
476    #[test]
477    fn test_get_lib_dir_windows() {
478        let base = PathBuf::from("/opt/simics/win64");
479        let lib_dir = get_lib_dir(&base, &WINDOWS_CONFIG);
480        assert_eq!(lib_dir, PathBuf::from("/opt/simics/win64/bin"));
481    }
482
483    #[test]
484    fn test_filter_removes_generic_unix() {
485        let mut files = vec![
486            PathBuf::from("/lib/libpython3.so"),
487            PathBuf::from("/lib/libpython3.9.so.1.0"),
488        ];
489        filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
490        assert_eq!(files.len(), 1);
491        assert!(files[0].to_string_lossy().contains("libpython3.9.so.1.0"));
492    }
493
494    #[test]
495    fn test_filter_removes_generic_windows() {
496        let mut files = vec![
497            PathBuf::from("/bin/python3.dll"),
498            PathBuf::from("/bin/python3.10.dll"),
499        ];
500        filter_python_libraries_with_config(&mut files, &WINDOWS_CONFIG);
501        assert_eq!(files.len(), 1);
502        assert!(files[0].to_string_lossy().contains("python3.10.dll"));
503    }
504
505    #[test]
506    fn test_filter_prefers_versioned_so_unix() {
507        let mut files = vec![
508            PathBuf::from("/lib/libpython3.9.so"),
509            PathBuf::from("/lib/libpython3.9.so.1.0"),
510        ];
511        filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
512        assert_eq!(files.len(), 1);
513        assert!(files[0].to_string_lossy().ends_with(".so.1.0"));
514    }
515
516    #[test]
517    fn test_filter_keeps_single_file() {
518        let mut files = vec![PathBuf::from("/lib/libpython3.9.so.1.0")];
519        filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
520        assert_eq!(files.len(), 1);
521    }
522
523    // ========================================================================
524    // Integration tests with mock filesystem
525    // ========================================================================
526
527    #[test]
528    fn test_find_libpython_unix_structure() -> Result<()> {
529        let temp_dir = TempDir::new()?;
530        let lib_dir = temp_dir.path().join("sys").join("lib");
531        fs::create_dir_all(&lib_dir)?;
532        fs::write(lib_dir.join("libpython3.9.so.1.0"), "")?;
533
534        let base_path = temp_dir.path();
535        let actual_lib_dir = get_lib_dir(base_path, &UNIX_CONFIG);
536        let (found_dir, found_path) =
537            find_libpython_in_dir_with_config(&actual_lib_dir, &UNIX_CONFIG)?;
538
539        assert_eq!(found_dir, lib_dir);
540        assert!(found_path.to_string_lossy().contains("libpython3.9.so.1.0"));
541        Ok(())
542    }
543
544    #[test]
545    fn test_find_libpython_windows_structure() -> Result<()> {
546        let temp_dir = TempDir::new()?;
547        let bin_dir = temp_dir.path().join("bin");
548        fs::create_dir_all(&bin_dir)?;
549        fs::write(bin_dir.join("python3.10.dll"), "")?;
550
551        let base_path = temp_dir.path();
552        let actual_lib_dir = get_lib_dir(base_path, &WINDOWS_CONFIG);
553        let (found_dir, found_path) =
554            find_libpython_in_dir_with_config(&actual_lib_dir, &WINDOWS_CONFIG)?;
555
556        assert_eq!(found_dir, bin_dir);
557        assert!(found_path.to_string_lossy().contains("python3.10.dll"));
558        Ok(())
559    }
560
561    #[test]
562    fn test_multiple_libraries_error_unix() -> Result<()> {
563        let temp_dir = TempDir::new()?;
564        let lib_dir = temp_dir.path();
565        fs::write(lib_dir.join("libpython3.9.so.1.0"), "")?;
566        fs::write(lib_dir.join("libpython3.10.so.1.0"), "")?;
567
568        let err = find_libpython_in_dir_with_config(lib_dir, &UNIX_CONFIG).unwrap_err();
569        assert!(err.to_string().contains("Multiple Python library files"));
570        Ok(())
571    }
572
573    #[test]
574    fn test_multiple_libraries_error_windows() -> Result<()> {
575        let temp_dir = TempDir::new()?;
576        let lib_dir = temp_dir.path();
577        fs::write(lib_dir.join("python3.9.dll"), "")?;
578        fs::write(lib_dir.join("python3.10.dll"), "")?;
579
580        let err = find_libpython_in_dir_with_config(lib_dir, &WINDOWS_CONFIG).unwrap_err();
581        assert!(err.to_string().contains("Multiple Python library files"));
582        Ok(())
583    }
584
585    #[test]
586    fn test_no_library_error() -> Result<()> {
587        let temp_dir = TempDir::new()?;
588        let lib_dir = temp_dir.path();
589        // Create an empty directory
590
591        let err = find_libpython_in_dir_with_config(lib_dir, &UNIX_CONFIG).unwrap_err();
592        assert!(err.to_string().contains("No Python library file found"));
593        Ok(())
594    }
595
596    #[test]
597    fn test_nonexistent_directory_error() {
598        let lib_dir = PathBuf::from("/nonexistent/path");
599        let err = find_libpython_in_dir_with_config(&lib_dir, &UNIX_CONFIG).unwrap_err();
600        assert!(err.to_string().contains("does not exist"));
601    }
602
603    // ========================================================================
604    // Full discovery tests (platform-specific due to PLATFORM_CONFIG)
605    // ========================================================================
606
607    fn create_mock_simics_structure(base_dir: &Path, python_version: &str) -> Result<()> {
608        let host_dir = base_dir.join(HOST_DIRNAME);
609        fs::create_dir_all(host_dir.join("bin"))?;
610        fs::create_dir_all(
611            host_dir
612                .join("include")
613                .join(format!("python{}", python_version)),
614        )?;
615
616        // Create mock mini-python
617        let mini_python = if cfg!(windows) {
618            "mini-python.exe"
619        } else {
620            "mini-python"
621        };
622        fs::write(host_dir.join("bin").join(mini_python), "")?;
623
624        // Create platform-specific library structure
625        #[cfg(unix)]
626        {
627            fs::create_dir_all(host_dir.join("sys").join("lib"))?;
628            fs::write(
629                host_dir
630                    .join("sys")
631                    .join("lib")
632                    .join(format!("libpython{}.so", python_version)),
633                "",
634            )?;
635        }
636
637        #[cfg(windows)]
638        {
639            fs::write(
640                host_dir
641                    .join("bin")
642                    .join(format!("python{}.dll", python_version)),
643                "",
644            )?;
645        }
646
647        Ok(())
648    }
649
650    #[test]
651    fn test_discover_traditional_structure() -> Result<()> {
652        let temp_dir = TempDir::new()?;
653        let base_path = temp_dir.path();
654
655        create_mock_simics_structure(base_path, "3.9")?;
656
657        let env = discover_python_environment_from_base(base_path)?;
658
659        assert_eq!(env.package_source, PackageSource::Traditional);
660        assert_eq!(env.version.major, 3);
661        assert_eq!(env.version.minor, 9);
662        assert!(env.mini_python.exists());
663        assert!(env.include_dir.exists());
664        assert!(env.lib_path.exists());
665
666        Ok(())
667    }
668
669    #[test]
670    fn test_version_parsing() -> Result<()> {
671        let temp_dir = TempDir::new()?;
672        let include_dir = temp_dir.path().join("python3.9.10");
673        fs::create_dir_all(&include_dir)?;
674
675        let version = PythonVersion::parse_from_include_dir(&include_dir)?;
676        assert_eq!(version.major, 3);
677        assert_eq!(version.minor, 9);
678        assert_eq!(version.patch, 10);
679
680        Ok(())
681    }
682
683    #[test]
684    fn test_multiple_python_include_dirs_error() -> Result<()> {
685        let temp_dir = TempDir::new()?;
686        let include_dir = temp_dir.path().join("include");
687        fs::create_dir_all(include_dir.join("python3.9"))?;
688        fs::create_dir_all(include_dir.join("python3.10"))?;
689
690        let err = find_python_subdir(&include_dir).unwrap_err();
691        assert!(err.to_string().contains("Multiple python* subdirectories"));
692
693        Ok(())
694    }
695
696    #[test]
697    fn test_detect_simics_major_version_from_base() -> Result<()> {
698        let base = PathBuf::from("/opt/simics/simics-7.38.0");
699        let major = detect_simics_major_version_from_base(&base)?;
700        assert_eq!(major, 7);
701
702        Ok(())
703    }
704
705    #[test]
706    fn test_detect_simics_major_version_from_base_v6() -> Result<()> {
707        let base = PathBuf::from("/opt/simics/simics-6.0.191");
708        let major = detect_simics_major_version_from_base(&base)?;
709        assert_eq!(major, 6);
710
711        Ok(())
712    }
713
714    #[test]
715    fn test_detect_simics_major_version_from_base_invalid() {
716        let base = PathBuf::from("/opt/simics/current");
717        let err = detect_simics_major_version_from_base(&base).unwrap_err();
718        assert!(err
719            .to_string()
720            .contains("expected directory name like simics-x.x.x"));
721    }
722}