Skip to content
This repository has been archived by the owner on Nov 22, 2023. It is now read-only.

Validate that all assets can be loaded in CI #746

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- doccheck
- doctest
- test
- assets
include:
- ci-argument: clippy
toolchain-components: clippy
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tools/ci/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ license = "MIT OR Apache-2.0"
[dependencies]
xshell = "0.2"
bevy = "0.10"
itertools = "0.10"
148 changes: 148 additions & 0 deletions tools/ci/src/asset_loading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use bevy::{
app::AppExit,
asset::LoadState,
audio::AudioPlugin,
core_pipeline::CorePipelinePlugin,
gltf::GltfPlugin,
pbr::PbrPlugin,
prelude::*,
render::RenderPlugin,
scene::ScenePlugin,
utils::{Duration, HashMap, Instant},
window::{WindowClosed, WindowCreated, WindowResized},
};
use itertools::Itertools;

use std::fmt::{Display, Formatter};

/// The path to the asset folder, from the root of the repository.
const ROOT_ASSET_FOLDER: &str = "emergence_game/assets";

/// The path to the asset folder from the perspective of the [`AssetPlugin`] is specified relative to the executable.
///
/// As a result, we need to go up two levels to translate.
const PATH_ADAPTOR: &str = "../../";

pub(super) fn verify_assets_load() {
App::new()
.init_resource::<AssetHandles>()
.insert_resource(TimeOut {
start: Instant::now(),
max: Duration::from_secs(10),
})
.add_plugins(MinimalPlugins)
// This must come before the asset format plugins for AssetServer to exist
.add_plugin(AssetPlugin {
asset_folder: format!("{PATH_ADAPTOR}{ROOT_ASSET_FOLDER}"),
watch_for_changes: false,
})
// These plugins are required for the asset loaders to be detected.
// Without this, AssetServer::load_folder will return an empty list
// as file types without an associated loader registered are silently skipped.
.add_plugin(ScenePlugin)
.add_plugin(AudioPlugin)
.init_resource::<Events<WindowResized>>()
.init_resource::<Events<WindowCreated>>()
.init_resource::<Events<WindowClosed>>()
.add_plugin(RenderPlugin::default())
.add_plugin(ImagePlugin::default())
.add_plugin(CorePipelinePlugin)
.add_plugin(PbrPlugin::default())
.add_plugin(GltfPlugin)
.add_startup_system(load_assets)
.add_system(check_if_assets_loaded)
.run()
}

#[derive(Default, Resource, Debug, Clone, PartialEq)]
struct AssetHandles {
handles: HashMap<String, HandleStatus>,
}

impl Display for AssetHandles {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut string = String::new();

// Sort the handles alphabetically by name
for name in self.handles.keys().sorted() {
let handle = self.handles.get(name).unwrap();

string += &format!(" {} - {:?}\n", name, handle.load_state);
}

write!(f, "{string}")
}
}

impl AssetHandles {
fn all_loaded(&self) -> bool {
self.handles
.values()
.all(|handle| handle.load_state == LoadState::Loaded)
}
}

#[derive(Debug, Clone, PartialEq)]
struct HandleStatus {
handle: HandleUntyped,
load_state: LoadState,
}

#[derive(Resource, Debug)]
struct TimeOut {
start: Instant,
max: Duration,
}

impl TimeOut {
fn timed_out(&self) -> bool {
self.start.elapsed() > self.max
}
}

fn load_assets(asset_server: Res<AssetServer>, mut asset_handles: ResMut<AssetHandles>) {
// Try to load all assets
let all_handles = asset_server.load_folder(".").unwrap();
assert!(!all_handles.is_empty());
for handle in all_handles {
let asset_path = asset_server.get_handle_path(&handle).unwrap();
asset_handles.handles.insert(
asset_path.path().to_str().unwrap().to_string(),
HandleStatus {
handle,
load_state: LoadState::NotLoaded,
},
);
}
}

fn check_if_assets_loaded(
mut asset_handles: ResMut<AssetHandles>,
asset_server: Res<AssetServer>,
mut app_exit_events: ResMut<Events<AppExit>>,
time_out: Res<TimeOut>,
mut previous_asset_handles: Local<AssetHandles>,
) {
*previous_asset_handles = asset_handles.clone();

for mut handle_status in asset_handles.handles.values_mut() {
if handle_status.load_state == LoadState::NotLoaded {
handle_status.load_state =
asset_server.get_load_state(handle_status.handle.clone_weak());
}
}

if asset_handles.all_loaded() {
println!("{}", *asset_handles);
println!("All assets loaded successfully, exiting.");
app_exit_events.send(AppExit);
} else {
if *asset_handles != *previous_asset_handles {
println!("{}", *asset_handles);
}

if time_out.timed_out() {
panic!("Timed out waiting for assets to load.");
}
}
}
12 changes: 12 additions & 0 deletions tools/ci/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

use std::process;

use bevy::prelude::*;
use bevy::utils::HashSet;
use xshell::{cmd, Shell};

mod asset_loading;

/// The checks that can be run in CI.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Check {
Expand All @@ -20,6 +23,7 @@ enum Check {
DocTest,
DocCheck,
CompileCheck,
ValidateAssets,
}

impl Check {
Expand All @@ -32,6 +36,7 @@ impl Check {
Check::DocTest,
Check::DocCheck,
Check::CompileCheck,
Check::ValidateAssets,
]
.iter()
.copied()
Expand All @@ -47,6 +52,7 @@ impl Check {
Check::DocTest => "doctest",
Check::DocCheck => "doccheck",
Check::CompileCheck => "compilecheck",
Check::ValidateAssets => "assets",
}
}

Expand All @@ -59,6 +65,7 @@ impl Check {
"doctest" => Some(Check::DocTest),
"doccheck" => Some(Check::DocCheck),
"compilecheck" => Some(Check::CompileCheck),
"assets" => Some(Check::ValidateAssets),
_ => None,
}
}
Expand Down Expand Up @@ -140,6 +147,11 @@ fn main() {
.run()
.expect("Please fix compiler errors in above output.");
}

if what_to_run.contains(&Check::ValidateAssets) {
info!("Starting Bevy app to check assets...");
asset_loading::verify_assets_load();
}
}

#[cfg(test)]
Expand Down