diff --git a/lib/sdf/erb.rb b/lib/sdf/erb.rb new file mode 100644 index 0000000..0e71d90 --- /dev/null +++ b/lib/sdf/erb.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "erb" + +module SDF + # Module for handling ERB files + module ERB + module_function + + # Parses an ERB string and returns the raw rendered string + # + # @param [String] erb_content ERB template file content as string + # @param [Hash] erb_args the configuration arguments to evaluate + # @return [String] the raw rendered XML string representing the model + def parse_erb_as_str(erb_content, **erb_args) + erb_engine = ::ERB.new(erb_content, trim_mode: "-") + + # Render the ERB template with the passed hash arguments + erb_engine.result_with_hash(erb_args) + end + + # Renders an ERB template and returns it as a REXML::Document + # + # @return [REXML::Document] the rendered sdf model + def render_erb_sdf_model(path, **erb_args) + erb_content = File.read(path) + solved_erb_as_sdf_str = parse_erb_as_str(erb_content, **erb_args) + + REXML::Document.new(solved_erb_as_sdf_str) + end + end +end diff --git a/lib/sdf/erb_loader.rb b/lib/sdf/erb_loader.rb new file mode 100644 index 0000000..77751b1 --- /dev/null +++ b/lib/sdf/erb_loader.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative "erb" +require_relative "sdf_loader" + +module SDF + # class to load SDF and ERB templated SDF files + class ERBLoader < Loader + def initialize(erb_args: {}) + super() + @erb_args = erb_args + end + + def parse_sdf_document(sdf_file) + SDF::ERB.render_erb_sdf_model(sdf_file, **@erb_args) + end + end +end diff --git a/lib/sdf/exceptions.rb b/lib/sdf/exceptions.rb index 5efb385..a0cb2b6 100644 --- a/lib/sdf/exceptions.rb +++ b/lib/sdf/exceptions.rb @@ -1,3 +1,24 @@ module SDF class InternalError < RuntimeError; end + + module XML + # Exception raised when trying to load a model URI, but the model does + # not contain a SDF entry for the required SDF version + class UnavailableSDFVersionInModel < ArgumentError; end + # Exception raised when trying to load a file that is not a SDF file + class NotSDF < ArgumentError; end + # Exception raised when trying to load a malformed XML file + class InvalidXML < ArgumentError; end + + # Exception raised when trying to resolve a model that cannot be found + # in {model_path} + class NoSuchModel < ArgumentError + attr_reader :model_name + + def initialize(model_name) + super + @model_name = model_name + end + end + end end diff --git a/lib/sdf/root.rb b/lib/sdf/root.rb index cdbabba..5d7035c 100644 --- a/lib/sdf/root.rb +++ b/lib/sdf/root.rb @@ -28,12 +28,12 @@ def initialize(xml, metadata = {}) # @raise [XML::NotSDF] if the file is not a SDF file # @raise [XML::InvalidXML] if the file is not a valid XML file # @return [Root] - def self.load(sdf_file, expected_sdf_version = nil, flatten: true) + def self.load(sdf_file, expected_sdf_version = nil, flatten: true, loader: SDF::Loader.new) if sdf_file =~ %r{^model://(.*)} load_from_model_name(::Regexp.last_match(1), expected_sdf_version, - flatten: flatten) + flatten: flatten, loader: loader) else - xml, metadata = XML.load_sdf(sdf_file, flatten: flatten, metadata: true) + xml, metadata = XML.load_sdf(sdf_file, flatten: flatten, metadata: true, loader: loader) new(xml.root, metadata) end end @@ -48,9 +48,9 @@ def self.load(sdf_file, expected_sdf_version = nil, flatten: true) # (as version * 100, i.e. version 1.5 is represented by 150). Leave to # nil to always read the latest. # @return [Root] - def self.load_from_model_name(model_name, sdf_version = nil, flatten: true) + def self.load_from_model_name(model_name, sdf_version = nil, flatten: true, loader: SDF::Loader.new) xml, metadata = XML.model_from_name(model_name, sdf_version, - flatten: flatten, metadata: true) + flatten: flatten, metadata: true, loader: loader) new(xml.root, metadata) end diff --git a/lib/sdf/sdf_loader.rb b/lib/sdf/sdf_loader.rb new file mode 100644 index 0000000..1daf41f --- /dev/null +++ b/lib/sdf/sdf_loader.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative "erb" +require_relative "exceptions" + +module SDF + # class to load SDF and ERB templated SDF files + class Loader + # Open a SDF file SDF file and returns its XML representation. + # + # @param [String] sdf_file the path to the SDF file + # @raise [Errno::ENOENT] if the files does not exist + # @raise [NotSDF] if the file is not a SDF file + # @raise [InvalidXML] if the file is not a valid XML file + # @return [REXML::Element] sdf_file's content as a REXML::Element instance + def load_sdf_raw(sdf_file) + find_or_raise_file_not_found(sdf_file) + + sdf = begin + parse_sdf_document(sdf_file) + rescue REXML::ParseException => e + unless e.message.include?("No root") + raise SDF::XML::InvalidXML, + "Cannot load #{sdf_file}: #{e.message}" + end + + REXML::Document.new + end + validate_sdf_root(sdf, sdf_file) + + sdf + end + + private + + def find_or_raise_file_not_found(sdf_file) + return if File.exist?(sdf_file) + + file_name = File.basename(sdf_file) + dir_path = File.dirname(sdf_file) + raise Errno::ENOENT, + "Cannot find '#{file_name}' in '#{dir_path}'." \ + "You probably want to update the GAZEBO_MODEL_PATH " \ + "environment variable, or set SDF.model_path explicitly." + end + + def parse_sdf_document(sdf_file) + File.open(sdf_file) do |io| + REXML::Document.new(io) + end + end + + def validate_sdf_root(sdf, sdf_file) + unless sdf.root + raise SDF::XML::NotSDF, + "#{sdf_file} can be parsed as an XML file, but it " \ + "does not have a root" + end + return if %w[sdf gazebo].include?(sdf.root.name) + + raise SDF::XML::NotSDF, "#{sdf_file} is not a SDF file" + end + end +end diff --git a/lib/sdf/xml.rb b/lib/sdf/xml.rb index 66ed023..035f05e 100644 --- a/lib/sdf/xml.rb +++ b/lib/sdf/xml.rb @@ -1,4 +1,7 @@ require "rexml/document" +require_relative "exceptions" +require_relative "sdf_loader" +require_relative "erb_loader" module SDF module XML @@ -7,24 +10,6 @@ module XML # (as version * 100, i.e. version 1.5 is represented by 150). Leave to # nil to always read the latest. - # Exception raised when trying to load a model URI, but the model does - # not contain a SDF entry for the required SDF version - class UnavailableSDFVersionInModel < ArgumentError; end - # Exception raised when trying to load a file that is not a SDF file - class NotSDF < ArgumentError; end - # Exception raised when trying to load a malformed XML file - class InvalidXML < ArgumentError; end - - # Exception raised when trying to resolve a model that cannot be found - # in {model_path} - class NoSuchModel < ArgumentError - attr_reader :model_name - - def initialize(model_name) - @model_name = model_name - end - end - # The search path for models # # It defaults to GAZEBO_MODEL_PATH @@ -83,9 +68,9 @@ def self.clear_cache # SDF file for the required SDF version # # @return [REXML::Element] - def self.load_gazebo_model(dir, sdf_version = nil, metadata: false, flatten: true) + def self.load_gazebo_model(dir, sdf_version = nil, metadata: false, flatten: true, loader: SDF::Loader.new) load_sdf(model_path_of(dir, sdf_version), metadata: metadata, - flatten: flatten) + flatten: flatten, loader: loader) end # Find model string into model.config path @@ -128,7 +113,7 @@ def self.model_path_of(dir, sdf_version = nil) # # @!macro sdf_version # @return [Hash] - def self.gazebo_models(sdf_version = nil) + def self.gazebo_models(sdf_version = nil, loader: SDF::ERBLoader.new) @gazebo_models[sdf_version] ||= {} @model_path.each do |p| Dir.glob(File.join(p, "*")) do |subdir| @@ -140,7 +125,7 @@ def self.gazebo_models(sdf_version = nil) begin sdf_file_path = model_path_of(subdir, sdf_version) sdf, metadata = load_sdf(sdf_file_path, metadata: true, - flatten: false) + flatten: false, loader: loader) @gazebo_models[sdf_version][File.basename(subdir)] = ModelCacheEntry.new(sdf_file_path, sdf, metadata) rescue UnavailableSDFVersionInModel @@ -165,7 +150,7 @@ def self.gazebo_models(sdf_version = nil) # @raise (see model_path_of) # @raise [NoSuchModel] if the provided model name does not resolve to a # model in {model_path} - # @return [REXML::Element] + # @return [String] the path to the SDF file for the model def self.model_path_from_name(model_name, model_path: @model_path, sdf_version: nil) @gazebo_models[sdf_version] ||= {} cache = (@gazebo_models[sdf_version][model_name] ||= ModelCacheEntry.new) @@ -193,12 +178,12 @@ def self.model_path_from_name(model_name, model_path: @model_path, sdf_version: # model in {model_path} # @return [REXML::Element] def self.model_from_name( - model_name, sdf_version = nil, metadata: false, flatten: true + model_name, sdf_version = nil, metadata: false, flatten: true, loader: SDF::Loader.new ) path = model_path_from_name(model_name, sdf_version: sdf_version) cache = @gazebo_models[sdf_version][model_name] unless cache.xml - cache.xml, cache.metadata = load_sdf(path, metadata: true, flatten: false) + cache.xml, cache.metadata = load_sdf(path, metadata: true, flatten: false, loader: loader) end xml = cache.xml if flatten @@ -213,6 +198,23 @@ def self.model_from_name( end end + # Resolves relative paths and model:// URIs in the XML tree in-place + # + # This method traverses the XML tree starting from the given node, and + # expands any relative paths or `model://` URIs inside `` tags to + # absolute paths on the local filesystem. + # + # It skips `` tags because those are resolved separately during + # {.add_include_tags}. + # + # @example Replaces a model:// mesh path: + # # Before: model://robot_model/hull.dae + # # After: /path/to/workspace/robot_models/models/sdf/robot_model/hull.dae + # + # @param [REXML::Element] node the XML element to traverse + # @!macro sdf_version + # @param [String] base_path the base directory path used to resolve relative paths + # @return [void] def self.resolve_relative_uris(node, sdf_version, base_path) nodes = [node] until nodes.empty? @@ -264,16 +266,34 @@ def self.deep_copy_xml(node) # This method modifies the XML tree by replacing the include tags found # as direct children of the provided element by the included content. # + # @example + # # Before calling add_include_tags: + # # + # # + # # model://my_sensor + # # custom_sensor + # # 1 0 0 0 0 0 + # # + # # + # # + # # After calling add_include_tags: + # # + # # + # # 1 0 0 0 0 0 + # # ... + # # + # # + # # @param [REXML::Element] elem element to find include tags # @!macro sdf_version # @return [void] - def self.add_include_tags(elem, sdf_version, base_path) + def self.add_include_tags(elem, sdf_version, base_path, loader: SDF::Loader.new) includes = {} replacements = [] elem.elements.each do |inc| if inc.name == "world" || inc.name == "model" # model-within-model - added_includes = add_include_tags(inc, sdf_version, base_path) + added_includes = add_include_tags(inc, sdf_version, base_path, loader: loader) includes.merge! added_includes do |_, old, new| old + new end @@ -309,11 +329,11 @@ def self.add_include_tags(elem, sdf_version, base_path) included_sdf, included_metadata = model_from_name(model_name, sdf_version, metadata: true, - flatten: false) + flatten: false, loader: loader) elsif File.directory?(uri_path = File.expand_path(uri, base_path)) included_sdf, included_metadata = load_gazebo_model(uri_path, sdf_version, metadata: true, - flatten: false) + flatten: false, loader: loader) else raise ArgumentError, "URI #{uri} is neither a model:// URI nor an existing directory" @@ -362,39 +382,6 @@ def self.add_include_tags(elem, sdf_version, base_path) includes end - # Open a SDF file and returns the XML representation - # - # Unlike {.load_sdf}, this really only loads the XML information, not - # resolving the include tags. - # - # @param [String] sdf_file the path to the SDF file - # @raise [Errno::ENOENT] if the files does not exist - # @raise [NotSDF] if the file is not a SDF file - # @raise [InvalidXML] if the file is not a valid XML file - # @return [REXML::Element] - def self.load_sdf_raw(sdf_file) - sdf = File.open(sdf_file) do |io| - REXML::Document.new(io) - rescue REXML::ParseException => e - unless e.message.match?(/No root/) - raise InvalidXML, "cannot load #{sdf_file}: #{e.message}" - end - - REXML::Document.new - end - - unless sdf.root - raise NotSDF, - "#{sdf_file} can be parsed as an XML file, but it does not have a root" - end - - if sdf.root.name != "sdf" && sdf.root.name != "gazebo" - raise NotSDF, "#{sdf_file} is not a SDF file" - end - - sdf - end - # Get sdf_version # # @param [REXML::Element] sdf element @@ -441,6 +428,9 @@ def self.sdf_version_of(sdf) # @param [Boolean] metadata whether the method should return a metadata hash # about the various inclusions that have been performed. See above for # the hash format + # @param [#load_sdf_raw] loader object that acts as a loader.Takes a file path as input + # and returns a REXML::Element with its content. Must respond to + # `load_sdf_raw(path: String) -> REXML::Element` # @return [REXML::Element,(REXML::Element,Hash)] either the XML tree by itself # if `metadata` is false, or the pair of the tree and the metadata hash # otherwise. @@ -448,12 +438,12 @@ def self.sdf_version_of(sdf) # @raise [NotSDF] if the file is not a SDF file # @raise [InvalidXML] if the file is not a valid XML file # @return [REXML::Element] - def self.load_sdf(sdf_file, flatten: true, metadata: false) - sdf = load_sdf_raw(sdf_file) + def self.load_sdf(sdf_file, flatten: true, metadata: false, loader: SDF::Loader.new) + sdf = loader.load_sdf_raw(sdf_file) sdf_version = sdf_version_of(sdf) sdf_metadata = Hash["includes" => {}, "path" => sdf_file] - includes = add_include_tags(sdf.root, sdf_version, File.dirname(sdf_file)) + includes = add_include_tags(sdf.root, sdf_version, File.dirname(sdf_file), loader: loader) sdf_metadata["includes"].merge!(includes) do |_, old, new| old + new end diff --git a/test/data/models/simple_model_erb/model.config b/test/data/models/simple_model_erb/model.config new file mode 100644 index 0000000..8751eb6 --- /dev/null +++ b/test/data/models/simple_model_erb/model.config @@ -0,0 +1,5 @@ + + + simple_model + model.sdf.erb + diff --git a/test/data/models/simple_model_erb/model.sdf.erb b/test/data/models/simple_model_erb/model.sdf.erb new file mode 100644 index 0000000..bf43744 --- /dev/null +++ b/test/data/models/simple_model_erb/model.sdf.erb @@ -0,0 +1,42 @@ + +<% + default_gps_pose = [-0.679, 0.0, 1.920, 0.0, 0.0, 0.0] + default_gps2_pose = [2.571, 0.044, 0.808, 0.0, 0.0, 0.0] + + gps1_pose = (defined?(links) && links.find { |link| link[:name] == "gps" }&.dig(:pose)) || default_gps_pose + gps2_pose = (defined?(links) && links.find { |link| link[:name] == "gps2" }&.dig(:pose)) || default_gps2_pose +%> + + + + + + + + root + child + + + + + + <%= gps1_pose.join(' ') %> + + + root + gps + + + + <%= gps2_pose.join(' ') %> + + + root + gps2 + + + + + + + diff --git a/test/test_erb.rb b/test/test_erb.rb new file mode 100644 index 0000000..fbdad05 --- /dev/null +++ b/test/test_erb.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require "sdf/erb" +require "sdf/test" + +describe SDF::ERB do + it "parse_erb_as_str" do + erb_content = <<~XML + + + + + + + + + root + child + + + + + <% gps_sensors.each do |gps| %> + + <%= gps[:pose].join(' ') %> + + + root + <%= gps[:name] %> + + <% end %> + + + + + + + XML + + erb_args = { + model_name: "my_model_name", + gps_sensors: [ + { + name: "gps", + pose: [-0.679, 0.0, 1.920, 0.0, 0.0, 0.0] + }, + { + name: "gps2", + pose: [2.571, 0.044, 0.808, 0, 0, 0] + } + ] + } + resulting_sdf = SDF::ERB.parse_erb_as_str(erb_content, **erb_args) + + expected_content = <<~XML + + + + + + + + + root + child + + + + + + -0.679 0.0 1.92 0.0 0.0 0.0 + + + root + gps + + + + 2.571 0.044 0.808 0 0 0 + + + root + gps2 + + + + + + + + XML + + formatted_erb = resulting_sdf.gsub(/\s+/, " ").strip + formatted_expected = expected_content.gsub(/\s+/, " ").strip + + assert_equal(formatted_expected, formatted_erb) + end + + it "parse_erb_as_str_with_extra_unused_args" do + erb_content = <<~XML + + + + + + + + + root + child + + + + + <% gps_sensors.each do |gps| %> + + <%= gps[:pose].join(' ') %> + + + root + <%= gps[:name] %> + + <% end %> + + + + + + + XML + + erb_args = { + model_name: "my_model_name", + gps_sensors: [ + { + name: "gps", + pose: [-0.679, 0.0, 1.920, 0.0, 0.0, 0.0] + }, + { + name: "gps2", + pose: [2.571, 0.044, 0.808, 0, 0, 0] + } + ], + random_key: "random_value", + random_array: [1, 2, 3], + random_hash: { key1: "value1", key2: "value2" } + } + resulting_sdf = SDF::ERB.parse_erb_as_str(erb_content, **erb_args) + + expected_content = <<~XML + + + + + + + + + root + child + + + + + + -0.679 0.0 1.92 0.0 0.0 0.0 + + + root + gps + + + + 2.571 0.044 0.808 0 0 0 + + + root + gps2 + + + + + + + + XML + + formatted_erb = resulting_sdf.gsub(/\s+/, " ").strip + formatted_expected = expected_content.gsub(/\s+/, " ").strip + + assert_equal(formatted_expected, formatted_erb) + end + + it "parse_erb_as_str_raises_on_missing_args" do + erb_content = "" + # Missing :model_name in erb_args + assert_raises(NameError) do + SDF::ERB.parse_erb_as_str(erb_content) + end + end +end diff --git a/test/test_erb_loader.rb b/test/test_erb_loader.rb new file mode 100644 index 0000000..aef95f3 --- /dev/null +++ b/test/test_erb_loader.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "sdf/erb_loader" +require "sdf/test" + +describe SDF::ERBLoader do + describe "#loads real file" do + before(:all) do + @models_dir = File.expand_path("data/models", __dir__) + @simple_model = File.join(@models_dir, "/simple_model_erb/model.sdf.erb") + end + + it "loads a real .sdf.erb file with args" do + erb_args = { + links: [ + { + name: "gps", + pose: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + }, + { + name: "gps2", + pose: [6.0, 7.0, 8.0, 9.0, 0.0, 1.0] + } + ] + } + loader = SDF::ERBLoader.new(erb_args: erb_args) + + erb_content = loader.load_sdf_raw(@simple_model) + + assert_equal "simple_model_erb", + REXML::XPath.first(erb_content, "//model").attributes["name"] + poses = REXML::XPath.match(erb_content, "//pose").map(&:text) + assert_includes poses, "0.0 1.0 2.0 3.0 4.0 5.0" + assert_includes poses, "6.0 7.0 8.0 9.0 0.0 1.0" + end + + it "loads a real .sdf.erb file without args" do + erb_content = SDF::ERBLoader.new.load_sdf_raw(@simple_model) + + assert_equal "simple_model_erb", + REXML::XPath.first(erb_content, "//model").attributes["name"] + poses = REXML::XPath.match(erb_content, "//pose").map(&:text) + assert_includes poses, "-0.679 0.0 1.92 0.0 0.0 0.0" + assert_includes poses, "2.571 0.044 0.808 0.0 0.0 0.0" + end + + it "validates that the file is a XML file" do + assert_raises(SDF::XML::InvalidXML) do + SDF::ERBLoader.new.load_sdf_raw(File.join(@models_dir, "not_xml.xml")) + end + end + it "validates that the file has a root" do + assert_raises(SDF::XML::NotSDF) do + SDF::ERBLoader.new.load_sdf_raw(File.join(@models_dir, "no_root.xml")) + end + end + it "validates that the file is a SDF file" do + assert_raises(SDF::XML::NotSDF) do + SDF::ERBLoader.new.load_sdf_raw(File.join(@models_dir, "not_sdf.xml")) + end + end + it "validates that the file exists" do + assert_raises(Errno::ENOENT) do + SDF::ERBLoader.new.load_sdf_raw(File.join(@models_dir, + "does_not_exist.xml")) + end + end + end +end diff --git a/test/test_root.rb b/test/test_root.rb index a9da105..0853338 100644 --- a/test/test_root.rb +++ b/test/test_root.rb @@ -68,10 +68,11 @@ def regressions_dir end it "calls load_from_model_name if given a URI" do version = flexmock + loader = SDF::Loader.new flexmock(SDF::Root).should_receive(:load_from_model_name).once.with( - "model_in_uri", version, flatten: true + "model_in_uri", version, flatten: true, loader: loader ).and_return(obj = flexmock) - assert_equal obj, SDF::Root.load("model://model_in_uri", version) + assert_equal obj, SDF::Root.load("model://model_in_uri", version, loader: loader) end end diff --git a/test/test_sdf_loader.rb b/test/test_sdf_loader.rb new file mode 100644 index 0000000..9ebef29 --- /dev/null +++ b/test/test_sdf_loader.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "sdf/sdf_loader" +require "sdf/test" + +describe SDF::Loader do + it "fallback from .sdf to .sdf.erb and loads file without model and args" do + loader = SDF::Loader.new + + sdf_file_path = File.expand_path( + "data/models/simple_model/model.sdf", __dir__ + ) + content = loader.load_sdf_raw(sdf_file_path) + + assert_equal "simple test model", + REXML::XPath.first(content, "//model").attributes["name"] + end +end diff --git a/test/test_xml.rb b/test/test_xml.rb index ee5724f..bb6e45e 100644 --- a/test/test_xml.rb +++ b/test/test_xml.rb @@ -60,7 +60,7 @@ def invalid_models_dir describe "gazebo_models" do it "loads all models available in the path" do models = SDF::XML.gazebo_models - assert_equal 23, models.size + assert_equal 24, models.size assert(sdf = models["simple_model"]) model = sdf.elements.enum_for(:each, "sdf/model").first @@ -169,10 +169,12 @@ def invalid_models_dir sdf = SDF::XML.load_sdf(File.join(models_dir, "model_with_relative_file_in_uri", "model.sdf")) uri = sdf.elements.to_a("//uri").first - assert_equal( - File.join(models_dir, "model_with_relative_file_in_uri", - "visual.dae"), uri.text + expected_full_path = File.expand_path( + File.join( + models_dir, "model_with_relative_file_in_uri", "visual.dae" + ) ) + assert_equal(expected_full_path, uri.text) end it "resolves relative paths to other model's paths in tags" do sdf = SDF::XML.load_sdf(File.join(models_dir, @@ -184,10 +186,10 @@ def invalid_models_dir sdf = SDF::XML.load_sdf(File.join(models_dir, "model_that_includes_a_model_with_relative_paths", "model.sdf")) uri = sdf.elements.to_a("//uri").first - assert_equal( - File.join(models_dir, "model_with_relative_uris", - "visual.dae"), uri.text + expected_full_path = File.expand_path( + File.join(models_dir, "model_with_relative_uris", "visual.dae") ) + assert_equal(expected_full_path, uri.text) end it "resolves model:// in tags" do sdf = SDF::XML.load_sdf(File.join(models_dir, @@ -204,9 +206,9 @@ def invalid_models_dir metadata: true ) - model_full_path = File.expand_path(File.join( - "data", "models", "simple_model", "model.sdf" - ), __dir__) + model_full_path = File.join( + models_dir, "simple_model", "model.sdf" + ) expected = [ "w::child_of_world", "w::model::child_of_model", @@ -214,7 +216,6 @@ def invalid_models_dir "root_model::child_of_root_model", "root_model::model_in_root_model::child_of_model_in_root_model" ] - assert_equal [model_full_path], metadata["includes"].keys assert_equal expected.sort, metadata["includes"][model_full_path].sort @@ -227,12 +228,10 @@ def invalid_models_dir metadata: true ) - ur10_full_path = File.expand_path(File.join( - "data", "regressions", "ur10", "ur10.sdf" - ), __dir__) - dual_ur10_full_path = File.expand_path(File.join( - "data", "regressions", "dual_ur10", "model.sdf" - ), __dir__) + ur10_full_path = File.join(regressions_dir, "ur10", "ur10.sdf") + dual_ur10_full_path = File.join( + regressions_dir, "dual_ur10", "model.sdf" + ) expected = Hash[ ur10_full_path => %w[ empty_world::dual_ur10_fixed::dual_ur10::left_arm