diff --git a/Libs/Project/Project.cpp b/Libs/Project/Project.cpp index e963cdd999e..289e5638749 100644 --- a/Libs/Project/Project.cpp +++ b/Libs/Project/Project.cpp @@ -190,6 +190,18 @@ void Project::set_subjects(const std::vector>& subjects update_subjects(); } +//--------------------------------------------------------------------------- +void Project::clear_particle_filenames() { + for (auto& subject : subjects_) { + if (subject->is_fixed()) { + continue; // fixed subjects' particles are pre-existing inputs, not optimize outputs + } + subject->set_local_particle_filenames({}); + subject->set_world_particle_filenames({}); + } + update_subjects(); +} + //--------------------------------------------------------------------------- void Project::update_subjects() { originals_present_ = false; diff --git a/Libs/Project/Project.h b/Libs/Project/Project.h index a2bd7556952..3677f569050 100644 --- a/Libs/Project/Project.h +++ b/Libs/Project/Project.h @@ -83,6 +83,12 @@ class Project { void update_subjects(); + //! Clear the local and world particle filenames for all non-fixed subjects. + /*! Used when an optimization fails or is aborted so that the project isn't left + * referencing particle files that were never written (#2455). Fixed subjects + * keep their filenames since those particles are pre-existing inputs. */ + void clear_particle_filenames(); + //! Return if originals are present bool get_originals_present() const; diff --git a/Studio/Optimize/OptimizeTool.cpp b/Studio/Optimize/OptimizeTool.cpp index 4def54d7e45..a79a39b009d 100644 --- a/Studio/Optimize/OptimizeTool.cpp +++ b/Studio/Optimize/OptimizeTool.cpp @@ -227,6 +227,11 @@ void OptimizeTool::handle_optimize_failed() { optimization_is_running_ = false; Q_EMIT progress(100); + // The optimizer assigns output particle filenames to each subject during setup, but on + // failure those files are never written. Clear them so saving the project doesn't leave + // dangling particle references that make it unreadable on reload (#2455). + session_->get_project()->clear_particle_filenames(); + std::string duration = QString::number(elapsed_timer_.elapsed() / 1000.0, 'f', 1).toStdString(); SW_LOG("Optimize Failed. Duration: " + duration + " seconds"); Q_EMIT optimize_complete(); diff --git a/Testing/ProjectTests/ProjectTests.cpp b/Testing/ProjectTests/ProjectTests.cpp index c3ad705ba96..2b884dbaf07 100644 --- a/Testing/ProjectTests/ProjectTests.cpp +++ b/Testing/ProjectTests/ProjectTests.cpp @@ -51,6 +51,47 @@ TEST(ProjectTests, conversion_to_json_test) { compare_projects(project_compare1, project_compare2); } +//--------------------------------------------------------------------------- +// #2455: after a failed/aborted optimization, the project should not be left referencing +// particle files that were never written. clear_particle_filenames() clears them for +// non-fixed subjects (fixed subjects keep their pre-existing input particles). +TEST(ProjectTests, clear_particle_filenames_test) { + TestUtils::Instance().prep_temp(std::string(TEST_DATA_DIR) + "/project", "project_clear_particle_filenames_test"); + + ProjectHandle project = std::make_shared(); + project->load("json_read.swproj"); + + auto& subjects = project->get_subjects(); + ASSERT_GE(subjects.size(), 2); + + // mark the first subject fixed; its particles are pre-existing inputs and must be kept + subjects[0]->set_fixed(true); + ASSERT_FALSE(subjects[1]->get_world_particle_filenames().empty()); + + project->clear_particle_filenames(); + + // the fixed subject keeps its particle filenames + EXPECT_FALSE(subjects[0]->get_local_particle_filenames().empty()); + EXPECT_FALSE(subjects[0]->get_world_particle_filenames().empty()); + + // non-fixed subjects are cleared + for (size_t i = 1; i < subjects.size(); i++) { + EXPECT_TRUE(subjects[i]->get_local_particle_filenames().empty()); + EXPECT_TRUE(subjects[i]->get_world_particle_filenames().empty()); + } + + // and a saved+reloaded project no longer references the missing particle files + project->save("cleared.swproj"); + ProjectHandle reloaded = std::make_shared(); + reloaded->load("cleared.swproj"); + auto reloaded_subjects = reloaded->get_subjects(); + ASSERT_EQ(reloaded_subjects.size(), subjects.size()); + for (size_t i = 1; i < reloaded_subjects.size(); i++) { + EXPECT_TRUE(reloaded_subjects[i]->get_local_particle_filenames().empty()); + EXPECT_TRUE(reloaded_subjects[i]->get_world_particle_filenames().empty()); + } +} + //--------------------------------------------------------------------------- TEST(ProjectTests, conversion_to_excel_test) { TestUtils::Instance().prep_temp(std::string(TEST_DATA_DIR) + "/project", "project_convert_to_excel_test");