provenant-cli 0.0.8

Provenant is a high-performance Rust scanner for licenses, packages, and source provenance.
Documentation
#[cfg(test)]
mod tests {
    use std::fs;

    use super::super::scan_test_utils::{assert_file_links_to_package, scan_and_assemble};
    use crate::models::{DatasourceId, PackageType};

    #[test]
    fn test_python_metadata_scan_assigns_referenced_site_packages_files() {
        let temp_dir = tempfile::TempDir::new().expect("create temp dir");
        let site_packages = temp_dir.path().join("venv/lib/python3.11/site-packages");
        let dist_info = site_packages.join("click-8.0.4.dist-info");
        let package_dir = site_packages.join("click");

        std::fs::create_dir_all(&dist_info).expect("create dist-info dir");
        std::fs::create_dir_all(&package_dir).expect("create package dir");
        std::fs::write(
            dist_info.join("METADATA"),
            "Metadata-Version: 2.1\nName: click\nVersion: 8.0.4\n",
        )
        .unwrap();
        std::fs::write(
            dist_info.join("RECORD"),
            "click/__init__.py,,0\nclick/core.py,,10\nclick-8.0.4.dist-info/LICENSE.rst,,20\n",
        )
        .unwrap();
        std::fs::write(dist_info.join("LICENSE.rst"), "license text").unwrap();
        std::fs::write(package_dir.join("__init__.py"), "").unwrap();
        std::fs::write(package_dir.join("core.py"), "def click():\n    pass\n").unwrap();

        let (files, result) = scan_and_assemble(temp_dir.path());
        let package = result
            .packages
            .iter()
            .find(|package| package.name.as_deref() == Some("click"))
            .unwrap();
        let core_file = files
            .iter()
            .find(|file| file.path.ends_with("site-packages/click/core.py"))
            .unwrap();
        let license_file = files
            .iter()
            .find(|file| {
                file.path
                    .ends_with("site-packages/click-8.0.4.dist-info/LICENSE.rst")
            })
            .unwrap();
        assert!(core_file.for_packages.contains(&package.package_uid));
        assert!(license_file.for_packages.contains(&package.package_uid));
    }

    #[test]
    fn test_python_pkg_info_scan_assigns_installed_files_entries() {
        let temp_dir = tempfile::TempDir::new().expect("create temp dir");
        let site_packages = temp_dir.path().join("venv/lib/python3.11/site-packages");
        let egg_info = site_packages.join("examplepkg.egg-info");
        let package_dir = site_packages.join("examplepkg");

        std::fs::create_dir_all(&egg_info).unwrap();
        std::fs::create_dir_all(&package_dir).unwrap();
        std::fs::write(
            egg_info.join("PKG-INFO"),
            "Metadata-Version: 1.2\nName: examplepkg\nVersion: 1.0.0\n",
        )
        .unwrap();
        std::fs::write(
            egg_info.join("installed-files.txt"),
            "../examplepkg/__init__.py\n../examplepkg/core.py\n",
        )
        .unwrap();
        std::fs::write(package_dir.join("__init__.py"), "").unwrap();
        std::fs::write(package_dir.join("core.py"), "VALUE = 1\n").unwrap();

        let (files, result) = scan_and_assemble(temp_dir.path());
        let package = result
            .packages
            .iter()
            .find(|package| package.name.as_deref() == Some("examplepkg"))
            .unwrap();
        let core_file = files
            .iter()
            .find(|file| file.path.ends_with("site-packages/examplepkg/core.py"))
            .unwrap();
        assert!(core_file.for_packages.contains(&package.package_uid));
    }

    #[test]
    fn test_python_pkg_info_scan_assigns_sources_entries() {
        let temp_dir = tempfile::TempDir::new().expect("create temp dir");
        let egg_info = temp_dir.path().join("PyJPString.egg-info");
        let package_dir = temp_dir.path().join("jpstring");

        std::fs::create_dir_all(&egg_info).unwrap();
        std::fs::create_dir_all(&package_dir).unwrap();
        std::fs::write(
            egg_info.join("PKG-INFO"),
            "Metadata-Version: 1.0\nName: PyJPString\nVersion: 0.0.3\n",
        )
        .unwrap();
        std::fs::write(
            egg_info.join("SOURCES.txt"),
            "setup.py\nPyJPString.egg-info/PKG-INFO\nPyJPString.egg-info/top_level.txt\njpstring/__init__.py\n",
        )
        .unwrap();
        std::fs::write(
            temp_dir.path().join("setup.py"),
            "from setuptools import setup\n",
        )
        .unwrap();
        std::fs::write(egg_info.join("top_level.txt"), "jpstring\n").unwrap();
        std::fs::write(package_dir.join("__init__.py"), "").unwrap();

        let (files, result) = scan_and_assemble(temp_dir.path());
        let package = result
            .packages
            .iter()
            .find(|package| package.name.as_deref() == Some("PyJPString"))
            .unwrap();
        let setup_file = files
            .iter()
            .find(|file| file.path.ends_with("setup.py"))
            .unwrap();
        let module_init = files
            .iter()
            .find(|file| file.path.ends_with("jpstring/__init__.py"))
            .unwrap();
        let top_level = files
            .iter()
            .find(|file| file.path.ends_with("PyJPString.egg-info/top_level.txt"))
            .unwrap();
        assert!(setup_file.for_packages.contains(&package.package_uid));
        assert!(module_init.for_packages.contains(&package.package_uid));
        assert!(top_level.for_packages.contains(&package.package_uid));
    }

    #[test]
    fn test_python_wheel_origin_scan_assembles_distribution_and_origin_metadata() {
        let temp_dir = tempfile::TempDir::new().expect("create temp dir");
        let cache_dir = temp_dir.path().join(".cache/pip/wheels/eb/60/37/cachehash");
        std::fs::create_dir_all(&cache_dir).expect("create pip cache dir");
        std::fs::copy(
            "testdata/python/golden/pip_cache/wheels/construct/construct-2.10.68-py3-none-any.whl",
            cache_dir.join("construct-2.10.68-py3-none-any.whl"),
        )
        .expect("copy wheel fixture");
        std::fs::copy(
            "testdata/python/golden/pip_cache/wheels/construct/origin.json",
            cache_dir.join("origin.json"),
        )
        .expect("copy origin fixture");

        let (files, result) = scan_and_assemble(temp_dir.path());

        let package = result
            .packages
            .iter()
            .find(|package| package.name.as_deref() == Some("construct"))
            .expect("construct package should be assembled");

        assert_eq!(package.package_type, Some(PackageType::Pypi));
        assert_eq!(package.version.as_deref(), Some("2.10.68"));
        assert_eq!(
            package.purl.as_deref(),
            Some("pkg:pypi/construct@2.10.68?extension=py3-none-any")
        );
        assert_file_links_to_package(
            &files,
            "/construct-2.10.68-py3-none-any.whl",
            &package.package_uid,
            DatasourceId::PypiWheel,
        );
        assert_file_links_to_package(
            &files,
            "/origin.json",
            &package.package_uid,
            DatasourceId::PypiPipOriginJson,
        );
    }

    #[test]
    fn test_python_pip_inspect_scan_assembles_with_pyproject() {
        let temp_dir = tempfile::TempDir::new().expect("create temp dir");
        fs::write(
            temp_dir.path().join("pyproject.toml"),
            r#"[project]
name = "univers"
version = "0.0.0"
"#,
        )
        .expect("write pyproject.toml");
        fs::copy(
            "testdata/python/pip-inspect/pip-inspect.deplock",
            temp_dir.path().join("pip-inspect.deplock"),
        )
        .expect("copy pip-inspect fixture");

        let (files, result) = scan_and_assemble(temp_dir.path());

        let package = result
            .packages
            .iter()
            .find(|package| package.name.as_deref() == Some("univers"))
            .expect("pyproject + pip-inspect should assemble univers package");

        assert_eq!(package.package_type, Some(PackageType::Pypi));
        assert_eq!(package.version.as_deref(), Some("0.0.0"));
        assert!(
            package
                .datasource_ids
                .contains(&DatasourceId::PypiInspectDeplock)
        );
        assert_file_links_to_package(
            &files,
            "/pip-inspect.deplock",
            &package.package_uid,
            DatasourceId::PypiInspectDeplock,
        );
    }

    #[test]
    fn test_python_requirements_subdir_scan_assigns_to_project_package() {
        let temp_dir = tempfile::TempDir::new().expect("create temp dir");
        let requirements_dir = temp_dir.path().join("requirements");
        fs::create_dir_all(&requirements_dir).expect("create requirements dir");

        fs::write(
            temp_dir.path().join("pyproject.toml"),
            r#"[project]
name = "req-demo"
version = "1.0.0"
"#,
        )
        .expect("write pyproject.toml");
        fs::write(requirements_dir.join("dev.txt"), "pytest==8.3.5\n")
            .expect("write requirements/dev.txt");

        let (files, result) = scan_and_assemble(temp_dir.path());

        let package = result
            .packages
            .iter()
            .find(|package| package.name.as_deref() == Some("req-demo"))
            .expect("pyproject should assemble into a Python package");

        assert_eq!(package.package_type, Some(PackageType::Pypi));
        assert_eq!(package.version.as_deref(), Some("1.0.0"));
        assert!(
            package
                .datasource_ids
                .contains(&DatasourceId::PipRequirements)
        );
        assert!(
            package
                .datafile_paths
                .iter()
                .any(|path| path.ends_with("requirements/dev.txt"))
        );
        assert_file_links_to_package(
            &files,
            "/requirements/dev.txt",
            &package.package_uid,
            DatasourceId::PipRequirements,
        );
        assert!(result.dependencies.iter().any(|dep| {
            dep.datafile_path.ends_with("requirements/dev.txt")
                && dep.purl.is_some()
                && dep.for_package_uid.as_deref() == Some(package.package_uid.as_str())
        }));
    }
}