diff --git a/pxr/usd/usd/CMakeLists.txt b/pxr/usd/usd/CMakeLists.txt index a6dc3abea5..5f6301fb16 100644 --- a/pxr/usd/usd/CMakeLists.txt +++ b/pxr/usd/usd/CMakeLists.txt @@ -158,6 +158,8 @@ pxr_library(usd wrapTyped.cpp wrapUsdFileFormat.cpp wrapUtils.cpp + wrapValidationError.cpp + wrapValidator.cpp wrapVariantSets.cpp wrapVersion.cpp wrapZipFile.cpp @@ -272,6 +274,8 @@ pxr_test_scripts( testenv/testUsdTimeSamples.py testenv/testUsdTimeValueAuthoring.py testenv/testUsdUsdzFileFormat.py + testenv/testUsdValidationError.py + testenv/testUsdValidatorMetadata.py testenv/testUsdValueClips.py testenv/testUsdVariantEditing.py testenv/testUsdVariantFallbacks.py @@ -1332,3 +1336,15 @@ pxr_register_test(testUsdOpaqueAttributes COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdOpaqueAttributes" EXPECTED_RETURN_CODE 0 ) + +pxr_register_test(testUsdValidatorMetadata + PYTHON + COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdValidatorMetadata" + EXPECTED_RETURN_CODE 0 +) + +pxr_register_test(testUsdValidationError + PYTHON + COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdValidationError" + EXPECTED_RETURN_CODE 0 +) diff --git a/pxr/usd/usd/module.cpp b/pxr/usd/usd/module.cpp index b984549688..897c47ea5f 100644 --- a/pxr/usd/usd/module.cpp +++ b/pxr/usd/usd/module.cpp @@ -38,6 +38,8 @@ TF_WRAP_MODULE TF_WRAP(UsdSpecializes); TF_WRAP(UsdPrimRange); TF_WRAP(UsdVariantSets); + TF_WRAP(UsdValidationError); + TF_WRAP(UsdValidator); // SchemaBase, APISchemaBase and subclasses. TF_WRAP(UsdSchemaBase); diff --git a/pxr/usd/usd/testenv/testUsdValidationError.py b/pxr/usd/usd/testenv/testUsdValidationError.py new file mode 100644 index 0000000000..3ec6423b98 --- /dev/null +++ b/pxr/usd/usd/testenv/testUsdValidationError.py @@ -0,0 +1,210 @@ +#!/pxrpythonsubst +# +# Copyright 2024 Pixar +# +# Licensed under the terms set forth in the LICENSE.txt file available at +# https://openusd.org/license. + +import unittest + +from pxr import Plug, Sdf, Usd + + +class TestUsdValidationError(unittest.TestCase): + def test_create_default_error_site(self): + errorSite = Usd.ValidationErrorSite() + self.assertFalse(errorSite.IsValid()) + self.assertFalse(errorSite.IsValidSpecInLayer()) + self.assertFalse(errorSite.IsPrim()) + self.assertFalse(errorSite.IsProperty()) + self.assertFalse(errorSite.GetPropertySpec()) + self.assertFalse(errorSite.GetPrimSpec()) + self.assertFalse(errorSite.GetProperty()) + self.assertFalse(errorSite.GetPrim()) + self.assertFalse(errorSite.GetLayer()) + self.assertFalse(errorSite.GetStage()) + + def _verify_error_site_with_layer(self, errorSite: Usd.ValidationErrorSite, layer: Sdf.Layer, objectPath: Sdf.Path): + self.assertTrue(errorSite.IsValid()) + self.assertTrue(errorSite.IsValidSpecInLayer()) + self.assertFalse(errorSite.IsPrim()) + self.assertFalse(errorSite.IsProperty()) + expected_property_spec = layer.GetPropertyAtPath(objectPath) if objectPath.IsPropertyPath() else None + self.assertEqual(errorSite.GetPropertySpec(), expected_property_spec) + expected_prim_spec = layer.GetPrimAtPath(objectPath) if objectPath.IsPrimPath() else None + self.assertEqual(errorSite.GetPrimSpec(), expected_prim_spec) + self.assertFalse(errorSite.GetProperty()) + self.assertFalse(errorSite.GetPrim()) + self.assertEqual(errorSite.GetLayer(), layer) + self.assertFalse(errorSite.GetStage()) + + def test_create_error_site_with_layer_and_prim_spec(self): + stage = Usd.Stage.CreateInMemory() + test_prim_path = Sdf.Path("/test") + stage.DefinePrim(test_prim_path, "Xform") + errorSite = Usd.ValidationErrorSite(stage.GetRootLayer(), test_prim_path) + self._verify_error_site_with_layer(errorSite, stage.GetRootLayer(), test_prim_path) + + def test_create_error_site_with_layer_and_property_spec(self): + stage = Usd.Stage.CreateInMemory() + test_prim_path = Sdf.Path("/test") + test_prim = stage.DefinePrim(test_prim_path, "Xform") + test_attr = test_prim.CreateAttribute("attr", Sdf.ValueTypeNames.Int) + test_attr_path = test_attr.GetPath() + errorSite = Usd.ValidationErrorSite(stage.GetRootLayer(), test_attr_path) + self._verify_error_site_with_layer(errorSite, stage.GetRootLayer(), test_attr_path) + + def _verify_error_site_with_stage(self, errorSite: Usd.ValidationErrorSite, stage: Usd.Stage, objectPath: Sdf.Path): + self.assertTrue(errorSite.IsValid()) + self.assertFalse(errorSite.IsValidSpecInLayer()) + self.assertEqual(errorSite.IsPrim(), objectPath.IsPrimPath()) + self.assertEqual(errorSite.IsProperty(), objectPath.IsPropertyPath()) + self.assertFalse(errorSite.GetPropertySpec()) + self.assertFalse(errorSite.GetPrimSpec()) + expected_property = stage.GetPropertyAtPath(objectPath) if objectPath.IsPropertyPath() else Usd.Property() + self.assertEqual(errorSite.GetProperty(), expected_property) + expected_prim = stage.GetPrimAtPath(objectPath) if objectPath.IsPrimPath() else Usd.Prim() + self.assertEqual(errorSite.GetPrim(), expected_prim) + self.assertFalse(errorSite.GetLayer()) + self.assertEqual(errorSite.GetStage(), stage) + + def _verify_error_site_with_stage_and_layer(self, errorSite: Usd.ValidationErrorSite, stage: Usd.Stage, layer: Sdf.Layer, objectPath: Sdf.Path): + self.assertTrue(errorSite.IsValid()) + self.assertTrue(errorSite.IsValidSpecInLayer()) + self.assertEqual(errorSite.IsPrim(), objectPath.IsPrimPath()) + self.assertEqual(errorSite.IsProperty(), objectPath.IsPropertyPath()) + + expected_property_spec = layer.GetPropertyAtPath(objectPath) if objectPath.IsPropertyPath() else None + self.assertEqual(expected_property_spec, errorSite.GetPropertySpec()) + expected_prim_spec = layer.GetPrimAtPath(objectPath) if objectPath.IsPrimPath() else None + self.assertEqual(expected_prim_spec, errorSite.GetPrimSpec()) + expected_property = stage.GetPropertyAtPath(objectPath) if objectPath.IsPropertyPath() else Usd.Property() + self.assertEqual(expected_property, errorSite.GetProperty()) + expected_prim = stage.GetPrimAtPath(objectPath) if objectPath.IsPrimPath() else Usd.Prim() + self.assertEqual(expected_prim, errorSite.GetPrim()) + + self.assertEqual(errorSite.GetLayer(), layer) + self.assertEqual(errorSite.GetStage(), stage) + + def test_create_error_site_with_stage_and_prim(self): + stage = Usd.Stage.CreateInMemory() + test_prim_path = Sdf.Path("/test") + stage.DefinePrim(test_prim_path, "Xform") + errorSite = Usd.ValidationErrorSite(stage, test_prim_path) + self._verify_error_site_with_stage(errorSite, stage, test_prim_path) + + # With layer also + errorSite = Usd.ValidationErrorSite(stage, test_prim_path, stage.GetRootLayer()) + self._verify_error_site_with_stage_and_layer(errorSite, stage, stage.GetRootLayer(), test_prim_path) + + def test_create_error_site_with_stage_and_property(self): + stage = Usd.Stage.CreateInMemory() + test_prim_path = Sdf.Path("/test") + test_prim = stage.DefinePrim(test_prim_path, "Xform") + test_attr = test_prim.CreateAttribute("attr", Sdf.ValueTypeNames.Int) + test_attr_path = test_attr.GetPath() + errorSite = Usd.ValidationErrorSite(stage, test_attr_path) + self._verify_error_site_with_stage(errorSite, stage, test_attr_path) + + # With layer also + errorSite = Usd.ValidationErrorSite(stage, test_attr_path, stage.GetRootLayer()) + self._verify_error_site_with_stage_and_layer(errorSite, stage, stage.GetRootLayer(), test_attr_path) + + def test_create_error_site_with_invalid_args(self): + stage = Usd.Stage.CreateInMemory() + test_prim_path = Sdf.Path("/test") + stage.DefinePrim(test_prim_path, "Xform") + errors = { + "Wrong Stage Type": { + "stage": "wrong stage", + "layer": stage.GetRootLayer(), + "objectPath": test_prim_path + }, + "Wrong Layer Type": { + "stage": stage, + "layer": "wrong layer", + "objectPath": test_prim_path + }, + "Wrong Path Type": { + "stage": stage, + "layer": stage.GetRootLayer(), + "objectPath": 123 + }, + } + + for error_category, args in errors.items(): + with self.subTest(errorType=error_category): + with self.assertRaises(Exception): + Usd.ValidationErrorSite(**args) + + def _verify_validation_error(self, error, errorType=Usd.ValidationErrorType.None_, errorSites=[], errorMessage=""): + self.assertEqual(error.GetType(), errorType) + self.assertEqual(error.GetSites(), errorSites) + self.assertEqual(error.GetMessage(), errorMessage) + if errorType != Usd.ValidationErrorType.None_: + self.assertTrue(error.GetErrorAsString()) + self.assertFalse(error.HasNoError()) + else: + self.assertFalse(error.GetErrorAsString()) + self.assertTrue(error.HasNoError()) + + def test_create_default_validation_error(self): + validation_error = Usd.ValidationError() + self._verify_validation_error(validation_error) + + def test_create_validation_error_with_keyword_args(self): + errors = [ + { + "errorType": Usd.ValidationErrorType.None_, + "errorSites": [], + "errorMessage": "" + }, + { + "errorType": Usd.ValidationErrorType.Error, + "errorSites": [Usd.ValidationErrorSite()], + "errorMessage": "This is an error." + }, + { + "errorType": Usd.ValidationErrorType.Warn, + "errorSites": [Usd.ValidationErrorSite()], + "errorMessage": "This is a warning." + }, + { + "errorType": Usd.ValidationErrorType.Info, + "errorSites": [Usd.ValidationErrorSite(), Usd.ValidationErrorSite()], + "errorMessage": "This is an info." + }, + ] + + for error in errors: + with self.subTest(errorType=error["errorType"]): + validation_error = Usd.ValidationError(**error) + self._verify_validation_error(validation_error, **error) + + def test_create_validation_error_with_invalid_args(self): + errors = { + "Wrong Error Type": { + "errorType": "wrong_type", + "errorSites": [], + "errorMessage": "" + }, + "Wrong Sites Type": { + "errorType": Usd.ValidationErrorType.None_, + "errorSites": "wong_type", + "errorMessage": "" + }, + "Wrong Message Type": { + "errorType": Usd.ValidationErrorType.None_, + "errorSites": [], + "errorMessage": 123 + }, + } + + for error_category, error in errors.items(): + with self.subTest(errorType=error_category): + with self.assertRaises(Exception): + Usd.ValidationError(**error) + + +if __name__ == "__main__": + unittest.main() diff --git a/pxr/usd/usd/testenv/testUsdValidatorMetadata.py b/pxr/usd/usd/testenv/testUsdValidatorMetadata.py new file mode 100644 index 0000000000..e349705d18 --- /dev/null +++ b/pxr/usd/usd/testenv/testUsdValidatorMetadata.py @@ -0,0 +1,126 @@ +#!/pxrpythonsubst +# +# Copyright 2024 Pixar +# +# Licensed under the terms set forth in the LICENSE.txt file available at +# https://openusd.org/license. + +import unittest + +from pxr import Plug, Sdf, Usd + + +class TestUsdValidatorMetadata(unittest.TestCase): + def _verify_metadata( + self, + metadata: Usd.ValidatorMetadata, + name="", + doc="", + keywords=[], + schemaTypes=[], + plugin=None, + isSuite=False + ): + self.assertEqual(metadata.name, name) + self.assertEqual(metadata.doc, doc) + self.assertEqual(metadata.keywords, keywords) + self.assertEqual(metadata.schemaTypes, schemaTypes) + self.assertEqual(metadata.plugin, plugin) + self.assertEqual(metadata.isSuite, isSuite) + + def test_create_default_metadata(self): + metadata = Usd.ValidatorMetadata() + self._verify_metadata(metadata) + + def test_create_metadata_with_valid_keyword_args(self): + all_plugins = Plug.Registry().GetAllPlugins() + expected_plugin = all_plugins[0] if all_plugins else None + valid_metadatas = [ + { + "name": "empty_validator" + }, + { + "name": "validator1", + "doc": "This is a test validator.", + "keywords": ["validator1", "test"], + "schemaTypes": ["SomePrimType"], + "plugin": None, + "isSuite": False + }, + { + "name": "validator2", + "doc": "This is another test validator.", + "keywords": ["validator2", "test"], + "schemaTypes": ["NewPrimType"], + "plugin": expected_plugin, + "isSuite": False + } + ] + + for args in valid_metadatas: + with self.subTest(name=args["name"]): + metadata = Usd.ValidatorMetadata(**args) + self._verify_metadata(metadata, **args) + + def test_create_metadata_with_invalid_keyword_args(self): + invalid_metadatas = { + "Wrong Name Type": { + "name": 123 + }, + "Wrong Doc Type": { + "doc": 123 + }, + "Wrong Keywords Type": { + "keywords": 123 + }, + "Wrong Schema Types": { + "schemaTypes": 123 + }, + "Wrong Plugin Type": { + "plugin": 123 + }, + "Wrong IsSuite Type": { + "isSuite": "wrong type" + } + } + + for error_category, args in invalid_metadatas.items(): + with self.subTest(error_type=error_category): + with self.assertRaises(Exception): + Usd.ValidatorMetadata(**args) + + def test_metadata_name_immutable(self): + metadata = Usd.ValidatorMetadata() + with self.assertRaises(Exception): + metadata.name = "test" + + def test_metadata_doc_immutable(self): + metadata = Usd.ValidatorMetadata() + with self.assertRaises(Exception): + metadata.doc = "doc" + + def test_metadata_keywords_immutable(self): + metadata = Usd.ValidatorMetadata() + with self.assertRaises(Exception): + metadata.keywords = ["keywords"] + + def test_metadata_schemaTypes_immutable(self): + metadata = Usd.ValidatorMetadata() + with self.assertRaises(Exception): + metadata.schemaTypes = "PrimType1" + + def test_metadata_plugin_immutable(self): + all_plugins = Plug.Registry().GetAllPlugins() + expected_plugin = all_plugins[0] if all_plugins else None + metadata = Usd.ValidatorMetadata() + with self.assertRaises(Exception): + metadata.plugin = expected_plugin + + def test_metadata_is_suite_immutable(self): + metadata = Usd.ValidatorMetadata() + with self.assertRaises(Exception): + metadata.isSuite = True + + +if __name__ == "__main__": + unittest.main() diff --git a/pxr/usd/usd/validationError.cpp b/pxr/usd/usd/validationError.cpp index a3a78f19a7..9db883593d 100644 --- a/pxr/usd/usd/validationError.cpp +++ b/pxr/usd/usd/validationError.cpp @@ -5,10 +5,19 @@ // https://openusd.org/license. // +#include "pxr/base/tf/enum.h" #include "pxr/usd/usd/validationError.h" PXR_NAMESPACE_OPEN_SCOPE +TF_REGISTRY_FUNCTION(TfEnum) +{ + TF_ADD_ENUM_NAME(UsdValidationErrorType::None, "None"); + TF_ADD_ENUM_NAME(UsdValidationErrorType::Error, "Error"); + TF_ADD_ENUM_NAME(UsdValidationErrorType::Warn, "Warn"); + TF_ADD_ENUM_NAME(UsdValidationErrorType::Info, "Info"); +} + UsdValidationErrorSite::UsdValidationErrorSite( const SdfLayerHandle &layer, const SdfPath &objectPath) : _layer(layer), _objectPath(objectPath) @@ -39,24 +48,8 @@ UsdValidationError::UsdValidationError(const UsdValidationErrorType &type, std::string UsdValidationError::GetErrorAsString() const { - std::string errorTypeAsString; - switch(_errorType) { - case UsdValidationErrorType::None: - return _errorMsg; - break; - case UsdValidationErrorType::Error: - errorTypeAsString = "Error"; - break; - case UsdValidationErrorType::Warn: - errorTypeAsString = "Warn"; - break; - case UsdValidationErrorType::Info: - errorTypeAsString = "Info"; - break; - } - - const std::string separator = ": "; - return errorTypeAsString + separator + _errorMsg; + return _errorType == UsdValidationErrorType::None ? _errorMsg : TfStringPrintf( + "%s: %s", TfEnum::GetDisplayName(_errorType).c_str(), _errorMsg.c_str()); } void diff --git a/pxr/usd/usd/wrapValidationError.cpp b/pxr/usd/usd/wrapValidationError.cpp new file mode 100644 index 0000000000..792882f6ec --- /dev/null +++ b/pxr/usd/usd/wrapValidationError.cpp @@ -0,0 +1,54 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/pxr.h" +#include "pxr/usd/usd/validationError.h" +#include "pxr/usd/usd/validator.h" + +#include "pxr/base/tf/pyContainerConversions.h" +#include "pxr/base/tf/pyEnum.h" +#include "pxr/base/tf/pyPtrHelpers.h" +#include "pxr/base/tf/pyResultConversions.h" + +#include + +using namespace boost::python; + +PXR_NAMESPACE_USING_DIRECTIVE + +void wrapUsdValidationError() +{ + TfPyWrapEnum("ValidationErrorType"); + + class_("ValidationErrorSite", no_init) + .def(init<>()) + .def(init(args("layer", "objectPath"))) + .def(init((arg("stage"), arg("objectPath"), arg("layer") = SdfLayerHandle{}))) + .def("IsValid", &UsdValidationErrorSite::IsValid) + .def("IsValidSpecInLayer", &UsdValidationErrorSite::IsValidSpecInLayer) + .def("IsPrim", &UsdValidationErrorSite::IsPrim) + .def("IsProperty", &UsdValidationErrorSite::IsProperty) + .def("GetPropertySpec", &UsdValidationErrorSite::GetPropertySpec) + .def("GetPrimSpec", &UsdValidationErrorSite::GetPrimSpec) + .def("GetLayer", &UsdValidationErrorSite::GetLayer, return_value_policy()) + .def("GetStage", &UsdValidationErrorSite::GetStage, return_value_policy()) + .def("GetPrim", &UsdValidationErrorSite::GetPrim) + .def("GetProperty", &UsdValidationErrorSite::GetProperty) + .def(self == self) + .def(self != self); + + TfPyRegisterStlSequencesFromPython(); + class_("ValidationError", no_init) + .def(init<>()) + .def(init(args("errorType", "errorSites", "errorMessage"))) + .def(self == self) + .def(self != self) + .def("GetType", &UsdValidationError::GetType) + .def("GetSites", make_function(&UsdValidationError::GetSites, return_value_policy())) + .def("GetMessage", &UsdValidationError::GetMessage, return_value_policy()) + .def("GetErrorAsString", &UsdValidationError::GetErrorAsString) + .def("HasNoError", &UsdValidationError::HasNoError); +} diff --git a/pxr/usd/usd/wrapValidator.cpp b/pxr/usd/usd/wrapValidator.cpp new file mode 100644 index 0000000000..8080aaf0de --- /dev/null +++ b/pxr/usd/usd/wrapValidator.cpp @@ -0,0 +1,60 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/pxr.h" +#include "pxr/usd/usd/validator.h" + +#include "pxr/base/tf/pyContainerConversions.h" +#include "pxr/base/tf/pyPtrHelpers.h" +#include "pxr/base/tf/pyResultConversions.h" + +#include +#include +#include +#include + +using namespace boost::python; + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace +{ + + UsdValidatorMetadata * + _NewMetadata( + const TfToken &name, + const PlugPluginPtr &plugin, + const TfTokenVector &keywords, + const TfToken &doc, + const TfTokenVector &schemaTypes, + bool isSuite) + { + return new UsdValidatorMetadata{name, plugin, keywords, doc, schemaTypes, isSuite}; + } + +} // anonymous namespace + +void wrapUsdValidator() +{ + class_("ValidatorMetadata", no_init) + .def("__init__", make_constructor(&_NewMetadata, default_call_policies(), + (arg("name") = TfToken(), + arg("plugin") = PlugPluginPtr(), + arg("keywords") = TfTokenVector(), + arg("doc") = TfToken(), + arg("schemaTypes") = TfTokenVector(), + arg("isSuite") = false))) + .add_property("name", make_getter( + &UsdValidatorMetadata::name, return_value_policy())) + .add_property("plugin", make_getter( + &UsdValidatorMetadata::pluginPtr, return_value_policy())) + .add_property("keywords", make_getter( + &UsdValidatorMetadata::keywords, return_value_policy())) + .def_readonly("doc", &UsdValidatorMetadata::doc) + .add_property("schemaTypes", make_getter( + &UsdValidatorMetadata::schemaTypes, return_value_policy())) + .def_readonly("isSuite", &UsdValidatorMetadata::isSuite); +}