Repository: vmware/govmomi
Pull request #3544 (2024-09-06 16:53:52)

api: Fault helpers
## Description

This patch adds support for the new, top-level `fault` package with helper functions such as `As`, `In`, and `Is` for testing and traversing Golang `error` types and VMware `fault` types.

Closes: `NA`

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

With the command:

```shell
go test -v -count 1 -cover ./fault
```

The results shows there is 100% code coverage:

```shell
=== RUN   TestAs
=== RUN   TestAs/err_is_nil
=== PAUSE TestAs/err_is_nil
=== RUN   TestAs/err_is_not_supported
=== PAUSE TestAs/err_is_not_supported
=== RUN   TestAs/target_is_nil
=== PAUSE TestAs/target_is_nil
=== RUN   TestAs/target_is_not_pointer
=== PAUSE TestAs/target_is_not_pointer
=== RUN   TestAs/target_is_not_pointer_to_expected_type
=== PAUSE TestAs/target_is_not_pointer_to_expected_type
=== RUN   TestAs/err_is_task.Error_with_fault
=== PAUSE TestAs/err_is_task.Error_with_fault
=== RUN   TestAs/err_unwraps_to_nil_error
=== PAUSE TestAs/err_unwraps_to_nil_error
=== RUN   TestAs/err_unwraps_to_nil_error_slice
=== PAUSE TestAs/err_unwraps_to_nil_error_slice
=== RUN   TestAs/err_is_wrapped_task.Error_with_fault
=== PAUSE TestAs/err_is_wrapped_task.Error_with_fault
=== RUN   TestAs/err_is_wrapped_nil_error_and_task.Error_with_fault
=== PAUSE TestAs/err_is_wrapped_nil_error_and_task.Error_with_fault
=== RUN   TestAs/err_is_types.BaseMethodFault
=== PAUSE TestAs/err_is_types.BaseMethodFault
=== RUN   TestAs/err_is_types.BaseMethodFault_that_returns_nil
=== PAUSE TestAs/err_is_types.BaseMethodFault_that_returns_nil
=== RUN   TestAs/err_is_asFaulter_that_returns_true_and_fault
=== PAUSE TestAs/err_is_asFaulter_that_returns_true_and_fault
=== RUN   TestAs/err_is_asFaulter_that_returns_false
=== PAUSE TestAs/err_is_asFaulter_that_returns_false
=== RUN   TestAs/err_is_*types.LocalizedMethodFault
=== PAUSE TestAs/err_is_*types.LocalizedMethodFault
=== RUN   TestAs/err_is_*types.LocalizedMethodFault_w_nil_fault
=== PAUSE TestAs/err_is_*types.LocalizedMethodFault_w_nil_fault
=== RUN   TestAs/err_is_task.Error_with_nested_fault
=== PAUSE TestAs/err_is_task.Error_with_nested_fault
=== CONT  TestAs/err_is_nil
=== CONT  TestAs/err_is_task.Error_with_nested_fault
=== CONT  TestAs/err_unwraps_to_nil_error_slice
=== CONT  TestAs/err_unwraps_to_nil_error
=== CONT  TestAs/err_is_wrapped_task.Error_with_fault
=== CONT  TestAs/err_is_task.Error_with_fault
=== CONT  TestAs/err_is_asFaulter_that_returns_false
=== CONT  TestAs/err_is_types.BaseMethodFault_that_returns_nil
=== CONT  TestAs/err_is_wrapped_nil_error_and_task.Error_with_fault
=== CONT  TestAs/err_is_types.BaseMethodFault
=== CONT  TestAs/target_is_not_pointer_to_expected_type
=== CONT  TestAs/target_is_not_pointer
=== CONT  TestAs/target_is_nil
=== CONT  TestAs/err_is_not_supported
=== CONT  TestAs/err_is_*types.LocalizedMethodFault_w_nil_fault
=== CONT  TestAs/err_is_*types.LocalizedMethodFault
=== CONT  TestAs/err_is_asFaulter_that_returns_true_and_fault
--- PASS: TestAs (0.00s)
    --- PASS: TestAs/err_is_nil (0.00s)
    --- PASS: TestAs/err_unwraps_to_nil_error_slice (0.00s)
    --- PASS: TestAs/err_unwraps_to_nil_error (0.00s)
    --- PASS: TestAs/err_is_task.Error_with_nested_fault (0.00s)
    --- PASS: TestAs/err_is_asFaulter_that_returns_false (0.00s)
    --- PASS: TestAs/err_is_types.BaseMethodFault (0.00s)
    --- PASS: TestAs/err_is_wrapped_task.Error_with_fault (0.00s)
    --- PASS: TestAs/err_is_task.Error_with_fault (0.00s)
    --- PASS: TestAs/err_is_types.BaseMethodFault_that_returns_nil (0.00s)
    --- PASS: TestAs/err_is_wrapped_nil_error_and_task.Error_with_fault (0.00s)
    --- PASS: TestAs/err_is_not_supported (0.00s)
    --- PASS: TestAs/err_is_*types.LocalizedMethodFault_w_nil_fault (0.00s)
    --- PASS: TestAs/err_is_*types.LocalizedMethodFault (0.00s)
    --- PASS: TestAs/err_is_asFaulter_that_returns_true_and_fault (0.00s)
    --- PASS: TestAs/target_is_not_pointer_to_expected_type (0.00s)
    --- PASS: TestAs/target_is_nil (0.00s)
    --- PASS: TestAs/target_is_not_pointer (0.00s)
=== RUN   TestIn
=== RUN   TestIn/err_is_nil
=== PAUSE TestIn/err_is_nil
=== RUN   TestIn/callback_is_nil
=== PAUSE TestIn/callback_is_nil
=== RUN   TestIn/err_is_unsupported
=== PAUSE TestIn/err_is_unsupported
=== RUN   TestIn/error_is_task.Error
=== PAUSE TestIn/error_is_task.Error
=== RUN   TestIn/error_is_task.Error_with_localized_message_and_fault
=== PAUSE TestIn/error_is_task.Error_with_localized_message_and_fault
=== RUN   TestIn/error_is_types.SystemError
=== PAUSE TestIn/error_is_types.SystemError
=== RUN   TestIn/error_is_task.Error_with_a_localized_message_but_no_fault
=== PAUSE TestIn/error_is_task.Error_with_a_localized_message_but_no_fault
=== RUN   TestIn/error_is_multiple,_wrapped_errors
=== PAUSE TestIn/error_is_multiple,_wrapped_errors
=== RUN   TestIn/error_has_nested_task.Error_with_localized_message_and_fault
=== PAUSE TestIn/error_has_nested_task.Error_with_localized_message_and_fault
=== RUN   TestIn/error_is_task.Error_with_localized_message_and_fault_with_localizable_messages
=== PAUSE TestIn/error_is_task.Error_with_localized_message_and_fault_with_localizable_messages
=== RUN   TestIn/error_is_task.Error_with_nested_faults
=== PAUSE TestIn/error_is_task.Error_with_nested_faults
=== RUN   TestIn/error_is_task.Error_with_nested_faults_but_returns_after_single_fault
=== PAUSE TestIn/error_is_task.Error_with_nested_faults_but_returns_after_single_fault
=== CONT  TestIn/err_is_nil
=== CONT  TestIn/error_is_task.Error_with_a_localized_message_but_no_fault
=== CONT  TestIn/error_has_nested_task.Error_with_localized_message_and_fault
=== CONT  TestIn/error_is_task.Error_with_localized_message_and_fault_with_localizable_messages
=== CONT  TestIn/error_is_types.SystemError
=== CONT  TestIn/error_is_task.Error_with_localized_message_and_fault
=== CONT  TestIn/error_is_task.Error
=== CONT  TestIn/err_is_unsupported
=== CONT  TestIn/callback_is_nil
=== CONT  TestIn/error_is_task.Error_with_nested_faults
=== CONT  TestIn/error_is_multiple,_wrapped_errors
=== CONT  TestIn/error_is_task.Error_with_nested_faults_but_returns_after_single_fault
--- PASS: TestIn (0.00s)
    --- PASS: TestIn/error_is_task.Error_with_a_localized_message_but_no_fault (0.00s)
    --- PASS: TestIn/err_is_nil (0.00s)
    --- PASS: TestIn/error_has_nested_task.Error_with_localized_message_and_fault (0.00s)
    --- PASS: TestIn/error_is_types.SystemError (0.00s)
    --- PASS: TestIn/error_is_task.Error_with_localized_message_and_fault_with_localizable_messages (0.00s)
    --- PASS: TestIn/error_is_task.Error_with_localized_message_and_fault (0.00s)
    --- PASS: TestIn/error_is_task.Error (0.00s)
    --- PASS: TestIn/err_is_unsupported (0.00s)
    --- PASS: TestIn/callback_is_nil (0.00s)
    --- PASS: TestIn/error_is_task.Error_with_nested_faults (0.00s)
    --- PASS: TestIn/error_is_multiple,_wrapped_errors (0.00s)
    --- PASS: TestIn/error_is_task.Error_with_nested_faults_but_returns_after_single_fault (0.00s)
=== RUN   TestIs
=== RUN   TestIs/err_is_nil
=== PAUSE TestIs/err_is_nil
=== RUN   TestIs/target_is_nil
=== PAUSE TestIs/target_is_nil
=== RUN   TestIs/target_and_error_are_nil
=== PAUSE TestIs/target_and_error_are_nil
=== RUN   TestIs/err_is_not_supported
=== PAUSE TestIs/err_is_not_supported
=== RUN   TestIs/err_and_target_are_same_value_type
=== PAUSE TestIs/err_and_target_are_same_value_type
=== RUN   TestIs/target_implements_IsFault
=== PAUSE TestIs/target_implements_IsFault
=== RUN   TestIs/err_is_*LocalizedMethodFault_with_nil_fault
=== PAUSE TestIs/err_is_*LocalizedMethodFault_with_nil_fault
=== RUN   TestIs/err_is_*LocalizedMethodFault_with_SystemError_fault
=== PAUSE TestIs/err_is_*LocalizedMethodFault_with_SystemError_fault
=== RUN   TestIs/err_is_*LocalizedMethodFault_with_InvalidVmConfig_fault
=== PAUSE TestIs/err_is_*LocalizedMethodFault_with_InvalidVmConfig_fault
=== RUN   TestIs/err_is_task.Error_with_nested_InvalidVmConfig_fault
=== PAUSE TestIs/err_is_task.Error_with_nested_InvalidVmConfig_fault
=== RUN   TestIs/err_is_*LocalizedMethodFault_with_nested_InvalidVmConfig_fault
=== PAUSE TestIs/err_is_*LocalizedMethodFault_with_nested_InvalidVmConfig_fault
=== RUN   TestIs/err_is_*LocalizedMethodFault_with_nested_nil_fault
=== PAUSE TestIs/err_is_*LocalizedMethodFault_with_nested_nil_fault
=== RUN   TestIs/err_is_wrapped_task.Error_with_nested_InvalidVmConfig_fault
=== PAUSE TestIs/err_is_wrapped_task.Error_with_nested_InvalidVmConfig_fault
=== RUN   TestIs/err_is_wrapped_nil_error
=== PAUSE TestIs/err_is_wrapped_nil_error
=== RUN   TestIs/err_is_wrapped_error_slice_with_expected_value
=== PAUSE TestIs/err_is_wrapped_error_slice_with_expected_value
=== RUN   TestIs/err_is_wrapped_error_slice_sans_expected_value
=== PAUSE TestIs/err_is_wrapped_error_slice_sans_expected_value
=== CONT  TestIs/err_is_nil
=== CONT  TestIs/err_is_*LocalizedMethodFault_with_InvalidVmConfig_fault
=== CONT  TestIs/err_is_wrapped_task.Error_with_nested_InvalidVmConfig_fault
=== CONT  TestIs/err_is_*LocalizedMethodFault_with_nested_InvalidVmConfig_fault
=== CONT  TestIs/err_is_*LocalizedMethodFault_with_SystemError_fault
=== CONT  TestIs/err_is_task.Error_with_nested_InvalidVmConfig_fault
=== CONT  TestIs/err_is_*LocalizedMethodFault_with_nested_nil_fault
=== CONT  TestIs/err_is_wrapped_error_slice_sans_expected_value
=== CONT  TestIs/err_is_wrapped_error_slice_with_expected_value
=== CONT  TestIs/err_is_*LocalizedMethodFault_with_nil_fault
=== CONT  TestIs/target_implements_IsFault
=== CONT  TestIs/err_is_not_supported
=== CONT  TestIs/target_and_error_are_nil
=== CONT  TestIs/target_is_nil
=== CONT  TestIs/err_is_wrapped_nil_error
=== CONT  TestIs/err_and_target_are_same_value_type
--- PASS: TestIs (0.00s)
    --- PASS: TestIs/err_is_nil (0.00s)
    --- PASS: TestIs/err_is_*LocalizedMethodFault_with_InvalidVmConfig_fault (0.00s)
    --- PASS: TestIs/err_is_*LocalizedMethodFault_with_SystemError_fault (0.00s)
    --- PASS: TestIs/err_is_*LocalizedMethodFault_with_nested_InvalidVmConfig_fault (0.00s)
    --- PASS: TestIs/err_is_*LocalizedMethodFault_with_nested_nil_fault (0.00s)
    --- PASS: TestIs/err_is_wrapped_task.Error_with_nested_InvalidVmConfig_fault (0.00s)
    --- PASS: TestIs/err_is_task.Error_with_nested_InvalidVmConfig_fault (0.00s)
    --- PASS: TestIs/err_is_wrapped_error_slice_sans_expected_value (0.00s)
    --- PASS: TestIs/err_is_wrapped_error_slice_with_expected_value (0.00s)
    --- PASS: TestIs/err_is_*LocalizedMethodFault_with_nil_fault (0.00s)
    --- PASS: TestIs/target_implements_IsFault (0.00s)
    --- PASS: TestIs/err_is_not_supported (0.00s)
    --- PASS: TestIs/target_and_error_are_nil (0.00s)
    --- PASS: TestIs/target_is_nil (0.00s)
    --- PASS: TestIs/err_is_wrapped_nil_error (0.00s)
    --- PASS: TestIs/err_and_target_are_same_value_type (0.00s)
=== RUN   ExampleAs_faultByAddress
--- PASS: ExampleAs_faultByAddress (0.00s)
=== RUN   ExampleAs_faultByValue
--- PASS: ExampleAs_faultByValue (0.00s)
=== RUN   ExampleIs_baseMethodFault
--- PASS: ExampleIs_baseMethodFault (0.00s)
=== RUN   ExampleIs_nestedFault
--- PASS: ExampleIs_nestedFault (0.00s)
=== RUN   ExampleIn_printAllTypeNamesAndMessages
--- PASS: ExampleIn_printAllTypeNamesAndMessages (0.00s)
=== RUN   ExampleIn_printFirstDiscoveredTypeNameAndMessage
--- PASS: ExampleIn_printFirstDiscoveredTypeNameAndMessage (0.00s)
PASS
coverage: 100.0% of statements
ok  	github.com/vmware/govmomi/fault	0.303s	coverage: 100.0% of statements
```

## Checklist:

- [x] My code follows the `CONTRIBUTION` [guidelines] of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged

[guidelines]: https://github.com/vmware/govmomi/blob/main/CONTRIBUTING.md


akutz: @dougm I will add an `As` function before we merge this.
dougm: Noting related issues on this topic:
https://github.com/vmware/govmomi/issues/2048
https://github.com/vmware/govmomi/issues/2685

Personally am still in favor of just adding `fault.Is` and `fault.As`, rather than adding generated methods for all fault types to support `errors.Is` and `errors.As`. Though maybe there's a simpler way, been a while since I looked.
akutz: > Sweet, thanks @akutz I was trying it out before I saw your comment about adding `As` after merge. Looking forward to that!
> 
> ```go
> if fault.Is(err, &types.InvalidPowerState{}) {
> 
> }
> ```
> 
> ```go
> func Is(err error, target types.BaseMethodFault) bool {
> 	kind := reflect.TypeOf(target).Elem()
> 	is := false
> 	InError(err, func(fault types.BaseMethodFault, _ string, _ []types.LocalizableMessage) bool {
> 		source := reflect.TypeOf(fault).Elem()
> 		if kind == source {
> 			is = true
> 		}
> 		return is
> 	})
> 	return is
> }
> ```

I just added the `As` function.
akutz: @dougm Please note, I did not add `Is` yet because the question is what we match. Should it be just the fault type? Should it be the fault data? Should the fault data include all of the localized message properties? I think we should merge _this_ PR and then add `fault.Is` later after we think more about what it should be. Thoughts?
dougm: > Because otherwise, `Is` just becomes a wrapper for `As`. The stdlib [`errors.Is`](https://pkg.go.dev/errors#Is) just compares the type, so if we want to do the same, I can do that real quick. Thoughts?

Yes, that's what I was thinking. A few examples I'd like to replace with `fault.Is`:
https://github.com/vmware/govmomi/blob/d3cb5c6df6cc08f044ce65fe2992026e8fc7b632/govc/datastore/ls.go#L77-L86

https://github.com/vmware/govmomi/blob/d3cb5c6df6cc08f044ce65fe2992026e8fc7b632/govc/vm/power.go#L109-L118

https://github.com/vmware/govmomi/blob/d3cb5c6df6cc08f044ce65fe2992026e8fc7b632/govc/vm/upgrade.go#L39-L46

There's more of those, and I can take care of replacing them, just pointing out some use cases.

akutz: Okay @dougm , go nuts! There are now just three functions: `As`, `In`, and `Is`. Please read the GoDoc, tests, and examples for `As` carefully. I constructed both `As` and `Is` to behave exactly as their stdlib counterparts, and this means that `As` accepts a target type of `**types.SystemError` (as an example) instead of `*types.SystemError`. This is because you must send in the address of an object that is the type that implements `types.BaseMethodFault`. Since `*types.SystemError` implements `types.BaseMethodFault`, it is necessary to send in `**types.SystemError` as the target.

There are Golang-style examples showing both.
akutz: @dougm You will love it even more -- I just updated the PR to support any types that implement `Fault() types.BaseMethodFault`, which now includes errors wrapped with `soap.ToSoapFault` (if there is an internal VIM error) and `soap.ToVimFault`.

Patch:
diff --git a/fault/fault.go b/fault/fault.go
new file mode 100644
index 00000000..e16478ee
--- /dev/null
+++ b/fault/fault.go
@@ -0,0 +1,315 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fault
+
+import (
+	"reflect"
+
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+// As finds the first fault in the error's tree that matches target, and if one
+// is found, sets the target to that fault value and returns the fault's
+// localized message and true. Otherwise, false is returned.
+//
+// The tree is inspected according to the object type. If the object implements
+// Golang's error interface, the Unwrap() error or Unwrap() []error methods are
+// repeatedly checked for additional errors. If the object implements GoVmomi's
+// BaseMethodFault or HasLocalizedMethodFault interfaces, the object is checked
+// for an underlying FaultCause. When err wraps multiple errors or faults, err
+// is examined followed by a depth-first traversal of its children.
+//
+// An error matches target if the error's concrete value is assignable to the
+// value pointed to by target, or if the error has a method
+// AsFault(BaseMethodFault) (string, bool) such that AsFault(BaseMethodFault)
+// returns true. In the latter case, the AsFault method is responsible for
+// setting target.
+//
+// An error type might provide an AsFault method so it can be treated as if it
+// were a different error type.
+//
+// This function panics if err does not implement error, types.BaseMethodFault,
+// types.HasLocalizedMethodFault, Fault() types.BaseMethodFault, or if target is
+// not a pointer.
+func As(err, target any) (localizedMessage string, okay bool) {
+	if err == nil {
+		return
+	}
+	if target == nil {
+		panic("fault: target cannot be nil")
+	}
+	val := reflect.ValueOf(target)
+	typ := val.Type()
+	if typ.Kind() != reflect.Ptr || val.IsNil() {
+		panic("fault: target must be a non-nil pointer")
+	}
+	targetType := typ.Elem()
+	if targetType.Kind() != reflect.Interface &&
+		!targetType.Implements(baseMethodFaultType) {
+		panic("fault: *target must be interface or implement BaseMethodFault")
+	}
+	if !as(err, target, val, targetType, &localizedMessage) {
+		return "", false
+	}
+	return localizedMessage, true
+}
+
+func as(
+	err,
+	target any,
+	targetVal reflect.Value,
+	targetType reflect.Type,
+	localizedMsg *string) bool {
+
+	for {
+		if reflect.TypeOf(err).AssignableTo(targetType) {
+			targetVal.Elem().Set(reflect.ValueOf(err))
+			return true
+		}
+		if tErr, ok := err.(hasAsFault); ok {
+			if msg, ok := tErr.AsFault(target); ok {
+				*localizedMsg = msg
+				return true
+			}
+			return false
+		}
+		switch tErr := err.(type) {
+		case types.HasLocalizedMethodFault:
+			if fault := tErr.GetLocalizedMethodFault(); fault != nil {
+				*localizedMsg = fault.LocalizedMessage
+				if fault.Fault != nil {
+					return as(
+						fault.Fault,
+						target,
+						targetVal,
+						targetType,
+						localizedMsg)
+				}
+			}
+			return false
+		case types.BaseMethodFault:
+			if fault := tErr.GetMethodFault(); fault != nil {
+				if fault.FaultCause != nil {
+					*localizedMsg = fault.FaultCause.LocalizedMessage
+					return as(
+						fault.FaultCause,
+						target,
+						targetVal,
+						targetType,
+						localizedMsg)
+				}
+			}
+			return false
+		case hasFault:
+			if fault := tErr.Fault(); fault != nil {
+				return as(fault, target, targetVal, targetType, localizedMsg)
+			}
+			return false
+		case unwrappableError:
+			if err = tErr.Unwrap(); err == nil {
+				return false
+			}
+		case unwrappableErrorSlice:
+			for _, err := range tErr.Unwrap() {
+				if err == nil {
+					continue
+				}
+				return as(err, target, targetVal, targetType, localizedMsg)
+			}
+			return false
+		default:
+			return false
+		}
+	}
+}
+
+// Is reports whether any fault in err's tree matches target.
+//
+// The tree is inspected according to the object type. If the object implements
+// Golang's error interface, the Unwrap() error or Unwrap() []error methods are
+// repeatedly checked for additional errors. If the object implements GoVmomi's
+// BaseMethodFault or HasLocalizedMethodFault interfaces, the object is checked
+// for an underlying FaultCause. When err wraps multiple errors or faults, err
+// is examined followed by a depth-first traversal of its children.
+//
+// An error is considered to match a target if it is equal to that target or if
+// it implements a method IsFault(BaseMethodFault) bool such that
+// IsFault(BaseMethodFault) returns true.
+//
+// An error type might provide an IsFault method so it can be treated as
+// equivalent to an existing fault. For example, if MyFault defines:
+//
+//	func (m MyFault) IsFault(target BaseMethodFault) bool {
+//		return target == &types.NotSupported{}
+//	}
+//
+// then IsFault(MyError{}, &types.NotSupported{}) returns true. An IsFault
+// method should only shallowly compare err and the target and not unwrap
+// either.
+func Is(err any, target types.BaseMethodFault) bool {
+	if target == nil {
+		return err == target
+	}
+	isComparable := reflect.TypeOf(target).Comparable()
+	return is(err, target, isComparable)
+}
+
+func is(err any, target types.BaseMethodFault, targetComparable bool) bool {
+	for {
+		if targetComparable && err == target {
+			return true
+		}
+		if tErr, ok := err.(hasIsFault); ok && tErr.IsFault(target) {
+			return true
+		}
+		switch tErr := err.(type) {
+		case types.HasLocalizedMethodFault:
+			fault := tErr.GetLocalizedMethodFault()
+			if fault == nil {
+				return false
+			}
+			err = fault.Fault
+		case types.BaseMethodFault:
+			if reflect.ValueOf(err).Type() == reflect.ValueOf(target).Type() {
+				return true
+			}
+			fault := tErr.GetMethodFault()
+			if fault == nil {
+				return false
+			}
+			err = fault.FaultCause
+		case hasFault:
+			if err = tErr.Fault(); err == nil {
+				return false
+			}
+		case unwrappableError:
+			if err = tErr.Unwrap(); err == nil {
+				return false
+			}
+		case unwrappableErrorSlice:
+			for _, err := range tErr.Unwrap() {
+				if is(err, target, targetComparable) {
+					return true
+				}
+			}
+			return false
+		default:
+			return false
+		}
+	}
+}
+
+// OnFaultFn is called for every fault encountered when inspecting an error
+// or fault for a fault tree. The In function returns when the entire tree is
+// inspected or the OnFaultFn returns true.
+type OnFaultFn func(
+	fault types.BaseMethodFault,
+	localizedMessage string,
+	localizableMessages []types.LocalizableMessage) bool
+
+// In invokes onFaultFn for each fault in err's tree.
+//
+// The tree is inspected according to the object type. If the object implements
+// Golang's error interface, the Unwrap() error or Unwrap() []error methods are
+// repeatedly checked for additional errors. If the object implements GoVmomi's
+// BaseMethodFault or HasLocalizedMethodFault interfaces, the object is checked
+// for an underlying FaultCause. When err wraps multiple errors or faults, err
+// is examined followed by a depth-first traversal of its children.
+//
+// This function panics if err does not implement error, types.BaseMethodFault,
+// types.HasLocalizedMethodFault, Fault() types.BaseMethodFault, or if onFaultFn
+// is nil.
+func In(err any, onFaultFn OnFaultFn) {
+	if onFaultFn == nil {
+		panic("fault: onFaultFn must not be nil")
+	}
+	switch tErr := err.(type) {
+	case types.HasLocalizedMethodFault:
+		inFault(tErr.GetLocalizedMethodFault(), onFaultFn)
+	case types.BaseMethodFault:
+		inFault(&types.LocalizedMethodFault{Fault: tErr}, onFaultFn)
+	case hasFault:
+		if fault := tErr.Fault(); fault != nil {
+			inFault(&types.LocalizedMethodFault{Fault: fault}, onFaultFn)
+		}
+	case unwrappableError:
+		In(tErr.Unwrap(), onFaultFn)
+	case unwrappableErrorSlice:
+		for _, uErr := range tErr.Unwrap() {
+			if uErr == nil {
+				continue
+			}
+			In(uErr, onFaultFn)
+		}
+	default:
+		panic("fault: err must implement error, types.BaseMethodFault, or " +
+			"types.HasLocalizedMethodFault")
+	}
+}
+
+func inFault(
+	localizedMethodFault *types.LocalizedMethodFault,
+	onFaultFn OnFaultFn) {
+
+	if localizedMethodFault == nil {
+		return
+	}
+
+	fault := localizedMethodFault.Fault
+	if fault == nil {
+		return
+	}
+
+	var (
+		faultCause    *types.LocalizedMethodFault
+		faultMessages []types.LocalizableMessage
+	)
+
+	if methodFault := fault.GetMethodFault(); methodFault != nil {
+		faultCause = methodFault.FaultCause
+		faultMessages = methodFault.FaultMessage
+	}
+
+	if onFaultFn(fault, localizedMethodFault.LocalizedMessage, faultMessages) {
+		return
+	}
+
+	// Check the fault's children.
+	inFault(faultCause, onFaultFn)
+}
+
+type hasFault interface {
+	Fault() types.BaseMethodFault
+}
+
+type hasAsFault interface {
+	AsFault(target any) (string, bool)
+}
+
+type hasIsFault interface {
+	IsFault(target types.BaseMethodFault) bool
+}
+
+type unwrappableError interface {
+	Unwrap() error
+}
+
+type unwrappableErrorSlice interface {
+	Unwrap() []error
+}
+
+var baseMethodFaultType = reflect.TypeOf((*types.BaseMethodFault)(nil)).Elem()
diff --git a/simulator/cluster_compute_resource.go b/simulator/cluster_compute_resource.go
index 01c39362..bdac24a0 100644
--- a/simulator/cluster_compute_resource.go
+++ b/simulator/cluster_compute_resource.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/google/uuid"
 
-	"github.com/vmware/govmomi/object"
 	"github.com/vmware/govmomi/simulator/esx"
 	"github.com/vmware/govmomi/vim25/methods"
 	"github.com/vmware/govmomi/vim25/mo"
@@ -456,9 +455,10 @@ func (c *ClusterComputeResource) PlaceVm(ctx *Context, req *types.PlaceVm) soap.
 	case types.PlacementSpecPlacementTypeReconfigure:
 		// Validate input.
 		if req.PlacementSpec.ConfigSpec == nil {
-			body.Fault_ = Fault("", &types.InvalidArgument{
+			invalidArg := &types.InvalidArgument{
 				InvalidProperty: "PlacementSpec.configSpec",
-			})
+			}
+			body.Fault_ = Fault("", invalidArg)
 			return body
 		}
 
@@ -475,24 +475,6 @@ func (c *ClusterComputeResource) PlaceVm(ctx *Context, req *types.PlaceVm) soap.
 			TargetHost:   spec.Host,
 			RelocateSpec: spec,
 		})
-	case types.PlacementSpecPlacementTypeRelocate:
-		// Validate fields of req.PlacementSpec, if explicitly provided.
-		if !validatePlacementSpecForPlaceVmRelocate(ctx, req, body) {
-			return body
-		}
-
-		// After validating req.PlacementSpec, we must have a valid req.PlacementSpec.Vm.
-		vmObj := ctx.Map.Get(*req.PlacementSpec.Vm).(*VirtualMachine)
-
-		// Populate RelocateSpec's common fields, if not explicitly provided.
-		populateRelocateSpecForPlaceVmRelocate(&req.PlacementSpec.RelocateSpec, vmObj)
-
-		// Update PlacementResult.
-		res.Action = append(res.Action, &types.PlacementAction{
-			Vm:           req.PlacementSpec.Vm,
-			TargetHost:   req.PlacementSpec.RelocateSpec.Host,
-			RelocateSpec: req.PlacementSpec.RelocateSpec,
-		})
 	default:
 		log.Printf("unsupported placement type: %s", req.PlacementSpec.PlacementType)
 		body.Fault_ = Fault("", new(types.NotSupported))
@@ -508,134 +490,6 @@ func (c *ClusterComputeResource) PlaceVm(ctx *Context, req *types.PlaceVm) soap.
 	return body
 }
 
-// validatePlacementSpecForPlaceVmRelocate validates the fields of req.PlacementSpec for a relocate placement type.
-// Returns true if the fields are valid, false otherwise.
-func validatePlacementSpecForPlaceVmRelocate(ctx *Context, req *types.PlaceVm, body *methods.PlaceVmBody) bool {
-	if req.PlacementSpec.Vm == nil {
-		body.Fault_ = Fault("", &types.InvalidArgument{
-			InvalidProperty: "PlacementSpec",
-		})
-		return false
-	}
-
-	// Oddly when the VM is not found, PlaceVm complains about configSpec being invalid, despite this being
-	// a relocate placement type. Possibly due to treating the missing VM as a create placement type
-	// internally, which requires the configSpec to be present.
-	vmObj, exist := ctx.Map.Get(*req.PlacementSpec.Vm).(*VirtualMachine)
-	if !exist {
-		body.Fault_ = Fault("", &types.InvalidArgument{
-			InvalidProperty: "PlacementSpec.configSpec",
-		})
-		return false
-	}
-
-	return validateRelocateSpecForPlaceVmRelocate(ctx, req.PlacementSpec.RelocateSpec, body, vmObj)
-}
-
-// validateRelocateSpecForPlaceVmRelocate validates the fields of req.PlacementSpec.RelocateSpec for a relocate
-// placement type. Returns true if the fields are valid, false otherwise.
-func validateRelocateSpecForPlaceVmRelocate(ctx *Context, spec *types.VirtualMachineRelocateSpec, body *methods.PlaceVmBody, vmObj *VirtualMachine) bool {
-	if spec == nil {
-		// An empty relocate spec is valid, as it will be populated with default values.
-		return true
-	}
-
-	if spec.Host != nil {
-		if _, exist := ctx.Map.Get(*spec.Host).(*HostSystem); !exist {
-			body.Fault_ = Fault("", &types.ManagedObjectNotFound{
-				Obj: *spec.Host,
-			})
-			return false
-		}
-	}
-
-	if spec.Datastore != nil {
-		if _, exist := ctx.Map.Get(*spec.Datastore).(*Datastore); !exist {
-			body.Fault_ = Fault("", &types.ManagedObjectNotFound{
-				Obj: *spec.Datastore,
-			})
-			return false
-		}
-	}
-
-	if spec.Pool != nil {
-		if _, exist := ctx.Map.Get(*spec.Pool).(*ResourcePool); !exist {
-			body.Fault_ = Fault("", &types.ManagedObjectNotFound{
-				Obj: *spec.Pool,
-			})
-			return false
-		}
-	}
-
-	if spec.Disk != nil {
-		deviceList := object.VirtualDeviceList(vmObj.Config.Hardware.Device)
-		vdiskList := deviceList.SelectByType(&types.VirtualDisk{})
-		for _, disk := range spec.Disk {
-			var diskFound bool
-			for _, vdisk := range vdiskList {
-				if disk.DiskId == vdisk.GetVirtualDevice().Key {
-					diskFound = true
-					break
-				}
-			}
-			if !diskFound {
-				body.Fault_ = Fault("", &types.InvalidArgument{
-					InvalidProperty: "PlacementSpec.vm",
-				})
-				return false
-			}
-
-			// Unlike a non-existing spec.Datastore that throws ManagedObjectNotFound, a non-existing disk.Datastore
-			// throws InvalidArgument.
-			if _, exist := ctx.Map.Get(disk.Datastore).(*Datastore); !exist {
-				body.Fault_ = Fault("", &types.InvalidArgument{
-					InvalidProperty: "RelocateSpec",
-				})
-				return false
-			}
-		}
-	}
-
-	return true
-}
-
-// populateRelocateSpecForPlaceVmRelocate populates the fields of req.PlacementSpec.RelocateSpec for a relocate
-// placement type, if not explicitly provided.
-func populateRelocateSpecForPlaceVmRelocate(specPtr **types.VirtualMachineRelocateSpec, vmObj *VirtualMachine) {
-	if *specPtr == nil {
-		*specPtr = &types.VirtualMachineRelocateSpec{}
-	}
-
-	spec := *specPtr
-
-	if spec.DiskMoveType == "" {
-		spec.DiskMoveType = string(types.VirtualMachineRelocateDiskMoveOptionsMoveAllDiskBackingsAndDisallowSharing)
-	}
-
-	if spec.Datastore == nil {
-		spec.Datastore = &vmObj.Datastore[0]
-	}
-
-	if spec.Host == nil {
-		spec.Host = vmObj.Runtime.Host
-	}
-
-	if spec.Pool == nil {
-		spec.Pool = vmObj.ResourcePool
-	}
-
-	if spec.Disk == nil {
-		deviceList := object.VirtualDeviceList(vmObj.Config.Hardware.Device)
-		for _, vdisk := range deviceList.SelectByType(&types.VirtualDisk{}) {
-			spec.Disk = append(spec.Disk, types.VirtualMachineRelocateSpecDiskLocator{
-				DiskId:       vdisk.GetVirtualDevice().Key,
-				Datastore:    *spec.Datastore,
-				DiskMoveType: spec.DiskMoveType,
-			})
-		}
-	}
-}
-
 func CreateClusterComputeResource(ctx *Context, f *Folder, name string, spec types.ClusterConfigSpecEx) (*ClusterComputeResource, types.BaseMethodFault) {
 	if e := ctx.Map.FindByName(name, f.ChildEntity); e != nil {
 		return nil, &types.DuplicateName{
diff --git a/vapi/library/library.go b/vapi/library/library.go
index 7c9b3f1e..14846493 100644
--- a/vapi/library/library.go
+++ b/vapi/library/library.go
@@ -50,13 +50,6 @@ type Library struct {
 	Publication           *Publication     `json:"publish_info,omitempty"`
 	SecurityPolicyID      string           `json:"security_policy_id,omitempty"`
 	UnsetSecurityPolicyID bool             `json:"unset_security_policy_id,omitempty"`
-	ServerGUID            string           `json:"server_guid,omitempty"`
-	StateInfo             *StateInfo       `json:"state_info,omitempty"`
-}
-
-// StateInfo provides the state info of a content library.
-type StateInfo struct {
-	State string `json:"state"`
 }
 
 // Subscription info
diff --git a/vapi/simulator/simulator.go b/vapi/simulator/simulator.go
index 70ad660a..9dd036e9 100644
--- a/vapi/simulator/simulator.go
+++ b/vapi/simulator/simulator.go
@@ -919,7 +919,6 @@ func (s *handler) library(w http.ResponseWriter, r *http.Request) {
 
 			id := uuid.New().String()
 			spec.Library.ID = id
-			spec.Library.ServerGUID = uuid.New().String()
 			spec.Library.CreationTime = types.NewTime(time.Now())
 			spec.Library.LastModifiedTime = types.NewTime(time.Now())
 			spec.Library.UnsetSecurityPolicyID = spec.Library.SecurityPolicyID == ""
@@ -954,8 +953,6 @@ func (s *handler) library(w http.ResponseWriter, r *http.Request) {
 				}
 			}
 
-			spec.Library.StateInfo = &library.StateInfo{State: "ACTIVE"}
-
 			OK(w, id)
 		}
 	case http.MethodGet:
diff --git a/vim25/soap/error.go b/vim25/soap/error.go
index fd30e3ff..5d4fff45 100644
--- a/vim25/soap/error.go
+++ b/vim25/soap/error.go
@@ -62,6 +62,26 @@ func (s soapFaultError) MarshalJSON() ([]byte, error) {
 	return json.Marshal(out)
 }
 
+func (s soapFaultError) Fault() types.BaseMethodFault {
+	if s.fault != nil {
+		fault := s.fault.Detail.Fault
+		if fault == nil {
+			return nil
+		}
+		if f, ok := fault.(types.BaseMethodFault); ok {
+			return f
+		}
+		if val := reflect.ValueOf(fault); val.Kind() != reflect.Pointer {
+			ptrVal := reflect.New(val.Type())
+			ptrVal.Elem().Set(val)
+			if f, ok := ptrVal.Interface().(types.BaseMethodFault); ok {
+				return f
+			}
+		}
+	}
+	return nil
+}
+
 type vimFaultError struct {
 	fault types.BaseMethodFault
 }
diff --git a/vim25/types/fault.go b/vim25/types/fault.go
index 813ea9ec..8b0c1a8d 100644
--- a/vim25/types/fault.go
+++ b/vim25/types/fault.go
@@ -41,3 +41,15 @@ func IsAlreadyExists(err error) bool {
 
 	return false
 }
+
+// HasLocalizedMethodFault is any type that has a LocalizedMethodFault.
+type HasLocalizedMethodFault interface {
+
+	// GetLocalizedMethodFault returns the LocalizedMethodFault instance.
+	GetLocalizedMethodFault() *LocalizedMethodFault
+}
+
+// GetLocalizedMethodFault returns this LocalizedMethodFault.
+func (f *LocalizedMethodFault) GetLocalizedMethodFault() *LocalizedMethodFault {
+	return f
+}


Test patch:
diff --git a/eam/simulator/simulator_test.go b/eam/simulator/simulator_test.go
index 5becdca5..0da8d39d 100644
--- a/eam/simulator/simulator_test.go
+++ b/eam/simulator/simulator_test.go
@@ -29,6 +29,7 @@ import (
 	"github.com/vmware/govmomi/eam"
 	"github.com/vmware/govmomi/eam/object"
 	"github.com/vmware/govmomi/eam/types"
+	"github.com/vmware/govmomi/fault"
 	"github.com/vmware/govmomi/find"
 	vimobject "github.com/vmware/govmomi/object"
 	"github.com/vmware/govmomi/session"
@@ -348,53 +349,6 @@ func TestSimulator(t *testing.T) {
 func TestNotAuthenticated(t *testing.T) {
 	vcsim.Test(func(ctx context.Context, vimClient *vim25.Client) {
 
-		isNotAuthenticatedFault := func(fault vim.AnyType) bool {
-			switch fault.(type) {
-			case vim.NotAuthenticated, *vim.NotAuthenticated:
-				return true
-			default:
-				return false
-			}
-		}
-
-		isEamInvalidLoginFault := func(fault vim.AnyType) bool {
-			switch fault.(type) {
-			case types.EamInvalidLogin, *types.EamInvalidLogin:
-				return true
-			default:
-				return false
-			}
-		}
-
-		validateFault := func(
-			err error,
-			faultType string,
-			faultIsTypeFn func(vim.AnyType) bool) error {
-
-			if err == nil {
-				return fmt.Errorf("%s expected", faultType)
-			}
-
-			validateVimFault := func(vimFault vim.AnyType) error {
-				if !faultIsTypeFn(vimFault) {
-					return fmt.Errorf(
-						"err is not %[1]s: %[2]T %[2]v", faultType, vimFault)
-				}
-				return nil
-			}
-
-			if soap.IsSoapFault(err) {
-				return validateVimFault(soap.ToSoapFault(err).VimFault())
-			}
-
-			if soap.IsVimFault(err) {
-				return validateVimFault(soap.ToVimFault(err))
-			}
-
-			return fmt.Errorf(
-				"err is not a Soap or Vim fault: %[1]T %[1]v", err)
-		}
-
 		t.Run("TerminateSession", func(t *testing.T) {
 			// Terminate the session.
 			sessionManager := session.NewManager(vimClient)
@@ -415,11 +369,7 @@ func TestNotAuthenticated(t *testing.T) {
 				// error.
 				_, err := finder.DefaultDatacenter(ctx)
 
-				if err := validateFault(
-					err,
-					"NotAuthenticated",
-					isNotAuthenticatedFault); err != nil {
-
+				if !fault.Is(err, &vim.NotAuthenticated{}) {
 					t.Fatal(err)
 				}
 			})
@@ -436,12 +386,8 @@ func TestNotAuthenticated(t *testing.T) {
 				// Try to list the agencies, but receive an EamInvalidLogin error.
 				_, err := mgr.Agencies(ctx)
 
-				if err := validateFault(
-					err,
-					"EamInvalidLogin",
-					isEamInvalidLoginFault); err != nil {
-
-					t.Fatal(err)
+				if !fault.Is(err, &types.EamInvalidLogin{}) {
+					t.Fatalf("err=%[1]T %+[1]v", err)
 				}
 			})
 		})
diff --git a/fault/fault_example_test.go b/fault/fault_example_test.go
new file mode 100644
index 00000000..db043a91
--- /dev/null
+++ b/fault/fault_example_test.go
@@ -0,0 +1,260 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fault_test
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/vmware/govmomi/fault"
+	"github.com/vmware/govmomi/task"
+	"github.com/vmware/govmomi/vim25/soap"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+func ExampleAs_faultByAddress() {
+	var (
+		err    any
+		target *types.InvalidPowerState
+	)
+
+	err = task.Error{
+		LocalizedMethodFault: &types.LocalizedMethodFault{
+			Fault: &types.InvalidPowerState{
+				ExistingState:  types.VirtualMachinePowerStatePoweredOn,
+				RequestedState: types.VirtualMachinePowerStatePoweredOff,
+			},
+			LocalizedMessage: "vm must be powered off to encrypt",
+		},
+	}
+
+	localizedMessage, ok := fault.As(err, &target)
+
+	fmt.Printf("result              = %v\n", ok)
+	fmt.Printf("localizedMessage    = %v\n", localizedMessage)
+	fmt.Printf("existingPowerState  = %v\n", target.ExistingState)
+	fmt.Printf("requestedPowerState = %v\n", target.RequestedState)
+
+	// Output:
+	// result              = true
+	// localizedMessage    = vm must be powered off to encrypt
+	// existingPowerState  = poweredOn
+	// requestedPowerState = poweredOff
+}
+
+type valueFault uint8
+
+func (f valueFault) GetMethodFault() *types.MethodFault {
+	return nil
+}
+
+func ExampleAs_faultByValue() {
+	var (
+		err    any
+		target valueFault
+	)
+
+	err = &types.SystemError{
+		RuntimeFault: types.RuntimeFault{
+			MethodFault: types.MethodFault{
+				FaultCause: &types.LocalizedMethodFault{
+					Fault:            valueFault(1),
+					LocalizedMessage: "fault by value",
+				},
+			},
+		},
+	}
+
+	localizedMessage, ok := fault.As(err, &target)
+
+	fmt.Printf("result              = %v\n", ok)
+	fmt.Printf("localizedMessage    = %v\n", localizedMessage)
+	fmt.Printf("value               = %d\n", target)
+
+	// Output:
+	// result              = true
+	// localizedMessage    = fault by value
+	// value               = 1
+}
+
+func ExampleIs_baseMethodFault() {
+	var (
+		err    any
+		target types.BaseMethodFault
+	)
+
+	err = &types.SystemError{}
+	target = &types.SystemError{}
+
+	fmt.Printf("result = %v\n", fault.Is(err, target))
+
+	// Output:
+	// result = true
+}
+
+func ExampleIs_nestedFault() {
+	var (
+		err    any
+		target types.BaseMethodFault
+	)
+
+	err = task.Error{
+		LocalizedMethodFault: &types.LocalizedMethodFault{
+			Fault: &types.RuntimeFault{
+				MethodFault: types.MethodFault{
+					FaultCause: &types.LocalizedMethodFault{
+						Fault: &types.SystemError{},
+					},
+				},
+			},
+		},
+	}
+	target = &types.SystemError{}
+
+	fmt.Printf("result = %v\n", fault.Is(err, target))
+
+	// Output:
+	// result = true
+}
+
+func ExampleIs_soapFault() {
+	var (
+		err    any
+		target types.BaseMethodFault
+	)
+
+	err = soap.WrapSoapFault(&soap.Fault{
+		Detail: struct {
+			Fault types.AnyType "xml:\",any,typeattr\""
+		}{
+			Fault: &types.RuntimeFault{
+				MethodFault: types.MethodFault{
+					FaultCause: &types.LocalizedMethodFault{
+						Fault: &types.SystemError{},
+					},
+				},
+			},
+		},
+	})
+	target = &types.SystemError{}
+
+	fmt.Printf("result = %v\n", fault.Is(err, target))
+
+	// Output:
+	// result = true
+}
+
+func ExampleIs_vimFault() {
+	var (
+		err    any
+		target types.BaseMethodFault
+	)
+
+	err = soap.WrapVimFault(&types.RuntimeFault{
+		MethodFault: types.MethodFault{
+			FaultCause: &types.LocalizedMethodFault{
+				Fault: &types.SystemError{},
+			},
+		},
+	})
+	target = &types.SystemError{}
+
+	fmt.Printf("result = %v\n", fault.Is(err, target))
+
+	// Output:
+	// result = true
+}
+
+func ExampleIn_printAllTypeNamesAndMessages() {
+	var (
+		err     any
+		onFault fault.OnFaultFn
+	)
+
+	err = task.Error{
+		LocalizedMethodFault: &types.LocalizedMethodFault{
+			Fault: &types.RuntimeFault{
+				MethodFault: types.MethodFault{
+					FaultCause: &types.LocalizedMethodFault{
+						Fault:            &types.SystemError{},
+						LocalizedMessage: "inner message",
+					},
+				},
+			},
+			LocalizedMessage: "outer message",
+		},
+	}
+
+	onFault = func(
+		fault types.BaseMethodFault,
+		localizedMessage string,
+		localizableMessages []types.LocalizableMessage) bool {
+
+		fmt.Printf("type             = %s\n", reflect.ValueOf(fault).Elem().Type().Name())
+		fmt.Printf("localizedMessage = %s\n", localizedMessage)
+
+		// Return false to continue discovering faults.
+		return false
+	}
+
+	fault.In(err, onFault)
+
+	// Output:
+	// type             = RuntimeFault
+	// localizedMessage = outer message
+	// type             = SystemError
+	// localizedMessage = inner message
+}
+
+func ExampleIn_printFirstDiscoveredTypeNameAndMessage() {
+	var (
+		err     any
+		onFault fault.OnFaultFn
+	)
+
+	err = task.Error{
+		LocalizedMethodFault: &types.LocalizedMethodFault{
+			Fault: &types.RuntimeFault{
+				MethodFault: types.MethodFault{
+					FaultCause: &types.LocalizedMethodFault{
+						Fault:            &types.SystemError{},
+						LocalizedMessage: "inner message",
+					},
+				},
+			},
+			LocalizedMessage: "outer message",
+		},
+	}
+
+	onFault = func(
+		fault types.BaseMethodFault,
+		localizedMessage string,
+		localizableMessages []types.LocalizableMessage) bool {
+
+		fmt.Printf("type             = %s\n", reflect.ValueOf(fault).Elem().Type().Name())
+		fmt.Printf("localizedMessage = %s\n", localizedMessage)
+
+		// Return true to force discovery to halt.
+		return true
+	}
+
+	fault.In(err, onFault)
+
+	// Output:
+	// type             = RuntimeFault
+	// localizedMessage = outer message
+}
diff --git a/fault/fault_test.go b/fault/fault_test.go
new file mode 100644
index 00000000..0191b276
--- /dev/null
+++ b/fault/fault_test.go
@@ -0,0 +1,968 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fault_test
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/vmware/govmomi/fault"
+	"github.com/vmware/govmomi/task"
+	"github.com/vmware/govmomi/vim25/soap"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+func asPtr[T any](args ...T) *T {
+	if len(args) == 0 {
+		var t T
+		return &t
+	}
+	return &args[0]
+}
+
+var (
+	nilSystemErr       = asPtr[*types.SystemError]()
+	nilInvalidVmConfig = asPtr[*types.InvalidVmConfig]()
+)
+
+type nilErrWrapper struct{}
+
+func (e nilErrWrapper) Unwrap() error {
+	return nil
+}
+
+type unwrappableErrSlice []error
+
+func (e unwrappableErrSlice) Unwrap() []error {
+	return e
+}
+
+type nilBaseMethodFault struct{}
+
+func (f nilBaseMethodFault) GetMethodFault() *types.MethodFault {
+	return nil
+}
+
+type asFaulter struct {
+	val  any
+	okay bool
+	msg  string
+}
+
+func (f asFaulter) AsFault(target any) (string, bool) {
+	if !f.okay {
+		return "", false
+	}
+	targetVal := reflect.ValueOf(target)
+	targetVal.Elem().Set(reflect.ValueOf(f.val))
+	return f.msg, true
+}
+
+type isFaulter bool
+
+func (f isFaulter) GetMethodFault() *types.MethodFault {
+	return nil
+}
+
+func (f isFaulter) IsFault(target types.BaseMethodFault) bool {
+	return bool(f)
+}
+
+func TestAs(t *testing.T) {
+
+	testCases := []struct {
+		name                     string
+		err                      any
+		target                   any
+		expectedLocalizedMessage string
+		expectedOkay             bool
+		expectedPanic            any
+		expectedTarget           any
+	}{
+
+		{
+			name:           "err is nil",
+			err:            nil,
+			target:         asPtr[*types.SystemError](),
+			expectedTarget: asPtr[*types.SystemError](),
+		},
+		{
+			name:           "err is not supported",
+			err:            struct{}{},
+			target:         asPtr[*types.SystemError](),
+			expectedTarget: asPtr[*types.SystemError](),
+		},
+		{
+			name:           "target is nil",
+			err:            errors.New("error"),
+			target:         nil,
+			expectedTarget: nil,
+			expectedPanic:  "fault: target cannot be nil",
+		},
+		{
+			name:           "target is not pointer",
+			err:            errors.New("error"),
+			target:         types.SystemError{},
+			expectedTarget: types.SystemError{},
+			expectedPanic:  "fault: target must be a non-nil pointer",
+		},
+		{
+			name:           "target is not pointer to expected type",
+			err:            errors.New("error"),
+			target:         &types.SystemError{},
+			expectedTarget: &types.SystemError{},
+			expectedPanic:  "fault: *target must be interface or implement BaseMethodFault",
+		},
+		{
+			name: "err is task.Error with fault",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					Fault: &types.SystemError{},
+				},
+			},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr(&types.SystemError{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "",
+		},
+		{
+			name:           "err unwraps to nil error",
+			err:            nilErrWrapper{},
+			target:         asPtr[*types.SystemError](),
+			expectedTarget: asPtr[*types.SystemError](),
+		},
+		{
+			name:           "err unwraps to nil error slice",
+			err:            unwrappableErrSlice{},
+			target:         asPtr[*types.SystemError](),
+			expectedTarget: asPtr[*types.SystemError](),
+		},
+		{
+			name: "err is wrapped task.Error with fault",
+			err: fmt.Errorf("my error: %w", task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					Fault: &types.SystemError{},
+				},
+			}),
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr(&types.SystemError{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "",
+		},
+		{
+			name: "err is wrapped nil error and task.Error with fault",
+			err: unwrappableErrSlice{
+				nil,
+				task.Error{
+					LocalizedMethodFault: &types.LocalizedMethodFault{
+						Fault: &types.SystemError{},
+					},
+				},
+			},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr(&types.SystemError{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "",
+		},
+		{
+			name:                     "err is types.BaseMethodFault",
+			err:                      &types.SystemError{},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr(&types.SystemError{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "",
+		},
+		{
+			name:                     "err is types.BaseMethodFault that returns nil",
+			err:                      nilBaseMethodFault{},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr[*types.SystemError](),
+			expectedLocalizedMessage: "",
+		},
+		{
+			name:                     "err is asFaulter that returns true and fault",
+			err:                      asFaulter{okay: true, msg: "Hello, world.", val: &types.SystemError{}},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr(&types.SystemError{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "Hello, world.",
+		},
+		{
+			name:                     "err is asFaulter that returns false",
+			err:                      asFaulter{okay: false, msg: "Hello, world."},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr[*types.SystemError](),
+			expectedLocalizedMessage: "",
+		},
+		{
+			name: "err is *types.LocalizedMethodFault",
+			err: &types.LocalizedMethodFault{
+				LocalizedMessage: "fake",
+				Fault:            &types.SystemError{},
+			},
+			target:                   asPtr[*types.SystemError](),
+			expectedTarget:           asPtr(&types.SystemError{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "fake",
+		},
+		{
+			name: "err is *types.LocalizedMethodFault w nil fault",
+			err: &types.LocalizedMethodFault{
+				LocalizedMessage: "fake",
+				Fault:            nil,
+			},
+			target:         asPtr[*types.SystemError](),
+			expectedTarget: asPtr[*types.SystemError](),
+			expectedOkay:   false,
+		},
+		{
+			name: "err is task.Error with nested fault",
+			err: &types.LocalizedMethodFault{
+				LocalizedMessage: "fake1",
+				Fault: &types.SystemError{
+					RuntimeFault: types.RuntimeFault{
+						MethodFault: types.MethodFault{
+							FaultCause: &types.LocalizedMethodFault{
+								LocalizedMessage: "fake2",
+								Fault:            &types.InvalidVmConfig{},
+							},
+						},
+					},
+				},
+			},
+			target:                   asPtr[*types.InvalidVmConfig](),
+			expectedTarget:           asPtr(&types.InvalidVmConfig{}),
+			expectedOkay:             true,
+			expectedLocalizedMessage: "fake2",
+		},
+		{
+			name: "err is soap fault",
+			err: soap.WrapSoapFault(&soap.Fault{
+				Detail: struct {
+					Fault types.AnyType "xml:\",any,typeattr\""
+				}{
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultCause: &types.LocalizedMethodFault{
+									Fault: &types.InvalidVmConfig{},
+								},
+							},
+						},
+					},
+				},
+			}),
+			target:         asPtr[*types.InvalidVmConfig](),
+			expectedTarget: asPtr(&types.InvalidVmConfig{}),
+			expectedOkay:   true,
+		},
+		{
+			name: "err is soap fault with nil vim fault",
+			err: soap.WrapSoapFault(&soap.Fault{
+				Detail: struct {
+					Fault types.AnyType "xml:\",any,typeattr\""
+				}{
+					Fault: nil,
+				},
+			}),
+			target:         asPtr[*types.InvalidVmConfig](),
+			expectedTarget: asPtr[*types.InvalidVmConfig](),
+			expectedOkay:   false,
+		},
+		{
+			name: "err is vim fault",
+			err: soap.WrapVimFault(&types.SystemError{
+				RuntimeFault: types.RuntimeFault{
+					MethodFault: types.MethodFault{
+						FaultCause: &types.LocalizedMethodFault{
+							Fault: &types.InvalidVmConfig{},
+						},
+					},
+				},
+			}),
+			target:         asPtr[*types.InvalidVmConfig](),
+			expectedTarget: asPtr(&types.InvalidVmConfig{}),
+			expectedOkay:   true,
+		},
+		{
+			name: "err is vim fault with nil vim fault",
+			err: soap.WrapVimFault(&types.SystemError{
+				RuntimeFault: types.RuntimeFault{
+					MethodFault: types.MethodFault{
+						FaultCause: &types.LocalizedMethodFault{
+							Fault: nil,
+						},
+					},
+				},
+			}),
+			target:         asPtr[*types.InvalidVmConfig](),
+			expectedTarget: asPtr[*types.InvalidVmConfig](),
+			expectedOkay:   false,
+		},
+	}
+
+	for i := range testCases {
+		tc := testCases[i]
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			var (
+				okay             bool
+				localizedMessage string
+			)
+
+			if tc.expectedPanic != nil {
+				assert.PanicsWithValue(
+					t,
+					tc.expectedPanic,
+					func() {
+						localizedMessage, okay = fault.As(tc.err, tc.target)
+					})
+			} else {
+				localizedMessage, okay = fault.As(tc.err, tc.target)
+			}
+
+			assert.Equal(t, tc.expectedOkay, okay)
+			assert.Equal(t, tc.expectedLocalizedMessage, localizedMessage)
+			assert.Equal(t, tc.expectedTarget, tc.target)
+		})
+	}
+}
+
+type faultResult struct {
+	fault               types.BaseMethodFault
+	localizedMessage    string
+	localizableMessages []types.LocalizableMessage
+}
+
+type testHarness struct {
+	isNil                bool
+	numCalls             int
+	numCallsToReturnTrue int
+	results              []faultResult
+}
+
+func (h *testHarness) Callback(
+	fault types.BaseMethodFault,
+	localizedMessage string,
+	localizableMessages []types.LocalizableMessage) bool {
+
+	h.numCalls++
+
+	rvFault := reflect.ValueOf(fault)
+	if rvFault.Kind() == reflect.Pointer {
+		rvFault = rvFault.Elem()
+	}
+	emptyFault := reflect.New(rvFault.Type())
+
+	h.results = append(h.results, faultResult{
+		fault:               emptyFault.Interface().(types.BaseMethodFault),
+		localizedMessage:    localizedMessage,
+		localizableMessages: localizableMessages,
+	})
+	return h.numCallsToReturnTrue == h.numCalls
+}
+
+func TestIn(t *testing.T) {
+	const unsupported = "fault: err must implement error, types.BaseMethodFault, or types.HasLocalizedMethodFault"
+
+	testCases := []struct {
+		name             string
+		err              any
+		callback         *testHarness
+		expectedNumCalls int
+		expectedResults  []faultResult
+		expectedPanic    any
+	}{
+		{
+			name:             "err is nil",
+			err:              nil,
+			callback:         &testHarness{},
+			expectedNumCalls: 0,
+			expectedPanic:    unsupported,
+		},
+		{
+			name:             "callback is nil",
+			err:              errors.New("error"),
+			callback:         &testHarness{isNil: true},
+			expectedNumCalls: 0,
+			expectedPanic:    "fault: onFaultFn must not be nil",
+		},
+		{
+			name:             "err is unsupported",
+			err:              struct{}{},
+			callback:         &testHarness{},
+			expectedNumCalls: 0,
+			expectedPanic:    unsupported,
+		},
+		{
+			name:             "error is task.Error",
+			err:              task.Error{},
+			callback:         &testHarness{},
+			expectedNumCalls: 0,
+		},
+		{
+			name: "error is task.Error with localized message and fault",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					LocalizedMessage: "fake",
+					Fault:            &types.SystemError{},
+				},
+			},
+			callback:         &testHarness{},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					localizedMessage: "fake",
+					fault:            &types.SystemError{},
+				},
+			},
+		},
+		{
+			name:             "error is types.SystemError",
+			err:              &types.SystemError{},
+			callback:         &testHarness{},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					fault: &types.SystemError{},
+				},
+			},
+		},
+		{
+			name: "error is task.Error with a localized message but no fault",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					LocalizedMessage: "fake",
+				},
+			},
+			callback:         &testHarness{},
+			expectedNumCalls: 0,
+		},
+		{
+			name: "error is multiple, wrapped errors",
+			err: unwrappableErrSlice{
+				nil,
+				task.Error{
+					LocalizedMethodFault: &types.LocalizedMethodFault{
+						LocalizedMessage: "fake",
+						Fault:            &types.SystemError{},
+					},
+				},
+			},
+			callback:         &testHarness{},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					localizedMessage: "fake",
+					fault:            &types.SystemError{},
+				},
+			},
+		},
+		{
+			name: "error has nested task.Error with localized message and fault",
+			err: fmt.Errorf(
+				"error 1: %w",
+				fmt.Errorf(
+					"error 2: %w",
+					task.Error{
+						LocalizedMethodFault: &types.LocalizedMethodFault{
+							LocalizedMessage: "fake",
+							Fault:            &types.SystemError{},
+						},
+					},
+				),
+			),
+			callback:         &testHarness{},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					localizedMessage: "fake",
+					fault:            &types.SystemError{},
+				},
+			},
+		},
+		{
+			name: "error is task.Error with localized message and fault with localizable messages",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					LocalizedMessage: "fake",
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultMessage: []types.LocalizableMessage{
+									{
+										Key:     "fake.id",
+										Message: "fake",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			callback:         &testHarness{},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					fault:            &types.SystemError{},
+					localizedMessage: "fake",
+					localizableMessages: []types.LocalizableMessage{
+						{
+							Key:     "fake.id",
+							Message: "fake",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "error is task.Error with nested faults",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					LocalizedMessage: "fake1",
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultMessage: []types.LocalizableMessage{
+									{
+										Key:     "fake1.id",
+										Message: "fake1",
+									},
+								},
+								FaultCause: &types.LocalizedMethodFault{
+									LocalizedMessage: "fake2",
+									Fault: &types.NotSupported{
+										RuntimeFault: types.RuntimeFault{
+											MethodFault: types.MethodFault{
+												FaultMessage: []types.LocalizableMessage{
+													{
+														Key:     "fake2.id",
+														Message: "fake2",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			callback:         &testHarness{},
+			expectedNumCalls: 2,
+			expectedResults: []faultResult{
+				{
+					fault:            &types.SystemError{},
+					localizedMessage: "fake1",
+					localizableMessages: []types.LocalizableMessage{
+						{
+							Key:     "fake1.id",
+							Message: "fake1",
+						},
+					},
+				},
+				{
+					fault:            &types.NotSupported{},
+					localizedMessage: "fake2",
+					localizableMessages: []types.LocalizableMessage{
+						{
+							Key:     "fake2.id",
+							Message: "fake2",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "error is task.Error with nested faults but returns after single fault",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					LocalizedMessage: "fake1",
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultMessage: []types.LocalizableMessage{
+									{
+										Key:     "fake1.id",
+										Message: "fake1",
+									},
+								},
+								FaultCause: &types.LocalizedMethodFault{
+									LocalizedMessage: "fake2",
+									Fault: &types.NotSupported{
+										RuntimeFault: types.RuntimeFault{
+											MethodFault: types.MethodFault{
+												FaultMessage: []types.LocalizableMessage{
+													{
+														Key:     "fake2.id",
+														Message: "fake2",
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			callback:         &testHarness{numCallsToReturnTrue: 1},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					fault:            &types.SystemError{},
+					localizedMessage: "fake1",
+					localizableMessages: []types.LocalizableMessage{
+						{
+							Key:     "fake1.id",
+							Message: "fake1",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "err is soap fault",
+			err: soap.WrapSoapFault(&soap.Fault{
+				Detail: struct {
+					Fault types.AnyType "xml:\",any,typeattr\""
+				}{
+					Fault: &types.SystemError{},
+				},
+			}),
+			callback:         &testHarness{numCallsToReturnTrue: 1},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					fault: &types.SystemError{},
+				},
+			},
+		},
+		{
+			name:             "err is vim fault",
+			err:              soap.WrapVimFault(&types.SystemError{}),
+			callback:         &testHarness{numCallsToReturnTrue: 1},
+			expectedNumCalls: 1,
+			expectedResults: []faultResult{
+				{
+					fault: &types.SystemError{},
+				},
+			},
+		},
+	}
+
+	for i := range testCases {
+		tc := testCases[i]
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			var callback fault.OnFaultFn
+
+			if !tc.callback.isNil {
+				callback = tc.callback.Callback
+			}
+
+			if tc.expectedPanic != nil {
+				assert.PanicsWithValue(
+					t,
+					tc.expectedPanic,
+					func() {
+						fault.In(tc.err, callback)
+					})
+			} else {
+				fault.In(tc.err, callback)
+			}
+
+			assert.Equal(t, tc.expectedNumCalls, tc.callback.numCalls)
+			assert.Equal(t, tc.expectedResults, tc.callback.results)
+		})
+	}
+}
+
+func TestIs(t *testing.T) {
+
+	testCases := []struct {
+		name          string
+		err           any
+		target        types.BaseMethodFault
+		expectedOkay  bool
+		expectedPanic any
+	}{
+		{
+			name:         "err is nil",
+			err:          nil,
+			target:       &types.SystemError{},
+			expectedOkay: false,
+		},
+		{
+			name:         "target is nil",
+			err:          &types.SystemError{},
+			target:       nil,
+			expectedOkay: false,
+		},
+		{
+			name:         "target and error are nil",
+			err:          nil,
+			target:       nil,
+			expectedOkay: true,
+		},
+		{
+			name:         "err is not supported",
+			err:          struct{}{},
+			expectedOkay: false,
+		},
+		{
+			name:         "err and target are same value type",
+			err:          nilBaseMethodFault{},
+			target:       nilBaseMethodFault{},
+			expectedOkay: true,
+		},
+		{
+			name:         "target implements IsFault",
+			err:          isFaulter(true),
+			target:       &types.SystemError{},
+			expectedOkay: true,
+		},
+		{
+			name:         "err is *LocalizedMethodFault with nil fault",
+			err:          &types.LocalizedMethodFault{},
+			target:       &types.SystemError{},
+			expectedOkay: false,
+		},
+		{
+			name: "err is *LocalizedMethodFault with SystemError fault",
+			err: &types.LocalizedMethodFault{
+				Fault: &types.SystemError{},
+			},
+			target:       &types.SystemError{},
+			expectedOkay: true,
+		},
+		{
+			name: "err is *LocalizedMethodFault with InvalidVmConfig fault",
+			err: &types.LocalizedMethodFault{
+				Fault: &types.SystemError{},
+			},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: false,
+		},
+		{
+			name: "err is task.Error with nested InvalidVmConfig fault",
+			err: task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultCause: &types.LocalizedMethodFault{
+									Fault: &types.InvalidVmConfig{},
+								},
+							},
+						},
+					},
+				},
+			},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: true,
+		},
+		{
+			name: "err is *LocalizedMethodFault with nested InvalidVmConfig fault",
+			err: &types.LocalizedMethodFault{
+				Fault: &types.SystemError{
+					RuntimeFault: types.RuntimeFault{
+						MethodFault: types.MethodFault{
+							FaultCause: &types.LocalizedMethodFault{
+								Fault: &types.InvalidVmConfig{},
+							},
+						},
+					},
+				},
+			},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: true,
+		},
+		{
+			name: "err is *LocalizedMethodFault with nested nil fault",
+			err: &types.LocalizedMethodFault{
+				Fault: &types.SystemError{
+					RuntimeFault: types.RuntimeFault{
+						MethodFault: types.MethodFault{
+							FaultCause: &types.LocalizedMethodFault{
+								Fault: nilBaseMethodFault{},
+							},
+						},
+					},
+				},
+			},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: false,
+		},
+		{
+			name: "err is wrapped task.Error with nested InvalidVmConfig fault",
+			err: fmt.Errorf("my error: %w", task.Error{
+				LocalizedMethodFault: &types.LocalizedMethodFault{
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultCause: &types.LocalizedMethodFault{
+									Fault: &types.InvalidVmConfig{},
+								},
+							},
+						},
+					},
+				},
+			}),
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: true,
+		},
+		{
+			name:         "err is wrapped nil error",
+			err:          nilErrWrapper{},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: false,
+		},
+		{
+			name: "err is wrapped error slice with expected value",
+			err: unwrappableErrSlice{
+				nil,
+				task.Error{
+					LocalizedMethodFault: &types.LocalizedMethodFault{
+						Fault: &types.SystemError{
+							RuntimeFault: types.RuntimeFault{
+								MethodFault: types.MethodFault{
+									FaultCause: &types.LocalizedMethodFault{
+										Fault: &types.InvalidVmConfig{},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: true,
+		},
+		{
+			name: "err is wrapped error slice sans expected value",
+			err: unwrappableErrSlice{
+				nil,
+				task.Error{
+					LocalizedMethodFault: &types.LocalizedMethodFault{
+						Fault: &types.SystemError{
+							RuntimeFault: types.RuntimeFault{
+								MethodFault: types.MethodFault{
+									FaultCause: &types.LocalizedMethodFault{
+										Fault: &types.SystemError{},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: false,
+		},
+		{
+			name: "err is soap fault",
+			err: soap.WrapSoapFault(&soap.Fault{
+				Detail: struct {
+					Fault types.AnyType "xml:\",any,typeattr\""
+				}{
+					Fault: &types.SystemError{
+						RuntimeFault: types.RuntimeFault{
+							MethodFault: types.MethodFault{
+								FaultCause: &types.LocalizedMethodFault{
+									Fault: &types.InvalidVmConfig{},
+								},
+							},
+						},
+					},
+				},
+			}),
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: true,
+		},
+		{
+			name: "err is soap fault with nil vim fault",
+			err: soap.WrapSoapFault(&soap.Fault{
+				Detail: struct {
+					Fault types.AnyType "xml:\",any,typeattr\""
+				}{
+					Fault: nil,
+				},
+			}),
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: false,
+		},
+		{
+			name: "err is vim fault",
+			err: soap.WrapVimFault(&types.SystemError{
+				RuntimeFault: types.RuntimeFault{
+					MethodFault: types.MethodFault{
+						FaultCause: &types.LocalizedMethodFault{
+							Fault: &types.InvalidVmConfig{},
+						},
+					},
+				},
+			}),
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: true,
+		},
+		{
+			name: "err is vim fault with nil vim fault",
+			err: soap.WrapVimFault(&types.SystemError{
+				RuntimeFault: types.RuntimeFault{
+					MethodFault: types.MethodFault{
+						FaultCause: &types.LocalizedMethodFault{
+							Fault: nil,
+						},
+					},
+				},
+			}),
+			target:       &types.InvalidVmConfig{},
+			expectedOkay: false,
+		},
+	}
+
+	for i := range testCases {
+		tc := testCases[i]
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			var okay bool
+
+			if tc.expectedPanic != nil {
+				assert.PanicsWithValue(
+					t,
+					tc.expectedPanic,
+					func() {
+						okay = fault.Is(tc.err, tc.target)
+					})
+			} else {
+				okay = fault.Is(tc.err, tc.target)
+			}
+
+			assert.Equal(t, tc.expectedOkay, okay)
+		})
+	}
+}
diff --git a/simulator/cluster_compute_resource_test.go b/simulator/cluster_compute_resource_test.go
index 9ff0ad11..7f1aaba3 100644
--- a/simulator/cluster_compute_resource_test.go
+++ b/simulator/cluster_compute_resource_test.go
@@ -156,7 +156,7 @@ func TestPlaceVmReconfigure(t *testing.T) {
 		{
 			"unsupported placement type",
 			nil,
-			types.PlacementSpecPlacementType("unsupported"),
+			types.PlacementSpecPlacementTypeRelocate,
 			"NotSupported",
 		},
 		{
@@ -206,163 +206,3 @@ func TestPlaceVmReconfigure(t *testing.T) {
 		})
 	}
 }
-
-func TestPlaceVmRelocate(t *testing.T) {
-	Test(func(ctx context.Context, c *vim25.Client) {
-		// Test env setup.
-		finder := find.NewFinder(c, true)
-		datacenter, err := finder.DefaultDatacenter(ctx)
-		if err != nil {
-			t.Fatalf("failed to get default datacenter: %v", err)
-		}
-		finder.SetDatacenter(datacenter)
-
-		vmMoRef := Map.Any("VirtualMachine").(*VirtualMachine).Reference()
-		hostMoRef := Map.Any("HostSystem").(*HostSystem).Reference()
-		dsMoRef := Map.Any("Datastore").(*Datastore).Reference()
-
-		tests := []struct {
-			name         string
-			relocateSpec *types.VirtualMachineRelocateSpec
-			vmMoRef      *types.ManagedObjectReference
-			expectedErr  string
-		}{
-			{
-				"relocate without a spec",
-				nil,
-				&vmMoRef,
-				"",
-			},
-			{
-				"relocate with an empty spec",
-				&types.VirtualMachineRelocateSpec{},
-				&vmMoRef,
-				"",
-			},
-			{
-				"relocate without a vm in spec",
-				&types.VirtualMachineRelocateSpec{
-					Host: &hostMoRef,
-				},
-				nil,
-				"InvalidArgument",
-			},
-			{
-				"relocate with a non-existing vm in spec",
-				&types.VirtualMachineRelocateSpec{
-					Host: &hostMoRef,
-				},
-				&types.ManagedObjectReference{
-					Type:  "VirtualMachine",
-					Value: "fake-vm-999",
-				},
-				"InvalidArgument",
-			},
-			{
-				"relocate with a diskId in spec.dick that does not exist in the vm",
-				&types.VirtualMachineRelocateSpec{
-					Host: &hostMoRef,
-					Disk: []types.VirtualMachineRelocateSpecDiskLocator{
-						{
-							DiskId:    1,
-							Datastore: dsMoRef,
-						},
-					},
-				},
-				&vmMoRef,
-				"InvalidArgument",
-			},
-			{
-				"relocate with a non-existing datastore in spec.disk",
-				&types.VirtualMachineRelocateSpec{
-					Host: &hostMoRef,
-					Disk: []types.VirtualMachineRelocateSpecDiskLocator{
-						{
-							DiskId: 204, // The default diskId in simulator.
-							Datastore: types.ManagedObjectReference{
-								Type:  "Datastore",
-								Value: "fake-datastore-999",
-							},
-						},
-					},
-				},
-				&vmMoRef,
-				"InvalidArgument",
-			},
-			{
-				"relocate with a valid spec.disk",
-				&types.VirtualMachineRelocateSpec{
-					Host: &hostMoRef,
-					Disk: []types.VirtualMachineRelocateSpecDiskLocator{
-						{
-							DiskId:    204, // The default diskId in simulator.
-							Datastore: dsMoRef,
-						},
-					},
-				},
-				&vmMoRef,
-				"",
-			},
-			{
-				"relocate with a valid host in spec",
-				&types.VirtualMachineRelocateSpec{
-					Host: &hostMoRef,
-				},
-				&vmMoRef,
-				"",
-			},
-			{
-				"relocate with a non-existing host in spec",
-				&types.VirtualMachineRelocateSpec{
-					Host: &types.ManagedObjectReference{
-						Type:  "HostSystem",
-						Value: "fake-host-999",
-					},
-				},
-				&vmMoRef,
-				"ManagedObjectNotFound",
-			},
-			{
-				"relocate with a non-existing datastore in spec",
-				&types.VirtualMachineRelocateSpec{
-					Datastore: &types.ManagedObjectReference{
-						Type:  "Datastore",
-						Value: "fake-datastore-999",
-					},
-				},
-				&vmMoRef,
-				"ManagedObjectNotFound",
-			},
-			{
-				"relocate with a non-existing resource pool in spec",
-				&types.VirtualMachineRelocateSpec{
-					Pool: &types.ManagedObjectReference{
-						Type:  "ResourcePool",
-						Value: "fake-resource-pool-999",
-					},
-				},
-				&vmMoRef,
-				"ManagedObjectNotFound",
-			},
-		}
-
-		for _, test := range tests {
-			test := test // assign to local var since loop var is reused
-			// PlaceVm.
-			placementSpec := types.PlacementSpec{
-				Vm:            test.vmMoRef,
-				RelocateSpec:  test.relocateSpec,
-				PlacementType: string(types.PlacementSpecPlacementTypeRelocate),
-			}
-
-			clusterMoRef := Map.Any("ClusterComputeResource").(*ClusterComputeResource).Reference()
-			clusterObj := object.NewClusterComputeResource(c, clusterMoRef)
-			_, err = clusterObj.PlaceVm(ctx, placementSpec)
-			if err == nil && test.expectedErr != "" {
-				t.Fatalf("expected error %q, got nil", test.expectedErr)
-			} else if err != nil && !strings.Contains(err.Error(), test.expectedErr) {
-				t.Fatalf("expected error %q, got %v", test.expectedErr, err)
-			}
-		}
-	})
-}
diff --git a/vapi/library/library_test.go b/vapi/library/library_test.go
deleted file mode 100644
index a7c3434d..00000000
--- a/vapi/library/library_test.go
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
-Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package library_test
-
-import (
-	"context"
-	"testing"
-
-	"github.com/vmware/govmomi/find"
-	"github.com/vmware/govmomi/simulator"
-	"github.com/vmware/govmomi/vapi/library"
-	"github.com/vmware/govmomi/vapi/rest"
-	"github.com/vmware/govmomi/vim25"
-
-	_ "github.com/vmware/govmomi/vapi/simulator"
-)
-
-func TestManagerCreateLibrary(t *testing.T) {
-	simulator.Test(func(ctx context.Context, vc *vim25.Client) {
-		c := rest.NewClient(vc)
-
-		err := c.Login(ctx, simulator.DefaultLogin)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		ds, err := find.NewFinder(vc).DefaultDatastore(ctx)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		m := library.NewManager(c)
-
-		libName := "example"
-		libType := "LOCAL"
-		id, err := m.CreateLibrary(ctx, library.Library{
-			Name: libName,
-			Type: libType,
-			Storage: []library.StorageBacking{{
-				DatastoreID: ds.Reference().Value,
-				Type:        "DATASTORE",
-			}},
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		l, err := m.GetLibraryByID(ctx, id)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		if l.ID == "" {
-			t.Fatal("library ID should be generated")
-		}
-		if l.ServerGUID == "" {
-			t.Fatal("library server GUID should be generated")
-		}
-		if l.Name != libName {
-			t.Fatalf("expected library name %s, got %s", libName, l.Name)
-		}
-		if l.Type != libType {
-			t.Fatalf("expected library type %s, got %s", libType, l.Type)
-		}
-		if len(l.Storage) == 0 {
-			t.Fatal("library should have a storage backing")
-		}
-		if l.Storage[0].Type != "DATASTORE" {
-			t.Fatalf("expected library storage type DATASTORE, got %s", l.Storage[0].Type)
-		}
-		if l.Storage[0].DatastoreID != ds.Reference().Value {
-			t.Fatalf("expected library datastore ref %s, got %s", ds.Reference().Value, l.Storage[0].DatastoreID)
-		}
-		if l.StateInfo == nil || l.StateInfo.State != "ACTIVE" {
-			t.Fatal("library should have state ACTIVE")
-		}
-	})
-}