diff --git a/generators/input_loader.rb b/generators/input_loader.rb index 006a528c5d458d61457506767af153334e4753fa..de15424499157cb3c0926bc6cdf429dd03c10296 100755 --- a/generators/input_loader.rb +++ b/generators/input_loader.rb @@ -1,41 +1,87 @@ #!/usr/bin/ruby +require 'pp' require 'pathname' require 'yaml' require 'json' +# Merge a and b. If a and b are hashes, they are recursively merged. +# - a and b might be strings or nil. +# - b values have the highest priority (if not nil). +def deep_merge_entries(a, b) + if b.is_a?(Hash) + a.is_a?(Hash) ? a.deep_merge(b) : b + else + b.nil? ? a : b + end +end + # Extend Hash with helper methods needed to convert input data files to ruby Hash class ::Hash - # Recursively merge this Hash with another - def deep_merge(second) + # Recursively merge this Hash with another (ie. merge nested hash) + # Returns a new hash containing the contents of other_hash and the contents of hash. The value for entries with duplicate keys will be that of other_hash: + # a = {"key": "value_a"} + # b = {"key": "value_b"} + # pp a.deep_merge(b) => {:key=>"value_b"} + # pp b.deep_merge(a) => {:key=>"value_a"} + def deep_merge(other_hash) merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 } - self.merge(second, &merger) + self.merge(other_hash, &merger) end - # Merge keys that match "PREFIX-<a-b>" with others keys that begins by - # "PREFIX-" and that ends with x, where a<=x<=b - # a and/or b may be ommited, meaning that there are no lower and/or upper bound for x - # This is done recursively (for this Hash and every Hashes it contains) - # i.e.: - # {"foo-1": {a: 0}, "foo-2": {a: 0}, "foo-3": {a: 0}, "foo-<2->": {b: 1}}.kbracket_merge() - # -> {"foo-2": {a: 0, b:1}, "foo-3": {a: 0, b: 0}} - # TODO: only "<->" suffix is currently implemented - def kbracket_merge() - self.each { |k_all, v_all| - if k_all.to_s.end_with?('-<->') and v_all.is_a?(Hash) - key_prefix_to_merge = k_all.to_s.sub(/-<->$/,'') - self.each { |k, v| - if k.to_s.start_with?(key_prefix_to_merge) - self[k] = v_all.deep_merge(v) - end + # Merge keys that match "PREFIX-<a-b>" with others keys that begins by "PREFIX-" + # and that ends with x, where a<=x<=b. + # - This is done recursively (for this Hash and every Hashes it may contain). + # - PREFIX-<a-b> values have lower priority on existing PREFIX-x keys. + # - "a" and/or "b" may be omited (ie. "PREFIX-<a->", "PREFIX-<-b>" or "PREFIX-<->"), meaning that there are no lower and/or upper bound for x. + # * If only a is omited, a == 1. + # * If b is omited, only existing keys are modified (no keys are created). Otherwise, PREFIX-<a> to PREFIX-<b> entries are created (if missing). + # Example: + # {"foo-1": {a: 0}, "foo-2": {a: 0}, "foo-3": {a: 0}, "foo-<2->": {b: 1}}.expand_angle_brackets() + # -> {"foo-1": {a: 0}, "foo-2": {a: 0, b:1}, "foo-3": {a: 0, b: 0}} + def expand_angle_brackets() + dup = self.clone # because can't add a new key into hash during iteration + + # Looking up for PREFIX-<a-b> keys + dup.each { |key_ab, value_ab| + prefix, a, b = key_ab.to_s.scan(/^(.*)-<(\d*)-(\d*)>$/).first + next if not a and not b # not found + a != "" ? a = a.to_i : a = 1 + b != "" ? b = b.to_i : b + + if b != "" + + # Merge keys, creating missing entries if needed. + (a..b).each { |x| + key = "#{prefix}-#{x}" + key = key.to_sym if key_ab.is_a?(Symbol) + + # For duplicate entries, the value of PREFIX-x is kept. + self[key] = deep_merge_entries(value_ab, self[key]) + } + + else + # Modify only existing keys. Looking up for PREFIX-x keys. + dup.each { |key_x, value_x| + next if key_x.class != key_ab.class + x = key_x.to_s.scan(/^#{prefix}-(\d*)$/).first + x = x.first if x + next if not x or x.to_i < a + + # For duplicate entries, the value of PREFIX-x is kept. + self[key_x] = deep_merge_entries(value_ab, value_x) } - self.delete(k_all) end + + # Delete entry "PREFIX-<a-b>" + self.delete(key_ab) } - self.each { |k, v| - if v.is_a?(Hash) - v.kbracket_merge() + + # Do it recursivly + self.each { |key, value| + if value.is_a?(Hash) + value.expand_angle_brackets() end } end @@ -47,21 +93,37 @@ class ::Hash end end -#TODO: Ensure that deepest elements in input files hierarchy have lowest priority -data = Hash.new +global_hash = {} # the global data structure + +Dir.chdir("../input/grid5000/") -Dir.chdir("input") -Dir['**/*.y*ml'].each { |f| - node_path = Pathname.new(f) - node_dir, _ = node_path.split() +# Recursively list the .yaml files. +# The order in which the results are returned depends on the system (http://ruby-doc.org/core-2.2.3/Dir.html). +# => List deepest files first as they have lowest priority when hash keys are duplicated. +list_of_yaml_files = Dir['**/*.y*ml'].sort_by { |x| -x.count('/') } - node_hierarchy = node_dir.to_s.split('/') - node_value = YAML::load_file(node_path) +list_of_yaml_files.each { |filename| - node_data = Hash.from_array(node_hierarchy,node_value) - data = data.deep_merge(node_data) + # Load YAML + file_hash = YAML::load_file(filename) + if not file_hash + puts "Error loading '#{filename}'" + next + end + + # Expand the hash + file_hash.expand_angle_brackets() + + # Inject the file content into the global_hash, at the right place + path_hierarchy = File.dirname(filename).split('/') # Split the file path (path relative to input/) + file_hash = Hash.from_array(path_hierarchy, file_hash) # Build the nested hash hierarchy according to the file path + global_hash = global_hash.deep_merge(file_hash) # Merge global_hash and file_hash. The value for entries with duplicate keys will be that of file_hash } -data.kbracket_merge() -puts JSON.generate(data) +#pp global_hash + +#pp data +#puts JSON.generate(data) + + diff --git a/generators/input_loader_tests.rb b/generators/input_loader_tests.rb new file mode 100644 index 0000000000000000000000000000000000000000..7abaa80f59dcd69b0b3c5d2bc574eddfc071fa9b --- /dev/null +++ b/generators/input_loader_tests.rb @@ -0,0 +1,125 @@ +require_relative "input_loader" +require "test/unit" +require "hashdiff" + +# Helper test routine. Test if the hashes are equal. Also do the test for symbolized keys. +def assert_equal_hash(hash1, hash2) + diff = HashDiff.diff(hash1, hash2) + assert_equal([], diff) +end + +# Helper test routine. Test if the hashes are equal after having been expanded. +def assert_equal_expanded_hash(hash1, hash2) + expanded_hash1 = hash1.clone.expand_angle_brackets() + expanded_hash2 = hash2.clone.expand_angle_brackets() + assert_equal_hash(expanded_hash1, expanded_hash2) +end + +class TestInputLoader < Test::Unit::TestCase + + def test__deep_merge_entries + a, b = 1, 2 + ha = {key: "value_a"} + hb = {key: "value_b"} + + # Check + assert_equal_hash(deep_merge_entries(a, b), b) # numeric - numeric + assert_equal_hash(deep_merge_entries(a, hb), hb) # numeric - hash + assert_equal_hash(deep_merge_entries(ha, b), b) # hash - numeric + assert_equal_hash(deep_merge_entries(ha, hb), hb) # hash - hash + + assert_equal_hash(deep_merge_entries(a, nil), a) # numeric - nil + assert_equal_hash(deep_merge_entries(nil, b), b) # nil - numeric + assert_equal_hash(deep_merge_entries(ha, nil), ha) # hash - nil + assert_equal_hash(deep_merge_entries(nil, hb), hb) # nil - hash + assert_equal_hash(deep_merge_entries(nil, nil), nil) # nil - nil + + # Check recursivity + # At the last level + a = {"c-1": {"c-2": {"a-3": 0}}} + b = {"c-1": {"c-2": {"b-3": 0}}} + assert_equal_hash(deep_merge_entries(a, b), {"c-1": {"c-2": {"a-3": 0, "b-3": 0}}}) + + # At an intermediate level + a = {"c-1": {"a-2": {"a-3": 0}}} + b = {"c-1": {"b-2": {"b-3": 0}}} + assert_equal_hash(deep_merge_entries(a, b), {"c-1": { + "a-2": {"a-3": 0}, + "b-2": {"b-3": 0} + } + }) + end + + # Test the example given in documentation + def test__expand_angle_brackets__doc_example + hash = { + "foo-1": {a: 0}, + "foo-2": {a: 0}, + "foo-3": {a: 0}, + "foo-<2->": {b: 1} + } + + expected_expanded_hash = { + "foo-1": {a: 0}, + "foo-2": {a: 0, b: 1}, + "foo-3": {a: 0, b: 1} + } + + assert_equal_expanded_hash(hash, expected_expanded_hash) + end + + # The 'a' parameter + def test__expand_angle_brackets__a_values + assert_equal_expanded_hash({"foo-<-3>": 0}, {"foo-1": 0, "foo-2": 0, "foo-3": 0}) # Default 'a' value is 1 + assert_equal_expanded_hash({"foo-<2-3>": 0}, {"foo-2": 0, "foo-3": 0}) # Simply check if the value of 'a' is taken into account + end + + def test__expand_angle_brackets__create_keys + # + # If 'b' is given, create missing keys. Also, the keys must be of the same type (Symbol or String). + # + + # With symbol keys and numeric values + assert_equal_expanded_hash({"foo-<2-3>": 0}, {"foo-2": 0, "foo-3": 0}) + assert_equal_expanded_hash({"foo-<-2>": 0}, {"foo-1": 0, "foo-2": 0}) + + # With symbol keys and hash values + assert_equal_expanded_hash({"foo-<2-3>": {a: 0}}, {"foo-2": {a: 0}, "foo-3": {a: 0}}) + assert_equal_expanded_hash({"foo-<-2>": {a: 0}}, {"foo-1": {a: 0}, "foo-2": {a: 0}}) + + # With string keys and numeric values + assert_equal_expanded_hash({"foo-<2-3>" => 0}, {"foo-2" => 0, "foo-3" => 0}) + assert_equal_expanded_hash({"foo-<-2>" => 0}, {"foo-1" => 0, "foo-2" => 0}) + + # With string keys and hash values + assert_equal_expanded_hash({"foo-<2-3>" => {a: 0}}, {"foo-2" => {a: 0}, "foo-3" => {a: 0}}) + assert_equal_expanded_hash({"foo-<-2>" => {a: 0}}, {"foo-1" => {a: 0}, "foo-2" => {a: 0}}) + + # + # If 'b' is not given, do not create any new key + # + assert_equal_expanded_hash({"foo-<->": 0}, {}) # All + assert_equal_expanded_hash({"foo-<2->": 0}, {}) + + end + + def test__expand_angle_brackets__keep_existing_keys + # + # Do not modify existing keys + # + + [0, {h: 0}].each { |v| + assert_equal_expanded_hash({"foo-<1-3>": v, "foo-2": 1}, {"foo-1": v, "foo-2": 1, "foo-3": v}) # b given + assert_equal_expanded_hash({"foo-<2->": v, "foo-2": 1}, {"foo-2": 1}) # b not given + } + end + + # Some tests with nil values (=> nil values are created, existing nil values are overriden) + def test__expand_angle_brackets__nil + assert_equal_expanded_hash({"foo-<2-3>": nil}, {"foo-2": nil, "foo-3": nil}) # PREFIX-<a-b> is nil + assert_equal_expanded_hash({"foo-<2-3>": 0, "foo-2": nil}, {"foo-2": 0, "foo-3": 0}) # PREFIX-x is nil + assert_equal_expanded_hash({"foo-<2->": nil, "foo-3": 0}, {"foo-3": 0}) # PREFIX-<a-> is nil + assert_equal_expanded_hash({"foo-<2->": 0, "foo-3": nil}, {"foo-3": 0}) # PREFIX-x is nil + end + +end