diff --git a/framework/doc/content/source/interfaces/DataFileInterface.md b/framework/doc/content/source/interfaces/DataFileInterface.md index c4e431e3c9da..874fd3f7d15d 100644 --- a/framework/doc/content/source/interfaces/DataFileInterface.md +++ b/framework/doc/content/source/interfaces/DataFileInterface.md @@ -19,8 +19,13 @@ If the provided path is absolute, no searching will take place and the absolute path will be used. Otherwise, `getDataFileName` will search (in this order) - relative to the input file -- relative to the running binary in the shared directory (assuming the application is installed) -- relative to all registered data file directories (which are determined by the source file locations when compiling and registered using the `registerDataFilePath` macro in `Registry.h`) +- relative to all installed and registered data file directories (for an installed application) +- relative to all in-tree registered data file directories (for an in-tree build) + +The "registered" data file directories are directories that are registered via: + +- the `registerAppDataFilePath` macro in `Registry.h`, where an applications data in its root `data` directory is registered +- the `registerDataFilePath` macro in `Registry.h`, where a general data directory is registered ## `getDataFileNameByName` diff --git a/framework/include/base/Registry.h b/framework/include/base/Registry.h index e8796fab3212..e0406d17198e 100644 --- a/framework/include/base/Registry.h +++ b/framework/include/base/Registry.h @@ -19,6 +19,8 @@ #include "libmesh/utility.h" +#include + #define combineNames1(X, Y) X##Y #define combineNames(X, Y) combineNames1(X, Y) @@ -76,7 +78,12 @@ #define registerADMooseObjectRenamed(app, orig_class, time, new_class) \ registerMooseObjectRenamed(app, orig_class, time, new_class) -#define registerDataFilePath() Registry::addDataFilePath(__FILE__) +/// Register a data file path (folder name must be data) +#define registerDataFilePath(name, path) Registry::addDataFilePath(name, path) +/// Register a data file path for an application. Uses the current file to register +/// ../../data as a path. The app name must be the APPLICATION_NAME used to build +/// the app (solid_mechanics instead of SolidMechanicsApp, for example) +#define registerAppDataFilePath(app) Registry::addAppDataFilePath(app, __FILE__) #define registerRepository(repo_name, repo_url) Registry::addRepository(repo_name, repo_url); @@ -198,8 +205,11 @@ class Registry /// addKnownLabel whitelists a label as valid for purposes of the checkLabels function. static char addKnownLabel(const std::string & label); - /// register search paths for built-in data files - static void addDataFilePath(const std::string & path); + /// register general search paths (folder name must be data) + static void addDataFilePath(const std::string & name, const std::string & in_tree_path); + /// register search paths for an application (path determined relative to app_path); + /// app_path should be passed as __FILE__ from the application source file + static void addAppDataFilePath(const std::string & app_name, const std::string & app_path); /// register a repository static void addRepository(const std::string & repo_name, const std::string & repo_url); @@ -225,12 +235,19 @@ class Registry return getRegistry()._name_to_entry.count(name); } - /// Returns a vector of all registered data file paths - static const std::vector & getDataFilePaths() + /// Returns a map of all registered data file paths (name -> path) + static const std::map & getDataFilePaths() { return getRegistry()._data_file_paths; } + /** + * Gets a data path for the registered name. + * + * Finds either the installed path or the in-tree path. + */ + static std::string getDataFilePath(const std::string & name); + /// Returns the repository URL associated with \p repo_name static const std::string & getRepositoryURL(const std::string & repo_name); @@ -247,13 +264,21 @@ class Registry ///@} private: + FRIEND_TEST(RegistryTest, determineFilePath); + FRIEND_TEST(RegistryTest, determineFilePathFailed); + Registry(){}; + /// Internal helper for determing a root data file path (in-tree vs installed) + static std::string determineDataFilePath(const std::string & name, + const std::string & in_tree_path); + std::map> _name_to_entry; std::map>> _per_label_objects; std::map>> _per_label_actions; std::set _known_labels; - std::vector _data_file_paths; + /// Data file registry; name -> in-tree path + std::map _data_file_paths; /// Repository name -> repository URL; used for mooseDocumentedError std::map _repos; std::map _type_to_classname; diff --git a/framework/src/base/Moose.C b/framework/src/base/Moose.C index 83eeff4ea832..91250cd67442 100644 --- a/framework/src/base/Moose.C +++ b/framework/src/base/Moose.C @@ -59,7 +59,7 @@ registerAll(Factory & f, ActionFactory & af, Syntax & s) registerObjects(f, {"MooseApp"}); associateSyntaxInner(s, af); registerActions(s, af, {"MooseApp"}); - registerDataFilePath(); + registerAppDataFilePath("moose"); registerRepository("moose", "github.com/idaholab/moose"); } diff --git a/framework/src/base/Registry.C b/framework/src/base/Registry.C index e6acd84addd7..0f3286b92bce 100644 --- a/framework/src/base/Registry.C +++ b/framework/src/base/Registry.C @@ -16,6 +16,7 @@ #include "libmesh/libmesh_common.h" #include +#include Registry & Registry::getRegistry() @@ -86,21 +87,48 @@ Registry::addKnownLabel(const std::string & label) } void -Registry::addDataFilePath(const std::string & fullpath) +Registry::addDataFilePath(const std::string & name, const std::string & in_tree_path) { + mooseAssert(std::filesystem::path(in_tree_path).parent_path().filename() == "data", + "Must end with data"); + + // Find either the installed or in-tree path + const auto path = determineDataFilePath(name, in_tree_path); + auto & dfp = getRegistry()._data_file_paths; + const auto it = dfp.find(name); + // Not registered yet + if (it == dfp.end()) + dfp.emplace(name, path); + // Registered, but with a different value + else if (it->second != path) + mooseError("While registering data file path '", + path, + "' for '", + name, + "': the path '", + it->second, + "' is already registered"); +} +void +Registry::addAppDataFilePath(const std::string & app_name, const std::string & app_path) +{ // split the *App.C filename from its containing directory - const auto path = MooseUtils::splitFileName(fullpath).first; - + const auto dir = MooseUtils::splitFileName(app_path).first; // This works for both build/unity_src/ and src/base/ as the *App.C file location, // in case __FILE__ doesn't get overriden in unity build - const auto data_dir = MooseUtils::pathjoin(path, "../../data"); + addDataFilePath(app_name, MooseUtils::pathjoin(dir, "../../data")); +} - // if the data directory exists and hasn't been added before, add it - if (MooseUtils::pathIsDirectory(data_dir) && - std::find(dfp.begin(), dfp.end(), data_dir) == dfp.end()) - dfp.push_back(data_dir); +std::string +Registry::getDataFilePath(const std::string & name) +{ + const auto & dfps = getRegistry()._data_file_paths; + const auto it = dfps.find(name); + if (it == dfps.end()) + mooseError("Registry::getDataFilePath(): A data file path for '", name, "' is not registered"); + return it->second; } void @@ -124,3 +152,26 @@ Registry::getRepositoryURL(const std::string & repo_name) return it->second; mooseError("Registry::getRepositoryURL(): The repository '", repo_name, "' is not registered."); } + +std::string +Registry::determineDataFilePath(const std::string & name, const std::string & in_tree_path) +{ + // Installed data + const auto installed_path = + MooseUtils::pathjoin(Moose::getExecutablePath(), "..", "share", name, "data"); + const std::string abs_installed_path = std::filesystem::weakly_canonical(installed_path).c_str(); + if (MooseUtils::checkFileReadable(abs_installed_path, false, false, false)) + return installed_path; + + // In tree data + const std::string abs_in_tree_path = std::filesystem::weakly_canonical(in_tree_path).c_str(); + if (MooseUtils::checkFileReadable(abs_in_tree_path, false, false, false)) + return abs_in_tree_path; + + mooseError("Failed to determine data file path for '", + name, + "'. Paths searched:\n\n installed: ", + abs_installed_path, + "\n in-tree: ", + abs_in_tree_path); +} diff --git a/framework/src/interfaces/DataFileInterface.C b/framework/src/interfaces/DataFileInterface.C index f192d77cac8e..b02bdb820a8a 100644 --- a/framework/src/interfaces/DataFileInterface.C +++ b/framework/src/interfaces/DataFileInterface.C @@ -33,7 +33,8 @@ DataFileInterface::getDataFileName(const std::string & param) const // Look relative to the input file const auto base = _parent.parameters().getParamFileBase(param); - const std::string relative_to_context = std::filesystem::absolute(base / value_path).c_str(); + const std::string relative_to_context = + std::filesystem::weakly_canonical(base / value_path).c_str(); if (MooseUtils::checkFileReadable(relative_to_context, false, false, false)) { _parent.paramInfo(param, "Data file '", value, "' found relative to the input file."); @@ -48,42 +49,45 @@ std::string DataFileInterface::getDataFileNameByName(const std::string & relative_path, const std::string * param) const { - /// - relative to the running binary (assuming the application is installed) - const auto share_dir = MooseUtils::pathjoin(Moose::getExecutablePath(), "..", "share"); - if (MooseUtils::pathIsDirectory(share_dir)) + std::map found; + + // Search each registered data path for the relative path + for (const auto & [name, path] : Registry::getRegistry().getDataFilePaths()) { - const auto dirs = MooseUtils::listDir(share_dir, false); - for (const auto & data_dir : dirs) + const auto file_path = MooseUtils::pathjoin(path, relative_path); + if (MooseUtils::checkFileReadable(file_path, false, false, false)) { - const auto path = MooseUtils::pathjoin(data_dir, "data", relative_path); - if (MooseUtils::checkFileReadable(path, false, false, false)) - { - if (param) - _parent.paramInfo( - *param, "Data file '", path, "' found in an installed app distribution."); - else - mooseInfo("Data file '", path, "' found in an installed app distribution."); - return path; - } + const std::string abs_file_path = std::filesystem::weakly_canonical(file_path).c_str(); + found.emplace(name, abs_file_path); } } - /// - relative to all registered data file directories - for (const auto & data_dir : Registry::getRegistry().getDataFilePaths()) + // Found exactly one + if (found.size() == 1) { - const auto path = MooseUtils::pathjoin(data_dir, relative_path); - if (MooseUtils::checkFileReadable(path, false, false, false)) - { - if (param) - _parent.paramInfo(*param, "Data file '", path, "' found in a source repository."); - else - mooseInfo("Data file '", path, "' found in a source repository."); - return path; - } + const auto & [name, path] = *(found.begin()); + if (param) + _parent.paramInfo(*param, "Using data file '", path, "' from ", name); + else + _parent.mooseInfo("Using data file '", path, "' from ", name); + return path; + } + + std::stringstream oss; + // Found multiple + if (found.size() > 1) + { + oss << "Multiple files were found when searching for the data file '" << relative_path + << "':\n\n"; + for (const auto & [name, path] : found) + oss << " - " << name << ": " << path << "\n"; } + // Found none + else + oss << "Unable to find the data file '" << relative_path << "' anywhere"; - mooseException(param ? _parent.parameters().inputLocation(*param) : _parent.name(), - ": Unable to find data file '", - relative_path, - "' anywhere"); + if (param) + _parent.paramError(*param, oss.str()); + else + _parent.mooseError(oss.str()); } diff --git a/modules/solid_mechanics/src/base/SolidMechanicsApp.C b/modules/solid_mechanics/src/base/SolidMechanicsApp.C index 3681d801920e..823fb59744d4 100644 --- a/modules/solid_mechanics/src/base/SolidMechanicsApp.C +++ b/modules/solid_mechanics/src/base/SolidMechanicsApp.C @@ -191,7 +191,7 @@ SolidMechanicsApp::registerAll(Factory & f, ActionFactory & af, Syntax & s) Registry::registerObjectsTo(f, {"SolidMechanicsApp"}); Registry::registerActionsTo(af, {"SolidMechanicsApp"}); associateSyntaxInner(s, af); - registerDataFilePath(); + registerAppDataFilePath("solid_mechanics"); } void diff --git a/unit/other_data/data/README.md b/unit/other_data/data/README.md new file mode 100644 index 000000000000..afd48cceceda --- /dev/null +++ b/unit/other_data/data/README.md @@ -0,0 +1 @@ +Used for RegistryTest.addDataFilePathMismatch diff --git a/unit/src/RegistryTest.C b/unit/src/RegistryTest.C index 9100b0bc5b2d..d41cdf0e4223 100644 --- a/unit/src/RegistryTest.C +++ b/unit/src/RegistryTest.C @@ -15,6 +15,8 @@ #include "MaterialRealAux.h" #include "CheckOutputAction.h" +#include + TEST(RegistryTest, getClassName) { // This is a simple non-templated case @@ -29,6 +31,100 @@ TEST(RegistryTest, getClassName) EXPECT_EQ(Registry::getClassName(), "CheckOutputAction"); } +TEST(RegistryTest, addDataFilePathMismatch) +{ + const std::string name = "data_mismatch"; + const std::string path = "data"; + const std::string abs_path = std::filesystem::weakly_canonical(path).c_str(); + + Registry::addDataFilePath(name, path); + + const std::string other_path = "other_data/data"; + const std::string other_abs_path = std::filesystem::weakly_canonical(other_path).c_str(); + + EXPECT_THROW( + { + try + { + Registry::addDataFilePath(name, other_path); + } + catch (const std::exception & e) + { + EXPECT_EQ(std::string(e.what()), + "While registering data file path '" + other_abs_path + "' for '" + name + + "': the path '" + abs_path + "' is already registered"); + throw; + } + }, + std::exception); +} + +TEST(RegistryTest, getDataPath) +{ + const std::string name = "data_working"; + const std::string path = "data"; + const std::string abs_path = std::filesystem::weakly_canonical(path).c_str(); + + Registry::addDataFilePath(name, path); + EXPECT_EQ(Registry::getDataFilePath(name), abs_path); + + Registry::addDataFilePath(name, path); + EXPECT_EQ(Registry::getDataFilePath(name), abs_path); +} + +TEST(RegistryTest, getDataPathUnregistered) +{ + const std::string name = "unregistered"; + EXPECT_THROW( + { + try + { + Registry::getDataFilePath(name); + } + catch (const std::exception & e) + { + EXPECT_EQ(std::string(e.what()), + "Registry::getDataFilePath(): A data file path for '" + name + + "' is not registered"); + throw; + } + }, + std::exception); +} + +TEST(RegistryTest, determineFilePath) +{ + const std::string path = "data"; + const std::string abs_path = std::filesystem::weakly_canonical(path).c_str(); + EXPECT_EQ(Registry::determineDataFilePath("unused", path), abs_path); +} + +TEST(RegistryTest, determineFilePathFailed) +{ + const std::string name = "unused"; + const std::string path = "foo"; + const std::string abs_path = std::filesystem::weakly_canonical(path).c_str(); + const std::string installed_path = "../share/" + name + "/data"; + const std::string installed_abs_path = std::filesystem::weakly_canonical(installed_path).c_str(); + + EXPECT_THROW( + { + try + { + Registry::determineDataFilePath(name, path); + } + catch (const std::exception & e) + { + EXPECT_EQ(std::string(e.what()), + "Failed to determine data file path for '" + name + + "'. Paths searched:\n\n installed: " + installed_abs_path + + "\n in-tree: " + abs_path); + throw; + } + }, + std::exception); +} + TEST(RegistryTest, repositoryURL) { const std::string repo_name = "bar";