diff --git a/.github/build_openassetio_mediacreation/action.yml b/.github/build_openassetio_mediacreation/action.yml new file mode 100644 index 0000000..a0f708c --- /dev/null +++ b/.github/build_openassetio_mediacreation/action.yml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 The Foundry Visionmongers Ltd + +# Composite action for reuse within other workflows. +# Builds OpenAssetIO-MediaCreation. +# Should be run on a ghcr.io/openassetio/openassetio-build container. + +name: Build OpenAssetIO-MediaCreation +description: Builds OpenAssetIO-MediaCreation and publishes an artifact +runs: + using: "composite" + steps: + - name: Checkout OpenAssetIO-MediaCreation + uses: actions/checkout@v3 + with: + repository: OpenAssetIO/OpenAssetIO-MediaCreation + path: openassetio-mediacreation-checkout + + - name: Build OpenAssetIO-MediaCreation + shell: bash + run: | + cd openassetio-mediacreation-checkout + mkdir build + python -m pip install openassetio-traitgen + cmake -G Ninja -S . -B build + cmake --build build + cmake --install build + + - uses: actions/upload-artifact@v3 + with: + name: OpenAssetIO-MediaCreation Build + path: openassetio-mediacreation-checkout/build/dist + retention-days: 1 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index a222d4f..f4d6d3f 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -54,6 +54,16 @@ jobs: - name: Build uses: ./.github/build_openassetio + build-openassetio-mediacreation: + name: Build OpenAssetIO-MediaCreation + runs-on: ubuntu-latest + container: + image: ghcr.io/openassetio/openassetio-build + steps: + - uses: actions/checkout@v3 + - name: Build + uses: ./.github/build_openassetio_mediacreation + cpp-linters: name: C++ linters runs-on: ubuntu-20.04 @@ -82,9 +92,16 @@ jobs: name: OpenAssetIO Build path: ./openassetio-build + - name: Get OpenAssetIO-MediaCreation + uses: actions/download-artifact@v3 + with: + name: OpenAssetIO-MediaCreation Build + path: ./openassetio-mediacreation-build + - name: Configure CMake build run: > - cmake -DCMAKE_PREFIX_PATH="./openassetio-build" -S . -B build -G Ninja + cmake -DCMAKE_PREFIX_PATH="./openassetio-build;./openassetio-mediacreation-build" + -S . -B build -G Ninja --install-prefix ${{ github.workspace }}/dist --preset lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 152a07b..0e1acf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,16 @@ jobs: - name: Build uses: ./.github/build_openassetio + build-openassetio-mediacreation: + name: Build OpenAssetIO-MediaCreation + runs-on: ubuntu-latest + container: + image: ghcr.io/openassetio/openassetio-build + steps: + - uses: actions/checkout@v3 + - name: Build + uses: ./.github/build_openassetio_mediacreation + test: name: Test-Resolver runs-on: ubuntu-latest @@ -36,14 +46,21 @@ jobs: name: OpenAssetIO Build path: ./openassetio-build + - name: Get OpenAssetIO-MediaCreation + uses: actions/download-artifact@v3 + with: + name: OpenAssetIO-MediaCreation Build + path: ./openassetio-mediacreation-build + - name: Build and install Resolver run: | - cmake -S . -DCMAKE_PREFIX_PATH="./openassetio-build" -B build + cmake -S . -DCMAKE_PREFIX_PATH="./openassetio-build;./openassetio-mediacreation-build" -B build cmake --build build cmake --install build - run: | python -m pip install -r tests/requirements.txt + python -m pip install importlib-metadata # PYTHONPATH here is extended with `/usr/local/lib/python` # because the USD install on this docker container is odd, and diff --git a/CMakeLists.txt b/CMakeLists.txt index 8552ca1..bb62e02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ find_package(pxr REQUIRED) # Find OpenAssetIO find_package(OpenAssetIO REQUIRED) +find_package(OpenAssetIO-MediaCreation REQUIRED) # Add Static analysis targets include(StaticAnalyzers) diff --git a/README.md b/README.md index d7bba2a..9a95182 100644 --- a/README.md +++ b/README.md @@ -161,19 +161,20 @@ To enable debug logging from the resolver. ## Testing -To run tests, from the project root +To run tests, once built, from the project root: ```sh -export PXR_PLUGINPATH_NAME=$(pwd)/build/dist/resources/plugInfo.json -cd tests python -m pip install -r requirements.txt -pytest +python -m pytest ``` > **Note** > > Refer to the [Running](#running) section for the environmental -> prerequisites to run these tests. +> prerequisites to run these tests. The tests will set +> `OPENASSETIO_DEFAULT_CONFIG` appropriately, and unless otherwise +> defined, attempt to set `PXR_PLUGINPATH_NAME` if the standard `build` +> directory was used. ## A note on Python diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cec6d06..64a9684 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries(${PLUGIN_NAME} ar OpenAssetIO::openassetio-core OpenAssetIO::openassetio-python-bridge + OpenAssetIO-MediaCreation::openassetio-mediacreation ) #----------------------------------------------------------------------- diff --git a/src/resolver.cpp b/src/resolver.cpp index fd42cfe..bb5cf51 100644 --- a/src/resolver.cpp +++ b/src/resolver.cpp @@ -4,6 +4,7 @@ #include "resolver.h" #include +#include #include #include @@ -23,6 +24,8 @@ #include #include +#include + // NOLINTNEXTLINE PXR_NAMESPACE_USING_DIRECTIVE PXR_NAMESPACE_OPEN_SCOPE @@ -34,7 +37,26 @@ TF_DEBUG_CODES(OPENASSETIO_RESOLVER) PXR_NAMESPACE_CLOSE_SCOPE namespace { -/// Converter logger from OpenAssetIO log framing to USD log outputs. +/* + * Replaces all occurrences of the search string in the subject with + * the supplied replacement. + */ +void replaceAllInString(std::string &subject, const std::string &search, + const std::string &replace) { + const size_t searchLength = search.length(); + const size_t replaceLength = replace.length(); + size_t pos = 0; + while ((pos = subject.find(search, pos)) != std::string::npos) { + subject.replace(pos, searchLength, replace); + pos += replaceLength; + } +} + +/* + * OpenAssetIO LoggerInterface implementation + * + * Converter logger from OpenAssetIO log framing to USD log outputs. + */ class UsdOpenAssetIOResolverLogger : public openassetio::log::LoggerInterface { public: void log(Severity severity, const openassetio::Str &message) override { @@ -47,8 +69,6 @@ class UsdOpenAssetIOResolverLogger : public openassetio::log::LoggerInterface { TF_DEBUG(OPENASSETIO_RESOLVER).Msg(message + "\n"); break; case Severity::kError: - // TODO(EM) : Review to see which error types are most appropriate, - // are all errors (not criticals) non fatal? TF_ERROR(TfDiagnosticType::TF_DIAGNOSTIC_NONFATAL_ERROR_TYPE, message); break; case Severity::kInfo: @@ -62,6 +82,12 @@ class UsdOpenAssetIOResolverLogger : public openassetio::log::LoggerInterface { } }; +/** + * OpenAssetIO HostInterface implementation + * + * This identifies the Ar2 plugin uniquely should the manager plugin + * wish to adapt its behaviour. + */ class UsdOpenAssetIOHostInterface : public openassetio::hostApi::HostInterface { public: [[nodiscard]] openassetio::Identifier identifier() const override { @@ -73,69 +99,44 @@ class UsdOpenAssetIOHostInterface : public openassetio::hostApi::HostInterface { } }; -// TODO(DF): Replace with C++ trait views, once they exist. -const openassetio::trait::TraitId kLocateableContentTraitId = // NOLINT - "openassetio-mediacreation:content.LocatableContent"; -const openassetio::trait::property::Key kLocateableContentLocationPropertyKey = // NOLINT - "location"; - /** * Retrieve the resolved file path of an entity reference. * - * If the given `assetPath` is not an entity reference, assume it's - * already a path and pass through unmodified. - * - * Otherwise, resolve the "locateableContent" trait of the entity and + * This will resolve the "locateableContent" trait of the entity and * return the "location" property of it, converted from a URL to a - * posix file path. + * file path. * * @param manager OpenAssetIO manager plugin wrapper. * @param context OpenAssetIO calling context. - * @param assetPath String-like asset path that may or may not be an - * entity reference. - * @return Resolved file path if `assetPath` is an entity reference, - * `assetPath` otherwise. + * @param entityReference The reference to resolve. + * @return Resolved file path. */ -template -Ref locationInManagerContextForEntity(const openassetio::hostApi::ManagerPtr &manager, - const openassetio::ContextConstPtr &context, Ref assetPath) { - // Check if the assetPath is an OpenAssetIO entity reference. - if (auto maybeEntityReference = manager->createEntityReferenceIfValid(assetPath)) { - openassetio::TraitsDataPtr traitsData; - - // Resolve the locateableContent trait in order to get the - // (absolute) path to the asset. - manager->resolve( - {std::move(*maybeEntityReference)}, {kLocateableContentTraitId}, context, - [&traitsData]([[maybe_unused]] std::size_t idx, - const openassetio::TraitsDataPtr &resolvedTraitsData) { - // Success callback. - traitsData = resolvedTraitsData; - }, - []([[maybe_unused]] std::size_t idx, const openassetio::BatchElementError &error) { - // Error callback. - // TODO(DF): Better conversion of BatchElementError to - // appropriate exception type. - std::string errorMsg = "error code "; - errorMsg += std::to_string(static_cast(error.code)); - errorMsg += ": "; - errorMsg += error.message; - throw std::runtime_error{errorMsg}; - }); - - if (openassetio::trait::property::Value propValue; traitsData->getTraitProperty( - &propValue, kLocateableContentTraitId, kLocateableContentLocationPropertyKey)) { - // We've successfully got the locateableContent trait for the - // entity. - static constexpr std::size_t kProtocolSize = std::string_view{"file://"}.size(); - return Ref{std::get(propValue).substr(kProtocolSize)}; - } - // TODO(DF): Here we fall through to returning `assetPath` verbatim - // if the "locateableContent" trait isn't found. We need to - // consider if this is the correct thing to do. +std::string resolveToPath(const openassetio::hostApi::ManagerPtr &manager, + const openassetio::ContextConstPtr &context, + const openassetio::EntityReference &entityReference) { + using openassetio_mediacreation::traits::content::LocatableContentTrait; + + // Resolve the locatable content trait, this will provide a URL + // that points to the final content + openassetio::TraitsDataPtr traitsData = + manager->resolve(entityReference, {LocatableContentTrait::kId}, context); + + // OpenAssetIO is URL based, but we need a path, so check the + // scheme and decode into a path + + static constexpr std::string_view kFileURLScheme{"file://"}; + static constexpr std::size_t kProtocolSize = kFileURLScheme.size(); + + openassetio::Str url = LocatableContentTrait(traitsData).getLocation(); + if (url.rfind(kFileURLScheme, 0) == openassetio::Str::npos) { + std::string msg = "Only file URLs are supported: "; + msg += url; + throw std::runtime_error(msg); } - return assetPath; + // TODO(tc): Decode % escape sequences properly + replaceAllInString(url, "%20", " "); + return url.substr(kProtocolSize); } /** @@ -181,7 +182,11 @@ auto catchAndLogExceptions(Fn &&fn, const openassetio::log::LoggerInterfacePtr & } // namespace // ------------------------------------------------------------ -/* Ar Resolver Implementation */ + +/* + * Ar Resolver Implementation + */ + UsdOpenAssetIOResolver::UsdOpenAssetIOResolver() { // Note: it is safe to throw exceptions from the constructor. USD will // error gracefully, unlike exceptions from other member functions, @@ -204,27 +209,18 @@ UsdOpenAssetIOResolver::UsdOpenAssetIOResolver() { openassetio::hostApi::ManagerFactory::kDefaultManagerConfigEnvVarName}; } - // TODO(DF): Set appropriate locale. - readContext_ = openassetio::Context::make(openassetio::Context::Access::kRead, - openassetio::Context::Retention::kTransient); - - logger_->debug(TF_FUNC_NAME()); + readContext_ = openassetio::Context::make(openassetio::Context::Access::kRead); } -UsdOpenAssetIOResolver::~UsdOpenAssetIOResolver() { logger_->debug(TF_FUNC_NAME()); } +UsdOpenAssetIOResolver::~UsdOpenAssetIOResolver() = default; + +/* + * Read + */ std::string UsdOpenAssetIOResolver::_CreateIdentifier( const std::string &assetPath, const ArResolvedPath &anchorAssetPath) const { - const std::string fnName = TF_FUNC_NAME(); - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " << fnName - << "\n assetPath: " << assetPath - << "\n anchorAssetPath: " << anchorAssetPath.GetPathString(); - logger_->debug(logMsg.str()); - } - - auto result = catchAndLogExceptions( + return catchAndLogExceptions( [&] { std::string identifier; @@ -235,211 +231,95 @@ std::string UsdOpenAssetIOResolver::_CreateIdentifier( // resolve to an absolute path, making the anchorAssetPath redundant // (for now). // TODO(DF): Allow the manager to provide an identifier representing - // an "anchored" entity reference. + // an "anchored" entity reference via getWithRelationship. identifier = assetPath; } else { - identifier = ArDefaultResolver::_CreateIdentifier( - assetPath, - locationInManagerContextForEntity(manager_, readContext_, anchorAssetPath)); + identifier = ArDefaultResolver::_CreateIdentifier(assetPath, anchorAssetPath); } return identifier; }, - logger_, fnName); - - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " - << "result: " << result; - logger_->debug(logMsg.str()); - } - return result; -} - -// TODO(DF): Implement for publishing workflow. -std::string UsdOpenAssetIOResolver::_CreateIdentifierForNewAsset( - const std::string &assetPath, const ArResolvedPath &anchorAssetPath) const { - auto result = ArDefaultResolver::_CreateIdentifierForNewAsset(assetPath, anchorAssetPath); - logger_->debug(TF_FUNC_NAME()); - - logger_->debug(" assetPath: " + assetPath); - logger_->debug(" anchorAssetPath: " + anchorAssetPath.GetPathString()); - logger_->debug(" result: " + result); - - return result; + logger_, TF_FUNC_NAME()); } ArResolvedPath UsdOpenAssetIOResolver::_Resolve(const std::string &assetPath) const { - const std::string fnName = TF_FUNC_NAME(); - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " << fnName - << "\n assetPath: " << assetPath; - logger_->debug(logMsg.str()); - } - - auto result = catchAndLogExceptions( + return catchAndLogExceptions( [&] { - if (manager_->isEntityReferenceString(assetPath)) { - // TODO(DF): We may wish to do more here, e.g. - // `finalizedEntityVersion()` - return ArResolvedPath{assetPath}; + if (auto entityReference = manager_->createEntityReferenceIfValid(assetPath)) { + return ArResolvedPath{resolveToPath(manager_, readContext_, *entityReference)}; } return ArDefaultResolver::_Resolve(assetPath); }, - logger_, fnName); + logger_, TF_FUNC_NAME()); +} - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " - << "result: " << result.GetPathString(); - logger_->debug(logMsg.str()); - } +/* + * Write + * + * We don't currently support writes to OpenAssetIO entity references. + * In order to call register when the ArAsset is closed, we'll need to + * not resolve in _ResolveForNewAsset, and pass the entity reference + * through. + */ - return result; +std::string UsdOpenAssetIOResolver::_CreateIdentifierForNewAsset( + const std::string &assetPath, const ArResolvedPath &anchorAssetPath) const { + if (manager_->isEntityReferenceString(assetPath)) { + std::string message = "Writes to OpenAssetIO entity references are not currently supported "; + message += assetPath; + logger_->critical(message); + return ""; + } + return ArDefaultResolver::_CreateIdentifierForNewAsset(assetPath, anchorAssetPath); } -// TODO(DF): Implement for publishing workflow. ArResolvedPath UsdOpenAssetIOResolver::_ResolveForNewAsset(const std::string &assetPath) const { - auto result = ArDefaultResolver::_ResolveForNewAsset(assetPath); - - logger_->debug(TF_FUNC_NAME()); - logger_->debug(" assetPath: " + assetPath); - logger_->debug(" result: " + result.GetPathString()); - - return result; + if (manager_->isEntityReferenceString(assetPath)) { + std::string message = "Writes to OpenAssetIO entity references are not currently supported "; + message += assetPath; + logger_->critical(message); + return ArResolvedPath(""); + } + return ArDefaultResolver::_ResolveForNewAsset(assetPath); } -/* Asset Operations*/ +/* + * Pass-through asset operations + * + * These methods are simply relayed to the ArDefaultResolver. There may + * be interest in fetching data for some of these from the manager, but + * we don't have a real use case just yet. Doing so increases complexity + * as we'd need to return both the resolved path _and_ the original + * entity reference from _Resolve, so we could make queries in these + * methods. We'll need this for publishing operations, but avoiding that + * overhead for the more common (and hot) read case is critical. + * + */ std::string UsdOpenAssetIOResolver::_GetExtension(const std::string &assetPath) const { - const std::string fnName = TF_FUNC_NAME(); - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " << fnName - << "\n assetPath: " << assetPath; - logger_->debug(logMsg.str()); - } - - auto result = catchAndLogExceptions( - [&] { - // TODO(DF): Give the manager a chance to provide the file extension. - return ArDefaultResolver::_GetExtension( - locationInManagerContextForEntity(manager_, readContext_, assetPath)); - }, - logger_, fnName); - - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " - << "result: " << result; - logger_->debug(logMsg.str()); - } - - return result; + return ArDefaultResolver::_GetExtension(assetPath); } ArAssetInfo UsdOpenAssetIOResolver::_GetAssetInfo(const std::string &assetPath, const ArResolvedPath &resolvedPath) const { - const std::string fnName = TF_FUNC_NAME(); - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " << fnName - << "\n assetPath: " << assetPath - << "\n resolvedPath: " << resolvedPath.GetPathString(); - logger_->debug(logMsg.str()); - } - // TODO(DF): Determine if there is value in supporting this via the - // manager, including any useful data that can be stuffed into the - // generic `resolverInfo` VtValue member. - auto result = ArDefaultResolver::_GetAssetInfo(assetPath, resolvedPath); - - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " - << "\n result.assetName: " << result.assetName - << "\n result.repoPath: " << result.repoPath; - logger_->debug(logMsg.str()); - } - - return result; + return ArDefaultResolver::_GetAssetInfo(assetPath, resolvedPath); } ArTimestamp UsdOpenAssetIOResolver::_GetModificationTimestamp( const std::string &assetPath, const ArResolvedPath &resolvedPath) const { - const std::string fnName = TF_FUNC_NAME(); - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " << fnName - << "\n assetPath: " << assetPath - << "\n resolvedPath: " << resolvedPath.GetPathString(); - logger_->debug(logMsg.str()); - } - - auto result = catchAndLogExceptions( - [&] { - if (manager_->isEntityReferenceString(assetPath)) { - // Deliberately use a valid fixed timestamp, which will force caching - // until we implement a more considered method. - // TODO(DF): Consider how best to handle this via OpenAssetIO. - return ArTimestamp{0}; - } - return ArDefaultResolver::_GetModificationTimestamp(assetPath, resolvedPath); - }, - logger_, fnName); - - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " - << "result: " << result.GetTime(); - logger_->debug(logMsg.str()); - } - - return result; + return ArDefaultResolver::_GetModificationTimestamp(assetPath, resolvedPath); } std::shared_ptr UsdOpenAssetIOResolver::_OpenAsset( const ArResolvedPath &resolvedPath) const { - const std::string fnName = TF_FUNC_NAME(); - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " << fnName - << "\n resolvedPath: " << resolvedPath.GetPathString(); - logger_->debug(logMsg.str()); - } - - auto result = catchAndLogExceptions( - [&] { - return ArDefaultResolver::_OpenAsset( - locationInManagerContextForEntity(manager_, readContext_, resolvedPath)); - }, - logger_, fnName); - - { - std::ostringstream logMsg; - logMsg << "[tid=" << std::this_thread::get_id() << "] " - << "result: " << result.get(); - logger_->debug(logMsg.str()); - } - return result; + return ArDefaultResolver::_OpenAsset(resolvedPath); } -// TODO(DF): Implement for publishing workflow. bool UsdOpenAssetIOResolver::_CanWriteAssetToPath(const ArResolvedPath &resolvedPath, std::string *whyNot) const { - auto result = ArDefaultResolver::CanWriteAssetToPath(resolvedPath, whyNot); - - logger_->debug(TF_FUNC_NAME()); - logger_->debug(" resolvedPath: " + resolvedPath.GetPathString()); - logger_->debug(" result: " + std::to_string(static_cast(result))); - - return result; + return ArDefaultResolver::_CanWriteAssetToPath(resolvedPath, whyNot); } -// TODO(DF): Implement for publishing workflow. std::shared_ptr UsdOpenAssetIOResolver::_OpenAssetForWrite( const ArResolvedPath &resolvedPath, WriteMode writeMode) const { - logger_->debug(TF_FUNC_NAME()); - logger_->debug(" resolvedPath: " + resolvedPath.GetPathString()); - return ArDefaultResolver::_OpenAssetForWrite(resolvedPath, writeMode); } diff --git a/tests/requirements.txt b/tests/requirements.txt index 30f2918..2fa4ff4 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,2 @@ pytest==6.2.4 -openassetio-manager-bal==1.0.0a6 +openassetio-manager-bal diff --git a/tests/resources/integration_test_data/bal_library.json b/tests/resources/integration_test_data/bal_library.json index c1d848e..5621f02 100644 --- a/tests/resources/integration_test_data/bal_library.json +++ b/tests/resources/integration_test_data/bal_library.json @@ -5,7 +5,7 @@ { "traits": { "openassetio-mediacreation:content.LocatableContent": { - "location": "file://${TEST_DATA_ROOT}/assetized_child_ref_non_assetized_grandchild/floor1.usd" + "location": "file://${bal_library_dir}/assetized_child_ref_non_assetized_grandchild/floor1.usd" } } } @@ -16,7 +16,7 @@ { "traits": { "openassetio-mediacreation:content.LocatableContent": { - "location": "file://${TEST_DATA_ROOT}/non_assetized_child_ref_assetized_grandchild/car.usd" + "location": "file://${bal_library_dir}/non_assetized_child_ref_assetized_grandchild/car.usd" } } } @@ -27,7 +27,8 @@ { "traits": { "openassetio-mediacreation:content.LocatableContent": { - "location": "file://${TEST_DATA_ROOT}/recursive_assetized_resolve/floors/floor1.usd" + "location": + "file://${bal_library_dir}/recursive_assetized_resolve/floors/floor%201.usd" } } } @@ -38,7 +39,18 @@ { "traits": { "openassetio-mediacreation:content.LocatableContent": { - "location": "file://${TEST_DATA_ROOT}/recursive_assetized_resolve/cars/car.usd" + "location": "file://${bal_library_dir}/recursive_assetized_resolve/cars/car.usd" + } + } + } + ] + }, + "not_a_file": { + "versions": [ + { + "traits": { + "openassetio-mediacreation:content.LocatableContent": { + "location": "https://stuffonmycat.com" } } } diff --git a/tests/resources/integration_test_data/recursive_assetized_resolve/floors/floor1.usd b/tests/resources/integration_test_data/recursive_assetized_resolve/floors/floor 1.usd similarity index 100% rename from tests/resources/integration_test_data/recursive_assetized_resolve/floors/floor1.usd rename to tests/resources/integration_test_data/recursive_assetized_resolve/floors/floor 1.usd diff --git a/tests/resources/openassetio.conf b/tests/resources/openassetio.conf deleted file mode 100644 index c379883..0000000 --- a/tests/resources/openassetio.conf +++ /dev/null @@ -1,2 +0,0 @@ -[manager] -identifier = "org.openassetio.examples.manager.bal" diff --git a/tests/resources/openassetio_config.toml b/tests/resources/openassetio_config.toml new file mode 100644 index 0000000..e584bca --- /dev/null +++ b/tests/resources/openassetio_config.toml @@ -0,0 +1,5 @@ +[manager] +identifier = "org.openassetio.examples.manager.bal" + +[manager.settings] +library_path = "${config_dir}/integration_test_data/bal_library.json" diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 46b36e8..7ff6120 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -10,40 +10,18 @@ # This environment var must be set before the usd imports. os.environ["TF_DEBUG"] = "OPENASSETIO_RESOLVER" -from pxr import Plug, Usd, Ar +if "PXR_PLUGINPATH_NAME" not in os.environ: + # Set up our resolver plugin if not already set up, and we can + root_dir = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) + plug_info = os.path.join(root_dir, "build", "dist", "resources", "pluginfo.json") + if os.path.exists(plug_info): + os.environ["PXR_PLUGINPATH_NAME"] = plug_info - -# Assume OpenAssetIO is configured as the custom primary resolver for -# all tests. If you're wondering where this is configured, it may -# just be set via the `PXR_PLUGINPATH_NAME` environment variable. +from pxr import Plug, Usd, Ar, Sdf, Tf # TODO(DF): More tests for error cases. -# This test can be removed once the logging transforms, alchemy like, -# into real functionality. -def test_open_stage_and_logging(capfd): - open_stage("resources/empty_shot.usda") - captured = capfd.readouterr() - - outputs = captured.out.split("UsdOpenAssetIOResolver::") - assert "UsdOpenAssetIOResolver" in outputs[1] - assert "_CreateIdentifier" in outputs[2] - assert "result" in outputs[2] - assert "_Resolve" in outputs[3] - assert "result" in outputs[3] - assert "_GetExtension" in outputs[4] - assert "result" in outputs[4] - assert "_GetAssetInfo" in outputs[5] - assert "result" in outputs[5] - assert "_OpenAsset" in outputs[6] - assert "result" in outputs[6] - assert "_GetModificationTimestamp" in outputs[7] - assert "result" in outputs[7] - assert "_GetExtension" in outputs[8] - assert "result" in outputs[8] - - # Given a USD document that references an asset via a direct relative # file path, then the asset is resolved to the file path as expected. def test_openassetio_resolver_has_no_effect_with_no_search_path(): @@ -117,24 +95,36 @@ def test_error_triggering_asset_ref(capfd): "resources/integration_test_data/error_triggering_asset_ref/parking_lot.usd" ) + logs = capfd.readouterr() + assert "MalformedEntityReference" in logs.err + + +def test_when_resolves_to_non_file_url_then_error(): + with pytest.raises(Tf.ErrorException) as exc: + Usd.Stage.Open("bal:///not_a_file") + # The exception seems to be truncated which looses the specifics of + # our message (doh!). Check at least we were in the stack + assert "UsdOpenAssetIOResolverLogger" in str(exc) + + +def test_when_writing_to_file_then_ok(capfd, tmp_path): + Sdf.Layer.CreateNew(str(tmp_path / "newLayer.usda")) logs = capfd.readouterr() assert ( - "OpenAssetIO error in UsdOpenAssetIOResolver::_GetExtension: RuntimeError: error code 130:" - in logs.err + "Writes to OpenAssetIO entity references are not currently supported" + not in logs.err ) -##### Utility Functions ##### +def test_when_writing_to_entity_ref_then_error(): + with pytest.raises(Tf.ErrorException) as exc: + Sdf.Layer.CreateNew("bal:///newLayer") + assert "Writes to OpenAssetIO entity references are not currently supported" in str( + exc + ) -# Verify OpenAssetIO configured as the AR resolver. -@pytest.fixture(autouse=True) -def openassetio_configured(): - plugin_registry = Plug.Registry() - plugin = plugin_registry.GetPluginWithName("usdOpenAssetIOResolver") - assert ( - plugin is not None - ), "usdOpenAssetIOResolver plugin not loaded, please check PXR_PLUGINPATH_NAME env variable" +##### Utility Functions ##### # Log openassetio resolver messages @@ -183,22 +173,14 @@ def open_stage(path_relative_from_file, context=None): @pytest.fixture(autouse=True) -def bal_library(monkeypatch, test_data_root): +def openasssetio_default_config(monkeypatch, resources_dir): monkeypatch.setenv( - "BAL_LIBRARY_PATH", os.path.join(test_data_root, "bal_library.json") + "OPENASSETIO_DEFAULT_CONFIG", + os.path.join(resources_dir, "openassetio_config.toml"), ) -@pytest.fixture(autouse=True) -def test_data_root_env_var(monkeypatch, test_data_root): - """ - The TEST_DATA_ROOT env var is expanded in the various BAL JSON - libraries, to provide portable absolute paths to file assets. - """ - monkeypatch.setenv("TEST_DATA_ROOT", test_data_root) - - @pytest.fixture -def test_data_root(): +def resources_dir(): script_dir = os.path.realpath(os.path.dirname(__file__)) - return os.path.join(script_dir, "resources", "integration_test_data") + return os.path.join(script_dir, "resources")