// SPDX-FileCopyrightText: 2024 Simon Bruder // // SPDX-License-Identifier: MPL-2.0 use std::rc::Rc; use pretty_assertions::assert_eq; use embed_licensing::{collect_from_manifest, CollectConfig, Crate, CrateLicense, Licensing}; mod common; use common::*; fn fixture_direct_dependencies() -> Result { let dependency = Rc::new(TestPackage::new(SimpleManifest { name: "dependency".to_string(), version: "1.0.0".to_string(), license: Some(spdx::Expression::parse("MIT")?), license_file: None, authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/dependency.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?); let dev_dependency = Rc::new(TestPackage::new(SimpleManifest { name: "dev_dependency".to_string(), version: "2.0.0".to_string(), license: Some(spdx::Expression::parse("Apache-2.0")?), license_file: None, authors: vec!["Jude Person ".to_string()], repository: Some("https://example.com/dev_dependency.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?); let build_dependency = Rc::new(TestPackage::new(SimpleManifest { name: "build_dependency".to_string(), version: "3.0.0".to_string(), license: Some(spdx::Expression::parse("LGPL-2.1-or-later")?), license_file: None, authors: vec!["John Person ".to_string()], repository: Some("https://example.com/build_dependency.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?); TestPackage::new(SimpleManifest { name: "foo".to_string(), version: "0.1.0".to_string(), license: Some(spdx::Expression::parse("GPL-3.0-or-later")?), license_file: None, authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![(None, dependency)].into(), dev_dependencies: vec![(None, dev_dependency)].into(), build_dependencies: vec![(None, build_dependency)].into(), }) } fn fixture_transitive_dependencies() -> Result { let transitive_dependency = Rc::new(TestPackage::new(SimpleManifest { name: "transitive_dependency".to_string(), version: "2.0.0".to_string(), license: Some(spdx::Expression::parse("ISC")?), license_file: None, authors: vec!["Jude Person ".to_string()], repository: Some("https://example.com/transitive_dependency.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?); let transitive_dev_dependency = Rc::new(TestPackage::new(SimpleManifest { name: "transitive_dev_dependency".to_string(), version: "3.0.0".to_string(), license: Some(spdx::Expression::parse("MIT")?), license_file: None, authors: vec!["Jennifer Person ".to_string()], repository: Some("https://example.com/transitive_dev_dependency.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?); let transitive_build_dependency = Rc::new(TestPackage::new(SimpleManifest { name: "transitive_build_dependency".to_string(), version: "4.0.0".to_string(), license: Some(spdx::Expression::parse("Apache-2.0")?), license_file: None, authors: vec!["Jasmine Person ".to_string()], repository: Some("https://example.com/transitive_build_dependency.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?); let dependency = Rc::new(TestPackage::new(SimpleManifest { name: "dependency".to_string(), version: "1.0.0".to_string(), license: Some(spdx::Expression::parse("MPL-2.0")?), license_file: None, authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/dependency.git".to_string()), dependencies: vec![(None, transitive_dependency)].into(), dev_dependencies: vec![(None, transitive_dev_dependency)].into(), build_dependencies: vec![(None, transitive_build_dependency)].into(), })?); TestPackage::new(SimpleManifest { name: "foo".to_string(), version: "0.1.0".to_string(), license: Some(spdx::Expression::parse("AGPL-3.0-or-later")?), license_file: None, authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![(None, dependency)].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), }) } #[test] fn no_dependencies() -> Result<()> { let pkg = TestPackage::new(SimpleManifest { name: "foo".to_string(), version: "1.0.0".to_string(), license: Some(spdx::Expression::parse("Apache-2.0 OR MIT")?), license_file: None, authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?; assert_eq!( collect_from_manifest(pkg.manifest_path(), CollectConfig::default())?, Licensing { packages: vec![Crate { name: "foo".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "Apache-2.0 OR MIT" )?), website: "https://example.com/foo.git".to_string() }], licenses: vec!["Apache-2.0", "MIT"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], } ); Ok(()) } #[test] fn direct_dependencies() -> Result<()> { let pkg = fixture_direct_dependencies()?; assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: true, build: true, ..Default::default() } )?, Licensing { packages: vec![ Crate { name: "build_dependency".to_string(), version: "3.0.0".to_string(), authors: vec!["John Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "LGPL-2.1-or-later" )?), website: "https://example.com/build_dependency.git".to_string() }, Crate { name: "dependency".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("MIT")?), website: "https://example.com/dependency.git".to_string() }, Crate { name: "dev_dependency".to_string(), version: "2.0.0".to_string(), authors: vec!["Jude Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("Apache-2.0")?), website: "https://example.com/dev_dependency.git".to_string() }, Crate { name: "foo".to_string(), version: "0.1.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "GPL-3.0-or-later" )?), website: "https://example.com/foo.git".to_string() }, ], licenses: vec!["Apache-2.0", "GPL-3.0", "LGPL-2.1", "MIT"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], } ); Ok(()) } #[test] fn direct_dependencies_no_dev() -> Result<()> { let pkg = fixture_direct_dependencies()?; assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: false, build: true, ..Default::default() } )?, Licensing { packages: vec![ Crate { name: "build_dependency".to_string(), version: "3.0.0".to_string(), authors: vec!["John Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "LGPL-2.1-or-later" )?), website: "https://example.com/build_dependency.git".to_string() }, Crate { name: "dependency".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("MIT")?), website: "https://example.com/dependency.git".to_string() }, Crate { name: "foo".to_string(), version: "0.1.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "GPL-3.0-or-later" )?), website: "https://example.com/foo.git".to_string() }, ], licenses: vec!["GPL-3.0", "LGPL-2.1", "MIT"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], } ); Ok(()) } #[test] fn direct_dependencies_no_build() -> Result<()> { let pkg = fixture_direct_dependencies()?; assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: true, build: false, ..Default::default() } )?, Licensing { packages: vec![ Crate { name: "dependency".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("MIT")?), website: "https://example.com/dependency.git".to_string() }, Crate { name: "dev_dependency".to_string(), version: "2.0.0".to_string(), authors: vec!["Jude Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("Apache-2.0")?), website: "https://example.com/dev_dependency.git".to_string() }, Crate { name: "foo".to_string(), version: "0.1.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "GPL-3.0-or-later" )?), website: "https://example.com/foo.git".to_string() }, ], licenses: vec!["Apache-2.0", "GPL-3.0", "MIT"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], } ); Ok(()) } #[test] fn direct_dependencies_no_dev_no_build() -> Result<()> { let pkg = fixture_direct_dependencies()?; assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: false, build: false, ..Default::default() } )?, Licensing { packages: vec![ Crate { name: "dependency".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("MIT")?), website: "https://example.com/dependency.git".to_string() }, Crate { name: "foo".to_string(), version: "0.1.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "GPL-3.0-or-later" )?), website: "https://example.com/foo.git".to_string() }, ], licenses: vec!["GPL-3.0", "MIT"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], } ); Ok(()) } #[test] fn transitive_dependencies() -> Result<()> { let pkg = fixture_transitive_dependencies()?; let expected = Licensing { packages: vec![ Crate { name: "dependency".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("MPL-2.0")?), website: "https://example.com/dependency.git".to_string(), }, Crate { name: "foo".to_string(), version: "0.1.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "AGPL-3.0-or-later", )?), website: "https://example.com/foo.git".to_string(), }, Crate { name: "transitive_build_dependency".to_string(), version: "4.0.0".to_string(), authors: vec!["Jasmine Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("Apache-2.0")?), website: "https://example.com/transitive_build_dependency.git".to_string(), }, // transitive_dev_dependency not included! Crate { name: "transitive_dependency".to_string(), version: "2.0.0".to_string(), authors: vec!["Jude Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("ISC")?), website: "https://example.com/transitive_dependency.git".to_string(), }, ], // transitive_dev_dependency’s MIT not included! licenses: vec!["AGPL-3.0", "Apache-2.0", "ISC", "MPL-2.0"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], }; assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: true, build: true, ..Default::default() } )?, expected ); // for transitive dependencies, it does not matter if dev dependencies are collected assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: false, build: true, ..Default::default() } )?, expected ); Ok(()) } #[test] fn transitive_dependencies_no_build() -> Result<()> { let pkg = fixture_transitive_dependencies()?; let expected = Licensing { packages: vec![ Crate { name: "dependency".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("MPL-2.0")?), website: "https://example.com/dependency.git".to_string(), }, Crate { name: "foo".to_string(), version: "0.1.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "AGPL-3.0-or-later", )?), website: "https://example.com/foo.git".to_string(), }, Crate { name: "transitive_dependency".to_string(), version: "2.0.0".to_string(), authors: vec!["Jude Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse("ISC")?), website: "https://example.com/transitive_dependency.git".to_string(), }, ], licenses: vec!["AGPL-3.0", "ISC", "MPL-2.0"] .into_iter() .map(spdx::license_id) .map(Option::unwrap) .collect(), exceptions: vec![], }; assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: true, build: false, ..Default::default() } )?, expected ); // see transitive_dependencies assert_eq!( collect_from_manifest( pkg.manifest_path(), CollectConfig { dev: false, build: false, ..Default::default() } )?, expected ); Ok(()) } #[test] fn license_file() -> Result<()> { let pkg = TestPackage::new(SimpleManifest { name: "foo".to_string(), version: "1.0.0".to_string(), license: None, license_file: Some(( "LICENSE".into(), "This software is available under the MY SPECIAL CUSTOM license.".to_string(), )), authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?; assert_eq!( collect_from_manifest(pkg.manifest_path(), CollectConfig::default())?, Licensing { packages: vec![Crate { name: "foo".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::Other( "This software is available under the MY SPECIAL CUSTOM license.".to_string() ), website: "https://example.com/foo.git".to_string() }], licenses: vec![], exceptions: vec![], } ); Ok(()) } #[test] fn exceptions() -> Result<()> { let pkg = TestPackage::new(SimpleManifest { name: "foo".to_string(), version: "1.0.0".to_string(), license: Some(spdx::Expression::parse("Apache-2.0 WITH LLVM-exception")?), license_file: None, authors: vec!["Jane Person ".to_string()], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?; assert_eq!( collect_from_manifest(pkg.manifest_path(), CollectConfig::default())?, Licensing { packages: vec![Crate { name: "foo".to_string(), version: "1.0.0".to_string(), authors: vec!["Jane Person ".to_string()], license: CrateLicense::SpdxExpression(spdx::Expression::parse( "Apache-2.0 WITH LLVM-exception" )?), website: "https://example.com/foo.git".to_string() }], licenses: vec![spdx::license_id("Apache-2.0").unwrap()], exceptions: vec![spdx::exception_id("LLVM-exception").unwrap()], } ); Ok(()) } #[test] fn error_no_license() -> Result<()> { let pkg = TestPackage::new(SimpleManifest { name: "foo_no_license".to_string(), version: "1.0.0".to_string(), license: None, license_file: None, authors: vec![], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?; match collect_from_manifest(pkg.manifest_path(), CollectConfig::default()).unwrap_err() { embed_licensing::Error::NoLicense(name) => assert_eq!(name, "foo_no_license"), _ => panic!("invalid error"), } Ok(()) } #[test] fn error_non_spdx_license() -> Result<()> { let pkg = TestPackage::new(SimpleManifest { name: "foo_non_spdx_license".to_string(), version: "1.0.0".to_string(), license: Some(spdx::Expression::parse( "AGPL-3.0-or-later OR LicenseRef-Mycompany-Commercial-License", )?), license_file: None, authors: vec![], repository: Some("https://example.com/foo.git".to_string()), dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?; match collect_from_manifest(pkg.manifest_path(), CollectConfig::default()).unwrap_err() { embed_licensing::Error::NonSpdxLicense(name) => assert_eq!(name, "foo_non_spdx_license"), _ => panic!("invalid error"), } Ok(()) } #[test] fn error_no_website() -> Result<()> { let pkg = TestPackage::new(SimpleManifest { name: "foo_no_website".to_string(), version: "1.0.0".to_string(), license: Some(spdx::Expression::parse("MIT")?), license_file: None, authors: vec![], repository: None, dependencies: vec![].into(), dev_dependencies: vec![].into(), build_dependencies: vec![].into(), })?; match collect_from_manifest(pkg.manifest_path(), CollectConfig::default()).unwrap_err() { embed_licensing::Error::NoWebsite(name) => assert_eq!(name, "foo_no_website"), _ => panic!("invalid error"), } Ok(()) }