vmware / govmomi

Go library for the VMware vSphere API
Apache License 2.0
2.32k stars 913 forks source link

api: Fault helpers #3544

Closed akutz closed 2 months ago

akutz commented 2 months ago

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:

How Has This Been Tested?

With the command:

go test -v -count 1 -cover ./fault

The results shows there is 100% code coverage:

=== 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:

akutz commented 2 months ago

@dougm I will add an As function before we merge this.

dougm commented 2 months ago

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 commented 2 months ago

Sweet, thanks @akutz I was trying it out before I saw your comment about adding As after merge. Looking forward to that!

if fault.Is(err, &types.InvalidPowerState{}) {

}
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 commented 2 months ago

@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. Because otherwise, Is just becomes a wrapper for As. The stdlib errors.Is just compares the type, so if we want to do the same, I can do that real quick. Thoughts?

dougm commented 2 months ago

Because otherwise, Is just becomes a wrapper for As. The stdlib 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 commented 2 months ago

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 commented 2 months ago

@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.