def setUp(self) -> None: self.collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.1) self.drone_config_map = {"DRONE1": DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1), "DRONE2": DroneConfig(init_position=(1, 0, 0), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=1)} self.state_updater_map = {"DRONE1": StateUpdater(DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)), "DRONE2": StateUpdater(DroneConfig(init_position=(1, 0, 0), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=1))} self.collision_checker = CollisionChecker(self.drone_config_map, self.collision_config)
def test_check_parallel_drone_commands_nested_should_update_states_correctly( self): drone_commands = \ [ ParallelDroneCommands([ [SingleDroneCommand("DRONE1", Command.wait(5))], [SingleDroneCommand("DRONE2", Command.wait(3)), ParallelDroneCommands([ [SingleDroneCommand("DRONE3", Command.wait(4))], [SingleDroneCommand("DRONE4", Command.wait(1))] ])] ]), SingleDroneCommand("DRONE1", Command.wait(1)) ] drones_involved = {"DRONE1", "DRONE2", "DRONE3", "DRONE4", "DRONE5"} state_updaters = { name: StateUpdater(DroneConfig((1, 0, 0), 2, 180, 2)) for name in drones_involved } drone_state_map = {name: State() for name in drones_involved} BoundaryChecker( self.boundary_config)._update_states_and_check_drone_commands( drone_commands, state_updaters, drone_state_map, drones_involved) expected = { name: State(time_used_seconds=8) for name in drones_involved } self.assertEqual(expected, drone_state_map)
def test_str(self): drone_config = DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1) self.assertEqual( "DroneConfig: { init_position: (0, 0, 0), speed_mps: 0.5, " + "rotate_speed_dps: 45, takeoff_height_meters: 1 }", str(drone_config))
def test_property(self): drone_config = DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1) self.assertEqual((0, 0, 0), drone_config.init_position) self.assertEqual(0.5, drone_config.speed_mps) self.assertEqual(45, drone_config.rotate_speed_dps) self.assertEqual(1, drone_config.takeoff_height_meters)
def test_get_init_state(self): self.assertEqual(State(x_meters=0, y_meters=0, z_meters=0), self.state_updater.get_init_state()) drone_config = DroneConfig(init_position=(1, 2, 3), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1) state_updater = StateUpdater(drone_config=drone_config) self.assertEqual(State(x_meters=1, y_meters=2, z_meters=3), state_updater.get_init_state())
def test_parse_if_provided_configs_should_parse_all_configs(self): config = """ { "drones": [{ "name": "DRONE1", "init_position": {"x": 1, "y": 2, "z": 3}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 },{ "name": "DRONE2", "init_position": {"x": 4, "y": 5, "z": 6}, "speed_mps": 1, "rotate_speed_dps": 90, "takeoff_height_meters": 1 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ actual_drone_config, actual_boundary_config, acutal_collision_config = ConfigParser.parse(config) expected_drone_configs = {"DRONE1": DroneConfig(init_position=(1, 2, 3), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2), "DRONE2": DroneConfig(init_position=(4, 5, 6), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_config) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, acutal_collision_config)
def setUp(self) -> None: self.boundary_config = BoundaryConfig(max_seconds=10, max_x_meters=5, max_y_meters=5, max_z_meters=5, min_x_meters=-5, min_y_meters=-5, min_z_meters=-5) self.state_updater_map = { "DRONE1": StateUpdater( DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)), "DRONE2": StateUpdater( DroneConfig(init_position=(1, 0, 0), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)) }
def test_init_with_wrong_parameter_should_give_error(self): for invalid_value in [-10, -1, 0]: with self.assertRaises(ValueError) as context: DroneConfig(init_position=(0, 0, 0), speed_mps=invalid_value, rotate_speed_dps=45, takeoff_height_meters=1) self.assertTrue("speed_mps should > 0" in str(context.exception)) with self.assertRaises(ValueError) as context: DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=invalid_value, takeoff_height_meters=1) self.assertTrue( "rotate_speed_dps should > 0" in str(context.exception)) with self.assertRaises(ValueError) as context: DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=invalid_value) self.assertTrue( "takeoff_height_meters should > 0" in str(context.exception))
def test_if_given_state_updater_should_use_it_to_update_state(self): commands = "main() {takeoff(); land();}" with self.assertRaises(SafetyCheckError) as context: generate_commands(commands, drone_config_map={ "DEFAULT": DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=10) }, boundary_checker=BoundaryChecker( BoundaryConfig(max_seconds=10, max_z_meters=1))) self.assertTrue( "Drone 'DEFAULT': the z coordinate 10 will go beyond its upper limit 1" in str(context.exception))
def test_parse_if_name_repeated_should_ignore_second_appearance(self): config = """ { "drones": [{ "name": "DRONE1", "init_position": {"x": 1, "y": 2, "z": 3}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 },{ "name": "DRONE1", "init_position": {"x": 4, "y": 5, "z": 6}, "speed_mps": 1, "rotate_speed_dps": 90, "takeoff_height_meters": 1 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:Drone name 'DRONE1' appeared more than ones in 'drones', ignored."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DRONE1": DroneConfig(init_position=(1, 2, 3), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_init_position_missing_fields_should_use_default_value(self): config = """ { "drones": [{ "name": "DRONE1", "init_position": {}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'x' missing when parsing drone 'DRONE1', using default value 0. " + "Position estimation may be inaccurate.", "WARNING:root:'y' missing when parsing drone 'DRONE1', using default value 0. " + "Position estimation may be inaccurate.", "WARNING:root:'z' missing when parsing drone 'DRONE1', using default value 0. " + "Position estimation may be inaccurate."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DRONE1": DroneConfig(init_position=(0, 0, 0), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_drone_object_missing_fields_should_use_default_value(self): config = """ { "drones": [{}], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'name' missing when parsing object in 'drones', using default value 'DEFAULT'.", "WARNING:root:'init_position' missing when parsing drone 'DEFAULT', " + "using default value (0, 0, 0). Position estimation may be inaccurate.", "WARNING:root:'speed_mps' missing when parsing drone 'DEFAULT', " + "using default value 1. Position estimation may be inaccurate.", "WARNING:root:'rotate_speed_dps' missing when parsing drone 'DEFAULT', " + "using default value 90. Position estimation may be inaccurate.", "WARNING:root:'takeoff_height_meters' missing when parsing drone 'DEFAULT', " + "using default value 1. Position estimation may be inaccurate."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DEFAULT": DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=90, takeoff_height_meters=1)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_parse_if_name_is_empty_string_should_use_default_value(self): config = """ { "drones": [{ "name": "", "init_position": {"x": 1, "y": 2, "z": 3}, "speed_mps": 2, "rotate_speed_dps": 180, "takeoff_height_meters": 2 }], "boundary_config": { "max_seconds": 100, "max_x_meters": 10, "max_y_meters": 20, "max_z_meters": 30, "min_x_meters": -10, "min_y_meters": -20, "min_z_meters": -30 }, "collision_config": { "collision_meters": 0.3, "time_interval_seconds": 0.5 } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'name' cannot be an empty string, using default value 'DEFAULT' instead."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DEFAULT": DroneConfig(init_position=(1, 2, 3), speed_mps=2, rotate_speed_dps=180, takeoff_height_meters=2)} expected_boundary_config = BoundaryConfig(max_seconds=100, max_x_meters=10, max_y_meters=20, max_z_meters=30, min_x_meters=-10, min_y_meters=-20, min_z_meters=-30) expected_collision_config = CollisionConfig(collision_meters=0.3, time_interval_seconds=0.5) self.assertEqual(expected_drone_configs, actual_drone_configs) self.assertEqual(expected_boundary_config, actual_boundary_config) self.assertEqual(expected_collision_config, actual_collision_config)
def test_convert_command(self): commands = [ SingleDroneCommand("DRONE1", Command.takeoff()), SingleDroneCommand("DRONE1", Command.up(1)), SingleDroneCommand("DRONE1", Command.down(1)), SingleDroneCommand("DRONE1", Command.left(1)), SingleDroneCommand("DRONE1", Command.right(1)), SingleDroneCommand("DRONE1", Command.forward(1)), SingleDroneCommand("DRONE1", Command.backward(1)), SingleDroneCommand("DRONE1", Command.rotate_left(1)), SingleDroneCommand("DRONE1", Command.rotate_right(1)), ParallelDroneCommands( [[ ParallelDroneCommands( [[], [SingleDroneCommand("DRONE1", Command.up(1))]]) ], [ SingleDroneCommand("DRONE2", Command.takeoff()), SingleDroneCommand("DRONE2", Command.land()) ]]), SingleDroneCommand("DRONE1", Command.land()), SingleDroneCommand("DRONE1", Command.wait(1)) ] drone_config_map = { "DRONE1": DroneConfig((1, 2, 3), 1, 90, 2), "DRONE2": DroneConfig((0, 0, 0), 1, 90, 1) } expected = { "config": [{ "name": "DRONE1", "init_pos": [1, 2, 3], "speed": 1, "rotate_speed": 90, "takeoff_height": 2 }, { "name": "DRONE2", "init_pos": [0, 0, 0], "speed": 1, "rotate_speed": 90, "takeoff_height": 1 }], "commands": [{ "type": "single", "name": "DRONE1", "action": "takeoff", "values": [] }, { "type": "single", "name": "DRONE1", "action": "up", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "down", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "left", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "right", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "forward", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "backward", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "rotate_left", "values": [1] }, { "type": "single", "name": "DRONE1", "action": "rotate_right", "values": [1] }, { "type": "parallel", "branches": [[{ "type": "parallel", "branches": [[], [{ "type": "single", "name": "DRONE1", "action": "up", "values": [1] }]] }], [{ "type": "single", "name": "DRONE2", "action": "takeoff", "values": [] }, { "type": "single", "name": "DRONE2", "action": "land", "values": [] }]] }, { "type": "single", "name": "DRONE1", "action": "land", "values": [] }, { "type": "single", "name": "DRONE1", "action": "wait", "values": [1] }] } actual = SimulationConverter().convert(commands, drone_config_map) self.assertEqual(expected, json.loads(zlib.decompress(b64decode(actual))))
def test_eq(self): self.assertEqual( DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1), DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1)) self.assertNotEqual( DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1), DroneConfig(init_position=(1, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1)) self.assertNotEqual( DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1), DroneConfig(init_position=(0, 0, 0), speed_mps=1, rotate_speed_dps=45, takeoff_height_meters=1)) self.assertNotEqual( DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1), DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=90, takeoff_height_meters=1)) self.assertNotEqual( DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1), DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=2)) self.assertNotEqual( None, DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=2))
def setUp(self) -> None: self.drone_config = DroneConfig(init_position=(0, 0, 0), speed_mps=0.5, rotate_speed_dps=45, takeoff_height_meters=1) self.state_updater = StateUpdater(drone_config=self.drone_config)
def _parse_drone_config(data: dict) -> Dict[str, DroneConfig]: drone_config_map = {} if "drones" in data: if len(data["drones"]) == 0: warning( "'drones' is empty when parsing configs, using default drone_config. " + "Position estimation may be inaccurate.") return {"DEFAULT": DefaultDroneConfig()} for drone in data["drones"]: if "name" in drone: name = drone["name"] if name == "": warning( "'name' cannot be an empty string, using default value 'DEFAULT' instead." ) name = "DEFAULT" else: warning( "'name' missing when parsing object in 'drones', using default value 'DEFAULT'." ) name = "DEFAULT" if name in drone_config_map: warning( "Drone name '{}' appeared more than ones in 'drones', ignored." .format(name)) continue if "init_position" in drone: position = drone["init_position"] init_position = [] for dim in ["x", "y", "z"]: if dim in position: init_position.append(position[dim]) else: warning( "'{}' missing when parsing drone '{}', using default value 0. " .format(dim, name) + "Position estimation may be inaccurate.") init_position.append(0) init_position = tuple(init_position) else: warning("'init_position' missing when parsing drone '{}', " .format(name) + "using default value (0, 0, 0). " + "Position estimation may be inaccurate.") init_position = (0, 0, 0) if "speed_mps" in drone: speed_mps = drone["speed_mps"] else: warning("'speed_mps' missing when parsing drone '{}', ". format(name) + "using default value 1. " + "Position estimation may be inaccurate.") speed_mps = 1 if "rotate_speed_dps" in drone: rotate_speed_dps = drone["rotate_speed_dps"] else: warning( "'rotate_speed_dps' missing when parsing drone '{}', ". format(name) + "using default value 90. " + "Position estimation may be inaccurate.") rotate_speed_dps = 90 if "takeoff_height_meters" in drone: takeoff_height_meters = drone["takeoff_height_meters"] else: warning( "'takeoff_height_meters' missing when parsing drone '{}', " .format(name) + "using default value 1. " + "Position estimation may be inaccurate.") takeoff_height_meters = 1 drone_config = DroneConfig(init_position, speed_mps, rotate_speed_dps, takeoff_height_meters) drone_config_map[name] = drone_config else: warning( "'drones' missing when parsing configs, using default drone_config. " + "Position estimation may be inaccurate.") return {"DEFAULT": DefaultDroneConfig()} return drone_config_map