summaryrefslogtreecommitdiff
path: root/qa/qa/resource/reusable.rb
diff options
context:
space:
mode:
Diffstat (limited to 'qa/qa/resource/reusable.rb')
-rw-r--r--qa/qa/resource/reusable.rb130
1 files changed, 115 insertions, 15 deletions
diff --git a/qa/qa/resource/reusable.rb b/qa/qa/resource/reusable.rb
index 24b0a1f6bce..6a9d0392ba2 100644
--- a/qa/qa/resource/reusable.rb
+++ b/qa/qa/resource/reusable.rb
@@ -4,8 +4,13 @@ module QA
module Resource
#
# This module includes methods that allow resource classes to be reused safely. It should be prepended to a new
- # reusable version of an existing resource class. See Resource::Project and ReusableResource::Project for an example
+ # reusable version of an existing resource class. See Resource::Project and ReusableResource::Project for an example.
+ # Reusable resource classes must also be registered with a resource collection that will manage cleanup.
#
+ # @example Register a resource class with a collection
+ # QA::Resource::ReusableCollection.register_resource_classes do |collection|
+ # QA::Resource::ReusableProject.register(collection)
+ # end
module Reusable
attr_accessor :reuse,
:reuse_as
@@ -16,7 +21,7 @@ module QA
base.extend(ClassMethods)
end
- # Gets an existing resource if it exists and the parameters of the new specification of the resource are valid.
+ # Gets an existing resource if it exists and the specified attributes of the resource are valid.
# Creates a new instance of the resource if it does not exist.
#
# @return [String] The URL of the resource.
@@ -27,33 +32,128 @@ module QA
rescue Errors::ResourceNotFoundError
super
ensure
- self.class.resources[reuse_as] = self
+ self.class.resources[reuse_as] ||= {
+ tests: Set.new,
+ resource: self
+ }
+
+ self.class.resources[reuse_as][:attributes] ||= all_attributes.each_with_object({}) do |attribute_name, attributes|
+ attributes[attribute_name] = instance_variable_get("@#{attribute_name}")
+ end
+ self.class.resources[reuse_as][:tests] << Runtime::Example.location
end
- # Including classes must confirm that the resource can be reused as defined. For example, a project can't be
- # fabricated with a unique name.
+ # Overrides remove_via_api! to log a debug message stating that removal will happen after the suite completes.
#
# @return [nil]
+ def remove_via_api!
+ QA::Runtime::Logger.debug("#{self.class.name} - deferring removal until after suite")
+ end
+
+ # Object comparison
+ #
+ # @param [QA::Resource::Base] other
+ # @return [Boolean]
+ def ==(other)
+ self.class <= other.class && comparable == other.comparable
+ end
+
+ # Confirms that reuse of the resource did not change it in a way that breaks later reuse.
+ # For example, this should fail if a reusable resource should have a specific name, but the name has been changed.
+ def validate_reuse
+ QA::Runtime::Logger.debug(["Validating a #{self.class.name} that was reused as #{reuse_as}", identifier].compact.join(' '))
+
+ fresh_resource = reference_resource
+ diff = reuse_validation_diff(fresh_resource)
+
+ if diff.present?
+ raise ResourceReuseError, <<~ERROR
+ The reused #{self.class.name} resource does not have the attributes expected.
+ The following change was found: #{diff}"
+ The resource's web_url is #{web_url}.
+ It was used in these tests: #{self.class.resources[reuse_as][:tests].to_a.join(', ')}
+ ERROR
+ end
+
+ ensure
+ fresh_resource.remove_via_api!
+ end
+
+ private
+
+ # Creates a new resource that can be compared to a reused resource, using the post body of the original.
+ # Must be implemented by classes that include this module.
+ def reference_resource
+ return super if defined?(super)
+
+ raise NotImplementedError
+ end
+
+ # Confirms that the resource attributes specified in its fabricate_via_api! block will allow it to be reused.
+ #
+ # @return [nil] returns nil unless an error is raised
def validate_reuse_preconditions
+ return unless self.class.resources.key?(reuse_as)
+
+ attributes = unique_identifiers.each_with_object({ proposed: {}, existing: {} }) do |id, attrs|
+ proposed = public_send(id)
+ existing = self.class.resources[reuse_as][:resource].public_send(id)
+
+ next if proposed == existing
+
+ attrs[:proposed][id] = proposed
+ attrs[:existing][id] = existing
+ end
+
+ unless attributes[:proposed].empty? && attributes[:existing].empty?
+ raise ResourceReuseError, "Reusable resources must use the same unique identifier(s). " \
+ "The #{self.class.name} to be reused as :#{reuse_as} has the identifier(s) #{attributes[:proposed]} " \
+ "but it should have #{attributes[:existing]}"
+ end
+ end
+
+ # Compares the attributes of the current reused resource with a reference instance.
+ #
+ # @return [Hash] any differences between the resources.
+ def reuse_validation_diff(other)
+ original, reference = prepare_reuse_validation_diff(other)
+
+ return if original == reference
+
+ diff_values = original.to_a - reference.to_a
+ diff_values.to_h
+ end
+
+ # Compares the current reusable resource to a reference instance, ignoring identifying unique attributes that
+ # had to be changed.
+ #
+ # @return [Hash, Hash] the current and reference resource attributes, respectively.
+ def prepare_reuse_validation_diff(other)
+ original = self.reload!.comparable
+ reference = other.reload!.comparable
+ unique_identifiers.each { |id| reference[id] = original[id] }
+ [original, reference]
+ end
+
+ # The attributes of the resource that should be the same whenever a test wants to reuse a resource. Must be
+ # implemented by classes that include this module.
+ #
+ # @return [Array<Symbol>] the attribute names.
+ def unique_identifiers
return super if defined?(super)
raise NotImplementedError
end
module ClassMethods
- # Removes all created resources of this type.
- #
- # @return [Hash<Symbol, QA::Resource>] the resources that were to be removed.
- def remove_all_via_api!
- resources.each do |reuse_as, resource|
- QA::Runtime::Logger.debug("#{self.name} - removing #{reuse_as}")
- resource.method(:remove_via_api!).super_method.call
- end
+ # Includes the resources created/reused by this class in the specified collection
+ def register(collection)
+ collection[self.name] = resources
end
- # The resources created by this resource class.
+ # The resources created/reused by this resource class.
#
- # @return [Hash<Symbol, QA::Resource>] the resources created by this resource class.
+ # @return [Hash<Symbol, Hash>] the resources created/reused by this resource class.
def resources
@resources ||= {}
end