lib/test_ids/allocator.rb in test_ids-0.8.2 vs lib/test_ids/allocator.rb in test_ids-1.0.0
- old
+ new
@@ -6,16 +6,24 @@
# There is one allocator instance per configuration, and each has its own database
# file.
class Allocator
STORE_FORMAT_REVISION = 2
- attr_reader :config
-
def initialize(configuration)
@config = configuration
end
+ def config(type = nil)
+ if type
+ type = type.to_s
+ type.chop! if type[-1] == 's'
+ TestIds.send("#{type}_config") || @config
+ else
+ @config
+ end
+ end
+
# Allocates a softbin number from the range specified in the test flow
# It also keeps a track of the last softbin assigned out from a particular range
# and uses that to increment the pointers accordingly.
# If a numeric number is passed to the softbin, it uses that number.
# The configuration for the TestId plugin needs to pass in the bin number and the options from the test flow
@@ -71,141 +79,100 @@
fail
end
assigned_value
end
- # Main method to inject generated bin and test numbers, the given
- # options instance is modified accordingly
- def allocate(instance, options)
- orig_options = options.dup
- clean(options)
- @callbacks = []
- name = extract_test_name(instance, options)
- name = "#{name}_#{options[:index]}" if options[:index]
-
- # First work out the test ID to be used for each of the numbers, and how many numbers
- # should be reserved
- if (options[:bin].is_a?(Symbol) || options[:bin].is_a?(String)) && options[:bin] != :none
- bin_id = options[:bin].to_s
+ # Returns an array containing :bin, :softbin, :number in the order that they should be calculated in order to fulfil
+ # the requirements of the current configuration and the given options.
+ # If an item is not required (e.g. if set to :none in the options), then it will not be present in the array.
+ def allocation_order(options)
+ items = []
+ items_required = 0
+ if allocation_required?(:bin, options) ||
+ (allocation_required?(:softbin, options) && config(:softbin).softbins.needs?(:bin)) ||
+ (allocation_required?(:number, options) && config(:number).numbers.needs?(:bin))
+ items_required += 1
else
- bin_id = name
+ bin_done = true
end
- if (options[:softbin].is_a?(Symbol) || options[:softbin].is_a?(String)) && options[:softbin] != :none
- softbin_id = options[:softbin].to_s
+ if allocation_required?(:softbin, options) ||
+ (allocation_required?(:bin, options) && config(:bin).bins.needs?(:softbin)) ||
+ (allocation_required?(:number, options) && config(:number).numbers.needs?(:softbin))
+ items_required += 1
else
- softbin_id = name
+ softbin_done = true
end
- if (options[:number].is_a?(Symbol) || options[:number].is_a?(String)) && options[:number] != :none
- number_id = options[:number].to_s
+ if allocation_required?(:number, options) ||
+ (allocation_required?(:bin, options) && config(:bin).bins.needs?(:number)) ||
+ (allocation_required?(:softbin, options) && config(:softbin).softbins.needs?(:number))
+ items_required += 1
else
- number_id = name
+ number_done = true
end
+ items_required.times do |i|
+ if !bin_done && (!config(:bin).bins.needs?(:softbin) || softbin_done) && (!config(:bin).bins.needs?(:number) || number_done)
+ items << :bin
+ bin_done = true
+ elsif !softbin_done && (!config(:softbin).softbins.needs?(:bin) || bin_done) && (!config(:softbin).softbins.needs?(:number) || number_done)
+ items << :softbin
+ softbin_done = true
+ elsif !number_done && (!config(:number).numbers.needs?(:bin) || bin_done) && (!config(:number).numbers.needs?(:softbin) || softbin_done)
+ items << :number
+ number_done = true
+ else
+ fail "Couldn't work out whether to generate next on iteration #{i} of #{items_required}, already picked: #{items}"
+ end
+ end
+ items
+ end
- bin_size = options[:bin_size] || config.bins.size
- softbin_size = options[:softbin_size] || config.softbins.size
- number_size = options[:number_size] || config.numbers.size
+ # Main method to inject generated bin and test numbers, the given
+ # options instance is modified accordingly
+ def allocate(instance, options)
+ orig_options = options.dup
+ clean(options)
+ name = extract_test_name(instance, options)
- bin = store['assigned']['bins'][bin_id] ||= {}
- softbin = store['assigned']['softbins'][softbin_id] ||= {}
- number = store['assigned']['numbers'][number_id] ||= {}
+ nones = []
- # If the user has supplied any of these, that number should be used
- # and reserved so that it is not automatically generated later
- if options[:bin] && options[:bin].is_a?(Numeric)
- bin['number'] = options[:bin]
- bin['size'] = bin_size
- store['manually_assigned']['bins'][options[:bin].to_s] = true
- # Regenerate the bin if the original allocation has since been applied
- # manually elsewhere
- elsif store['manually_assigned']['bins'][bin['number'].to_s]
- bin['number'] = nil
- bin['size'] = nil
- # Also regenerate these as they could be a function of the bin
- if config.softbins.function?
- softbin['number'] = nil
- softbin['size'] = nil
- end
- if config.numbers.function?
- number['number'] = nil
- number['size'] = nil
- end
+ # Record any :nones that are present for later
+ [:bin, :softbin, :number].each do |type|
+ nones << type if options[type] == :none
+ config(type).allocator.instance_variable_set('@needs_regenerated', {})
end
- if options[:softbin] && options[:softbin].is_a?(Numeric)
- softbin['number'] = options[:softbin]
- softbin['size'] = softbin_size
- store['manually_assigned']['softbins'][options[:softbin].to_s] = true
- elsif store['manually_assigned']['softbins'][softbin['number'].to_s]
- softbin['number'] = nil
- softbin['size'] = nil
- # Also regenerate the number as it could be a function of the softbin
- if config.numbers.function?
- number['number'] = nil
- number['size'] = nil
- end
- end
- if options[:number] && options[:number].is_a?(Numeric)
- number['number'] = options[:number]
- number['size'] = number_size
- store['manually_assigned']['numbers'][options[:number].to_s] = true
- elsif store['manually_assigned']['numbers'][number['number'].to_s]
- number['number'] = nil
- number['size'] = nil
- # Also regenerate the softbin as it could be a function of the number
- if config.softbins.function?
- softbin['number'] = nil
- softbin['size'] = nil
- end
- end
- # Otherwise generate the missing ones
- bin['number'] ||= allocate_bin(options.merge(size: bin_size))
- bin['size'] ||= bin_size
- # If the softbin is based on the test number, then need to calculate the
- # test number first.
- # Also do the number first if the softbin is a callback and the number is not.
- if (config.softbins.algorithm && config.softbins.algorithm.to_s =~ /n/) ||
- (config.softbins.callback && !config.numbers.function?)
- number['number'] ||= allocate_number(options.merge(bin: bin['number'], size: number_size))
- number['size'] ||= number_size
- softbin['number'] ||= allocate_softbin(options.merge(bin: bin['number'], number: number['number'], size: softbin_size))
- softbin['size'] ||= softbin_size
- else
- softbin['number'] ||= allocate_softbin(options.merge(bin: bin['number'], size: softbin_size))
- softbin['size'] ||= softbin_size
- number['number'] ||= allocate_number(options.merge(bin: bin['number'], softbin: softbin['number'], size: number_size))
- number['size'] ||= number_size
+ allocation_order(options).each do |type|
+ config(type).allocator.send(:_allocate, type, name, options)
end
- # Record that there has been a reference to the final numbers
- time = Time.now.to_f
- bin_size.times do |i|
- store['references']['bins'][(bin['number'] + i).to_s] = time if bin['number'] && options[:bin] != :none
+ # Turn any :nones into nils in the returned options
+ nones.each do |type|
+ options[type] = nil
+ options["#{type}_size"] = nil
end
- softbin_size.times do |i|
- store['references']['softbins'][(softbin['number'] + i).to_s] = time if softbin['number'] && options[:softbin] != :none
- end
- number_size.times do |i|
- store['references']['numbers'][(number['number'] + i).to_s] = time if number['number'] && options[:number] != :none
- end
- # Update the supplied options hash that will be forwarded to the program generator
- unless options.delete(:bin) == :none
- options[:bin] = bin['number']
- options[:bin_size] = bin['size']
- end
- unless options.delete(:softbin) == :none
- options[:softbin] = softbin['number']
- options[:softbin_size] = softbin['size']
- end
- unless options.delete(:number) == :none
- options[:number] = number['number']
- options[:number_size] = number['size']
- end
-
options
end
+ # Merge the given other store into the current one, it is assumed that both are formatted
+ # from the same (latest) revision
+ def merge_store(other_store)
+ store['pointers'] = store['pointers'].merge(other_store['pointers'])
+ @last_bin = store['pointers']['bins']
+ @last_softbin = store['pointers']['softbins']
+ @last_number = store['pointers']['numbers']
+ store['assigned']['bins'] = store['assigned']['bins'].merge(other_store['assigned']['bins'])
+ store['assigned']['softbins'] = store['assigned']['softbins'].merge(other_store['assigned']['softbins'])
+ store['assigned']['numbers'] = store['assigned']['numbers'].merge(other_store['assigned']['numbers'])
+ store['manually_assigned']['bins'] = store['manually_assigned']['bins'].merge(other_store['manually_assigned']['bins'])
+ store['manually_assigned']['softbins'] = store['manually_assigned']['softbins'].merge(other_store['manually_assigned']['softbins'])
+ store['manually_assigned']['numbers'] = store['manually_assigned']['numbers'].merge(other_store['manually_assigned']['numbers'])
+ store['references']['bins'] = store['references']['bins'].merge(other_store['references']['bins'])
+ store['references']['softbins'] = store['references']['softbins'].merge(other_store['references']['softbins'])
+ store['references']['numbers'] = store['references']['numbers'].merge(other_store['references']['numbers'])
+ end
+
def store
@store ||= begin
if file && File.exist?(file)
lines = File.readlines(file)
# Remove any header comment lines since these are not valid JSON
@@ -265,11 +232,11 @@
# allocator has moved onto the reclamation phase
#####################################################################
{ 'bins' => 'bins', 'softbins' => 'softbins', 'numbers' => 'test_numbers' }.each do |type, name|
if !config.send(type).function? && store['pointers'][type] == 'done'
Origen.log.info "Checking for missing #{name}..."
- recovered = add_missing_references(config.send(type), store['references'][type])
+ recovered = add_missing_references(config.send, store['references'][type])
if recovered == 0
Origen.log.info " All #{name} are already available."
else
Origen.log.success " Another #{recovered} #{name} have been made available!"
end
@@ -394,10 +361,76 @@
config.load_from_serialized(store['configuration']) if store['configuration']
end
private
+ def _allocate(type, name, options)
+ type_plural = "#{type}s"
+ conf = config.send(type_plural)
+
+ # First work out the test ID to be used for each of the numbers, and how many numbers
+ # should be reserved
+ if (options[type].is_a?(Symbol) || options[type].is_a?(String)) && options[type] != :none
+ id = options[type].to_s
+ else
+ id = name
+ end
+ id = "#{id}_#{options[:index]}" if options[:index]
+ id = "#{id}_#{options[:test_ids_flow_id]}" if config.unique_by_flow?
+
+ val = store['assigned'][type_plural][id] ||= {}
+
+ if options[type].is_a?(Integer)
+ unless val['number'] == options[type]
+ store['manually_assigned']["#{type}s"][options[type].to_s] = true
+ val['number'] = options[type]
+ end
+ else
+ # Will be set if an upstream dependent type has been marked for regeneration by the code below
+ if @needs_regenerated[type]
+ val['number'] = nil
+ val['size'] = nil
+ # Regenerate the number if the original allocation has since been applied manually elsewhere
+ elsif store['manually_assigned'][type_plural][val['number'].to_s]
+ val['number'] = nil
+ val['size'] = nil
+ # Also regenerate these as they could be a function of the number we just invalidated
+ ([:bin, :softbin, :number] - [type]).each do |t|
+ if config.send("#{t}s").needs?(type)
+ @needs_regenerated[t] = true
+ end
+ end
+ end
+ end
+
+ if size = options["#{type}_size".to_sym]
+ val['size'] = size
+ end
+
+ # Generate the missing ones
+ val['size'] ||= conf.size
+ val['number'] ||= allocate_item(type, options.merge(size: val['size']))
+
+ # Record that there has been a reference to the final numbers
+ time = Time.now.to_f
+ val['size'].times do |i|
+ store['references'][type_plural][(val['number'] + i).to_s] = time if val['number'] && options[type] != :none
+ end
+
+ # Update the supplied options hash that will be forwarded to the program generator
+ options[type] = val['number']
+ options["#{type}_size".to_sym] = val['size']
+ end
+
+ def allocation_required?(type, options)
+ if options[type] == :none
+ false
+ else
+ !config(type).send("#{type}s").empty?
+ end
+ end
+
def remove_invalid_references(config_item, references, manually_assigned)
removed = 0
references.each do |num, time|
unless config_item.valid?(num.to_i) || manually_assigned[num]
removed += 1
@@ -433,165 +466,38 @@
end
end
recovered
end
- # Returns the next available bin in the pool, if they have all been given out
- # the one that hasn't been used for the longest time will be given out
- def allocate_bin(options)
- # Not sure if this is the right way. IMO the following are true:
- # 1. config.bins will have a callback only when ranges are specified.
- # 2. If config.bins is empty but config.bins is not a callback, return nil to maintain functionality as before.
- return nil if config.bins.empty? && !config.bins.callback
- if store['pointers']['bins'] == 'done'
- reclaim_bin(options)
- elsif callback = config.bins.callback
- callback.call(options)
- else
- b = config.bins.include.next(after: @last_bin, size: options[:size])
- @last_bin = nil
- while b && (store['manually_assigned']['bins'][b.to_s] || config.bins.exclude.include?(b))
- b = config.bins.include.next(size: options[:size])
- end
- # When no bin is returned it means we have used them all, all future generation
- # now switches to reclaim mode
- if b
- store['pointers']['bins'] = b
- else
- store['pointers']['bins'] = 'done'
- reclaim_bin(options)
- end
- end
- end
-
- def reclaim_bin(options)
- store['references']['bins'] = store['references']['bins'].sort_by { |k, v| v }.to_h
- if options[:size] == 1
- store['references']['bins'].first[0].to_i
- else
- reclaim(store['references']['bins'], options)
- end
- end
-
- def allocate_softbin(options)
- bin = options[:bin]
- num = options[:number]
- return nil if config.softbins.empty?
- if config.softbins.algorithm
- algo = config.softbins.algorithm.to_s.downcase
- if algo.to_s =~ /^[b\dxn]+$/
+ def allocate_item(type, options)
+ type_plural = "#{type}s"
+ conf = config.send(type_plural)
+ if conf.algorithm
+ algo = conf.algorithm.to_s.downcase
+ if algo.to_s =~ /^[bsn\dx]+$/
number = algo.to_s
- bin = bin.to_s
- if number =~ /(b+)/
- max_bin_size = Regexp.last_match(1).size
- if bin.size > max_bin_size
- fail "Bin number (#{bin}) overflows the softbin number algorithm (#{algo})"
- end
- number = number.sub(/b+/, bin.rjust(max_bin_size, '0'))
- end
- if number =~ /(n+)/
- num = num.to_s
- max_num_size = Regexp.last_match(1).size
- if num.size > max_num_size
- fail "Test number (#{num}) overflows the softbin number algorithm (#{algo})"
- end
- number = number.sub(/n+/, num.rjust(max_num_size, '0'))
- end
- if number =~ /(x+)/
- max_counter_size = Regexp.last_match(1).size
- refs = store['references']['softbins']
- i = 0
- possible = []
- proposal = number.sub(/x+/, i.to_s.rjust(max_counter_size, '0'))
- possible << proposal
- while refs[proposal] && i.to_s.size <= max_counter_size
- i += 1
- proposal = number.sub(/x+/, i.to_s.rjust(max_counter_size, '0'))
- possible << proposal
- end
- # Overflowed, need to go search for the oldest duplicate now
- if i.to_s.size > max_counter_size
- i = 0
- # Not the most efficient search algorithm, but this should be hit very rarely
- # and even then only to generate the bin the first time around
- p = refs.sort_by { |bin, last_used| last_used }.find do |bin, last_used|
- possible.include?(bin)
+ ([:bin, :softbin, :number] - [type]).each do |t|
+ if number =~ /(#{t.to_s[0]}+)/
+ max_size = Regexp.last_match(1).size
+ num = options[t].to_s
+ if num.size > max_size
+ fail "The allocated number, #{num}, overflows the #{t} field in the #{type} algorithm - #{algo}"
end
- proposal = p[0]
+ number = number.sub(/#{t.to_s[0]}+/, num.rjust(max_size, '0'))
end
- number = proposal
end
- else
- fail "Unknown softbin algorithm: #{algo}"
- end
- number.to_i
- elsif callback = config.softbins.callback
- callback.call(bin, options)
- else
- if store['pointers']['softbins'] == 'done'
- reclaim_softbin(options)
- else
- b = config.softbins.include.next(after: @last_softbin, size: options[:size])
- @last_softbin = nil
- while b && (store['manually_assigned']['softbins'][b.to_s] || config.softbins.exclude.include?(b))
- b = config.softbins.include.next(size: options[:size])
- end
- # When no softbin is returned it means we have used them all, all future generation
- # now switches to reclaim mode
- if b
- store['pointers']['softbins'] = b
- else
- store['pointers']['softbins'] = 'done'
- reclaim_softbin(options)
- end
- end
- end
- end
- def reclaim_softbin(options)
- store['references']['softbins'] = store['references']['softbins'].sort_by { |k, v| v }.to_h
- if options[:size] == 1
- store['references']['softbins'].first[0].to_i
- else
- reclaim(store['references']['softbins'], options)
- end
- end
-
- def allocate_number(options)
- bin = options[:bin]
- softbin = options[:softbin]
- return nil if config.numbers.empty?
- if config.numbers.algorithm
- algo = config.numbers.algorithm.to_s.downcase
- if algo.to_s =~ /^[bs\dx]+$/
- number = algo.to_s
- bin = bin.to_s
- if number =~ /(b+)/
- max_bin_size = Regexp.last_match(1).size
- if bin.size > max_bin_size
- fail "Bin number (#{bin}) overflows the test number algorithm (#{algo})"
- end
- number = number.sub(/b+/, bin.rjust(max_bin_size, '0'))
- end
- softbin = softbin.to_s
- if number =~ /(s+)/
- max_softbin_size = Regexp.last_match(1).size
- if softbin.size > max_softbin_size
- fail "Softbin number (#{softbin}) overflows the test number algorithm (#{algo})"
- end
- number = number.sub(/s+/, softbin.rjust(max_softbin_size, '0'))
- end
if number =~ /(x+)/
max_counter_size = Regexp.last_match(1).size
- refs = store['references']['numbers']
+ refs = store['references'][type_plural]
i = 0
possible = []
- proposal = number.sub(/x+/, i.to_s.rjust(max_counter_size, '0'))
+ proposal = number.sub(/x+/, i.to_s.rjust(max_counter_size, '0')).to_i.to_s
possible << proposal
while refs[proposal] && i.to_s.size <= max_counter_size
i += 1
- proposal = number.sub(/x+/, i.to_s.rjust(max_counter_size, '0'))
+ proposal = number.sub(/x+/, i.to_s.rjust(max_counter_size, '0')).to_i.to_s
possible << proposal
end
# Overflowed, need to go search for the oldest duplicate now
if i.to_s.size > max_counter_size
i = 0
@@ -602,42 +508,45 @@
end
proposal = p[0]
end
number = proposal
end
- number.to_i
else
- fail "Unknown test number algorithm: #{algo}"
+ fail "Illegal algorithm: #{algo}"
end
- elsif callback = config.numbers.callback
- callback.call(bin, softbin, options)
+ number.to_i
+ elsif callback = conf.callback
+ callback.call(options)
else
- if store['pointers']['numbers'] == 'done'
- reclaim_number(options)
+ if store['pointers'][type_plural] == 'done'
+ reclaim_item(type, options)
else
- b = config.numbers.include.next(after: @last_number, size: options[:size])
- @last_number = nil
- while b && (store['manually_assigned']['numbers'][b.to_s] || config.numbers.exclude.include?(b))
- b = config.numbers.include.next(size: options[:size])
+ b = conf.include.next(after: instance_variable_get("@last_#{type}"), size: options[:size])
+ instance_variable_set("@last_#{type}", nil)
+ while b && (store['manually_assigned'][type_plural][b.to_s] || conf.exclude.include?(b))
+ b = conf.include.next(size: options[:size])
end
# When no number is returned it means we have used them all, all future generation
# now switches to reclaim mode
if b
- store['pointers']['numbers'] = b
+ store['pointers'][type_plural] = b + (options[:size] || 1) - 1
+ b
else
- store['pointers']['numbers'] = 'done'
- reclaim_number(options)
+ store['pointers'][type_plural] = 'done'
+ reclaim_item(type, options)
end
end
end
end
- def reclaim_number(options)
- store['references']['numbers'] = store['references']['numbers'].sort_by { |k, v| v }.to_h
+ def reclaim_item(type, options)
+ type_plural = "#{type}s"
+ store['references'][type_plural] = store['references'][type_plural].sort_by { |k, v| v }.to_h
if options[:size] == 1
- store['references']['numbers'].first[0].to_i
+ v = store['references'][type_plural].first
+ v[0].to_i if v
else
- reclaim(store['references']['numbers'], options)
+ reclaim(store['references'][type_plural], options)
end
end
# Returns the oldest number in the given reference hash, however also supports a :size option
# and in that case it will look for the oldest contiguous range of the given size