diff --git a/src/software/ai/hl/stp/play/BUILD b/src/software/ai/hl/stp/play/BUILD index a74371ace5..e3ccbedd34 100644 --- a/src/software/ai/hl/stp/play/BUILD +++ b/src/software/ai/hl/stp/play/BUILD @@ -76,10 +76,6 @@ py_test( srcs = [ "kickoff_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", @@ -94,10 +90,6 @@ py_test( ], # The default main would be stop_play_test.py; override to our file. main = "stop_play_test.py", - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", @@ -150,7 +142,6 @@ py_test( data = [ "//software:py_constants.so", ], - tags = ["exclusive"], deps = [ "//proto:import_all_protos", "//proto:software_py_proto", diff --git a/src/software/ai/hl/stp/play/ball_placement/BUILD b/src/software/ai/hl/stp/play/ball_placement/BUILD index 6621cec3d4..c5b5d0acd6 100644 --- a/src/software/ai/hl/stp/play/ball_placement/BUILD +++ b/src/software/ai/hl/stp/play/ball_placement/BUILD @@ -37,7 +37,6 @@ py_test( srcs = [ "ball_placement_play_test.py", ], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py b/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py index 77e4e196ab..65bb14fbd6 100644 --- a/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py +++ b/src/software/ai/hl/stp/play/ball_placement/ball_placement_play_test.py @@ -200,7 +200,7 @@ def run_ball_placement_scenario( inv_eventually_validation_sequence_set=placement_eventually_validation_sequence_set, ag_always_validation_sequence_set=[[]], ag_eventually_validation_sequence_set=placement_eventually_validation_sequence_set, - test_timeout_s=[15], + test_timeout_s=[30], ) simulated_test_runner.run_test( @@ -209,7 +209,7 @@ def run_ball_placement_scenario( inv_eventually_validation_sequence_set=drop_ball_eventually_validation_sequence_set, ag_always_validation_sequence_set=drop_ball_always_validation_sequence_set, ag_eventually_validation_sequence_set=drop_ball_eventually_validation_sequence_set, - test_timeout_s=[5], + test_timeout_s=[10], ) diff --git a/src/software/ai/hl/stp/play/crease_defense/BUILD b/src/software/ai/hl/stp/play/crease_defense/BUILD index ca6980db81..cc991f0f13 100644 --- a/src/software/ai/hl/stp/play/crease_defense/BUILD +++ b/src/software/ai/hl/stp/play/crease_defense/BUILD @@ -33,10 +33,6 @@ py_test( srcs = [ "crease_defense_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/defense/BUILD b/src/software/ai/hl/stp/play/defense/BUILD index 2f2d8de10e..8b12429e14 100644 --- a/src/software/ai/hl/stp/play/defense/BUILD +++ b/src/software/ai/hl/stp/play/defense/BUILD @@ -36,10 +36,6 @@ py_test( srcs = [ "defense_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD b/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD index de72dcf77f..663c6bc9dd 100644 --- a/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD +++ b/src/software/ai/hl/stp/play/enemy_ball_placement/BUILD @@ -28,7 +28,6 @@ py_test( srcs = [ "enemy_ball_placement_play_test.py", ], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/enemy_free_kick/BUILD b/src/software/ai/hl/stp/play/enemy_free_kick/BUILD index ca05d2fd7f..00b9690299 100644 --- a/src/software/ai/hl/stp/play/enemy_free_kick/BUILD +++ b/src/software/ai/hl/stp/play/enemy_free_kick/BUILD @@ -40,9 +40,6 @@ py_test( srcs = [ "enemy_free_kick_play_test.py", ], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/example/BUILD b/src/software/ai/hl/stp/play/example/BUILD index 1b1f1de048..f3ccd10a83 100644 --- a/src/software/ai/hl/stp/play/example/BUILD +++ b/src/software/ai/hl/stp/play/example/BUILD @@ -35,10 +35,6 @@ cc_test( py_test( name = "example_play_test", srcs = ["example_play_test.py"], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/free_kick/BUILD b/src/software/ai/hl/stp/play/free_kick/BUILD index bf0f2ee4df..49e5d6921d 100644 --- a/src/software/ai/hl/stp/play/free_kick/BUILD +++ b/src/software/ai/hl/stp/play/free_kick/BUILD @@ -37,7 +37,6 @@ cc_library( py_test( name = "free_kick_play_test", srcs = ["free_kick_play_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/halt_play/BUILD b/src/software/ai/hl/stp/play/halt_play/BUILD index f67983bfc4..9628823e55 100644 --- a/src/software/ai/hl/stp/play/halt_play/BUILD +++ b/src/software/ai/hl/stp/play/halt_play/BUILD @@ -37,10 +37,6 @@ py_test( srcs = [ "halt_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/kickoff_enemy/BUILD b/src/software/ai/hl/stp/play/kickoff_enemy/BUILD index 409b7ba865..aab88be734 100644 --- a/src/software/ai/hl/stp/play/kickoff_enemy/BUILD +++ b/src/software/ai/hl/stp/play/kickoff_enemy/BUILD @@ -29,7 +29,6 @@ cc_library( py_test( name = "kickoff_enemy_play_test", srcs = ["kickoff_enemy_play_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/kickoff_friendly/BUILD b/src/software/ai/hl/stp/play/kickoff_friendly/BUILD index 35325ce76a..1ca18ac954 100644 --- a/src/software/ai/hl/stp/play/kickoff_friendly/BUILD +++ b/src/software/ai/hl/stp/play/kickoff_friendly/BUILD @@ -28,7 +28,6 @@ cc_library( py_test( name = "kickoff_friendly_play_test", srcs = ["kickoff_friendly_play_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.py b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.py index a7e33d5d34..51b4bd48cc 100644 --- a/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.py +++ b/src/software/ai/hl/stp/play/kickoff_friendly/kickoff_friendly_play_test.py @@ -144,7 +144,7 @@ def setup(*args): inv_always_validation_sequence_set=always_validation_sequence_set, ag_eventually_validation_sequence_set=eventually_validation_sequence_set, ag_always_validation_sequence_set=always_validation_sequence_set, - test_timeout_s=10, + test_timeout_s=60, ) diff --git a/src/software/ai/hl/stp/play/offense/BUILD b/src/software/ai/hl/stp/play/offense/BUILD index 930f5d13d6..67687c03ee 100644 --- a/src/software/ai/hl/stp/play/offense/BUILD +++ b/src/software/ai/hl/stp/play/offense/BUILD @@ -33,10 +33,6 @@ py_test( srcs = [ "offense_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/penalty_kick_enemy/BUILD b/src/software/ai/hl/stp/play/penalty_kick_enemy/BUILD index b83140a6e5..9dc2cb7fb5 100644 --- a/src/software/ai/hl/stp/play/penalty_kick_enemy/BUILD +++ b/src/software/ai/hl/stp/play/penalty_kick_enemy/BUILD @@ -37,7 +37,6 @@ cc_test( py_test( name = "penalty_kick_enemy_play_test", srcs = ["penalty_kick_enemy_play_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/shoot_or_chip/BUILD b/src/software/ai/hl/stp/play/shoot_or_chip/BUILD index 2d74dd1993..f55d2d0bf5 100644 --- a/src/software/ai/hl/stp/play/shoot_or_chip/BUILD +++ b/src/software/ai/hl/stp/play/shoot_or_chip/BUILD @@ -36,10 +36,6 @@ py_test( srcs = [ "shoot_or_chip_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/play/shoot_or_pass/BUILD b/src/software/ai/hl/stp/play/shoot_or_pass/BUILD index bc366bd8a0..0c70c99acb 100644 --- a/src/software/ai/hl/stp/play/shoot_or_pass/BUILD +++ b/src/software/ai/hl/stp/play/shoot_or_pass/BUILD @@ -52,10 +52,6 @@ py_test( srcs = [ "shoot_or_pass_play_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/attacker/BUILD b/src/software/ai/hl/stp/tactic/attacker/BUILD index 86634c632e..9bacc3f3d6 100644 --- a/src/software/ai/hl/stp/tactic/attacker/BUILD +++ b/src/software/ai/hl/stp/tactic/attacker/BUILD @@ -38,7 +38,6 @@ cc_test( py_test( name = "attacker_tactic_test", srcs = ["attacker_tactic_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/chip/BUILD b/src/software/ai/hl/stp/tactic/chip/BUILD index 5b5124cb78..69bde4554a 100644 --- a/src/software/ai/hl/stp/tactic/chip/BUILD +++ b/src/software/ai/hl/stp/tactic/chip/BUILD @@ -34,9 +34,6 @@ cc_test( py_test( name = "chip_tactic_test", srcs = ["chip_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/crease_defender/BUILD b/src/software/ai/hl/stp/tactic/crease_defender/BUILD index 58467ae6a5..77aa1af69a 100644 --- a/src/software/ai/hl/stp/tactic/crease_defender/BUILD +++ b/src/software/ai/hl/stp/tactic/crease_defender/BUILD @@ -38,9 +38,6 @@ cc_test( py_test( name = "crease_defender_tactic_test", srcs = ["crease_defender_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/dribble/BUILD b/src/software/ai/hl/stp/tactic/dribble/BUILD index 7916893668..1b0c296620 100644 --- a/src/software/ai/hl/stp/tactic/dribble/BUILD +++ b/src/software/ai/hl/stp/tactic/dribble/BUILD @@ -36,10 +36,6 @@ py_test( srcs = [ "dribble_tactic_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/goalie/BUILD b/src/software/ai/hl/stp/tactic/goalie/BUILD index 47d8f9f8af..0f53b8a418 100644 --- a/src/software/ai/hl/stp/tactic/goalie/BUILD +++ b/src/software/ai/hl/stp/tactic/goalie/BUILD @@ -52,10 +52,6 @@ py_test( srcs = [ "goalie_tactic_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/halt/BUILD b/src/software/ai/hl/stp/tactic/halt/BUILD index f32f65094e..bc6cdc146c 100644 --- a/src/software/ai/hl/stp/tactic/halt/BUILD +++ b/src/software/ai/hl/stp/tactic/halt/BUILD @@ -32,9 +32,6 @@ cc_test( py_test( name = "halt_tactic_test", srcs = ["halt_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/kick/BUILD b/src/software/ai/hl/stp/tactic/kick/BUILD index 17c554eca9..d873ad65c3 100644 --- a/src/software/ai/hl/stp/tactic/kick/BUILD +++ b/src/software/ai/hl/stp/tactic/kick/BUILD @@ -34,9 +34,6 @@ cc_test( py_test( name = "kick_tactic_test", srcs = ["kick_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/move/BUILD b/src/software/ai/hl/stp/tactic/move/BUILD index 8d772fd72d..5343192526 100644 --- a/src/software/ai/hl/stp/tactic/move/BUILD +++ b/src/software/ai/hl/stp/tactic/move/BUILD @@ -33,9 +33,6 @@ cc_test( py_test( name = "move_tactic_test", srcs = ["move_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/pass_defender/BUILD b/src/software/ai/hl/stp/tactic/pass_defender/BUILD index b83739a0dd..b167a182d4 100644 --- a/src/software/ai/hl/stp/tactic/pass_defender/BUILD +++ b/src/software/ai/hl/stp/tactic/pass_defender/BUILD @@ -39,10 +39,6 @@ py_test( srcs = [ "pass_defender_tactic_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/penalty_kick/BUILD b/src/software/ai/hl/stp/tactic/penalty_kick/BUILD index 0c3b424a8a..52697136af 100644 --- a/src/software/ai/hl/stp/tactic/penalty_kick/BUILD +++ b/src/software/ai/hl/stp/tactic/penalty_kick/BUILD @@ -38,7 +38,6 @@ cc_test( py_test( name = "penalty_kick_tactic_test", srcs = ["penalty_kick_tactic_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/pivot_kick/BUILD b/src/software/ai/hl/stp/tactic/pivot_kick/BUILD index 7df9c6095d..6afb91869b 100644 --- a/src/software/ai/hl/stp/tactic/pivot_kick/BUILD +++ b/src/software/ai/hl/stp/tactic/pivot_kick/BUILD @@ -36,9 +36,6 @@ cc_test( py_test( name = "pivot_kick_tactic_test", srcs = ["pivot_kick_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/hl/stp/tactic/receiver/BUILD b/src/software/ai/hl/stp/tactic/receiver/BUILD index 2a6f5d59ac..77453ab057 100644 --- a/src/software/ai/hl/stp/tactic/receiver/BUILD +++ b/src/software/ai/hl/stp/tactic/receiver/BUILD @@ -38,9 +38,6 @@ cc_test( py_test( name = "receiver_tactic_test", srcs = ["receiver_tactic_test.py"], - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/ai/navigator/trajectory/BUILD b/src/software/ai/navigator/trajectory/BUILD index abc4f5c82f..630ed8fde3 100644 --- a/src/software/ai/navigator/trajectory/BUILD +++ b/src/software/ai/navigator/trajectory/BUILD @@ -153,7 +153,6 @@ cc_test( py_test( name = "simulated_hrvo_test", srcs = ["simulated_hrvo_test.py"], - tags = ["exclusive"], deps = [ "//proto/message_translation:py_tbots_protobuf", "//software:conftest", diff --git a/src/software/field_tests/BUILD b/src/software/field_tests/BUILD index 96ec19c6e3..5a45d08742 100644 --- a/src/software/field_tests/BUILD +++ b/src/software/field_tests/BUILD @@ -32,10 +32,6 @@ py_test( srcs = [ "movement_robot_field_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests:tbots_test_runner", @@ -49,10 +45,6 @@ py_test( srcs = [ "pivot_kick_field_test.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests:tbots_test_runner", diff --git a/src/software/field_tests/field_test_fixture.py b/src/software/field_tests/field_test_fixture.py index ebf0480ac3..c23a4fdbf8 100644 --- a/src/software/field_tests/field_test_fixture.py +++ b/src/software/field_tests/field_test_fixture.py @@ -202,6 +202,32 @@ def excepthook(args): __runner() +def get_runtime_dir(): + """Gets the base runtime directory for the test execution. + TODO: Refactor #3744 + + If running under Bazel, it uses TEST_TMPDIR to keep tests isolated. To prevent UNIX + socket path length limits from being exceeded by Bazel's long paths, it creates a short + symlink in /tmp to the TEST_TMPDIR. + + :return: The path to the runtime directory. + """ + test_tmpdir = os.environ.get("TEST_TMPDIR") + if not test_tmpdir: + return "/tmp/tbots" + import uuid + + symlink_path = os.path.join("/tmp", f"tbt_{uuid.uuid4().hex[:8]}") + try: + os.symlink(test_tmpdir, symlink_path) + except OSError: + pass + return symlink_path + + +RUNTIME_DIR = get_runtime_dir() + + def load_command_line_arguments(): """Load in command-line arguments using argparse @@ -213,19 +239,19 @@ def load_command_line_arguments(): "--simulator_runtime_dir", type=str, help="simulator runtime directory", - default="/tmp/tbots", + default=RUNTIME_DIR, ) parser.add_argument( "--blue_full_system_runtime_dir", type=str, help="blue full_system runtime directory", - default="/tmp/tbots/blue", + default=os.path.join(RUNTIME_DIR, "blue"), ) parser.add_argument( "--yellow_full_system_runtime_dir", type=str, help="yellow full_system runtime directory", - default="/tmp/tbots/yellow", + default=os.path.join(RUNTIME_DIR, "yellow"), ) parser.add_argument( "--layout", diff --git a/src/software/sensor_fusion/filter/BUILD b/src/software/sensor_fusion/filter/BUILD index 6116f49da2..ab07139bd8 100644 --- a/src/software/sensor_fusion/filter/BUILD +++ b/src/software/sensor_fusion/filter/BUILD @@ -91,7 +91,6 @@ cc_library( py_test( name = "ball_occlusion_test", srcs = ["ball_occlusion_test.py"], - tags = ["exclusive"], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/simulated_tests/BUILD b/src/software/simulated_tests/BUILD index d1acb78f3e..5cf8ae08b8 100644 --- a/src/software/simulated_tests/BUILD +++ b/src/software/simulated_tests/BUILD @@ -60,10 +60,6 @@ py_test( srcs = [ "simulated_test_ball_model.py", ], - # TODO (#2619) Remove tag to run in parallel - tags = [ - "exclusive", - ], deps = [ "//software:conftest", "//software/simulated_tests/validation:validations", diff --git a/src/software/simulated_tests/simulated_test_fixture.py b/src/software/simulated_tests/simulated_test_fixture.py index e3150d5db6..1edf1c7ebc 100644 --- a/src/software/simulated_tests/simulated_test_fixture.py +++ b/src/software/simulated_tests/simulated_test_fixture.py @@ -419,10 +419,41 @@ def run_test( assert failed_tests == 0 -def load_command_line_arguments(allow_unrecognized: bool = False): - """Load in command-line arguments using argparse +def get_runtime_dir(): + """Gets the base runtime directory for the test execution. + TODO: Refactor #3744 - NOTE: Pytest has its own built in argument parser (conftest.py, pytest_addoption) + If running under Bazel, it uses TEST_TMPDIR to keep tests isolated. To prevent UNIX + socket path length limits from being exceeded by Bazel's long paths, it creates a short + symlink in /tmp to the TEST_TMPDIR. + + :return: The path to the runtime directory. + """ + test_tmpdir = os.environ.get("TEST_TMPDIR") + if not test_tmpdir: + return "/tmp/tbots" + import uuid + + symlink_path = os.path.join("/tmp", f"tbt_{uuid.uuid4().hex[:8]}") + try: + os.symlink(test_tmpdir, symlink_path) + except OSError: + pass + return symlink_path + + +RUNTIME_DIR = get_runtime_dir() + + +def load_command_line_arguments(allow_unrecognized: bool = False) -> argparse.Namespace: + """Load command line arguments. + + We aren't using pytest.ini because it does not allow for dynamic defaults, + which we need to use TEST_TMPDIR when it's available. + + We aren't using conftest.py's pytest_addoption because we want to be able to + run the gamecontroller script directly from python without pytest. + Pytest does have a way to access parsed options (request.config.getoption), but it doesn't seem to play nicely with bazel. We just use argparse instead. :param allow_unrecognized: if true, does not raise an error for unrecognized arguments @@ -444,19 +475,19 @@ def load_command_line_arguments(allow_unrecognized: bool = False): "--simulator_runtime_dir", type=str, help="simulator runtime directory", - default="/tmp/tbots", + default=RUNTIME_DIR, ) parser.add_argument( "--blue_full_system_runtime_dir", type=str, help="blue full_system runtime directory", - default="/tmp/tbots/blue", + default=os.path.join(RUNTIME_DIR, "blue"), ) parser.add_argument( "--yellow_full_system_runtime_dir", type=str, help="yellow full_system runtime directory", - default="/tmp/tbots/yellow", + default=os.path.join(RUNTIME_DIR, "yellow"), ) parser.add_argument( "--layout", @@ -542,7 +573,8 @@ def simulated_test_runner(): current_test = current_test.replace("]", "") current_test = current_test.replace("[", "-") - test_name = current_test.split("-")[0] + # Truncate the test name to 25 characters for UNIX path length limits + test_name = current_test.split("-")[0][:25] # Launch all binaries with Simulator( diff --git a/src/software/thunderscope/binary_context_managers/game_controller.py b/src/software/thunderscope/binary_context_managers/game_controller.py index a57f298ba8..99555b0758 100644 --- a/src/software/thunderscope/binary_context_managers/game_controller.py +++ b/src/software/thunderscope/binary_context_managers/game_controller.py @@ -1,6 +1,7 @@ from __future__ import annotations import itertools +import fcntl import queue import random import logging @@ -35,6 +36,9 @@ class Gamecontroller: RESET_MATCH_DELAY_S = 1 NO_GAME_PROGRESS_DURATION_S = 120 + GC_PORT_LOCK = "/tmp/tbots_gc_port.lock" + GC_PORT_STATE = "/tmp/tbots_gc_last_port.txt" + def __init__( self, suppress_logs: bool = False, @@ -43,24 +47,16 @@ def __init__( ) -> None: """Run Gamecontroller - :param suppress_logs: Whether to suppress the logs - :param use_conventional_port: whether or not to use the conventional port! - :param automate_referee: whether or not referee commands should be automated + :param suppress_logs: True if logs should be suppressed + :param use_conventional_port: True when using static referee port. False for dynamic port assignments. + :param automate_referee: True if referee commands should be automated """ self.suppress_logs = suppress_logs self.automate_referee = automate_referee - # We default to using a non-conventional port to avoid emitting - # on the same port as what other teams may be listening on. - if use_conventional_port: - if not self.is_valid_port(SSL_REFEREE_PORT): - raise OSError(f"Cannot use port {SSL_REFEREE_PORT} for Gamecontroller") - - self.referee_port = SSL_REFEREE_PORT - else: - self.referee_port = self.next_free_port(random.randint(1024, 65535)) - - self.ci_port = self.next_free_port() + self.use_conventional_port = use_conventional_port + self.referee_port = None + self.ci_port = None # this allows gamecontroller to listen to override commands self.command_override_buffer = ThreadSafeBuffer( buffer_size=2, protobuf_type=ManualGCCommand @@ -92,23 +88,51 @@ def __enter__(self) -> Gamecontroller: """ command = ["/opt/tbotspython/gamecontroller", "--timeAcquisitionMode", "ci"] - # Kill any gamecontroller, even those with different IP/port, because - # of the port assignment logic when use_conventional_port=False - kill_cmd_if_running(command) + lock_fd = None + if not self.use_conventional_port: + # Acquire mutex for port assignments over IPC as they are critical sections + lock_fd = open(Gamecontroller.GC_PORT_LOCK, "w") + fcntl.flock(lock_fd, fcntl.LOCK_EX) - command += ["-publishAddress", f"{self.REFEREE_IP}:{self.referee_port}"] - command += ["-ciAddress", f"localhost:{self.ci_port}"] - - if self.suppress_logs: - with open(os.devnull, "w") as fp: - self.gamecontroller_proc = Popen(command, stdout=fp, stderr=fp) + try: + if self.use_conventional_port: + kill_cmd_if_running(command) + if not self.is_valid_port(SSL_REFEREE_PORT): + raise OSError( + f"Cannot use port {SSL_REFEREE_PORT} for Gamecontroller" + ) + self.referee_port = SSL_REFEREE_PORT + self.ci_port = self.next_free_port(Gamecontroller.GC_PORT_STATE) + else: + self.referee_port = self.next_free_port(Gamecontroller.GC_PORT_STATE) + self.ci_port = self.next_free_port(Gamecontroller.GC_PORT_STATE) + + command += ["-publishAddress", f"{self.REFEREE_IP}:{self.referee_port}"] + command += ["-ciAddress", f"localhost:{self.ci_port}"] + command += [ + "-address", + "localhost:0", + "-autorefAddress", + "localhost:0", + "-remoteControlAddress", + "localhost:0", + "-teamAddress", + "localhost:0", + "-backendOnly", + ] + + if self.suppress_logs: + with open(os.devnull, "w") as fp: + self.gamecontroller_proc = Popen(command, stdout=fp, stderr=fp) + else: + self.gamecontroller_proc = Popen(command) - else: - self.gamecontroller_proc = Popen(command) + time.sleep(Gamecontroller.CI_MODE_LAUNCH_DELAY_S) + finally: + if lock_fd is not None: + fcntl.flock(lock_fd, fcntl.LOCK_UN) + lock_fd.close() - # We can't connect to the ci port right away, it takes - # CI_MODE_LAUNCH_DELAY_S to start up the gamecontroller - time.sleep(Gamecontroller.CI_MODE_LAUNCH_DELAY_S) self.ci_socket = SslSocket(self.ci_port) return self @@ -160,20 +184,39 @@ def is_valid_port(self, port): except OSError: return False - def next_free_port(self, start_port: int = 40000, max_port: int = 65535) -> int: - """Find the next free port. We need to find 2 free ports to use for the gamecontroller - so that we can run multiple gamecontroller instances in parallel. + def next_free_port( + self, port_counter_path: str, start_port: int = 40000, max_port: int = 65535 + ) -> int: + """Find and claims the next free port using a file-backed counter to avoid race conditions. + Ports are assigned monotonically and tracked in port_counter_path. + Wraps to start_port, when max_port is reached. + :param port_counter_path: The shared data location for IPC of port assignments :param start_port: The port to start looking from :param max_port: The maximum port to look up to :return: The next free port """ - while start_port <= max_port: - if self.is_valid_port(start_port): - return start_port - start_port += 1 + assert start_port < max_port + + try: + with open(port_counter_path, "r") as f: + last_port = int(f.read().strip()) + except (FileNotFoundError, ValueError): + last_port = random.randint(start_port, max_port) + + port = last_port + 1 + if port > max_port: + port = start_port + + while not self.is_valid_port(port): + port += 1 + if port > max_port: + port = start_port + + with open(port_counter_path, "w") as f: + f.write(str(port)) - raise IOError("no free ports") + return port def setup_proto_unix_io( self,