def test_parallel_with_repeated_drones_in_branches_should_give_error(self): drone_config_map = { "DRONE1": DefaultDroneConfig(), "DRONE2": DefaultDroneConfig(), "DRONE3": DefaultDroneConfig() } with self.assertRaises(CompileError) as context: generate_commands(""" procedure foo() { DRONE1.takeoff(); DRONE2.takeoff(); DRONE1.land(); DRONE2.land(); } procedure bar() { DRONE2.takeoff(); DRONE3.takeoff(); DRONE2.land(); DRONE3.land(); } main() { {foo();} || {bar();} || {DRONE3.takeoff(); DRONE3.land();}; } """, drone_config_map=drone_config_map) self.assertTrue( "Parallel branches should have exclusive drone names, " + "but {'DRONE2'} appeared in more than one branches" in str( context.exception))
def test_parallel_with_different_drones_should_give_correct_commands(self): drone_config_map = { "DRONE1": DefaultDroneConfig(), "DRONE2": DefaultDroneConfig(), "DRONE3": DefaultDroneConfig() } actual = generate_commands(""" main() { {DRONE1.takeoff();} || {DRONE2.takeoff();} || {DRONE3.takeoff();}; { DRONE1.land(); } || { {DRONE2.land();} || {DRONE3.land();}; }; } """, drone_config_map=drone_config_map) expected = [ ParallelDroneCommands( [[SingleDroneCommand("DRONE1", Command.takeoff())], [SingleDroneCommand("DRONE2", Command.takeoff())], [SingleDroneCommand("DRONE3", Command.takeoff())]]), ParallelDroneCommands( [[SingleDroneCommand("DRONE1", Command.land())], [ ParallelDroneCommands( [[SingleDroneCommand("DRONE2", Command.land())], [SingleDroneCommand("DRONE3", Command.land())]]) ]]) ] self.assertEqual(expected, actual)
def test_eq(self): expressions1 = [ None, Expression(Type.int(), 1), Expression(Type.decimal(), 1.1), Expression(Type.boolean(), False), Expression(Type.vector(), [1.1, 2.2, -1.1]), Expression(Type.list_of(Type.int()), [1, 2, 3, 4]), Expression(Type.list_of(Type.int()), []), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0]), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]]), Expression(Type.int(), 1, "a"), Expression(Type.decimal(), 1.1, "a"), Expression(Type.boolean(), False, "a"), Expression(Type.vector(), [1.1, 2.2, -1.1], "a"), Expression(Type.drone(), Drone("DRONE", DefaultDroneConfig()), "a"), Expression(Type.list_of(Type.int()), [1, 2, 3, 4], "a"), Expression(Type.list_of(Type.int()), [], "a"), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0], "a"), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]], "a") ] expressions2 = [ None, Expression(Type.int(), 1), Expression(Type.decimal(), 1.1), Expression(Type.boolean(), False), Expression(Type.vector(), [1.1, 2.2, -1.1]), Expression(Type.list_of(Type.int()), [1, 2, 3, 4]), Expression(Type.list_of(Type.int()), []), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0]), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]]), Expression(Type.int(), 1, "a"), Expression(Type.decimal(), 1.1, "a"), Expression(Type.boolean(), False, "a"), Expression(Type.vector(), [1.1, 2.2, -1.1], "a"), Expression(Type.drone(), Drone("DRONE", DefaultDroneConfig()), "a"), Expression(Type.list_of(Type.int()), [1, 2, 3, 4], "a"), Expression(Type.list_of(Type.int()), [], "a"), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0], "a"), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]], "a") ] for i in range(len(expressions1)): for j in range(len(expressions2)): if i == j: self.assertEqual(expressions1[i], expressions2[j]) else: self.assertNotEqual(expressions1[i], expressions2[j])
def test_declare_and_assign_drone_should_change_symbol_table(self): actual = SymbolTable() generate_commands(""" main () { drone a <- DRONE1; } """, drone_config_map={"DRONE1": DefaultDroneConfig()}, symbol_table=actual) expected = SymbolTable() expected.store( "a", Expression(Type.drone(), Drone("DRONE1", DefaultDroneConfig()), ident="a")) self.assertEqual(expected, actual)
def test_movement_with_no_name_specified_if_multiple_drones_should_give_error( self): drone_config_map = { "DRONE1": DefaultDroneConfig(), "DRONE2": DefaultDroneConfig() } with self.assertRaises(CompileError) as context: generate_commands(""" main() { takeoff(); land(); } """, drone_config_map=drone_config_map) self.assertTrue( "Drone should be specified if there are multiple drones in config" in str(context.exception))
def test_call_procedure_with_parameter_should_shadow_constant(self): commands = generate_commands(""" procedure proc(drone DRONE2) { DRONE2.up(1); } main () { DRONE1.takeoff(); proc(DRONE1); DRONE1.land(); } """, drone_config_map={"DRONE1": DefaultDroneConfig(), "DRONE2": DefaultDroneConfig()}) expected_commands = [SingleDroneCommand("DRONE1", Command.takeoff()), SingleDroneCommand("DRONE1", Command.up(1)), SingleDroneCommand("DRONE1", Command.land())] self.assertEqual(expected_commands, commands)
def test_repeated_declare_and_assign_drone_constant_should_give_error( self): types = [ Type.int(), Type.decimal(), Type.string(), Type.boolean(), Type.vector(), Type.drone(), Type.list_of(Type.int()), Type.list_of(Type.list_of(Type.int())) ] for type in types: with self.assertRaises(CompileError) as context: generate_commands( """ main () {{ {} a; {} DRONE <- a; }} """.format(type.type_name, type.type_name), drone_config_map={"DRONE": DefaultDroneConfig()}) self.assertTrue( "Identifier DRONE already declared" in str(context.exception))
def test_to_expression_should_equal_self(self): expressions = [ Expression(Type.int(), 1), Expression(Type.decimal(), 1.1), Expression(Type.boolean(), False), Expression(Type.vector(), [1.1, 2.2, -1.1]), Expression(Type.list_of(Type.int()), [1, 2, 3, 4]), Expression(Type.list_of(Type.int()), []), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0]), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]]), Expression(Type.int(), 1, "a"), Expression(Type.decimal(), 1.1, "a"), Expression(Type.boolean(), False, "a"), Expression(Type.vector(), [1.1, 2.2, -1.1], "a"), Expression(Type.drone(), Drone("DRONE", DefaultDroneConfig()), "a"), Expression(Type.list_of(Type.int()), [1, 2, 3, 4], "a"), Expression(Type.list_of(Type.int()), [], "a"), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0], "a"), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]], "a") ] for expression in expressions: self.assertEqual(expression, expression.to_expression())
def test_parallel_return_with_value_in_branch_should_give_error(self): drone_config_map = { "DRONE1": DefaultDroneConfig(), "DRONE2": DefaultDroneConfig(), "DRONE3": DefaultDroneConfig() } with self.assertRaises(CompileError) as context: generate_commands(""" main() { {return 1; DRONE1.takeoff();} || {DRONE2.takeoff(); DRONE2.land();}; } """, drone_config_map=drone_config_map) self.assertTrue( "Parallel branch should not return anything, but {} is returned". format(Expression(Type.int(), 1)) in str(context.exception))
def generate_commands(program, drone_config_map: Dict[str, DroneConfig] = None, boundary_checker: BoundaryChecker = None, collision_checker: CollisionChecker = None, has_checks: bool = True, symbol_table: SymbolTable = None, function_table: FunctionTable = None): if drone_config_map is None: drone_config_map = {"DEFAULT": DefaultDroneConfig()} if boundary_checker is None: boundary_checker = BoundaryChecker(BoundaryConfig.no_limit()) if collision_checker is None: collision_checker = CollisionChecker(drone_config_map, DefaultCollisionConfig()) if symbol_table is None: symbol_table = SymbolTable() if function_table is None: function_table = FunctionTable() state_updater_map = { name: StateUpdater(config) for name, config in drone_config_map.items() } tree = _parse_program(program) drone_commands = Compiler(drone_config_map, symbol_table, function_table).visit(tree) if has_checks: boundary_checker.check(drone_commands, state_updater_map) collision_checker.check(drone_commands, state_updater_map) return drone_commands
def test_wait_with_drone_name_should_return_correct_command(self): actual = generate_commands( """ main() { DRONE1.wait(1); } """, drone_config_map={"DRONE1": DefaultDroneConfig()}) expected = [SingleDroneCommand("DRONE1", Command.wait(1))] self.assertEqual(expected, actual)
def test_movement_with_undefined_name_specified_should_give_error(self): drone_config_map = {"DRONE1": DefaultDroneConfig()} with self.assertRaises(CompileError) as context: generate_commands(""" main() { DRONE2.takeoff(); DRONE2.land(); } """, drone_config_map=drone_config_map) self.assertTrue("Identifier DRONE2 has not been declared" in str( context.exception))
def test_backward_with_drone_name_should_return_correct_command(self): actual = generate_commands( """ main() { DRONE1.takeoff(); DRONE1.backward(1); DRONE1.land(); } """, drone_config_map={"DRONE1": DefaultDroneConfig()}) expected = [ SingleDroneCommand("DRONE1", Command.takeoff()), SingleDroneCommand("DRONE1", Command.backward(1)), SingleDroneCommand("DRONE1", Command.land()) ] self.assertEqual(expected, actual)
def test_container_not_list_should_give_error(self): none_list_exprs = [ Expression(Type.int(), 1, "a"), Expression(Type.decimal(), 1.0, "a"), Expression(Type.boolean(), True, "a"), Expression(Type.string(), "abc", "a"), Expression(Type.vector(), [1.0, 1.0, 1.0], "a"), Expression(Type.drone(), Drone("DRONE", DefaultDroneConfig()), "a") ] for none_list_expr in none_list_exprs: with self.assertRaises(AssertionError) as context: ListElem("a", none_list_expr, 0)
def test_movement_with_no_name_specified_if_only_one_drone_should_use_that_drone( self): drone_config_map = {"THE_ONE": DefaultDroneConfig()} actual = generate_commands(""" main() { takeoff(); land(); } """, drone_config_map=drone_config_map) expected = [ SingleDroneCommand("THE_ONE", Command.takeoff()), SingleDroneCommand("THE_ONE", Command.land()) ] self.assertEqual(expected, actual)
def test_parallel_return_in_branch_should_return_early(self): drone_config_map = { "DRONE1": DefaultDroneConfig(), "DRONE2": DefaultDroneConfig(), "DRONE3": DefaultDroneConfig() } actual = generate_commands(""" main() { {return; DRONE1.takeoff();} || {DRONE2.takeoff(); DRONE2.land();}; } """, drone_config_map=drone_config_map) expected = [ ParallelDroneCommands( [[], [ SingleDroneCommand("DRONE2", Command.takeoff()), SingleDroneCommand("DRONE2", Command.land()) ]]) ] self.assertEqual(expected, actual)
def test_str(self): self.assertEqual("Expression: { type: int, value: 1, ident: None }", str(Expression(Type.int(), 1))) self.assertEqual( "Expression: { type: decimal, value: 1.1, ident: None }", str(Expression(Type.decimal(), 1.1))) self.assertEqual( "Expression: { type: boolean, value: False, ident: None }", str(Expression(Type.boolean(), False))) self.assertEqual( "Expression: { type: vector, value: [1.1, 2.2, -1.1], ident: None }", str(Expression(Type.vector(), [1.1, 2.2, -1.1]))) self.assertEqual( "Expression: { type: list[int], value: [1, 2, 3, 4], ident: None }", str(Expression(Type.list_of(Type.int()), [1, 2, 3, 4]))) self.assertEqual( "Expression: { type: list[int], value: [], ident: None }", str(Expression(Type.list_of(Type.int()), []))) self.assertEqual( "Expression: { type: list[list[vector]], value: [[[1.1, 2.2, -1.1], [1, 2, -1]]], ident: None }", str( Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]]))) self.assertEqual("Expression: { type: int, value: 1, ident: a }", str(Expression(Type.int(), 1, "a"))) self.assertEqual("Expression: { type: decimal, value: 1.1, ident: a }", str(Expression(Type.decimal(), 1.1, "a"))) self.assertEqual( "Expression: { type: boolean, value: False, ident: a }", str(Expression(Type.boolean(), False, "a"))) self.assertEqual( "Expression: { type: vector, value: [1.1, 2.2, -1.1], ident: a }", str(Expression(Type.vector(), [1.1, 2.2, -1.1], "a"))) self.assertEqual( "Expression: { type: drone, value: Drone: { name: DRONE, config: DroneConfig: " "{ init_position: (0, 0, 0), speed_mps: 1, rotate_speed_dps: 90, takeoff_height_meters: 1 } " "}, ident: a }", str( Expression(Type.drone(), Drone("DRONE", DefaultDroneConfig()), "a"))) self.assertEqual( "Expression: { type: list[int], value: [1, 2, 3, 4], ident: a }", str(Expression(Type.list_of(Type.int()), [1, 2, 3, 4], "a"))) self.assertEqual( "Expression: { type: list[int], value: [], ident: a }", str(Expression(Type.list_of(Type.int()), [], "a"))) self.assertEqual( "Expression: { type: list[list[vector]], value: [[[1.1, 2.2, -1.1], [1, 2, -1]]], ident: a }", str( Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]], "a")))
def test_parse_if_not_provided_configs_should_use_default_configs(self): config = "{}" with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, acutal_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'drones' missing when parsing configs, using default drone_config. " + "Position estimation may be inaccurate.", "WARNING:root:'boundary_config' missing when parsing configs, using unlimited boundary_config. " + "Time and position will be unlimited.", "WARNING:root:'collision_config' missing when parsing configs, using default collision_config."] self.assertEqual(expected_log, log.output) self.assertEqual({"DEFAULT": DefaultDroneConfig()}, actual_drone_configs) self.assertEqual(BoundaryConfig.no_limit(), actual_boundary_config) self.assertEqual(DefaultCollisionConfig(), acutal_collision_config)
def test_del_drone_constant_should_give_error(self): with self.assertRaises(CompileError) as context: generate_commands( """ main () { del DRONE1; } """, drone_config_map={"DRONE1": DefaultDroneConfig()}) self.assertTrue( "Identifier DRONE1 is a drone constant, cannot be deleted" in str( context.exception))
def test_to_expression_should_return_correct_value(self): expressions = [ None, Expression(Type.int(), 1, "a"), Expression(Type.decimal(), 1.1, "a"), Expression(Type.boolean(), False, "a"), Expression(Type.vector(), [1.1, 2.2, -1.1], "a"), Expression(Type.drone(), Drone("DRONE", DefaultDroneConfig()), "a"), Expression(Type.list_of(Type.int()), [1, 2, 3, 4], "a"), Expression(Type.list_of(Type.int()), [], "a"), Expression(Type.list_of(Type.decimal()), [1.0, 2.0, 3.0, 4.0], "a"), Expression(Type.list_of(Type.list_of(Type.vector())), [[[1.1, 2.2, -1.1], [1, 2, -1]]], "a") ] for expression in expressions: self.assertEqual(expression, Identifier("a", expression).to_expression())
def test_parse_if_configs_missing_fields_should_use_default_value(self): config = """ { "drones": [], "boundary_config": { }, "collision_config": { } } """ with self.assertLogs(logging.getLogger()) as log: actual_drone_configs, actual_boundary_config, actual_collision_config = ConfigParser.parse(config) expected_log = ["WARNING:root:'drones' is empty when parsing configs, using default drone_config. " + "Position estimation may be inaccurate.", "WARNING:root:'max_seconds' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_seconds'.", "WARNING:root:'max_x_meters' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_x_meters'.", "WARNING:root:'max_y_meters' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_y_meters'.", "WARNING:root:'max_z_meters' missing when parsing 'boundary_config', " + "using default value inf. There will be no limit on 'max_z_meters'.", "WARNING:root:'min_x_meters' missing when parsing 'boundary_config', " + "using default value -inf. There will be no limit on 'min_x_meters'.", "WARNING:root:'min_y_meters' missing when parsing 'boundary_config', " + "using default value -inf. There will be no limit on 'min_y_meters'.", "WARNING:root:'min_z_meters' missing when parsing 'boundary_config', " + "using default value -inf. There will be no limit on 'min_z_meters'.", "WARNING:root:'collision_meters' missing when parsing 'collision_config', " + "using default value 0. There will be no limit on 'collision_meters'.", "WARNING:root:'time_interval_seconds' missing when parsing 'collision_config', " + "using default value 0.1."] self.assertEqual(expected_log, log.output) expected_drone_configs = {"DEFAULT": DefaultDroneConfig()} expected_boundary_config = BoundaryConfig(max_seconds=float("inf"), max_x_meters=float("inf"), max_y_meters=float("inf"), max_z_meters=float("inf"), min_x_meters=float("-inf"), min_y_meters=float("-inf"), min_z_meters=float("-inf")) expected_collision_config = DefaultCollisionConfig() 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 __init__(self): super().__init__("null", DefaultDroneConfig())
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
def test_default_values(self): drone_config = DefaultDroneConfig() self.assertEqual(1, drone_config.speed_mps) self.assertEqual(90, drone_config.rotate_speed_dps) self.assertEqual(1, drone_config.takeoff_height_meters)