mirror of
https://github.com/loot/libloot.git
synced 2026-07-27 14:16:01 -07:00
Port unit tests from the C++ libloot implementation
This gives function/line/region coverage of 88%/91%/84%. Most of the translation bugs fixed in earlier commits were found while writing these tests: the tests were kept separate to not drown out the fixes in the commit diffs.
This commit is contained in:
@@ -17,7 +17,7 @@ Currently complete:
|
||||
- [x] Archive-related functionality
|
||||
- [x] Metadata-related functionality (excluding writing YAML)
|
||||
- [x] Sorting functionality
|
||||
- [ ] Unit tests
|
||||
- [x] Unit tests
|
||||
- [ ] Integration tests
|
||||
- [ ] C++ FFI
|
||||
- [ ] Python FFI
|
||||
@@ -35,7 +35,7 @@ $env:LIBLOOT_REVISION = git rev-parse --short HEAD
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
There aren't many tests, but those that exist can be run by first extracting the [testing-plugins](https://github.com/Ortham/testing-plugins) archive to this readme's directory (so that there's a `testing-plugins` directory there), then running:
|
||||
The tests include a complete port of libloot's tests, and can be run by first extracting the [testing-plugins](https://github.com/Ortham/testing-plugins) archive to this readme's directory (so that there's a `testing-plugins` directory there), then running:
|
||||
|
||||
```
|
||||
cargo test
|
||||
|
||||
+289
-6
@@ -195,12 +195,295 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn are_file_paths_equivalent_should_be_true_if_given_the_same_path_twice() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("test");
|
||||
std::fs::write(&file_path, "").unwrap();
|
||||
mod find_associated_archives {
|
||||
use std::path::absolute;
|
||||
|
||||
assert!(are_file_paths_equivalent(&file_path, &file_path))
|
||||
use rstest_reuse::apply;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::tests::{
|
||||
BLANK_DIFFERENT_ESM, BLANK_DIFFERENT_ESP, BLANK_ESM, BLANK_ESP,
|
||||
BLANK_MASTER_DEPENDENT_ESM, all_game_types, copy_file, source_plugins_path,
|
||||
};
|
||||
|
||||
const NON_ASCII_ESP: &str = "non\u{00C1}scii.esp";
|
||||
|
||||
struct Fixture {
|
||||
_temp_dir: TempDir,
|
||||
cache: GameCache,
|
||||
data_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub fn new(game_type: GameType) -> Self {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
|
||||
let mut cache = GameCache::default();
|
||||
|
||||
let data_path = tmp_dir.path().to_path_buf();
|
||||
|
||||
match game_type {
|
||||
GameType::TES3 | GameType::OpenMW => {}
|
||||
GameType::FO4 | GameType::FO4VR | GameType::Starfield => {
|
||||
let source = absolute("./testing-plugins/Fallout 4/Data").unwrap();
|
||||
copy_file(&source, &data_path, "Blank - Main.ba2");
|
||||
copy_file(&source, &data_path, "Blank - Textures.ba2");
|
||||
std::fs::copy(
|
||||
source.join("Blank - Main.ba2"),
|
||||
data_path.join("non\u{00C1}scii.ba2"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::copy(
|
||||
source.join("Blank - Main.ba2"),
|
||||
data_path.join("Blank - Different - Suffix.ba2"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cache.set_archive_paths(vec![
|
||||
data_path.join("Blank - Main.ba2"),
|
||||
data_path.join("Blank - Textures.ba2"),
|
||||
data_path.join("non\u{00C1}scii.ba2"),
|
||||
data_path.join("Blank - Different - Main.ba2"),
|
||||
]);
|
||||
}
|
||||
_ => {
|
||||
let source = source_plugins_path(game_type);
|
||||
copy_file(&source, &data_path, "Blank.bsa");
|
||||
std::fs::copy(
|
||||
source.join("Blank.bsa"),
|
||||
data_path.join("non\u{00C1}scii.bsa"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::copy(
|
||||
source.join("Blank.bsa"),
|
||||
data_path.join("Blank - Different - Main.bsa"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cache.set_archive_paths(vec![
|
||||
data_path.join("Blank.bsa"),
|
||||
data_path.join("non\u{00C1}scii.bsa"),
|
||||
data_path.join("Blank - Different - Suffix.bsa"),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
_temp_dir: tmp_dir,
|
||||
data_path,
|
||||
cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[apply(all_game_types)]
|
||||
fn should_return_empty_vec_if_no_matching_archives_are_found(game_type: GameType) {
|
||||
let fixture = Fixture::new(game_type);
|
||||
|
||||
let archives = find_associated_archives(
|
||||
game_type,
|
||||
&fixture.cache,
|
||||
&fixture.data_path.join(BLANK_MASTER_DEPENDENT_ESM),
|
||||
);
|
||||
|
||||
assert!(archives.is_empty());
|
||||
}
|
||||
|
||||
#[apply(all_game_types)]
|
||||
fn should_find_an_archive_that_exactly_matches_an_esm_file_basename_except_for_morrowind_and_oblivion(
|
||||
game_type: GameType,
|
||||
) {
|
||||
let fixture = Fixture::new(game_type);
|
||||
|
||||
let archives = find_associated_archives(
|
||||
game_type,
|
||||
&fixture.cache,
|
||||
&fixture.data_path.join(BLANK_ESM),
|
||||
);
|
||||
|
||||
if matches!(
|
||||
game_type,
|
||||
GameType::TES3 | GameType::OpenMW | GameType::TES4
|
||||
) {
|
||||
assert!(archives.is_empty());
|
||||
} else {
|
||||
assert!(!archives.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[apply(all_game_types)]
|
||||
fn should_find_an_archive_that_exactly_matches_a_non_ascii_esp_file_basename_except_for_morrowind_and_starfield(
|
||||
game_type: GameType,
|
||||
) {
|
||||
let fixture = Fixture::new(game_type);
|
||||
|
||||
let archives = find_associated_archives(
|
||||
game_type,
|
||||
&fixture.cache,
|
||||
&fixture.data_path.join(NON_ASCII_ESP),
|
||||
);
|
||||
|
||||
if matches!(
|
||||
game_type,
|
||||
GameType::TES3 | GameType::OpenMW | GameType::Starfield
|
||||
) {
|
||||
assert!(archives.is_empty());
|
||||
} else {
|
||||
assert!(!archives.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[apply(all_game_types)]
|
||||
fn should_find_an_archive_that_starts_with_an_esp_file_basename_except_for_morrowind_and(
|
||||
game_type: GameType,
|
||||
) {
|
||||
let fixture = Fixture::new(game_type);
|
||||
|
||||
let archives = find_associated_archives(
|
||||
game_type,
|
||||
&fixture.cache,
|
||||
&fixture.data_path.join(BLANK_ESP),
|
||||
);
|
||||
|
||||
if matches!(game_type, GameType::TES3 | GameType::OpenMW) {
|
||||
assert!(archives.is_empty());
|
||||
} else {
|
||||
assert!(!archives.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[apply(all_game_types)]
|
||||
fn should_find_an_archive_that_starts_with_an_esm_file_basename_only_for_fallout(
|
||||
game_type: GameType,
|
||||
) {
|
||||
let fixture = Fixture::new(game_type);
|
||||
|
||||
let archives = find_associated_archives(
|
||||
game_type,
|
||||
&fixture.cache,
|
||||
&fixture.data_path.join(BLANK_DIFFERENT_ESM),
|
||||
);
|
||||
|
||||
if matches!(
|
||||
game_type,
|
||||
GameType::FO3 | GameType::FONV | GameType::FO4 | GameType::FO4VR
|
||||
) {
|
||||
assert!(!archives.is_empty());
|
||||
} else {
|
||||
assert!(archives.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[apply(all_game_types)]
|
||||
fn should_find_an_archive_that_starts_with_an_esp_file_basename_only_for_oblivion_and_fallout(
|
||||
game_type: GameType,
|
||||
) {
|
||||
let fixture = Fixture::new(game_type);
|
||||
|
||||
let archives = find_associated_archives(
|
||||
game_type,
|
||||
&fixture.cache,
|
||||
&fixture.data_path.join(BLANK_DIFFERENT_ESP),
|
||||
);
|
||||
|
||||
if matches!(
|
||||
game_type,
|
||||
GameType::TES4 | GameType::FO3 | GameType::FONV | GameType::FO4 | GameType::FO4VR
|
||||
) {
|
||||
assert!(!archives.is_empty());
|
||||
} else {
|
||||
assert!(archives.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod are_file_paths_equivalent {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_given_equal_paths_that_exist() {
|
||||
let file_path = Path::new("README.md");
|
||||
|
||||
assert!(file_path.exists());
|
||||
assert!(are_file_paths_equivalent(file_path, file_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_given_equal_paths_that_do_not_exist() {
|
||||
let file_path = Path::new("missing");
|
||||
|
||||
assert!(!file_path.exists());
|
||||
assert!(are_file_paths_equivalent(file_path, file_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_given_case_insensitively_equal_paths_that_do_not_exist() {
|
||||
let file_path1 = Path::new("missing");
|
||||
let file_path2 = Path::new("MISSING");
|
||||
|
||||
assert!(!file_path1.exists());
|
||||
assert!(!file_path2.exists());
|
||||
assert!(!are_file_paths_equivalent(file_path1, file_path2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_given_case_insensitively_unequal_paths_that_exist() {
|
||||
let file_path1 = Path::new("README.md");
|
||||
let file_path2 = Path::new("LICENSE");
|
||||
|
||||
assert!(file_path1.exists());
|
||||
assert!(file_path2.exists());
|
||||
assert!(!are_file_paths_equivalent(file_path1, file_path2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn should_be_true_if_given_case_insensitively_equal_paths_that_exist() {
|
||||
let file_path1 = Path::new("README.md");
|
||||
let file_path2 = Path::new("readme.md");
|
||||
|
||||
assert!(file_path1.exists());
|
||||
assert!(file_path2.exists());
|
||||
assert!(are_file_paths_equivalent(file_path1, file_path2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn should_be_true_if_equal_paths_have_characters_that_are_unrepresentable_in_the_system_multi_byte_code_page()
|
||||
{
|
||||
let file_path =
|
||||
Path::new("\u{2551}\u{00BB}\u{00C1}\u{2510}\u{2557}\u{00FE}\u{00C3}\u{00CE}.txt");
|
||||
|
||||
assert!(are_file_paths_equivalent(file_path, file_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn should_be_false_if_case_insensitively_equal_paths_have_characters_that_are_unrepresentable_in_the_system_multi_byte_code_page_and_do_not_exist()
|
||||
{
|
||||
let file_path1 =
|
||||
Path::new("\u{2551}\u{00BB}\u{00C1}\u{2510}\u{2557}\u{00FE}\u{00E3}\u{00CE}.txt");
|
||||
let file_path2 =
|
||||
Path::new("\u{2551}\u{00BB}\u{00C1}\u{2510}\u{2557}\u{00FE}\u{00C3}\u{00CE}.txt");
|
||||
|
||||
assert!(!are_file_paths_equivalent(file_path1, file_path2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn should_be_false_if_given_case_insensitively_equal_paths_that_exist() {
|
||||
let tmp_dir = tempdir();
|
||||
let file_path1 = tmp_dir.path().join("test");
|
||||
let file_path2 = tmp_dir.path().join("TEST");
|
||||
|
||||
std::fs::File::create(&file_path1).unwrap();
|
||||
std::fs::File::create(&file_path2).unwrap();
|
||||
|
||||
assert!(file_path1.exists());
|
||||
assert!(file_path2.exists());
|
||||
assert!(!are_file_paths_equivalent(file_path1, file_path2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,66 @@ mod error;
|
||||
mod find;
|
||||
mod parse;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub use find::find_associated_archives;
|
||||
pub use parse::assets_in_archives;
|
||||
|
||||
pub fn do_assets_overlap(
|
||||
assets: &BTreeMap<u64, BTreeSet<u64>>,
|
||||
other_assets: &BTreeMap<u64, BTreeSet<u64>>,
|
||||
) -> bool {
|
||||
let mut assets_iter = assets.iter();
|
||||
let mut other_assets_iter = other_assets.iter();
|
||||
|
||||
let mut assets = assets_iter.next();
|
||||
let mut other_assets = other_assets_iter.next();
|
||||
while let (Some((folder, files)), Some((other_folder, other_files))) = (assets, other_assets) {
|
||||
if folder < other_folder {
|
||||
assets = assets_iter.next();
|
||||
} else if folder > other_folder {
|
||||
other_assets = other_assets_iter.next();
|
||||
} else if files.intersection(other_files).next().is_some() {
|
||||
return true;
|
||||
} else {
|
||||
// The folder hashes are equal but they don't contain any of the same
|
||||
// file hashes, move on to the next folder. It doesn't matter which
|
||||
// iterator gets incremented.
|
||||
assets = assets_iter.next();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod do_assets_overlap {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_return_true_if_the_same_file_exists_in_the_same_folder() {
|
||||
let path = PathBuf::from("./testing-plugins/Oblivion/Data/Blank.bsa");
|
||||
let assets = assets_in_archives(&[path]);
|
||||
|
||||
assert!(do_assets_overlap(&assets, &assets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_false_if_the_same_file_exists_in_different_folders() {
|
||||
let path = PathBuf::from("./testing-plugins/Oblivion/Data/Blank.bsa");
|
||||
let assets1 = assets_in_archives(&[path]);
|
||||
|
||||
let path = PathBuf::from("./testing-plugins/Skyrim/Data/Blank.bsa");
|
||||
let assets2 = assets_in_archives(&[path]);
|
||||
|
||||
assert_eq!(assets1.get(&0), assets2.get(&0x2E01002E));
|
||||
|
||||
assert!(!do_assets_overlap(&assets1, &assets2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,3 +108,195 @@ pub(super) fn to_u64(bytes: &[u8]) -> u64 {
|
||||
pub(super) fn to_usize(size: u32) -> usize {
|
||||
usize::try_from(size).expect("usize can hold a u32")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod get_assets_in_archive {
|
||||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
io::SeekFrom,
|
||||
};
|
||||
|
||||
use rstest::rstest;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn hash<T: Hash>(value: T) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
value.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_file_cannot_be_opened() {
|
||||
let path = Path::new("./invalid.bsa");
|
||||
assert!(get_assets_in_archive(path).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_support_v103_bsas() {
|
||||
let path = Path::new("./testing-plugins/Oblivion/Data/Blank.bsa");
|
||||
let assets = get_assets_in_archive(path).unwrap();
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
let expected_key = 0;
|
||||
assert_eq!(1, assets.len());
|
||||
assert_eq!(1, files_count);
|
||||
assert_eq!(expected_key, *assets.first_key_value().unwrap().0);
|
||||
assert_eq!(1, assets.get(&expected_key).unwrap().len());
|
||||
assert_eq!(
|
||||
0x4670B6836C077365,
|
||||
*assets.get(&expected_key).unwrap().first().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_support_v104_bsas() {
|
||||
let path = Path::new("./testing-plugins/Skyrim/Data/Blank.bsa");
|
||||
let assets = get_assets_in_archive(path).unwrap();
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
let expected_key = 0x2E01002E;
|
||||
assert_eq!(1, assets.len());
|
||||
assert_eq!(1, files_count);
|
||||
assert_eq!(expected_key, *assets.first_key_value().unwrap().0);
|
||||
assert_eq!(1, assets.get(&expected_key).unwrap().len());
|
||||
assert_eq!(
|
||||
0x4670B6836C077365,
|
||||
*assets.get(&expected_key).unwrap().first().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_support_v105_bsas() {
|
||||
let path = Path::new("./testing-plugins/SkyrimSE/Data/Blank.bsa");
|
||||
let assets = get_assets_in_archive(path).unwrap();
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
let expected_key = 0xB68102C964176E73;
|
||||
assert_eq!(1, assets.len());
|
||||
assert_eq!(1, files_count);
|
||||
assert_eq!(expected_key, *assets.first_key_value().unwrap().0);
|
||||
assert_eq!(1, assets.get(&expected_key).unwrap().len());
|
||||
assert_eq!(
|
||||
0x4670B6836C077365,
|
||||
*assets.get(&expected_key).unwrap().first().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_support_general_ba2s() {
|
||||
let path = Path::new("./testing-plugins/Fallout 4/Data/Blank - Main.ba2");
|
||||
let assets = get_assets_in_archive(path).unwrap();
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
let expected_key = hash("dev\\git\\testing-plugins".as_bytes());
|
||||
let expected_file_hash = hash("license.txt".as_bytes());
|
||||
|
||||
assert_eq!(1, assets.len());
|
||||
assert_eq!(1, files_count);
|
||||
|
||||
let (key, value) = assets.first_key_value().unwrap();
|
||||
assert_eq!(expected_key, *key);
|
||||
assert_eq!(1, value.len());
|
||||
assert_eq!(expected_file_hash, *value.first().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_support_texture_ba2s() {
|
||||
let path = Path::new("./testing-plugins/Fallout 4/Data/Blank - Textures.ba2");
|
||||
let assets = get_assets_in_archive(path).unwrap();
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
let expected_key = hash("dev\\git\\testing-plugins".as_bytes());
|
||||
let expected_file_hash = hash("blank.dds".as_bytes());
|
||||
|
||||
assert_eq!(1, assets.len());
|
||||
assert_eq!(1, files_count);
|
||||
|
||||
let (key, value) = assets.first_key_value().unwrap();
|
||||
assert_eq!(expected_key, *key);
|
||||
assert_eq!(1, value.len());
|
||||
assert_eq!(expected_file_hash, *value.first().unwrap());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn should_support_ba2_versions(#[values(1, 2, 3, 7, 8)] version: u32) {
|
||||
use std::io::{Seek, Write};
|
||||
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let path = tmp_dir.path().join("test.ba2");
|
||||
|
||||
std::fs::copy("./testing-plugins/Fallout 4/Data/Blank - Main.ba2", &path).unwrap();
|
||||
|
||||
{
|
||||
let mut file = File::options().write(true).open(&path).unwrap();
|
||||
file.seek(SeekFrom::Start(4)).unwrap();
|
||||
file.write_all(&version.to_le_bytes()).unwrap();
|
||||
}
|
||||
|
||||
let assets = get_assets_in_archive(&path).unwrap();
|
||||
assert!(!assets.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
mod assets_in_archives {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_skip_files_that_cannot_be_read() {
|
||||
let paths = [
|
||||
PathBuf::from("invalid.bsa"),
|
||||
PathBuf::from("./testing-plugins/Skyrim/Data/Blank.bsa"),
|
||||
];
|
||||
|
||||
let assets = assets_in_archives(&paths);
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
assert_eq!(1, assets.len());
|
||||
assert_eq!(1, files_count);
|
||||
|
||||
let (key, value) = assets.first_key_value().unwrap();
|
||||
assert_eq!(0x2E01002E, *key);
|
||||
assert_eq!(1, value.len());
|
||||
assert_eq!(0x4670B6836C077365, *value.first().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_combine_assets_from_each_loaded_archive() {
|
||||
let paths = [
|
||||
PathBuf::from("./testing-plugins/Oblivion/Data/Blank.bsa"),
|
||||
PathBuf::from("./testing-plugins/Skyrim/Data/Blank.bsa"),
|
||||
PathBuf::from("./testing-plugins/SkyrimSE/Data/Blank.bsa"),
|
||||
];
|
||||
|
||||
let assets = assets_in_archives(&paths);
|
||||
|
||||
let files_count: usize = assets.values().map(|v| v.len()).sum();
|
||||
|
||||
assert_eq!(3, assets.len());
|
||||
assert_eq!(3, files_count);
|
||||
|
||||
let value = assets.get(&0).unwrap();
|
||||
assert_eq!(1, value.len());
|
||||
assert_eq!(0x4670B6836C077365, *value.first().unwrap());
|
||||
|
||||
let value = assets.get(&0x2E01002E).unwrap();
|
||||
assert_eq!(1, value.len());
|
||||
assert_eq!(0x4670B6836C077365, *value.first().unwrap());
|
||||
|
||||
let value = assets.get(&0xB68102C964176E73).unwrap();
|
||||
assert_eq!(1, value.len());
|
||||
assert_eq!(0x4670B6836C077365, *value.first().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,3 +106,78 @@ fn filter_cleaning_data_on_conditions(
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod evaluate_all_conditions {
|
||||
use crate::{
|
||||
metadata::{Message, MessageType, Tag, TagSuggestion},
|
||||
tests::{BLANK_DIFFERENT_ESM, BLANK_ESM, BLANK_ESP, source_plugins_path},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_evaluate_all_conditions_on_metadata_in_plugin_metadata_object() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_group("group1".into());
|
||||
|
||||
let condition = "file(\"missing.esp\")".to_string();
|
||||
let file1 = File::new(BLANK_ESP.into());
|
||||
let file2 = File::new(BLANK_DIFFERENT_ESM.into()).with_condition(condition.clone());
|
||||
let files = vec![file1.clone(), file2];
|
||||
plugin.set_load_after_files(files.clone());
|
||||
plugin.set_requirements(files.clone());
|
||||
plugin.set_incompatibilities(files.clone());
|
||||
|
||||
let message1 = Message::new(MessageType::Say, "content1".into());
|
||||
let message2 =
|
||||
Message::new(MessageType::Say, "content2".into()).with_condition(condition.clone());
|
||||
plugin.set_messages(vec![message1.clone(), message2]);
|
||||
|
||||
let tag1 = Tag::new("Delev".into(), TagSuggestion::Addition);
|
||||
let tag2 =
|
||||
Tag::new("Relev".into(), TagSuggestion::Addition).with_condition(condition.clone());
|
||||
plugin.set_tags(vec![tag1.clone(), tag2]);
|
||||
|
||||
let info1 = PluginCleaningData::new(0x374E2A6F, "utility1".into());
|
||||
let info2 = PluginCleaningData::new(0xDEADBEEF, "utility2".into());
|
||||
plugin.set_dirty_info(vec![info1.clone(), info2.clone()]);
|
||||
plugin.set_clean_info(vec![info1.clone(), info2.clone()]);
|
||||
|
||||
let state = loot_condition_interpreter::State::new(
|
||||
loot_condition_interpreter::GameType::Oblivion,
|
||||
source_plugins_path(crate::GameType::TES4),
|
||||
);
|
||||
let result = evaluate_all_conditions(plugin, &state).unwrap().unwrap();
|
||||
|
||||
let expected_files = &[file1];
|
||||
let expected_info = &[info1];
|
||||
assert_eq!("group1", result.group().unwrap());
|
||||
assert_eq!(expected_files, result.load_after_files());
|
||||
assert_eq!(expected_files, result.requirements());
|
||||
assert_eq!(expected_files, result.incompatibilities());
|
||||
assert_eq!(&[message1], result.messages());
|
||||
assert_eq!(&[tag1], result.tags());
|
||||
assert_eq!(expected_info, result.dirty_info());
|
||||
assert_eq!(expected_info, result.clean_info());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_none_if_evaluated_plugin_metadata_has_name_only() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let file = File::new(BLANK_DIFFERENT_ESM.into())
|
||||
.with_condition("file(\"missing.esp\")".into());
|
||||
plugin.set_load_after_files(vec![file]);
|
||||
|
||||
let state = loot_condition_interpreter::State::new(
|
||||
loot_condition_interpreter::GameType::Oblivion,
|
||||
source_plugins_path(crate::GameType::TES4),
|
||||
);
|
||||
assert!(evaluate_all_conditions(plugin, &state).unwrap().is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+916
-1
File diff suppressed because it is too large
Load Diff
+1301
-36
File diff suppressed because it is too large
Load Diff
@@ -194,6 +194,131 @@ impl EmitYaml for File {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod file_eq {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_case_insensitive_on_name() {
|
||||
assert_eq!(File::new("name".into()), File::new("name".into()));
|
||||
assert_eq!(File::new("name".into()), File::new("NAME".into()));
|
||||
assert_ne!(File::new("name1".into()), File::new("name2".into()));
|
||||
}
|
||||
}
|
||||
|
||||
mod filename_eq {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_case_insensitive_on_name() {
|
||||
assert_eq!(Filename::new("name".into()), Filename::new("name".into()));
|
||||
assert_eq!(Filename::new("name".into()), Filename::new("NAME".into()));
|
||||
assert_ne!(Filename::new("name1".into()), Filename::new("name2".into()));
|
||||
}
|
||||
}
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_only_set_name_if_decoding_from_scalar() {
|
||||
let yaml = parse("name1");
|
||||
|
||||
let file = File::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("name1", file.name().as_str());
|
||||
assert!(file.display_name().is_none());
|
||||
assert!(file.condition().is_none());
|
||||
assert!(file.detail().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(File::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_name_is_missing() {
|
||||
let yaml = parse("{display: display1}");
|
||||
|
||||
assert!(File::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_an_invalid_condition() {
|
||||
let yaml = parse("{name: name1, condition: invalid}");
|
||||
|
||||
assert!(File::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_given_fields() {
|
||||
let yaml = parse(
|
||||
"{name: name1, display: display1, condition: 'file(\"Foo.esp\")', detail: 'details'}",
|
||||
);
|
||||
|
||||
let file = File::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("name1", file.name().as_str());
|
||||
assert_eq!("display1", file.display_name().unwrap());
|
||||
assert_eq!("file(\"Foo.esp\")", file.condition().unwrap());
|
||||
assert_eq!(&[MessageContent::new("details".into())], file.detail());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_optional_fields_empty_if_not_present() {
|
||||
let yaml = parse("{name: name1}");
|
||||
|
||||
let file = File::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("name1", file.name().as_str());
|
||||
assert!(file.display_name().is_none());
|
||||
assert!(file.condition().is_none());
|
||||
assert!(file.detail().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_read_all_listed_detail_message_contents() {
|
||||
let yaml = parse(
|
||||
"{name: name1, detail: [{text: english, lang: en}, {text: french, lang: fr}]}",
|
||||
);
|
||||
|
||||
let file = File::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
MessageContent::new("english".into()),
|
||||
MessageContent::new("french".into()).with_language("fr".into())
|
||||
],
|
||||
file.detail()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_error_if_one_detail_is_given_and_it_is_not_english() {
|
||||
let yaml = parse("name: name1\ndetail:\n - lang: fr\n text: content1");
|
||||
|
||||
let file = File::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[MessageContent::new("content1".into()).with_language("fr".into())],
|
||||
file.detail()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_multiple_details_are_given_and_none_are_english() {
|
||||
let yaml = parse(
|
||||
"name: name1\ndetail:\n - lang: de\n text: content1\n - lang: fr\n text: content2",
|
||||
);
|
||||
|
||||
assert!(File::try_from(&yaml).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use crate::metadata::emit;
|
||||
|
||||
|
||||
@@ -126,6 +126,48 @@ impl EmitYaml for Group {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(Group::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_name_is_missing() {
|
||||
let yaml = parse("{description: text}");
|
||||
|
||||
assert!(Group::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_given_fields() {
|
||||
let yaml = parse("{name: group1, description: text, after: [ other_group ]}");
|
||||
|
||||
let group = Group::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("group1", group.name());
|
||||
assert_eq!("text", group.description().unwrap());
|
||||
assert_eq!(&["other_group"], group.after_groups());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_optional_fields_empty_if_not_present() {
|
||||
let yaml = parse("{name: group1}");
|
||||
|
||||
let group = Group::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("group1", group.name());
|
||||
assert!(group.description().is_none());
|
||||
assert!(group.after_groups().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use super::*;
|
||||
use crate::metadata::emit;
|
||||
|
||||
@@ -105,6 +105,53 @@ impl EmitYaml for Location {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_only_set_name_if_decoding_from_scalar() {
|
||||
let yaml = parse("https://www.example.com");
|
||||
|
||||
let location = Location::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("https://www.example.com", location.url());
|
||||
assert!(location.name().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(Location::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_link_is_missing() {
|
||||
let yaml = parse("{name: example}");
|
||||
|
||||
assert!(Location::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_name_is_missing() {
|
||||
let yaml = parse("{link: https://www.example.com}");
|
||||
|
||||
assert!(Location::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_fields() {
|
||||
let yaml = parse("{link: https://www.example.com, name: example}");
|
||||
|
||||
let location = Location::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("https://www.example.com", location.url());
|
||||
assert_eq!("example", location.name().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use crate::metadata::emit;
|
||||
|
||||
@@ -112,7 +159,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_emit_url_only_if_there_is_no_name() {
|
||||
let location = Location::new("http://www.example.com".into());
|
||||
let location = Location::new("https://www.example.com".into());
|
||||
let yaml = emit(&location);
|
||||
|
||||
assert_eq!(format!("'{}'", location.url), yaml);
|
||||
@@ -121,7 +168,7 @@ mod tests {
|
||||
#[test]
|
||||
fn should_emit_map_if_there_is_a_name() {
|
||||
let location =
|
||||
Location::new("http://www.example.com".into()).with_name("example".into());
|
||||
Location::new("https://www.example.com".into()).with_name("example".into());
|
||||
let yaml = emit(&location);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
+292
-2
@@ -453,14 +453,128 @@ mod tests {
|
||||
#[test]
|
||||
fn should_return_the_only_element_of_a_single_element_slice() {
|
||||
let slice = &[MessageContent::new("test".into()).with_language("de".into())];
|
||||
let content = select_message_content(slice, "fr");
|
||||
let content = select_message_content(slice, "fr").unwrap();
|
||||
|
||||
assert_eq!(&slice[0], content.unwrap());
|
||||
assert_eq!(&slice[0], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_element_with_exactly_matching_locale_code() {
|
||||
let slice = &[
|
||||
MessageContent::new("test1".into()).with_language("en".into()),
|
||||
MessageContent::new("test2".into()).with_language("de".into()),
|
||||
MessageContent::new("test3".into()).with_language("pt".into()),
|
||||
MessageContent::new("test4".into()).with_language("pt_PT".into()),
|
||||
MessageContent::new("test5".into()).with_language("pt_BR".into()),
|
||||
];
|
||||
|
||||
let content = select_message_content(slice, "pt_BR").unwrap();
|
||||
|
||||
assert_eq!(&slice[4], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_element_with_matching_language_code_if_exactly_matching_local_code_is_not_present()
|
||||
{
|
||||
let slice = &[
|
||||
MessageContent::new("test1".into()).with_language("en".into()),
|
||||
MessageContent::new("test2".into()).with_language("de".into()),
|
||||
MessageContent::new("test3".into()).with_language("pt".into()),
|
||||
MessageContent::new("test4".into()).with_language("pt_PT".into()),
|
||||
];
|
||||
|
||||
let content = select_message_content(slice, "pt_BR").unwrap();
|
||||
|
||||
assert_eq!(&slice[2], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_element_with_en_language_code_if_no_matching_language_code_is_present() {
|
||||
let slice = &[
|
||||
MessageContent::new("test1".into()).with_language("en".into()),
|
||||
MessageContent::new("test2".into()).with_language("de".into()),
|
||||
MessageContent::new("test3".into()).with_language("pt_PT".into()),
|
||||
];
|
||||
|
||||
let content = select_message_content(slice, "pt_BR").unwrap();
|
||||
|
||||
assert_eq!(&slice[0], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_element_with_exactly_matching_language_code_if_language_code_is_given() {
|
||||
let slice = &[
|
||||
MessageContent::new("test1".into()).with_language("en".into()),
|
||||
MessageContent::new("test2".into()).with_language("de".into()),
|
||||
MessageContent::new("test3".into()).with_language("pt_BR".into()),
|
||||
MessageContent::new("test4".into()).with_language("pt".into()),
|
||||
];
|
||||
|
||||
let content = select_message_content(slice, "pt").unwrap();
|
||||
|
||||
assert_eq!(&slice[3], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_first_element_with_matching_language_code_if_language_code_is_given_and_no_exact_match_is_present()
|
||||
{
|
||||
let slice = &[
|
||||
MessageContent::new("test1".into()).with_language("en".into()),
|
||||
MessageContent::new("test2".into()).with_language("de".into()),
|
||||
MessageContent::new("test3".into()).with_language("pt_PT".into()),
|
||||
MessageContent::new("test4".into()).with_language("pt_BR".into()),
|
||||
];
|
||||
|
||||
let content = select_message_content(slice, "pt").unwrap();
|
||||
|
||||
assert_eq!(&slice[2], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_none_if_there_is_no_match_and_no_english_text() {
|
||||
let slice = &[
|
||||
MessageContent::new("test2".into()).with_language("de".into()),
|
||||
MessageContent::new("test3".into()).with_language("pt_PT".into()),
|
||||
MessageContent::new("test4".into()).with_language("pt_BR".into()),
|
||||
];
|
||||
|
||||
assert!(select_message_content(slice, "fr").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod message_content {
|
||||
use super::*;
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_scalar() {
|
||||
let yaml = parse("content");
|
||||
|
||||
assert!(MessageContent::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(MessageContent::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_given_fields() {
|
||||
let yaml = parse("{text: content, lang: fr}");
|
||||
|
||||
let content = MessageContent::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("content", content.text());
|
||||
assert_eq!("fr", content.language());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use super::*;
|
||||
|
||||
@@ -479,6 +593,182 @@ mod tests {
|
||||
|
||||
mod message {
|
||||
use super::*;
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_scalar() {
|
||||
let yaml = parse("content");
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_content_is_missing() {
|
||||
let yaml = parse("{type: say}");
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_an_invalid_condition() {
|
||||
let yaml = parse("{type: say, content: text, condition: invalid}");
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_given_fields() {
|
||||
let yaml = parse("{type: say, content: text, condition: 'file(\"Foo.esp\")'}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(MessageType::Say, message.message_type());
|
||||
assert_eq!(&[MessageContent::new("text".into())], message.content());
|
||||
assert_eq!("file(\"Foo.esp\")", message.condition().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_optional_fields_empty_if_not_present() {
|
||||
let yaml = parse("{type: say, content: text}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(MessageType::Say, message.message_type());
|
||||
assert_eq!(&[MessageContent::new("text".into())], message.content());
|
||||
assert!(message.condition().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_say_warn_and_error_message_types() {
|
||||
let yaml = parse("{type: say, content: text}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
assert_eq!(MessageType::Say, message.message_type());
|
||||
|
||||
let yaml = parse("{type: warn, content: text}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
assert_eq!(MessageType::Warn, message.message_type());
|
||||
|
||||
let yaml = parse("{type: error, content: text}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
assert_eq!(MessageType::Error, message.message_type());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_use_say_if_message_type_is_unrecognised() {
|
||||
let yaml = parse("{type: info, content: text}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
assert_eq!(MessageType::Say, message.message_type());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_read_all_listed_message_contents() {
|
||||
let yaml = parse(
|
||||
"{type: say, content: [{text: english, lang: en}, {text: french, lang: fr}]}",
|
||||
);
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
MessageContent::new("english".into()),
|
||||
MessageContent::new("french".into()).with_language("fr".into())
|
||||
],
|
||||
message.content()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_error_if_one_content_object_is_given_and_it_is_not_english() {
|
||||
let yaml = parse("type: say\ncontent:\n - lang: fr\n text: content1");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[MessageContent::new("content1".into()).with_language("fr".into())],
|
||||
message.content()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_multiple_contents_are_given_and_none_are_english() {
|
||||
let yaml = parse(
|
||||
"type: say\ncontent:\n - lang: de\n text: content1\n - lang: fr\n text: content2",
|
||||
);
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_apply_substitutions_when_there_is_only_one_content_string() {
|
||||
let yaml = parse("type: say\ncontent: con{0}tent1\nsubs:\n - sub1");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("consub1tent1", message.content()[0].text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_apply_substitutions_to_all_content_strings() {
|
||||
let yaml = parse(
|
||||
"type: say\ncontent:\n - lang: en\n text: content1 {0}\n - lang: fr\n text: content2 {0}\nsubs:\n - sub",
|
||||
);
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("content1 sub", message.content()[0].text());
|
||||
assert_eq!("content2 sub", message.content()[1].text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_the_message_has_more_substitutions_than_expected() {
|
||||
let yaml = parse("{type: say, content: 'content1', subs: [sub1]}");
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_the_content_string_expects_more_substitutions_than_exist() {
|
||||
let yaml = parse("{type: say, content: '{0} {1}', subs: [sub1]}");
|
||||
|
||||
assert!(Message::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_ignore_substution_syntax_if_no_substitutions_exist() {
|
||||
let yaml = parse("{type: say, content: 'content {0}'}");
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("content {0}", message.content()[0].text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_accept_percentage_placeholder_syntax() {
|
||||
let yaml = parse(
|
||||
"{type: say, content: 'content %1% %2% %3% %4% %5% %6% %7% %8% %9% %10% %11%', subs: [a, b, c, d, e, f, g, h, i, j, k]}",
|
||||
);
|
||||
|
||||
let message = Message::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("content a b c d e f g h i j k", message.content()[0].text());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use super::*;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,3 +26,11 @@ fn emit<T: yaml_emit::EmitYaml>(metadata: &T) -> String {
|
||||
|
||||
emitter.into_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse(yaml: &str) -> saphyr::MarkedYaml {
|
||||
saphyr::MarkedYaml::load_from_str(yaml)
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -190,6 +190,108 @@ impl EmitYaml for PluginCleaningData {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_scalar() {
|
||||
let yaml = parse("0x12345678");
|
||||
|
||||
assert!(PluginCleaningData::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(PluginCleaningData::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_crc_is_missing() {
|
||||
let yaml = parse("{util: cleaner}");
|
||||
|
||||
assert!(PluginCleaningData::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_util_is_missing() {
|
||||
let yaml = parse("{crc: 0x12345678}");
|
||||
|
||||
assert!(PluginCleaningData::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_given_fields() {
|
||||
let yaml =
|
||||
parse("{crc: 0x12345678, util: cleaner, detail: info, itm: 2, udr: 10, nav: 30}");
|
||||
|
||||
let data = PluginCleaningData::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(0x12345678, data.crc());
|
||||
assert_eq!("cleaner", data.cleaning_utility());
|
||||
assert_eq!(&[MessageContent::new("info".into())], data.detail());
|
||||
assert_eq!(2, data.itm_count());
|
||||
assert_eq!(10, data.deleted_reference_count());
|
||||
assert_eq!(30, data.deleted_navmesh_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_optional_fields_at_defaults_if_not_present() {
|
||||
let yaml = parse("{crc: 0x12345678, util: cleaner}");
|
||||
|
||||
let data = PluginCleaningData::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(0x12345678, data.crc());
|
||||
assert_eq!("cleaner", data.cleaning_utility());
|
||||
assert!(data.detail().is_empty());
|
||||
assert_eq!(0, data.itm_count());
|
||||
assert_eq!(0, data.deleted_reference_count());
|
||||
assert_eq!(0, data.deleted_navmesh_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_read_all_listed_detail_message_contents() {
|
||||
let yaml = parse(
|
||||
"{crc: 0x12345678, util: cleaner, detail: [{text: english, lang: en}, {text: french, lang: fr}]}",
|
||||
);
|
||||
|
||||
let data = PluginCleaningData::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
MessageContent::new("english".into()),
|
||||
MessageContent::new("french".into()).with_language("fr".into())
|
||||
],
|
||||
data.detail()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_error_if_one_detail_is_given_and_it_is_not_english() {
|
||||
let yaml =
|
||||
parse("crc: 0x12345678\nutil: cleaner\ndetail:\n - lang: fr\n text: content1");
|
||||
|
||||
let data = PluginCleaningData::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[MessageContent::new("content1".into()).with_language("fr".into())],
|
||||
data.detail()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_multiple_details_are_given_and_none_are_english() {
|
||||
let yaml = parse(
|
||||
"crc: 0x12345678\nutil: cleaner\ndetail:\n - lang: de\n text: content1\n - lang: fr\n text: content2",
|
||||
);
|
||||
|
||||
assert!(PluginCleaningData::try_from(&yaml).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use crate::metadata::emit;
|
||||
|
||||
|
||||
@@ -419,8 +419,380 @@ impl EmitYaml for PluginMetadata {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
metadata::{MessageType, TagSuggestion},
|
||||
tests::{BLANK_DIFFERENT_ESM, BLANK_DIFFERENT_ESP, BLANK_ESM, BLANK_ESP},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
mod name_matches {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_use_case_insensitive_comparison_for_non_regex_names() {
|
||||
let plugin = PluginMetadata::new("BLANK.ESM").unwrap();
|
||||
|
||||
assert!(plugin.name_matches("blank.esm"));
|
||||
assert!(!plugin.name_matches("other.esm"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_treat_given_plugin_name_as_literal() {
|
||||
let plugin = PluginMetadata::new("Blank.esm").unwrap();
|
||||
|
||||
assert!(!plugin.name_matches(".+"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_use_case_insensitive_regex_matching_for_a_regex_name() {
|
||||
let plugin = PluginMetadata::new("Blank.ES(m|p)").unwrap();
|
||||
|
||||
assert!(plugin.name_matches("blank.esm"));
|
||||
}
|
||||
}
|
||||
|
||||
mod merge_metadata {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_not_change_name() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let plugin2 = PluginMetadata::new(BLANK_DIFFERENT_ESM).unwrap();
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(BLANK_ESM, plugin1.name());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_use_other_group_if_current_group_is_set() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin1.set_group("group1".into());
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin2.set_group("group2".into());
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!("group1", plugin1.group().unwrap());
|
||||
|
||||
plugin2.unset_group();
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!("group1", plugin1.group().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_use_other_group_if_current_group_is_none() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin2.set_group("group2".into());
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!("group2", plugin1.group().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_load_after_files() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let file1 = File::new(BLANK_DIFFERENT_ESM.into());
|
||||
let file2 = File::new(BLANK_ESP.into());
|
||||
let file3 = File::new(BLANK_DIFFERENT_ESP.into());
|
||||
plugin1.set_load_after_files(vec![file1.clone(), file2.clone()]);
|
||||
plugin2.set_load_after_files(vec![file1.clone(), file3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[file1.clone(), file2.clone(), file3.clone()],
|
||||
plugin1.load_after_files()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_requirements() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let file1 = File::new(BLANK_DIFFERENT_ESM.into());
|
||||
let file2 = File::new(BLANK_ESP.into());
|
||||
let file3 = File::new(BLANK_DIFFERENT_ESP.into());
|
||||
plugin1.set_requirements(vec![file1.clone(), file2.clone()]);
|
||||
plugin2.set_requirements(vec![file1.clone(), file3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[file1.clone(), file2.clone(), file3.clone()],
|
||||
plugin1.requirements()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_incompatibilities() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let file1 = File::new(BLANK_DIFFERENT_ESM.into());
|
||||
let file2 = File::new(BLANK_ESP.into());
|
||||
let file3 = File::new(BLANK_DIFFERENT_ESP.into());
|
||||
plugin1.set_incompatibilities(vec![file1.clone(), file2.clone()]);
|
||||
plugin2.set_incompatibilities(vec![file1.clone(), file3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[file1.clone(), file2.clone(), file3.clone()],
|
||||
plugin1.incompatibilities()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_messages() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let message1 = Message::new(MessageType::Say, "content1".into());
|
||||
let message2 = Message::new(MessageType::Say, "content2".into());
|
||||
let message3 = Message::new(MessageType::Say, "content3".into());
|
||||
plugin1.set_messages(vec![message1.clone(), message2.clone()]);
|
||||
plugin2.set_messages(vec![message1.clone(), message3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
message1.clone(),
|
||||
message2.clone(),
|
||||
message1.clone(),
|
||||
message3.clone()
|
||||
],
|
||||
plugin1.messages()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_tags() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let tag1 = Tag::new("Relev".into(), TagSuggestion::Addition);
|
||||
let tag2 = Tag::new("Delev".into(), TagSuggestion::Addition);
|
||||
let tag3 = Tag::new("Relev".into(), TagSuggestion::Removal);
|
||||
plugin1.set_tags(vec![tag1.clone(), tag2.clone()]);
|
||||
plugin2.set_tags(vec![tag1.clone(), tag3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(&[tag1.clone(), tag2.clone(), tag3.clone()], plugin1.tags());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_dirty_info() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let data1 = PluginCleaningData::new(0x12345678, "util1".into());
|
||||
let data2 = PluginCleaningData::new(0xDEADBEEF, "util2".into());
|
||||
let data3 = PluginCleaningData::new(0xFEEDCAFE, "util3".into());
|
||||
plugin1.set_dirty_info(vec![data1.clone(), data2.clone()]);
|
||||
plugin2.set_dirty_info(vec![data1.clone(), data3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[data1.clone(), data2.clone(), data3.clone()],
|
||||
plugin1.dirty_info()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_clean_info() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let data1 = PluginCleaningData::new(0x12345678, "util1".into());
|
||||
let data2 = PluginCleaningData::new(0xDEADBEEF, "util2".into());
|
||||
let data3 = PluginCleaningData::new(0xFEEDCAFE, "util3".into());
|
||||
plugin1.set_clean_info(vec![data1.clone(), data2.clone()]);
|
||||
plugin2.set_clean_info(vec![data1.clone(), data3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[data1.clone(), data2.clone(), data3.clone()],
|
||||
plugin1.clean_info()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_merge_locations() {
|
||||
let mut plugin1 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
let mut plugin2 = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
let location1 = Location::new("url1".into());
|
||||
let location2 = Location::new("url2".into());
|
||||
let location3 = Location::new("url3".into());
|
||||
plugin1.set_locations(vec![location1.clone(), location2.clone()]);
|
||||
plugin2.set_locations(vec![location1.clone(), location3.clone()]);
|
||||
|
||||
plugin1.merge_metadata(&plugin2);
|
||||
|
||||
assert_eq!(
|
||||
&[location1.clone(), location2.clone(), location3.clone()],
|
||||
plugin1.locations()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_group_should_set_group_to_none() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
plugin.set_group("group1".into());
|
||||
plugin.unset_group();
|
||||
|
||||
assert!(plugin.group().is_none());
|
||||
}
|
||||
|
||||
mod has_name_only {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_only_a_name_is_set() {
|
||||
assert!(PluginMetadata::new(BLANK_ESM).unwrap().has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_a_group_is_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_group("group1".into());
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_load_after_files_are_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_load_after_files(vec![File::new(BLANK_DIFFERENT_ESM.into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_requirements_are_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_requirements(vec![File::new(BLANK_DIFFERENT_ESM.into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_incompatibilities_are_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_incompatibilities(vec![File::new(BLANK_DIFFERENT_ESM.into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_messages_are_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_messages(vec![Message::new(MessageType::Say, "content1".into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_tags_are_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_tags(vec![Tag::new("Relev".into(), TagSuggestion::Addition)]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_dirty_info_is_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_dirty_info(vec![PluginCleaningData::new(0x12345678, "util1".into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_clean_info_is_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_clean_info(vec![PluginCleaningData::new(0x12345678, "util1".into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_if_locations_are_set() {
|
||||
let mut plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
plugin.set_locations(vec![Location::new("url1".into())]);
|
||||
|
||||
assert!(!plugin.has_name_only());
|
||||
}
|
||||
}
|
||||
|
||||
mod is_regex_plugin {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_be_false_for_an_empty_name() {
|
||||
let plugin = PluginMetadata::new("").unwrap();
|
||||
|
||||
assert!(!plugin.is_regex_plugin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_false_for_an_exact_plugin_name() {
|
||||
let plugin = PluginMetadata::new(BLANK_ESM).unwrap();
|
||||
|
||||
assert!(!plugin.is_regex_plugin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_the_plugin_name_contains_a_colon() {
|
||||
let plugin = PluginMetadata::new("Blank:.esm").unwrap();
|
||||
|
||||
assert!(plugin.is_regex_plugin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_the_plugin_name_contains_a_backslash() {
|
||||
let plugin = PluginMetadata::new("Blank\\.esm").unwrap();
|
||||
|
||||
assert!(plugin.is_regex_plugin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_the_plugin_name_contains_an_asterisk() {
|
||||
let plugin = PluginMetadata::new("Blank*.esm").unwrap();
|
||||
|
||||
assert!(plugin.is_regex_plugin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_the_plugin_name_contains_a_question_mark() {
|
||||
let plugin = PluginMetadata::new("Blank?.esm").unwrap();
|
||||
|
||||
assert!(plugin.is_regex_plugin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_true_if_the_plugin_name_contains_a_vertical_bar() {
|
||||
let plugin = PluginMetadata::new("Blank|.esm").unwrap();
|
||||
|
||||
assert!(plugin.is_regex_plugin());
|
||||
}
|
||||
}
|
||||
|
||||
mod as_yaml {
|
||||
use super::*;
|
||||
|
||||
@@ -441,6 +813,119 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_scalar() {
|
||||
let yaml = parse("name1");
|
||||
|
||||
assert!(PluginMetadata::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(PluginMetadata::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_store_all_given_data() {
|
||||
let yaml = parse(
|
||||
"
|
||||
name: 'Blank.esp'
|
||||
after:
|
||||
- 'Blank.esm'
|
||||
req:
|
||||
- 'Blank - Different.esm'
|
||||
inc:
|
||||
- 'Blank - Different.esp'
|
||||
msg:
|
||||
- type: say
|
||||
content: 'content'
|
||||
tag:
|
||||
- Relev
|
||||
dirty:
|
||||
- crc: 0x5
|
||||
util: 'utility'
|
||||
clean:
|
||||
- crc: 0x6
|
||||
util: 'utility'
|
||||
url:
|
||||
- 'https://www.example.com'",
|
||||
);
|
||||
|
||||
let plugin = PluginMetadata::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!(BLANK_ESP, plugin.name());
|
||||
assert_eq!(&[File::new(BLANK_ESM.into())], plugin.load_after_files());
|
||||
assert_eq!(
|
||||
&[File::new(BLANK_DIFFERENT_ESM.into())],
|
||||
plugin.requirements()
|
||||
);
|
||||
assert_eq!(
|
||||
&[File::new(BLANK_DIFFERENT_ESP.into())],
|
||||
plugin.incompatibilities()
|
||||
);
|
||||
assert_eq!(
|
||||
&[Message::new(MessageType::Say, "content".into())],
|
||||
plugin.messages()
|
||||
);
|
||||
assert_eq!(
|
||||
&[Tag::new("Relev".into(), TagSuggestion::Addition)],
|
||||
plugin.tags()
|
||||
);
|
||||
assert_eq!(
|
||||
&[PluginCleaningData::new(0x5, "utility".into())],
|
||||
plugin.dirty_info()
|
||||
);
|
||||
assert_eq!(
|
||||
&[PluginCleaningData::new(0x6, "utility".into())],
|
||||
plugin.clean_info()
|
||||
);
|
||||
assert_eq!(
|
||||
&[Location::new("https://www.example.com".into())],
|
||||
plugin.locations()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_error_if_regex_metadata_contains_dirty_or_clean_info() {
|
||||
let yaml = parse(
|
||||
"
|
||||
name: 'Blank\\.esp'
|
||||
dirty:
|
||||
- crc: 0x5
|
||||
util: 'utility'
|
||||
clean:
|
||||
- crc: 0x6
|
||||
util: 'utility'",
|
||||
);
|
||||
|
||||
let plugin = PluginMetadata::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("Blank\\.esp", plugin.name());
|
||||
assert_eq!(
|
||||
&[PluginCleaningData::new(0x5, "utility".into())],
|
||||
plugin.dirty_info()
|
||||
);
|
||||
assert_eq!(
|
||||
&[PluginCleaningData::new(0x6, "utility".into())],
|
||||
plugin.clean_info()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_regex_name_is_invalid() {
|
||||
let yaml = parse("{name: 'RagnvaldBook(Farengar(+Ragnvald)?)?\\.esp'}");
|
||||
|
||||
assert!(PluginMetadata::try_from(&yaml).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use super::*;
|
||||
use crate::metadata::{MessageType, TagSuggestion, emit};
|
||||
|
||||
@@ -131,6 +131,77 @@ impl EmitYaml for Tag {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod try_from_yaml {
|
||||
use crate::metadata::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_only_set_name_and_suggestion_if_decoding_from_scalar() {
|
||||
let yaml = parse("Relev");
|
||||
|
||||
let tag = Tag::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("Relev", tag.name());
|
||||
assert!(tag.is_addition());
|
||||
assert!(tag.condition().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_only_set_name_and_suggestion_if_decoding_from_scalar_with_leading_hyphen() {
|
||||
let yaml = parse("-Relev");
|
||||
|
||||
let tag = Tag::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("Relev", tag.name());
|
||||
assert!(!tag.is_addition());
|
||||
assert!(tag.condition().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_a_list() {
|
||||
let yaml = parse("[0, 1, 2]");
|
||||
|
||||
assert!(Tag::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_name_is_missing() {
|
||||
let yaml = parse("{condition: 'file(\"Foo.esp\")'}");
|
||||
|
||||
assert!(Tag::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_given_an_invalid_condition() {
|
||||
let yaml = parse("{name: Relev, condition: invalid}");
|
||||
|
||||
assert!(Tag::try_from(&yaml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_all_fields() {
|
||||
let yaml = parse("{name: Relev, condition: 'file(\"Foo.esp\")'}");
|
||||
|
||||
let tag = Tag::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("Relev", tag.name());
|
||||
assert!(tag.is_addition());
|
||||
assert_eq!("file(\"Foo.esp\")", tag.condition().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_leave_optional_fields_empty_if_not_present() {
|
||||
let yaml = parse("{name: Relev}");
|
||||
|
||||
let tag = Tag::try_from(&yaml).unwrap();
|
||||
|
||||
assert_eq!("Relev", tag.name());
|
||||
assert!(tag.is_addition());
|
||||
assert!(tag.condition().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod emit_yaml {
|
||||
use crate::metadata::emit;
|
||||
|
||||
|
||||
+894
-24
File diff suppressed because it is too large
Load Diff
@@ -267,3 +267,213 @@ pub fn get_default_group_node(graph: &GroupsGraph) -> Result<NodeIndex, Undefine
|
||||
.find(|n| graph[*n] == Group::DEFAULT_NAME)
|
||||
.ok_or_else(|| UndefinedGroupError::new(Group::DEFAULT_NAME.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod build_groups_graph {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_an_after_group_does_not_exist() {
|
||||
let groups = &[Group::new("b".into()).with_after_groups(vec!["a".into()])];
|
||||
|
||||
match build_groups_graph(groups, &[]) {
|
||||
Err(BuildGroupsGraphError::UndefinedGroup(e)) => {
|
||||
assert_eq!("a", e.into_group_name())
|
||||
}
|
||||
_ => panic!("Expected an undefined group error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_masterlist_group_loads_after_user_group() {
|
||||
let masterlist = &[Group::new("b".into()).with_after_groups(vec!["a".into()])];
|
||||
let userlist = &[Group::new("a".into())];
|
||||
|
||||
match build_groups_graph(masterlist, userlist) {
|
||||
Err(BuildGroupsGraphError::UndefinedGroup(e)) => {
|
||||
assert_eq!("a", e.into_group_name())
|
||||
}
|
||||
_ => panic!("Expected an undefined group error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_after_groups_are_cyclic() {
|
||||
let masterlist = &[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
];
|
||||
let userlist = &[
|
||||
Group::new("a".into()).with_after_groups(vec!["c".into()]),
|
||||
Group::new("c".into()).with_after_groups(vec!["b".into()]),
|
||||
];
|
||||
|
||||
match build_groups_graph(masterlist, userlist) {
|
||||
Err(BuildGroupsGraphError::CycleFound(e)) => {
|
||||
let cycle = e.into_cycle();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
Vertex::new("a".into())
|
||||
.with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("b".into()).with_out_edge_type(EdgeType::UserLoadAfter),
|
||||
Vertex::new("c".into()).with_out_edge_type(EdgeType::UserLoadAfter),
|
||||
],
|
||||
cycle.as_slice()
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected a cyclic interaction error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_interaction_error_should_only_include_groups_that_are_part_of_the_cycle() {
|
||||
let masterlist = &[
|
||||
Group::new("a".into()).with_after_groups(vec!["b".into()]),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("c".into()).with_after_groups(vec!["b".into()]),
|
||||
];
|
||||
|
||||
match build_groups_graph(masterlist, &[]) {
|
||||
Err(BuildGroupsGraphError::CycleFound(e)) => {
|
||||
let cycle = e.into_cycle();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
Vertex::new("a".into())
|
||||
.with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("b".into())
|
||||
.with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
],
|
||||
cycle.as_slice()
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected a cyclic interaction error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod find_path {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_error_if_the_from_group_does_not_exist() {
|
||||
let masterlist = &[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
];
|
||||
let graph = build_groups_graph(masterlist, &[]).unwrap();
|
||||
|
||||
assert!(find_path(&graph, "c", "a").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_error_if_the_to_group_does_not_exist() {
|
||||
let masterlist = &[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
];
|
||||
let graph = build_groups_graph(masterlist, &[]).unwrap();
|
||||
|
||||
assert!(find_path(&graph, "a", "c").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_an_empty_vec_if_there_is_no_path() {
|
||||
let masterlist = &[Group::new("a".into()), Group::new("b".into())];
|
||||
let graph = build_groups_graph(masterlist, &[]).unwrap();
|
||||
|
||||
let path = find_path(&graph, "a", "b").unwrap();
|
||||
|
||||
assert!(path.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_find_the_shortest_path_if_there_is_no_user_metadata() {
|
||||
let masterlist = &[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("c".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("d".into()).with_after_groups(vec!["c".into()]),
|
||||
Group::new("e".into()).with_after_groups(vec!["b".into(), "d".into()]),
|
||||
];
|
||||
let graph = build_groups_graph(masterlist, &[]).unwrap();
|
||||
|
||||
let path = find_path(&graph, "a", "e").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
Vertex::new("a".into()).with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("b".into()).with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("e".into())
|
||||
],
|
||||
path.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_find_the_shortest_path_involving_user_metadata_if_there_no_user_metadata() {
|
||||
let masterlist = &[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("c".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("e".into()).with_after_groups(vec!["b".into()]),
|
||||
];
|
||||
let userlist = &[
|
||||
Group::new("d".into()).with_after_groups(vec!["c".into()]),
|
||||
Group::new("e".into()).with_after_groups(vec!["d".into()]),
|
||||
];
|
||||
let graph = build_groups_graph(masterlist, userlist).unwrap();
|
||||
|
||||
let path = find_path(&graph, "a", "e").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&[
|
||||
Vertex::new("a".into()).with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("c".into()).with_out_edge_type(EdgeType::UserLoadAfter),
|
||||
Vertex::new("d".into()).with_out_edge_type(EdgeType::UserLoadAfter),
|
||||
Vertex::new("e".into())
|
||||
],
|
||||
path.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_depend_on_the_after_group_definition_order() {
|
||||
let masterlists = &[
|
||||
&[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("c".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("d".into()).with_after_groups(vec!["b".into(), "c".into()]),
|
||||
Group::new("e".into()).with_after_groups(vec!["d".into()]),
|
||||
],
|
||||
&[
|
||||
Group::new("a".into()),
|
||||
Group::new("b".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("c".into()).with_after_groups(vec!["a".into()]),
|
||||
Group::new("d".into()).with_after_groups(vec!["c".into(), "b".into()]),
|
||||
Group::new("e".into()).with_after_groups(vec!["d".into()]),
|
||||
],
|
||||
];
|
||||
|
||||
for masterlist in masterlists {
|
||||
let graph = build_groups_graph(*masterlist, &[]).unwrap();
|
||||
|
||||
let path = find_path(&graph, "a", "e").unwrap();
|
||||
assert_eq!(
|
||||
&[
|
||||
Vertex::new("a".into()).with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("b".into()).with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("d".into()).with_out_edge_type(EdgeType::MasterlistLoadAfter),
|
||||
Vertex::new("e".into())
|
||||
],
|
||||
path.as_slice()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,78 @@ pub mod groups;
|
||||
pub mod plugins;
|
||||
mod validate;
|
||||
pub mod vertex;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::plugins::SortingPlugin;
|
||||
use crate::error::PluginDataError;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TestPlugin {
|
||||
name: String,
|
||||
masters: Vec<String>,
|
||||
pub is_master: bool,
|
||||
pub is_blueprint_plugin: bool,
|
||||
pub override_record_count: usize,
|
||||
pub asset_count: usize,
|
||||
overlapping_record_plugins: Vec<String>,
|
||||
overlapping_asset_plugins: Vec<String>,
|
||||
}
|
||||
|
||||
impl TestPlugin {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_master(&mut self, plugin_name: &str) {
|
||||
self.masters.push(plugin_name.to_string());
|
||||
}
|
||||
|
||||
pub fn add_overlapping_records(&mut self, plugin_name: &str) {
|
||||
self.overlapping_record_plugins
|
||||
.push(plugin_name.to_string());
|
||||
}
|
||||
|
||||
pub fn add_overlapping_assets(&mut self, plugin_name: &str) {
|
||||
self.overlapping_asset_plugins.push(plugin_name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SortingPlugin for TestPlugin {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn is_master(&self) -> bool {
|
||||
self.is_master
|
||||
}
|
||||
|
||||
fn is_blueprint_plugin(&self) -> bool {
|
||||
self.is_blueprint_plugin
|
||||
}
|
||||
|
||||
fn masters(&self) -> Result<Vec<String>, PluginDataError> {
|
||||
Ok(self.masters.clone())
|
||||
}
|
||||
|
||||
fn override_record_count(&self) -> Result<usize, PluginDataError> {
|
||||
Ok(self.override_record_count)
|
||||
}
|
||||
|
||||
fn asset_count(&self) -> usize {
|
||||
self.asset_count
|
||||
}
|
||||
|
||||
fn do_records_overlap(&self, other: &Self) -> Result<bool, PluginDataError> {
|
||||
Ok(self.overlapping_record_plugins.contains(&other.name))
|
||||
}
|
||||
|
||||
fn do_assets_overlap(&self, other: &Self) -> bool {
|
||||
self.overlapping_asset_plugins.contains(&other.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2566
-2
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user