diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h index 8fc2a2f040fc..0a429f28369d 100644 --- a/tests/core/io/test_resource.h +++ b/tests/core/io/test_resource.h @@ -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 = 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 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 = memnew(Resource); + resource->set_name("External Resource"); + resource->set_meta("Addend", addend); + sum += addend; + + if (i != 0) { + Ref 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 = memnew(Resource); + resource->set_name("Top Level Resource"); + resource->set_meta("Addend", 0); + resource->set_meta("ID", i); + + Ref 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 = (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 &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