From a3258eee22ca4b215d179738988cc7aa53afef0e Mon Sep 17 00:00:00 2001 From: Kei Okada Date: Wed, 11 Mar 2020 22:48:23 +0900 Subject: [PATCH 1/4] update backward compatibility on visual and collisions (#47) * add test to build robot model from python urdf classes * add more tests on python urdf build * update backward compatibility Link(visual=...) does not set on since 0.4.0, breaks backward compatibility * Robot accepts version kwarg Signed-off-by: Shane Loretz Co-authored-by: Shane Loretz --- src/urdf_parser_py/urdf.py | 8 +++ test/test_urdf.py | 139 ++++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/src/urdf_parser_py/urdf.py b/src/urdf_parser_py/urdf.py index 41fa2b7..005cad0 100644 --- a/src/urdf_parser_py/urdf.py +++ b/src/urdf_parser_py/urdf.py @@ -355,8 +355,12 @@ def __init__(self, name=None, visual=None, inertial=None, collision=None, self.aggregate_init() self.name = name self.visuals = [] + if visual: + self.visual = visual self.inertial = inertial self.collisions = [] + if collision: + self.collision = collision self.origin = origin def __get_visual(self): @@ -370,6 +374,8 @@ def __set_visual(self, visual): self.visuals[0] = visual else: self.visuals.append(visual) + if visual: + self.add_aggregate('visual', visual) def __get_collision(self): """Return the first collision or None.""" @@ -382,6 +388,8 @@ def __set_collision(self, collision): self.collisions[0] = collision else: self.collisions.append(collision) + if collision: + self.add_aggregate('collision', collision) # Properties for backwards compatibility visual = property(__get_visual, __set_visual) diff --git a/test/test_urdf.py b/test/test_urdf.py index 40c5c9b..dc4050c 100644 --- a/test/test_urdf.py +++ b/test/test_urdf.py @@ -31,6 +31,12 @@ def parse_and_compare(self, orig): rewritten = minidom.parseString(robot.to_xml_string()) self.assertTrue(xml_matches(xml, rewritten)) + def xml_and_compare(self, robot, xml): + robot_xml_string = robot.to_xml_string() + robot_xml = minidom.parseString(robot_xml_string) + orig_xml = minidom.parseString(xml) + self.assertTrue(xml_matches(robot_xml, orig_xml)) + def test_new_transmission(self): xml = ''' @@ -46,6 +52,18 @@ def test_new_transmission(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + trans = urdf.Transmission(name='simple_trans') + trans.type = 'transmission_interface/SimpleTransmission' + joint = urdf.TransmissionJoint(name='foo_joint') + joint.add_aggregate('hardwareInterface', 'EffortJointInterface') + trans.add_aggregate('joint', joint) + actuator = urdf.Actuator(name='foo_motor') + actuator.mechanicalReduction = 50.0 + trans.add_aggregate('actuator', actuator) + robot.add_aggregate('transmission', trans) + self.xml_and_compare(robot, xml) + def test_new_transmission_multiple_joints(self): xml = ''' @@ -65,6 +83,22 @@ def test_new_transmission_multiple_joints(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + trans = urdf.Transmission(name='simple_trans') + trans.type = 'transmission_interface/SimpleTransmission' + joint = urdf.TransmissionJoint(name='foo_joint') + joint.add_aggregate('hardwareInterface', 'EffortJointInterface') + trans.add_aggregate('joint', joint) + joint = urdf.TransmissionJoint(name='bar_joint') + joint.add_aggregate('hardwareInterface', 'EffortJointInterface') + joint.add_aggregate('hardwareInterface', 'EffortJointInterface') + trans.add_aggregate('joint', joint) + actuator = urdf.Actuator(name='foo_motor') + actuator.mechanicalReduction = 50.0 + trans.add_aggregate('actuator', actuator) + robot.add_aggregate('transmission', trans) + self.xml_and_compare(robot, xml) + def test_new_transmission_multiple_actuators(self): xml = ''' @@ -81,6 +115,20 @@ def test_new_transmission_multiple_actuators(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + trans = urdf.Transmission(name='simple_trans') + trans.type = 'transmission_interface/SimpleTransmission' + joint = urdf.TransmissionJoint(name='foo_joint') + joint.add_aggregate('hardwareInterface', 'EffortJointInterface') + trans.add_aggregate('joint', joint) + actuator = urdf.Actuator(name='foo_motor') + actuator.mechanicalReduction = 50.0 + trans.add_aggregate('actuator', actuator) + actuator = urdf.Actuator(name='bar_motor') + trans.add_aggregate('actuator', actuator) + robot.add_aggregate('transmission', trans) + self.xml_and_compare(robot, xml) + def test_new_transmission_missing_joint(self): xml = ''' @@ -113,6 +161,11 @@ def test_old_transmission(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + trans = urdf.PR2Transmission(name='PR2_trans', joint='foo_joint', actuator='foo_motor', type='SimpleTransmission', mechanicalReduction=1.0) + robot.add_aggregate('transmission', trans) + self.xml_and_compare(robot, xml) + def test_link_material_missing_color_and_texture(self): xml = ''' @@ -127,6 +180,20 @@ def test_link_material_missing_color_and_texture(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + link = urdf.Link(name='link', + visual=urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat'))) + robot.add_link(link) + self.xml_and_compare(robot, xml) + + robot = urdf.Robot(name='test', version='1.0') + link = urdf.Link(name='link') + link.visual = urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat')) + robot.add_link(link) + self.xml_and_compare(robot, xml) + def test_robot_material(self): xml = ''' @@ -136,6 +203,11 @@ def test_robot_material(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + material = urdf.Material(name='mat', color=urdf.Color([0.0, 0.0, 0.0, 1.0])) + robot.add_aggregate('material', material) + self.xml_and_compare(robot, xml) + def test_robot_material_missing_color_and_texture(self): xml = ''' @@ -163,6 +235,29 @@ def test_link_multiple_visual(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + link = urdf.Link(name='link') + link.visual = urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat')) + link.visual = urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), + material=urdf.Material(name='mat2')) + robot.add_link(link) + self.xml_and_compare(robot, xml) + + def test_visual_with_name(self): + xml = ''' + + + + + + + + + +''' + self.parse_and_compare(xml) + def test_link_multiple_collision(self): xml = ''' @@ -181,6 +276,13 @@ def test_link_multiple_collision(self): ''' self.parse_and_compare(xml) + robot = urdf.Robot(name='test', version='1.0') + link = urdf.Link(name='link') + link.collision = urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1)) + link.collision = urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5)) + robot.add_link(link) + self.xml_and_compare(robot, xml) + def test_version_attribute_not_enough_dots(self): xml = ''' @@ -293,7 +395,7 @@ def test_robot_link_defaults_xyz_set(self): class LinkMultiVisualsAndCollisionsTest(unittest.TestCase): xml = ''' - + @@ -345,6 +447,41 @@ def test_multi_collision_access(self): robot.links[0].collision = dummyObject self.assertEqual(id(dummyObject), id(robot.links[0].collisions[0])) + def test_xml_and_urdfdom_robot_compatible_with_kinetic(self): + robot = urdf.Robot(name='test', version='1.0') + link = urdf.Link(name='link') + link.visual = urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat')) + link.visual = urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), + material=urdf.Material(name='mat2')) + link.collision = urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1)) + link.collision = urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5)) + robot.add_link(link) + link = urdf.Link(name='link2') + robot.add_link(link) + # + robot_xml_string = robot.to_xml_string() + robot_xml = minidom.parseString(robot_xml_string) + orig_xml = minidom.parseString(self.xml) + self.assertTrue(xml_matches(robot_xml, orig_xml)) + + def test_xml_and_urdfdom_robot_only_supported_since_melodic(self): + robot = urdf.Robot(name='test', version='1.0') + link = urdf.Link(name='link') + link.add_aggregate('visual', urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat'))) + link.add_aggregate('visual', urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), + material=urdf.Material(name='mat2'))) + link.add_aggregate('collision', urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1))) + link.add_aggregate('collision', urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5))) + robot.add_link(link) + link = urdf.Link(name='link2') + robot.add_link(link) + # + robot_xml_string = robot.to_xml_string() + robot_xml = minidom.parseString(robot_xml_string) + orig_xml = minidom.parseString(self.xml) + self.assertTrue(xml_matches(robot_xml, orig_xml)) class TestCreateNew(unittest.TestCase): def test_new_urdf(self): From d4d94d22749ad5bede456cbbdb7c60cd5e689743 Mon Sep 17 00:00:00 2001 From: Adrian Zwiener Date: Tue, 11 Jul 2023 09:36:49 +0200 Subject: [PATCH 2/4] Remove test for visual with name The Visual class does not have an attribute name anymore. Consequently, we can not expect the test to work. --- test/test_urdf.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/test_urdf.py b/test/test_urdf.py index dc4050c..1033a3a 100644 --- a/test/test_urdf.py +++ b/test/test_urdf.py @@ -244,20 +244,6 @@ def test_link_multiple_visual(self): robot.add_link(link) self.xml_and_compare(robot, xml) - def test_visual_with_name(self): - xml = ''' - - - - - - - - - -''' - self.parse_and_compare(xml) - def test_link_multiple_collision(self): xml = ''' From ab75ec71e942df1a21b9a60da02b31e290d84989 Mon Sep 17 00:00:00 2001 From: Adrian Zwiener Date: Mon, 4 Sep 2023 10:52:44 +0200 Subject: [PATCH 3/4] Add method for multiple visual and collision props The attributes 'visual' and 'collision' exist only for back-compability reasons and shall only be used if there exists one and only one visual or collision element. For links with multiple visual or collision elements the new method is added which shall be used. This simplifies the usage of the API. --- src/urdf_parser_py/urdf.py | 18 ++++++++++++------ test/test_urdf.py | 28 ++++++++++++++-------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/urdf_parser_py/urdf.py b/src/urdf_parser_py/urdf.py index 005cad0..53100a9 100644 --- a/src/urdf_parser_py/urdf.py +++ b/src/urdf_parser_py/urdf.py @@ -371,12 +371,20 @@ def __get_visual(self): def __set_visual(self, visual): """Set the first visual.""" if self.visuals: - self.visuals[0] = visual - else: - self.visuals.append(visual) + self.remove_aggregate(self.visuals[0]) if visual: self.add_aggregate('visual', visual) + def add_visual(self, visual): + """Add a visual element to the link.""" + self.visuals.append(visual) + self.add_aggregate('visual', self.visuals[-1]) + + def add_collision(self, collision): + """Add a collision element for the link.""" + self.collisions.append(collision) + self.add_aggregate('collision', self.collisions[-1]) + def __get_collision(self): """Return the first collision or None.""" if self.collisions: @@ -385,9 +393,7 @@ def __get_collision(self): def __set_collision(self, collision): """Set the first collision.""" if self.collisions: - self.collisions[0] = collision - else: - self.collisions.append(collision) + self.remove_aggregate(self.collisions[0]) if collision: self.add_aggregate('collision', collision) diff --git a/test/test_urdf.py b/test/test_urdf.py index 1033a3a..32009a2 100644 --- a/test/test_urdf.py +++ b/test/test_urdf.py @@ -237,10 +237,10 @@ def test_link_multiple_visual(self): robot = urdf.Robot(name='test', version='1.0') link = urdf.Link(name='link') - link.visual = urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), - material=urdf.Material(name='mat')) - link.visual = urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), - material=urdf.Material(name='mat2')) + link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat'))) + link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), + material=urdf.Material(name='mat2'))) robot.add_link(link) self.xml_and_compare(robot, xml) @@ -264,8 +264,8 @@ def test_link_multiple_collision(self): robot = urdf.Robot(name='test', version='1.0') link = urdf.Link(name='link') - link.collision = urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1)) - link.collision = urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5)) + link.add_collision(urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1))) + link.add_collision(urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5))) robot.add_link(link) self.xml_and_compare(robot, xml) @@ -418,7 +418,7 @@ def test_multi_visual_access(self): self.assertEqual(None, robot.links[1].visual) dummyObject = set() - robot.links[0].visual = dummyObject + robot.links[0].visuals[0] = dummyObject self.assertEqual(id(dummyObject), id(robot.links[0].visuals[0])) def test_multi_collision_access(self): @@ -430,18 +430,18 @@ def test_multi_collision_access(self): self.assertEqual(None, robot.links[1].collision) dummyObject = set() - robot.links[0].collision = dummyObject + robot.links[0].collisions[0] = dummyObject self.assertEqual(id(dummyObject), id(robot.links[0].collisions[0])) def test_xml_and_urdfdom_robot_compatible_with_kinetic(self): robot = urdf.Robot(name='test', version='1.0') link = urdf.Link(name='link') - link.visual = urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), - material=urdf.Material(name='mat')) - link.visual = urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), - material=urdf.Material(name='mat2')) - link.collision = urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1)) - link.collision = urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5)) + link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat'))) + link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), + material=urdf.Material(name='mat2'))) + link.add_collision(urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1))) + link.add_collision(urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5))) robot.add_link(link) link = urdf.Link(name='link2') robot.add_link(link) From de254402e7740a74c1edbd51afdc9e6a9ffa782f Mon Sep 17 00:00:00 2001 From: Adrian Zwiener Date: Tue, 12 Sep 2023 16:35:24 +0200 Subject: [PATCH 4/4] Add methods to remove visuall and collision objs - In the previous version, collision and visual objects were added twice. Tests were updated accordingly. - Added tests to check if visual and collision objects can be removed. --- src/urdf_parser_py/urdf.py | 21 ++++++++---- test/test_urdf.py | 66 ++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/urdf_parser_py/urdf.py b/src/urdf_parser_py/urdf.py index 53100a9..c59fc94 100644 --- a/src/urdf_parser_py/urdf.py +++ b/src/urdf_parser_py/urdf.py @@ -334,6 +334,7 @@ def joint_type(self): return self.type @joint_type.setter def joint_type(self, value): self.type = value + xmlr.reflect(Joint, tag='joint', params=[ name_attribute, xmlr.Attribute('type', str), @@ -377,13 +378,19 @@ def __set_visual(self, visual): def add_visual(self, visual): """Add a visual element to the link.""" - self.visuals.append(visual) - self.add_aggregate('visual', self.visuals[-1]) + self.add_aggregate('visual', visual) + + def remove_visual(self, visual): + """Removes the provided visual object.""" + self.remove_aggregate(visual) def add_collision(self, collision): """Add a collision element for the link.""" - self.collisions.append(collision) - self.add_aggregate('collision', self.collisions[-1]) + self.add_aggregate('collision', collision) + + def remove_collision(self, collision): + """Removes a provided collision object.""" + self.remove_aggregate(collision) def __get_collision(self): """Return the first collision or None.""" @@ -492,7 +499,8 @@ def __init__(self, name=None, version="1.0"): self.name = name if version not in self.SUPPORTED_VERSIONS: - raise ValueError("Invalid version; only %s is supported" % (','.join(self.SUPPORTED_VERSIONS))) + raise ValueError("Invalid version; only %s is supported" % + (','.join(self.SUPPORTED_VERSIONS))) self.version = version self.joints = [] @@ -568,7 +576,8 @@ def post_read_xml(self): raise ValueError("Version number must be positive") if self.version not in self.SUPPORTED_VERSIONS: - raise ValueError("Invalid version; only %s is supported" % (','.join(self.SUPPORTED_VERSIONS))) + raise ValueError("Invalid version; only %s is supported" % + (','.join(self.SUPPORTED_VERSIONS))) xmlr.reflect(Robot, tag='robot', params=[ diff --git a/test/test_urdf.py b/test/test_urdf.py index 32009a2..e9c358b 100644 --- a/test/test_urdf.py +++ b/test/test_urdf.py @@ -1,4 +1,5 @@ +import urdf_parser_py.xml_reflection as xmlr import unittest from unittest import mock import os @@ -12,10 +13,10 @@ from xml.dom import minidom # noqa from xml_matching import xml_matches # noqa from urdf_parser_py import urdf # noqa -import urdf_parser_py.xml_reflection as xmlr + class ParseException(xmlr.core.ParseError): - def __init__(self, e = "", path = ""): + def __init__(self, e="", path=""): super(ParseException, self).__init__(e, path) @@ -162,7 +163,8 @@ def test_old_transmission(self): self.parse_and_compare(xml) robot = urdf.Robot(name='test', version='1.0') - trans = urdf.PR2Transmission(name='PR2_trans', joint='foo_joint', actuator='foo_motor', type='SimpleTransmission', mechanicalReduction=1.0) + trans = urdf.PR2Transmission(name='PR2_trans', joint='foo_joint', + actuator='foo_motor', type='SimpleTransmission', mechanicalReduction=1.0) robot.add_aggregate('transmission', trans) self.xml_and_compare(robot, xml) @@ -183,7 +185,7 @@ def test_link_material_missing_color_and_texture(self): robot = urdf.Robot(name='test', version='1.0') link = urdf.Link(name='link', visual=urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), - material=urdf.Material(name='mat'))) + material=urdf.Material(name='mat'))) robot.add_link(link) self.xml_and_compare(robot, xml) @@ -238,9 +240,9 @@ def test_link_multiple_visual(self): robot = urdf.Robot(name='test', version='1.0') link = urdf.Link(name='link') link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), - material=urdf.Material(name='mat'))) + material=urdf.Material(name='mat'))) link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), - material=urdf.Material(name='mat2'))) + material=urdf.Material(name='mat2'))) robot.add_link(link) self.xml_and_compare(robot, xml) @@ -341,6 +343,7 @@ def test_version_attribute_invalid_version(self): ''' self.assertRaises(ValueError, self.parse, xml) + class LinkOriginTestCase(unittest.TestCase): @mock.patch('urdf_parser_py.xml_reflection.on_error', mock.Mock(side_effect=ParseException)) @@ -433,15 +436,17 @@ def test_multi_collision_access(self): robot.links[0].collisions[0] = dummyObject self.assertEqual(id(dummyObject), id(robot.links[0].collisions[0])) - def test_xml_and_urdfdom_robot_compatible_with_kinetic(self): + def test_xml_and_urdfdom_add_collision_and_visual(self): robot = urdf.Robot(name='test', version='1.0') link = urdf.Link(name='link') link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), - material=urdf.Material(name='mat'))) + material=urdf.Material(name='mat'))) link.add_visual(urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), - material=urdf.Material(name='mat2'))) + material=urdf.Material(name='mat2'))) + self.assertEqual(len(link.visuals), 2) link.add_collision(urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1))) link.add_collision(urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5))) + self.assertEqual(len(link.collisions), 2) robot.add_link(link) link = urdf.Link(name='link2') robot.add_link(link) @@ -459,7 +464,8 @@ def test_xml_and_urdfdom_robot_only_supported_since_melodic(self): link.add_aggregate('visual', urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), material=urdf.Material(name='mat2'))) link.add_aggregate('collision', urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1))) - link.add_aggregate('collision', urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5))) + link.add_aggregate('collision', urdf.Collision( + geometry=urdf.Cylinder(length=4, radius=0.5))) robot.add_link(link) link = urdf.Link(name='link2') robot.add_link(link) @@ -469,6 +475,7 @@ def test_xml_and_urdfdom_robot_only_supported_since_melodic(self): orig_xml = minidom.parseString(self.xml) self.assertTrue(xml_matches(robot_xml, orig_xml)) + class TestCreateNew(unittest.TestCase): def test_new_urdf(self): testcase = urdf.URDF('robot_name').to_xml() @@ -485,5 +492,44 @@ def test_new_urdf_with_version(self): self.assertEqual(testcase.get('version'), '1.0') +class RemoveVisualsAndCollisionsTest(unittest.TestCase): + + def test_remove_visual(self): + link = urdf.Link(name='link') + visual_1 = urdf.Visual(geometry=urdf.Cylinder(length=1, radius=1), + material=urdf.Material(name='mat')) + visual_2 = urdf.Visual(geometry=urdf.Cylinder(length=4, radius=0.5), + material=urdf.Material(name='mat2')) + link.add_visual(visual_1) + link.add_visual(visual_2) + xml_str = link.to_xml_string() + num_visuals = len(link.visuals) + link.remove_visual(visual_2) + self.assertEqual(num_visuals - 1, len(link.visuals)) + self.assertFalse(visual_2 in link.visuals) + self.assertTrue('mat2' in xml_str) + self.assertFalse('mat2' in link.to_xml_string()) + link_xml_string = link.to_xml_string() + link_xml = minidom.parseString(link_xml_string) + orig_xml = minidom.parseString(xml_str) + self.assertFalse(xml_matches(link_xml, orig_xml)) + + def test_remove_collision(self): + link = urdf.Link(name='link') + coll_1 = urdf.Collision(geometry=urdf.Cylinder(length=1, radius=1)) + coll_2 = urdf.Collision(geometry=urdf.Cylinder(length=4, radius=0.5)) + link.add_collision(coll_1) + link.add_collision(coll_2) + xml_str = link.to_xml_string() + num_collisions = len(link.collisions) + link.remove_collision(coll_2) + self.assertEqual(num_collisions - 1, len(link.collisions)) + self.assertFalse(coll_2 in link.collisions) + link_xml_string = link.to_xml_string() + link_xml = minidom.parseString(link_xml_string) + orig_xml = minidom.parseString(xml_str) + self.assertFalse(xml_matches(link_xml, orig_xml)) + + if __name__ == '__main__': unittest.main()