Skip to content

Commit 7ad5fb7

Browse files
authored
Add tests for processor initialization and fix a bug with initialization. (#21855)
# Objective - We have no tests on the initialization process with an existing processing state. - There was a bug (caught by these tests!) where we were deleting directories while we were iterating through them, resulting in us missing directories in some cases. ## Solution - Add two tests to ensure processor init does what we expect with existing processed assets. - Store all the empty directories in a Vec while iterating and then delete them all after we're done iterating. ## Testing - Yup!
1 parent ec127c4 commit 7ad5fb7

File tree

2 files changed

+289
-18
lines changed

2 files changed

+289
-18
lines changed

crates/bevy_asset/src/processor/mod.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ pub use process::*;
4747
use crate::{
4848
io::{
4949
AssetReaderError, AssetSource, AssetSourceBuilders, AssetSourceEvent, AssetSourceId,
50-
AssetSources, AssetWriterError, ErasedAssetReader, ErasedAssetWriter,
51-
MissingAssetSourceError,
50+
AssetSources, AssetWriterError, ErasedAssetReader, MissingAssetSourceError,
5251
},
5352
meta::{
5453
get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta,
@@ -761,9 +760,9 @@ impl AssetProcessor {
761760
/// folders when they are discovered.
762761
async fn get_asset_paths(
763762
reader: &dyn ErasedAssetReader,
764-
clean_empty_folders_writer: Option<&dyn ErasedAssetWriter>,
765763
path: PathBuf,
766764
paths: &mut Vec<PathBuf>,
765+
mut empty_dirs: Option<&mut Vec<PathBuf>>,
767766
) -> Result<bool, AssetReaderError> {
768767
if reader.is_directory(&path).await? {
769768
let mut path_stream = reader.read_directory(&path).await?;
@@ -772,18 +771,19 @@ impl AssetProcessor {
772771
while let Some(child_path) = path_stream.next().await {
773772
contains_files |= Box::pin(get_asset_paths(
774773
reader,
775-
clean_empty_folders_writer,
776774
child_path,
777775
paths,
776+
empty_dirs.as_deref_mut(),
778777
))
779778
.await?;
780779
}
780+
// Add the current directory after all its subdirectories so we delete any empty
781+
// subdirectories before the current directory.
781782
if !contains_files
782783
&& path.parent().is_some()
783-
&& let Some(writer) = clean_empty_folders_writer
784+
&& let Some(empty_dirs) = empty_dirs
784785
{
785-
// it is ok for this to fail as it is just a cleanup job.
786-
let _ = writer.remove_empty_directory(&path).await;
786+
empty_dirs.push(path);
787787
}
788788
Ok(contains_files)
789789
} else {
@@ -802,23 +802,32 @@ impl AssetProcessor {
802802
let mut unprocessed_paths = Vec::new();
803803
get_asset_paths(
804804
source.reader(),
805-
None,
806805
PathBuf::from(""),
807806
&mut unprocessed_paths,
807+
None,
808808
)
809809
.await
810810
.map_err(InitializeError::FailedToReadSourcePaths)?;
811811

812812
let mut processed_paths = Vec::new();
813+
let mut empty_dirs = Vec::new();
813814
get_asset_paths(
814815
processed_reader,
815-
Some(processed_writer),
816816
PathBuf::from(""),
817817
&mut processed_paths,
818+
Some(&mut empty_dirs),
818819
)
819820
.await
820821
.map_err(InitializeError::FailedToReadDestinationPaths)?;
821822

823+
// Remove any empty directories from the processed path. Note: this has to happen after
824+
// we fetch all the paths, otherwise the path stream can skip over paths
825+
// (we're modifying a collection while iterating through it).
826+
for empty_dir in empty_dirs {
827+
// We don't care if this succeeds, since it's just a cleanup task. It is best-effort
828+
let _ = processed_writer.remove_empty_directory(&empty_dir).await;
829+
}
830+
822831
for path in unprocessed_paths {
823832
asset_infos.get_or_insert(AssetPath::from(path).with_source(source.id()));
824833
}

crates/bevy_asset/src/processor/tests.rs

Lines changed: 271 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ impl<R: AssetReader> AssetReader for LockGatedReader<R> {
9393
}
9494
}
9595

96+
/// Serializes `text` into a `CoolText` that can be loaded.
97+
///
98+
/// This doesn't support all the features of `CoolText`, so more complex scenarios may require doing
99+
/// this manually.
100+
fn serialize_as_cool_text(text: &str) -> String {
101+
let cool_text_ron = CoolTextRon {
102+
text: text.into(),
103+
dependencies: vec![],
104+
embedded_dependencies: vec![],
105+
sub_texts: vec![],
106+
};
107+
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
108+
}
109+
96110
fn create_app_with_asset_processor(extra_sources: &[String]) -> AppWithProcessor {
97111
let mut app = App::new();
98112
let source_gate = Arc::new(RwLock::new(()));
@@ -945,15 +959,6 @@ fn asset_processor_processes_all_sources() {
945959
// All the assets will have the same path, but they will still be separately processed since
946960
// they are in different sources.
947961
let path = Path::new("asset.cool.ron");
948-
let serialize_as_cool_text = |text: &str| {
949-
let cool_text_ron = CoolTextRon {
950-
text: text.into(),
951-
dependencies: vec![],
952-
embedded_dependencies: vec![],
953-
sub_texts: vec![],
954-
};
955-
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
956-
};
957962
default_source_dir.insert_asset_text(path, &serialize_as_cool_text("default asset"));
958963
custom_1_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 1 asset"));
959964
custom_2_source_dir.insert_asset_text(path, &serialize_as_cool_text("custom 2 asset"));
@@ -1244,3 +1249,260 @@ fn nested_loads_of_processed_asset_reprocesses_on_reload() {
12441249

12451250
assert_eq!(get_process_count(), 7);
12461251
}
1252+
1253+
#[test]
1254+
fn clears_invalid_data_from_processed_dir() {
1255+
let AppWithProcessor {
1256+
mut app,
1257+
source_gate,
1258+
default_source_dirs:
1259+
ProcessingDirs {
1260+
source: default_source_dir,
1261+
processed: default_processed_dir,
1262+
..
1263+
},
1264+
..
1265+
} = create_app_with_asset_processor(&[]);
1266+
1267+
type CoolTextProcessor = LoadTransformAndSave<
1268+
CoolTextLoader,
1269+
RootAssetTransformer<AddText, CoolText>,
1270+
CoolTextSaver,
1271+
>;
1272+
app.init_asset::<CoolText>()
1273+
.init_asset::<SubText>()
1274+
.register_asset_loader(CoolTextLoader)
1275+
.register_asset_processor(CoolTextProcessor::new(
1276+
RootAssetTransformer::new(AddText(" processed".to_string())),
1277+
CoolTextSaver,
1278+
))
1279+
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1280+
1281+
let guard = source_gate.write_blocking();
1282+
1283+
default_source_dir.insert_asset_text(Path::new("a.cool.ron"), &serialize_as_cool_text("a"));
1284+
default_source_dir.insert_asset_text(Path::new("dir/b.cool.ron"), &serialize_as_cool_text("b"));
1285+
default_source_dir.insert_asset_text(
1286+
Path::new("dir/subdir/c.cool.ron"),
1287+
&serialize_as_cool_text("c"),
1288+
);
1289+
1290+
// This asset has the right data, but no meta, so it should be reprocessed.
1291+
let a = Path::new("a.cool.ron");
1292+
default_processed_dir.insert_asset_text(a, &serialize_as_cool_text("a processed"));
1293+
// These assets aren't present in the unprocessed directory, so they should be deleted.
1294+
let missing1 = Path::new("missing1.cool.ron");
1295+
let missing2 = Path::new("dir/missing2.cool.ron");
1296+
let missing3 = Path::new("other_dir/missing3.cool.ron");
1297+
default_processed_dir.insert_asset_text(missing1, &serialize_as_cool_text("missing1"));
1298+
default_processed_dir.insert_meta_text(missing1, ""); // This asset has metadata.
1299+
default_processed_dir.insert_asset_text(missing2, &serialize_as_cool_text("missing2"));
1300+
default_processed_dir.insert_asset_text(missing3, &serialize_as_cool_text("missing3"));
1301+
// This directory is empty, so it should be deleted.
1302+
let empty_dir = Path::new("empty_dir");
1303+
let empty_dir_subdir = Path::new("empty_dir/empty_subdir");
1304+
default_processed_dir.get_or_insert_dir(empty_dir_subdir);
1305+
1306+
run_app_until_finished_processing(&mut app, guard);
1307+
1308+
assert_eq!(
1309+
read_asset_as_string(&default_processed_dir, a),
1310+
serialize_as_cool_text("a processed")
1311+
);
1312+
assert!(default_processed_dir.get_metadata(a).is_some());
1313+
1314+
assert!(default_processed_dir.get_asset(missing1).is_none());
1315+
assert!(default_processed_dir.get_metadata(missing1).is_none());
1316+
assert!(default_processed_dir.get_asset(missing2).is_none());
1317+
assert!(default_processed_dir.get_asset(missing3).is_none());
1318+
1319+
assert!(default_processed_dir.get_dir(empty_dir_subdir).is_none());
1320+
assert!(default_processed_dir.get_dir(empty_dir).is_none());
1321+
}
1322+
1323+
#[test]
1324+
fn only_reprocesses_wrong_hash_on_startup() {
1325+
let no_deps_asset = Path::new("no_deps.cool.ron");
1326+
let source_changed_asset = Path::new("source_changed.cool.ron");
1327+
let dep_unchanged_asset = Path::new("dep_unchanged.cool.ron");
1328+
let dep_changed_asset = Path::new("dep_changed.cool.ron");
1329+
let default_source_dir;
1330+
let default_processed_dir;
1331+
1332+
#[derive(TypePath, Clone)]
1333+
struct MergeEmbeddedAndAddText;
1334+
1335+
impl MutateAsset<CoolText> for MergeEmbeddedAndAddText {
1336+
fn mutate(&self, asset: &mut CoolText) {
1337+
asset.text.push_str(" processed");
1338+
if asset.embedded.is_empty() {
1339+
return;
1340+
}
1341+
asset.text.push(' ');
1342+
asset.text.push_str(&asset.embedded);
1343+
}
1344+
}
1345+
1346+
#[derive(TypePath, Clone)]
1347+
struct Count<T>(Arc<Mutex<u32>>, T);
1348+
1349+
impl<A: Asset, T: MutateAsset<A>> MutateAsset<A> for Count<T> {
1350+
fn mutate(&self, asset: &mut A) {
1351+
*self.0.lock().unwrap_or_else(PoisonError::into_inner) += 1;
1352+
self.1.mutate(asset);
1353+
}
1354+
}
1355+
1356+
let transformer = Count(Arc::new(Mutex::new(0)), MergeEmbeddedAndAddText);
1357+
type CoolTextProcessor = LoadTransformAndSave<
1358+
CoolTextLoader,
1359+
RootAssetTransformer<Count<MergeEmbeddedAndAddText>, CoolText>,
1360+
CoolTextSaver,
1361+
>;
1362+
1363+
// Create a scope so that the app is completely gone afterwards (and we can see what happens
1364+
// after reinitializing).
1365+
{
1366+
let AppWithProcessor {
1367+
mut app,
1368+
source_gate,
1369+
default_source_dirs,
1370+
..
1371+
} = create_app_with_asset_processor(&[]);
1372+
default_source_dir = default_source_dirs.source;
1373+
default_processed_dir = default_source_dirs.processed;
1374+
1375+
app.init_asset::<CoolText>()
1376+
.init_asset::<SubText>()
1377+
.register_asset_loader(CoolTextLoader)
1378+
.register_asset_processor(CoolTextProcessor::new(
1379+
RootAssetTransformer::new(transformer.clone()),
1380+
CoolTextSaver,
1381+
))
1382+
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1383+
1384+
let guard = source_gate.write_blocking();
1385+
1386+
let cool_text_with_embedded = |text: &str, embedded: &Path| {
1387+
let cool_text_ron = CoolTextRon {
1388+
text: text.into(),
1389+
dependencies: vec![],
1390+
embedded_dependencies: vec![embedded.to_string_lossy().into_owned()],
1391+
sub_texts: vec![],
1392+
};
1393+
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
1394+
};
1395+
1396+
default_source_dir.insert_asset_text(no_deps_asset, &serialize_as_cool_text("no_deps"));
1397+
default_source_dir.insert_asset_text(
1398+
source_changed_asset,
1399+
&serialize_as_cool_text("source_changed"),
1400+
);
1401+
default_source_dir.insert_asset_text(
1402+
dep_unchanged_asset,
1403+
&cool_text_with_embedded("dep_unchanged", no_deps_asset),
1404+
);
1405+
default_source_dir.insert_asset_text(
1406+
dep_changed_asset,
1407+
&cool_text_with_embedded("dep_changed", source_changed_asset),
1408+
);
1409+
1410+
run_app_until_finished_processing(&mut app, guard);
1411+
1412+
assert_eq!(
1413+
read_asset_as_string(&default_processed_dir, no_deps_asset),
1414+
serialize_as_cool_text("no_deps processed")
1415+
);
1416+
assert_eq!(
1417+
read_asset_as_string(&default_processed_dir, source_changed_asset),
1418+
serialize_as_cool_text("source_changed processed")
1419+
);
1420+
assert_eq!(
1421+
read_asset_as_string(&default_processed_dir, dep_unchanged_asset),
1422+
serialize_as_cool_text("dep_unchanged processed no_deps processed")
1423+
);
1424+
assert_eq!(
1425+
read_asset_as_string(&default_processed_dir, dep_changed_asset),
1426+
serialize_as_cool_text("dep_changed processed source_changed processed")
1427+
);
1428+
}
1429+
1430+
// Assert and reset the processing count.
1431+
assert_eq!(
1432+
core::mem::take(&mut *transformer.0.lock().unwrap_or_else(PoisonError::into_inner)),
1433+
4
1434+
);
1435+
1436+
// Hand-make the app, since we need to pass in our already existing Dirs from the last app.
1437+
let mut app = App::new();
1438+
let source_gate = Arc::new(RwLock::new(()));
1439+
1440+
let source_memory_reader = LockGatedReader::new(
1441+
source_gate.clone(),
1442+
MemoryAssetReader {
1443+
root: default_source_dir.clone(),
1444+
},
1445+
);
1446+
let processed_memory_reader = MemoryAssetReader {
1447+
root: default_processed_dir.clone(),
1448+
};
1449+
let processed_memory_writer = MemoryAssetWriter {
1450+
root: default_processed_dir.clone(),
1451+
};
1452+
1453+
app.register_asset_source(
1454+
AssetSourceId::Default,
1455+
AssetSourceBuilder::new(move || Box::new(source_memory_reader.clone()))
1456+
.with_processed_reader(move || Box::new(processed_memory_reader.clone()))
1457+
.with_processed_writer(move |_| Some(Box::new(processed_memory_writer.clone()))),
1458+
);
1459+
1460+
app.add_plugins((
1461+
TaskPoolPlugin::default(),
1462+
AssetPlugin {
1463+
mode: AssetMode::Processed,
1464+
use_asset_processor_override: Some(true),
1465+
..Default::default()
1466+
},
1467+
));
1468+
1469+
app.init_asset::<CoolText>()
1470+
.init_asset::<SubText>()
1471+
.register_asset_loader(CoolTextLoader)
1472+
.register_asset_processor(CoolTextProcessor::new(
1473+
RootAssetTransformer::new(transformer.clone()),
1474+
CoolTextSaver,
1475+
))
1476+
.set_default_asset_processor::<CoolTextProcessor>("cool.ron");
1477+
1478+
let guard = source_gate.write_blocking();
1479+
1480+
default_source_dir
1481+
.insert_asset_text(source_changed_asset, &serialize_as_cool_text("DIFFERENT"));
1482+
1483+
run_app_until_finished_processing(&mut app, guard);
1484+
1485+
// Only source_changed and dep_changed assets were reprocessed - all others still have the same
1486+
// hashes.
1487+
assert_eq!(
1488+
*transformer.0.lock().unwrap_or_else(PoisonError::into_inner),
1489+
2
1490+
);
1491+
1492+
assert_eq!(
1493+
read_asset_as_string(&default_processed_dir, no_deps_asset),
1494+
serialize_as_cool_text("no_deps processed")
1495+
);
1496+
assert_eq!(
1497+
read_asset_as_string(&default_processed_dir, source_changed_asset),
1498+
serialize_as_cool_text("DIFFERENT processed")
1499+
);
1500+
assert_eq!(
1501+
read_asset_as_string(&default_processed_dir, dep_unchanged_asset),
1502+
serialize_as_cool_text("dep_unchanged processed no_deps processed")
1503+
);
1504+
assert_eq!(
1505+
read_asset_as_string(&default_processed_dir, dep_changed_asset),
1506+
serialize_as_cool_text("dep_changed processed DIFFERENT processed")
1507+
);
1508+
}

0 commit comments

Comments
 (0)