Skip to content

Commit 4c20588

Browse files
committed
WIP
1 parent 3452ceb commit 4c20588

6 files changed

Lines changed: 494 additions & 113 deletions

File tree

src/software/field_tests/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ py_test(
3838
],
3939
deps = [
4040
"//software:conftest",
41-
"//software/gameplay_tests:tbots_test_runner",
41+
"//software/gameplay_tests:util",
4242
"//software/gameplay_tests/validation:validations",
4343
requirement("pytest"),
4444
],

src/software/field_tests/movement_robot_field_test.py

Lines changed: 65 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from proto.import_all_protos import *
2-
from software.field_tests.field_test_fixture import *
1+
import math
32

4-
from software.gameplay_tests.util import *
3+
from proto.import_all_protos import *
4+
from software.gameplay_tests.util import pytest_main
55
from software.logger.logger import create_logger
6-
import math
76

87
logger = create_logger(__name__)
98

109

11-
# TODO 2908: Support running this test in both simulator or field mode
10+
# TODO(#2908): Support running this test in both simulator or field mode
1211
# this test can be run either in simulation or on the field
1312
# @pytest.mark.parametrize(
1413
# "robot_x_destination, robot_y_destination",
@@ -66,64 +65,66 @@
6665
# test_timeout_s=5,
6766
# )
6867

69-
68+
# TODO(#2908): uncomment this
7069
# this test can only be run on the field
71-
def test_basic_rotation(field_test_runner):
72-
test_angles = [0, 45, 90, 180, 270, 0]
70+
# def test_basic_rotation(field_test_runner):
71+
# test_angles = [0, 45, 90, 180, 270, 0]
72+
#
73+
# world = field_test_runner.world_buffer.get(block=True, timeout=WORLD_BUFFER_TIMEOUT)
74+
# if len(world.friendly_team.team_robots) == 0:
75+
# raise Exception("The first world received had no robots in it!")
76+
#
77+
# print("Here are the robots:")
78+
# print(
79+
# [
80+
# robot.current_state.global_position
81+
# for robot in world.friendly_team.team_robots
82+
# ]
83+
# )
84+
#
85+
# id = world.friendly_team.team_robots[0].id
86+
# print(f"Running test on robot {id}")
87+
#
88+
# robot = world.friendly_team.team_robots[0]
89+
# rob_pos_p = robot.current_state.global_position
90+
# logger.info("staying in pos {rob_pos_p}")
91+
#
92+
# for angle in test_angles:
93+
# move_tactic = MoveTactic()
94+
# move_tactic.destination.CopyFrom(rob_pos_p)
95+
# move_tactic.dribbler_mode = DribblerMode.OFF
96+
# move_tactic.final_orientation.CopyFrom(Angle(radians=angle))
97+
# move_tactic.ball_collision_type = BallCollisionType.AVOID
98+
# move_tactic.auto_chip_or_kick.CopyFrom(
99+
# AutoChipOrKick(autokick_speed_m_per_s=0.0)
100+
# )
101+
# move_tactic.max_allowed_speed_mode = MaxAllowedSpeedMode.PHYSICAL_LIMIT
102+
# move_tactic.obstacle_avoidance_mode = ObstacleAvoidanceMode.SAFE
103+
#
104+
# # Setup Tactic
105+
# field_test_runner.set_tactics(
106+
# blue_tactics={id: move_tactic}, yellow_tactics=None
107+
# )
108+
# field_test_runner.run_test(
109+
# always_validation_sequence_set=[[]],
110+
# eventually_validation_sequence_set=[[]],
111+
# test_timeout_s=5,
112+
# )
113+
# # Send a halt tactic after the test finishes
114+
# field_test_runner.set_tactics(
115+
# blue_tactics={id: HaltTactic()}, yellow_tactics=None
116+
# )
117+
#
118+
# # validate by eye
119+
# logger.info(f"robot set to {angle} orientation")
120+
#
121+
# time.sleep(2)
73122

74-
world = field_test_runner.world_buffer.get(block=True, timeout=WORLD_BUFFER_TIMEOUT)
75-
if len(world.friendly_team.team_robots) == 0:
76-
raise Exception("The first world received had no robots in it!")
77123

78-
print("Here are the robots:")
79-
print(
80-
[
81-
robot.current_state.global_position
82-
for robot in world.friendly_team.team_robots
83-
]
124+
def test_one_robots_square(gameplay_test_runner):
125+
world = gameplay_test_runner.world_buffer.get(
126+
block=True, timeout=WORLD_BUFFER_TIMEOUT
84127
)
85-
86-
id = world.friendly_team.team_robots[0].id
87-
print(f"Running test on robot {id}")
88-
89-
robot = world.friendly_team.team_robots[0]
90-
rob_pos_p = robot.current_state.global_position
91-
logger.info("staying in pos {rob_pos_p}")
92-
93-
for angle in test_angles:
94-
move_tactic = MoveTactic()
95-
move_tactic.destination.CopyFrom(rob_pos_p)
96-
move_tactic.dribbler_mode = DribblerMode.OFF
97-
move_tactic.final_orientation.CopyFrom(Angle(radians=angle))
98-
move_tactic.ball_collision_type = BallCollisionType.AVOID
99-
move_tactic.auto_chip_or_kick.CopyFrom(
100-
AutoChipOrKick(autokick_speed_m_per_s=0.0)
101-
)
102-
move_tactic.max_allowed_speed_mode = MaxAllowedSpeedMode.PHYSICAL_LIMIT
103-
move_tactic.obstacle_avoidance_mode = ObstacleAvoidanceMode.SAFE
104-
105-
# Setup Tactic
106-
field_test_runner.set_tactics(
107-
blue_tactics={id: move_tactic}, yellow_tactics=None
108-
)
109-
field_test_runner.run_test(
110-
always_validation_sequence_set=[[]],
111-
eventually_validation_sequence_set=[[]],
112-
test_timeout_s=5,
113-
)
114-
# Send a halt tactic after the test finishes
115-
field_test_runner.set_tactics(
116-
blue_tactics={id: HaltTactic()}, yellow_tactics=None
117-
)
118-
119-
# validate by eye
120-
logger.info(f"robot set to {angle} orientation")
121-
122-
time.sleep(2)
123-
124-
125-
def test_one_robots_square(field_test_runner):
126-
world = field_test_runner.world_buffer.get(block=True, timeout=WORLD_BUFFER_TIMEOUT)
127128
if len(world.friendly_team.team_robots) == 0:
128129
raise Exception("The first world received had no robots in it!")
129130

@@ -184,20 +185,22 @@ def test_one_robots_square(field_test_runner):
184185
for tactic in tactics:
185186
print(f"Going to {tactic.destination}")
186187

187-
field_test_runner.set_tactics(
188+
gameplay_test_runner.set_tactics(
188189
blue_tactics={
189190
id: tactic,
190191
},
191192
yellow_tactics=None,
192193
)
193-
field_test_runner.run_test(
194+
gameplay_test_runner.run_test(
194195
always_validation_sequence_set=[[]],
195196
eventually_validation_sequence_set=[[]],
196197
test_timeout_s=4,
197198
)
198199

199200
# Send a halt tactic after the test finishes
200-
field_test_runner.set_tactics(blue_tactics={id: HaltTactic()}, yellow_tactics=None)
201+
gameplay_test_runner.set_tactics(
202+
blue_tactics={id: HaltTactic()}, yellow_tactics=None
203+
)
201204

202205

203206
if __name__ == "__main__":

src/software/gameplay_tests/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ py_library(
1616
],
1717
deps = [
1818
":util",
19+
":field_test_runner",
1920
":simulated_test_runner",
21+
"//software/thunderscope:estop_helpers",
22+
"//software/logger:py_logger",
2023
"//software/thunderscope:thunderscope",
2124
"//software/thunderscope/binary_context_managers:full_system",
2225
"//software/thunderscope/binary_context_managers:game_controller",
@@ -46,6 +49,14 @@ py_library(
4649
],
4750
)
4851

52+
py_library(
53+
name = "field_test_runner",
54+
srcs = [
55+
"field_test_runner.py",
56+
],
57+
deps = [],
58+
)
59+
4960
py_library(
5061
name = "simulated_test_runner",
5162
srcs = [
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import queue
2+
import time
3+
import threading
4+
5+
import pytest
6+
from proto.import_all_protos import *
7+
8+
from software.gameplay_tests.validation import validation
9+
from software.logger.logger import create_logger
10+
11+
12+
from software.gameplay_tests.tbots_test_runner import TbotsTestRunner
13+
from software.py_constants import *
14+
from typing import override
15+
16+
logger = create_logger(__name__)
17+
18+
WORLD_BUFFER_TIMEOUT = 5.0
19+
PROCESS_BUFFER_DELAY_S = 0.01
20+
PAUSE_AFTER_FAIL_DELAY_S = 3
21+
LAUNCH_DELAY_S = 0.1
22+
TEST_END_DELAY = 0.3
23+
24+
25+
class FieldTestRunner(TbotsTestRunner):
26+
"""Run a field test"""
27+
28+
def __init__(
29+
self,
30+
test_name,
31+
thunderscope,
32+
blue_full_system_proto_unix_io,
33+
yellow_full_system_proto_unix_io,
34+
gamecontroller,
35+
publish_validation_protos=True,
36+
is_yellow_friendly=False,
37+
):
38+
"""Initialize the FieldTestRunner
39+
40+
:param test_name: The name of the test to run
41+
:param thunderscope: The Thunderscope to use, None if not used
42+
:param blue_full_system_proto_unix_io: The blue full system proto unix io to use
43+
:param yellow_full_system_proto_unix_io: The yellow full system proto unix io to use
44+
:param gamecontroller: The gamecontroller context managed instance
45+
:param publish_validation_protos: whether to publish validation protos
46+
:param: is_yellow_friendly: if yellow is the friendly team
47+
"""
48+
super(FieldTestRunner, self).__init__(
49+
test_name,
50+
thunderscope,
51+
blue_full_system_proto_unix_io,
52+
yellow_full_system_proto_unix_io,
53+
gamecontroller,
54+
is_yellow_friendly,
55+
)
56+
self.publish_validation_protos = publish_validation_protos
57+
self.is_yellow_friendly = is_yellow_friendly
58+
59+
logger.info("determining robots on field")
60+
# survey field for available robot ids
61+
try:
62+
world = self.world_buffer.get(block=True, timeout=WORLD_BUFFER_TIMEOUT)
63+
self.initial_world = world
64+
self.friendly_robot_ids_field = [
65+
robot.id for robot in world.friendly_team.team_robots
66+
]
67+
68+
logger.info(f"friendly team ids {self.friendly_robot_ids_field}")
69+
70+
if len(self.friendly_robot_ids_field) == 0:
71+
raise Exception("no friendly robots found on field")
72+
73+
except queue.Empty:
74+
raise Exception(
75+
f"No Worlds were received with in {WORLD_BUFFER_TIMEOUT} seconds. Please make sure atleast 1 robot and 1 ball is present on the field."
76+
)
77+
78+
@override
79+
def send_gamecontroller_command(
80+
self,
81+
gc_command: proto.ssl_gc_state_pb2.Command,
82+
team: proto.ssl_gc_common_pb2.Team,
83+
final_ball_placement_point=None,
84+
):
85+
"""Send a command to the gamecontroller
86+
87+
:param gc_command: The command to send
88+
:param team: The team which the command as attributed to
89+
:param final_ball_placement_point: The ball placement point
90+
"""
91+
self.gamecontroller.send_ci_input(
92+
gc_command=gc_command,
93+
team=team,
94+
final_ball_placement_point=final_ball_placement_point,
95+
)
96+
97+
@override
98+
def run_test(
99+
self,
100+
always_validation_sequence_set=[[]],
101+
eventually_validation_sequence_set=[[]],
102+
test_timeout_s=3,
103+
):
104+
"""Run a test. In a field test this means beginning validation.
105+
106+
:param always_validation_sequence_set: Validation functions that should
107+
hold on every tick
108+
:param eventually_validation_sequence_set: Validation that should
109+
eventually be true, before the test ends
110+
:param test_timeout_s: The timeout for the test, if any eventually_validations
111+
remain after the timeout, the test fails.
112+
"""
113+
114+
def stop_test(delay):
115+
time.sleep(delay)
116+
if self.thunderscope:
117+
self.thunderscope.close()
118+
119+
def runner():
120+
time.sleep(LAUNCH_DELAY_S)
121+
122+
test_end_time = time.time() + test_timeout_s
123+
124+
while time.time() < test_end_time:
125+
while True:
126+
try:
127+
world = self.world_buffer.get(
128+
block=True, timeout=WORLD_BUFFER_TIMEOUT
129+
)
130+
break
131+
except queue.Empty:
132+
# If we timeout, that means full_system missed the last
133+
# wrapper and robot status, lets resend it.
134+
logger.warning(
135+
f"No World was received for {WORLD_BUFFER_TIMEOUT} seconds. Ending test early."
136+
)
137+
138+
# Validate
139+
(
140+
eventually_validation_proto_set,
141+
always_validation_proto_set,
142+
) = validation.run_validation_sequence_sets(
143+
world,
144+
eventually_validation_sequence_set,
145+
always_validation_sequence_set,
146+
)
147+
148+
if self.publish_validation_protos:
149+
# Set the test name
150+
eventually_validation_proto_set.test_name = self.test_name
151+
always_validation_proto_set.test_name = self.test_name
152+
153+
# Send out the validation proto to thunderscope
154+
self.blue_full_system_proto_unix_io.send_proto(
155+
ValidationProtoSet, eventually_validation_proto_set
156+
)
157+
self.blue_full_system_proto_unix_io.send_proto(
158+
ValidationProtoSet, always_validation_proto_set
159+
)
160+
161+
# Check that all always validations are always valid
162+
validation.check_validation(always_validation_proto_set)
163+
164+
# Check that all eventually validations are eventually valid
165+
validation.check_validation(eventually_validation_proto_set)
166+
stop_test(TEST_END_DELAY)
167+
168+
def excepthook(args):
169+
"""This function is _critical_ for show_thunderscope to work.
170+
If the test Thread will raises an exception we won't be able to close
171+
the window from the main thread.
172+
173+
:param args: The args passed in from the hook
174+
"""
175+
stop_test(delay=PAUSE_AFTER_FAIL_DELAY_S)
176+
self.last_exception = args.exc_value
177+
raise self.last_exception
178+
179+
threading.excepthook = excepthook
180+
181+
if self.thunderscope:
182+
run_test_thread = threading.Thread(target=runner, daemon=True)
183+
run_test_thread.start()
184+
self.thunderscope.show()
185+
run_test_thread.join()
186+
187+
if self.last_exception:
188+
pytest.fail(str(ex.last_exception))
189+
190+
else:
191+
runner()

0 commit comments

Comments
 (0)