Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests for threaded resource loading #73825

Closed
wants to merge 1 commit into from
Closed
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
152 changes: 152 additions & 0 deletions tests/core/io/test_resource.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,158 @@ TEST_CASE("[Resource] Breaking circular references on save") {
// Break circular reference to avoid memory leak
resource_c->remove_meta("next");
}

static void resource_fuzz_test(bool test_simultaneous_load, bool test_cyclic_dependency) {
const String test_dir = OS::get_singleton()->get_cache_path().path_join("godot_thread_test");
DirAccess::make_dir_absolute(test_dir);
for (const String &f : DirAccess::get_files_at(test_dir)) {
// Clean up files at the beginning in case there's a previously failed run
DirAccess::remove_absolute(test_dir.path_join(f));
}

const int count = 25;
const int cycle_length = 5;
int sum = 0;

// Create circular dependencies to test load failures.
// Also skip some so that we test failure to load nonexistent files.
for (int i = 2; i < count; i++) {
if (!test_cyclic_dependency) {
break;
}

const String resource_name = test_dir.path_join(itos(i) + "-cyc.tres");
Ref<Resource> resource = memnew(Resource);
resource->set_name("Cyclic Resource");
resource->set_meta("Addend", 0);

// Create groups of smaller cycles [0-4], [5-9], [10-14], ...
int link = ((i / cycle_length) * cycle_length) + ((i + 1) % cycle_length);
Ref<Resource> child_resource = memnew(Resource);
child_resource->set_path(itos(link) + "-cyc.tres");
resource->set_meta("other_resource", child_resource);

ResourceSaver::save(resource, resource_name);
}

// Create sequence of resources that each reference the previous one.
// 0-ext.tres <- 1-ext.tres <- 2-ext.tres...
for (int i = 0; i < count; i++) {
const int addend = Math::rand() % 100;
const String resource_name = test_dir.path_join(itos(i) + "-ext.tres");
Ref<Resource> resource = memnew(Resource);
resource->set_name("External Resource");
resource->set_meta("Addend", addend);
sum += addend;

if (i != 0) {
Ref<Resource> child_resource = memnew(Resource);
child_resource->set_path(itos(i - 1) + "-ext.tres");
resource->set_meta("other_resource", child_resource);
}
ResourceSaver::save(resource, resource_name);
}
// Create resources that reference the above chain.
for (int i = 0; i < count; i++) {
const String resource_name = test_dir.path_join(itos(i) + ".tres");
Ref<Resource> resource = memnew(Resource);
resource->set_name("Top Level Resource");
resource->set_meta("Addend", 0);
resource->set_meta("ID", i);

Ref<Resource> child_resource = memnew(Resource);
child_resource->set_path(itos(count - 1) + "-ext.tres");
resource->set_meta("other_resource", child_resource);

ResourceSaver::save(resource, resource_name);

CHECK(ResourceLoader::load_threaded_get_status(resource_name) == ResourceLoader::THREAD_LOAD_INVALID_RESOURCE);
}

// Since we're testing threaded loading, and the cyclic dependencies are designed to fail,
// there's no way to disable error messages at a finer granularity than this.
ERR_PRINT_OFF;

// Test threaded loading of above resources
for (int i = 0; i < 500; i++) {
const int id = Math::rand() % count;
const bool is_cycle = Math::rand() % 2;
const String resource_name = test_dir.path_join(itos(id) + (is_cycle ? "-cyc.tres" : ".tres"));

// Spawn a new thread at random
if (Math::rand() % 2) {
// Randomly decide sub-thread and cache settings
ResourceLoader::load_threaded_request(resource_name, "", Math::rand() % 2, ResourceFormatLoader::CacheMode(Math::rand() % 3));
CHECK(ResourceLoader::load_threaded_get_status(resource_name) != ResourceLoader::THREAD_LOAD_INVALID_RESOURCE);

if (test_simultaneous_load && (Math::rand() % 2)) {
continue;
}
}

const bool load_requested = ResourceLoader::load_threaded_get_status(resource_name) != ResourceLoader::THREAD_LOAD_INVALID_RESOURCE;

// Randomly wait for a previously spawned thread
if (load_requested && (Math::rand() % 2)) {
while (ResourceLoader::load_threaded_get_status(resource_name) == ResourceLoader::THREAD_LOAD_IN_PROGRESS) {
OS::get_singleton()->delay_usec(1);
}
if (is_cycle) {
CHECK(ResourceLoader::load_threaded_get_status(resource_name) == ResourceLoader::THREAD_LOAD_FAILED);
} else {
CHECK(ResourceLoader::load_threaded_get_status(resource_name) == ResourceLoader::THREAD_LOAD_LOADED);
}
}

// Join thread using a random method
const Ref<Resource> &resource = (load_requested && (Math::rand() % 2))
? ResourceLoader::load_threaded_get(resource_name)
: ResourceLoader::load(resource_name, "", ResourceFormatLoader::CacheMode(Math::rand() % 3));

if (is_cycle) {
continue;
}

REQUIRE(resource.is_valid());

// Check if resource data loaded correctly
int meta_id = resource->get_meta("ID");
CHECK(meta_id == id);

int loaded_sum = 0;
const Resource *r = resource.ptr();
while (r) {
int addend = r->get_meta("Addend");
loaded_sum += addend;

const Ref<Resource> &new_r = r->get_meta("other_resource");
r = new_r.ptr();
}

// Check if all external resources were loaded correctly
CHECK(loaded_sum == sum);
}

// Join threads.
for (int is_cycle = 0; is_cycle < 2; is_cycle++) {
for (int i = 0; i < count; i++) {
const String resource_name = test_dir.path_join(itos(i) + (is_cycle ? "-cyc.tres" : ".tres"));
while (ResourceLoader::load_threaded_get_status(resource_name) != ResourceLoader::THREAD_LOAD_INVALID_RESOURCE) {
ResourceLoader::load_threaded_get(resource_name);
}
}
}

ERR_PRINT_ON;
}

TEST_CASE("[Resource] Simultaneous loading fuzz test") {
resource_fuzz_test(true, false);
}
TEST_CASE_PENDING("[Resource] Cyclic dependencies fuzz test") {
resource_fuzz_test(true, true);
}

} // namespace TestResource

#endif // TEST_RESOURCE_H