lib/test_ids/allocator.rb in test_ids-0.6.1 vs lib/test_ids/allocator.rb in test_ids-0.7.0

- old
+ new

@@ -3,10 +3,12 @@ # existing assignments. # # There is one allocator instance per configuration, and each has its own database # file. class Allocator + STORE_FORMAT_REVISION = 1 + attr_reader :config def initialize(configuration) @config = configuration end @@ -16,66 +18,144 @@ def allocate(instance, options) clean(options) @callbacks = [] name = extract_test_name(instance, options) name = "#{name}_#{options[:index]}" if options[:index] - store['tests'][name] ||= {} - t = store['tests'][name] + + # 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 + else + bin_id = name + end + if (options[:softbin].is_a?(Symbol) || options[:softbin].is_a?(String)) && options[:softbin] != :none + softbin_id = options[:softbin].to_s + else + softbin_id = name + end + if (options[:number].is_a?(Symbol) || options[:number].is_a?(String)) && options[:number] != :none + number_id = options[:number].to_s + else + number_id = name + 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 + + bin = store['assigned']['bin'][bin_id] ||= {} + softbin = store['assigned']['softbin'][softbin_id] ||= {} + number = store['assigned']['number'][number_id] ||= {} + # 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) - t['bin'] = options[:bin] + bin['number'] = options[:bin] + bin['size'] = bin_size store['manually_assigned']['bin'][options[:bin].to_s] = true # Regenerate the bin if the original allocation has since been applied # manually elsewhere - elsif store['manually_assigned']['bin'][t['bin'].to_s] - t['bin'] = nil + elsif store['manually_assigned']['bin'][bin['number'].to_s] + bin['number'] = nil + bin['size'] = nil # Also regenerate these as they could be a function of the bin - t['softbin'] = nil if config.softbins.function? - t['number'] = nil if config.numbers.function? + if config.softbins.function? + softbin['number'] = nil + softbin['size'] = nil + end + if config.numbers.function? + number['number'] = nil + number['size'] = nil + end end if options[:softbin] && options[:softbin].is_a?(Numeric) - t['softbin'] = options[:softbin] + softbin['number'] = options[:softbin] + softbin['size'] = softbin_size store['manually_assigned']['softbin'][options[:softbin].to_s] = true - elsif store['manually_assigned']['softbin'][t['softbin'].to_s] - t['softbin'] = nil + elsif store['manually_assigned']['softbin'][softbin['number'].to_s] + softbin['number'] = nil + softbin['size'] = nil # Also regenerate the number as it could be a function of the softbin - t['number'] = nil if config.numbers.function? + if config.numbers.function? + number['number'] = nil + number['size'] = nil + end end if options[:number] && options[:number].is_a?(Numeric) - t['number'] = options[:number] + number['number'] = options[:number] + number['size'] = number_size store['manually_assigned']['number'][options[:number].to_s] = true - elsif store['manually_assigned']['number'][t['number'].to_s] - t['number'] = nil + elsif store['manually_assigned']['number'][number['number'].to_s] + number['number'] = nil + number['size'] = nil end + # Otherwise generate the missing ones - t['bin'] ||= allocate_bin - t['softbin'] ||= allocate_softbin(t['bin']) - t['number'] ||= allocate_number(t['bin'], t['softbin']) + bin['number'] ||= allocate_bin(size: bin_size) + bin['size'] ||= bin_size + softbin['number'] ||= allocate_softbin(bin: bin['number'], size: softbin_size) + softbin['size'] ||= softbin_size + number['number'] ||= allocate_number(bin: bin['number'], softbin: softbin['number'], size: number_size) + number['size'] ||= number_size + # Record that there has been a reference to the final numbers time = Time.now.to_f - store['references']['bin'][t['bin'].to_s] = time if t['bin'] && options[:bin] != :none - store['references']['softbin'][t['softbin'].to_s] = time if t['softbin'] && options[:softbin] != :none - store['references']['number'][t['number'].to_s] = time if t['number'] && options[:number] != :none - # Update the supplied options hash that will be forwarded to the - # program generator - options[:bin] = t['bin'] unless options.delete(:bin) == :none - options[:softbin] = t['softbin'] unless options.delete(:softbin) == :none - options[:number] = t['number'] unless options.delete(:number) == :none + bin_size.times do |i| + store['references']['bin'][(bin['number'] + i).to_s] = time if bin['number'] && options[:bin] != :none + end + softbin_size.times do |i| + store['references']['softbin'][(softbin['number'] + i).to_s] = time if softbin['number'] && options[:softbin] != :none + end + number_size.times do |i| + store['references']['number'][(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 def store @store ||= begin s = JSON.load(File.read(file)) if file && File.exist?(file) if s + if s['format_revision'] != STORE_FORMAT_REVISION + # Upgrade the original store format + t = { 'bin' => {}, 'softbin' => {}, 'number' => {} } + s['tests'].each do |name, numbers| + t['bin'][name] = { 'number' => numbers['bin'], 'size' => 1 } + t['softbin'][name] = { 'number' => numbers['softbin'], 'size' => 1 } + t['number'][name] = { 'number' => numbers['number'], 'size' => 1 } + end + s = { + 'format_revision' => STORE_FORMAT_REVISION, + 'assigned' => t, + 'manually_assigned' => s['manually_assigned'], + 'pointers' => s['pointers'], + 'references' => s['references'] + } + end @last_bin = s['pointers']['bin'] @last_softbin = s['pointers']['softbin'] @last_number = s['pointers']['number'] s else - { 'tests' => {}, + { + 'format_revision' => STORE_FORMAT_REVISION, + 'assigned' => { 'bin' => {}, 'softbin' => {}, 'number' => {} }, 'manually_assigned' => { 'bin' => {}, 'softbin' => {}, 'number' => {} }, 'pointers' => { 'bin' => nil, 'softbin' => nil, 'number' => nil }, 'references' => { 'bin' => {}, 'softbin' => {}, 'number' => {} } } end @@ -106,37 +186,42 @@ private # 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 + def allocate_bin(options) return nil if config.bins.empty? if store['pointers']['bin'] == 'done' - reclaim_bin + reclaim_bin(options) else - b = config.bins.include.next(@last_bin) + b = config.bins.include.next(after: @last_bin, size: options[:size]) @last_bin = nil while b && (store['manually_assigned']['bin'][b.to_s] || config.bins.exclude.include?(b)) - b = config.bins.include.next + 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']['bin'] = b else store['pointers']['bin'] = 'done' - reclaim_bin + reclaim_bin(options) end end end - def reclaim_bin + def reclaim_bin(options) store['references']['bin'] = store['references']['bin'].sort_by { |k, v| v }.to_h - store['references']['bin'].first[0].to_i + if options[:size] == 1 + store['references']['bin'].first[0].to_i + else + reclaim(store['references']['bin'], options) + end end - def allocate_softbin(bin) + def allocate_softbin(options) + bin = options[:bin] return nil if config.softbins.empty? if config.softbins.algorithm algo = config.softbins.algorithm.to_s.downcase if algo.to_s =~ /^[b\dx]+$/ number = algo.to_s @@ -178,35 +263,41 @@ number.to_i elsif callback = config.softbins.callback callback.call(bin) else if store['pointers']['softbin'] == 'done' - reclaim_softbin + reclaim_softbin(options) else - b = config.softbins.include.next(@last_softbin) + b = config.softbins.include.next(after: @last_softbin, size: options[:size]) @last_softbin = nil while b && (store['manually_assigned']['softbin'][b.to_s] || config.softbins.exclude.include?(b)) - b = config.softbins.include.next + 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']['softbin'] = b else store['pointers']['softbin'] = 'done' - reclaim_softbin + reclaim_softbin(options) end end end end - def reclaim_softbin + def reclaim_softbin(options) store['references']['softbin'] = store['references']['softbin'].sort_by { |k, v| v }.to_h - store['references']['softbin'].first[0].to_i + if options[:size] == 1 + store['references']['softbin'].first[0].to_i + else + reclaim(store['references']['softbin'], options) + end end - def allocate_number(bin, softbin) + 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 @@ -256,31 +347,83 @@ end elsif callback = config.numbers.callback callback.call(bin, softbin) else if store['pointers']['number'] == 'done' - reclaim_number + reclaim_number(options) else - b = config.numbers.include.next(@last_number) + b = config.numbers.include.next(after: @last_number, size: options[:size]) @last_number = nil while b && (store['manually_assigned']['number'][b.to_s] || config.numbers.exclude.include?(b)) - b = config.numbers.include.next + b = config.numbers.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']['number'] = b else store['pointers']['number'] = 'done' - reclaim_number + reclaim_number(options) end end end end - def reclaim_number + def reclaim_number(options) store['references']['number'] = store['references']['number'].sort_by { |k, v| v }.to_h - store['references']['number'].first[0].to_i + if options[:size] == 1 + store['references']['number'].first[0].to_i + else + reclaim(store['references']['number'], 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 + def reclaim(refs, options) + a = [] + i = 0 # Pointer to references hash, which is already sorted with oldest first + s = 0 # Largest contiguous size in the array of considered numbers + p = 0 # Pointer to start of a suitable contiguous section in the array + while s < options[:size] && i < refs.size + a << refs.keys[i].to_i + a.sort! + s, p = largest_contiguous_section(a) + i += 1 + end + a[p] + end + + def largest_contiguous_section(array) + max_ptr = 0 + max_size = 0 + p = nil + s = nil + prev = nil + array.each_with_index do |v, i| + if prev + if v == prev + 1 + s += 1 + else + if s > max_size + max_size = s + max_ptr = p + end + p = i + s = 1 + end + prev = v + else + p = i + s = 1 + prev = v + end + end + if s > max_size + max_size = s + max_ptr = p + end + [max_size, max_ptr] end def extract_test_name(instance, options) name = options[:test_id] || options[:name] unless name