blob: 40412c01523e89535b9400fcd1475bf971fd9d39 [file] [log] [blame]
Philipp Schrader94305722022-03-13 12:59:21 -07001"""This library is here to run the various servers involved in the scouting app.
2
3The servers are:
4 - The fake TBA server
5 - The actual web server
Philipp Schrader7365d322022-03-06 16:40:08 -08006 - The postgres database
Philipp Schrader94305722022-03-13 12:59:21 -07007"""
8
9import argparse
10import json
11import os
12from pathlib import Path
13import shutil
14import signal
15import socket
16import subprocess
17import sys
18import time
19from typing import List
20
Philipp Schraderceaddd62023-02-15 19:58:15 -080021from rules_python.python.runfiles import runfiles
22
23RUNFILES = runfiles.Create()
24
Ravago Jones5127ccc2022-07-31 16:32:45 -070025
Philipp Schrader94305722022-03-13 12:59:21 -070026def wait_for_server(port: int):
27 """Waits for the server at the specified port to respond to TCP connections."""
28 while True:
29 try:
30 connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
31 connection.connect(("localhost", port))
32 connection.close()
33 break
34 except ConnectionRefusedError:
35 connection.close()
36 time.sleep(0.01)
37
Ravago Jones5127ccc2022-07-31 16:32:45 -070038
Philipp Schrader7365d322022-03-06 16:40:08 -080039def create_db_config(tmpdir: Path) -> Path:
40 config = tmpdir / "db_config.json"
Ravago Jones5127ccc2022-07-31 16:32:45 -070041 config.write_text(
42 json.dumps({
43 "username": "test",
44 "password": "password",
45 "port": 5432,
46 }))
Philipp Schrader7365d322022-03-06 16:40:08 -080047 return config
48
Ravago Jones5127ccc2022-07-31 16:32:45 -070049
Philipp Schrader43c730b2023-02-26 20:27:44 -080050def create_tba_config(tmpdir: Path, year: int, event_code: str) -> Path:
51 # Configure the scouting webserver to scrape data from our fake TBA server.
Philipp Schrader94305722022-03-13 12:59:21 -070052 config = tmpdir / "scouting_config.json"
Ravago Jones5127ccc2022-07-31 16:32:45 -070053 config.write_text(
54 json.dumps({
55 "api_key": "dummy_key_that_is_not_actually_used_in_this_test",
56 "base_url": "http://localhost:7000",
Philipp Schrader43c730b2023-02-26 20:27:44 -080057 "year": year,
58 "event_code": event_code,
Ravago Jones5127ccc2022-07-31 16:32:45 -070059 }))
Philipp Schrader94305722022-03-13 12:59:21 -070060 return config
61
Ravago Jones5127ccc2022-07-31 16:32:45 -070062
Philipp Schrader94305722022-03-13 12:59:21 -070063def set_up_tba_api_dir(tmpdir: Path, year: int, event_code: str):
64 tba_api_dir = tmpdir / "api" / "v3" / "event" / f"{year}{event_code}"
65 tba_api_dir.mkdir(parents=True, exist_ok=True)
66 (tba_api_dir / "matches").write_text(
Philipp Schraderceaddd62023-02-15 19:58:15 -080067 Path(
68 RUNFILES.Rlocation(
69 f"org_frc971/scouting/scraping/test_data/{year}_{event_code}.json"
70 )).read_text())
Ravago Jones5127ccc2022-07-31 16:32:45 -070071
Philipp Schrader94305722022-03-13 12:59:21 -070072
73class Runner:
74 """Helps manage the services we need for testing the scouting app."""
75
Philipp Schrader43c730b2023-02-26 20:27:44 -080076 def start(self,
77 port: int,
78 notify_fd: int = 0,
79 year: int = 2016,
80 event_code: str = "nytr"):
Philipp Schraderceaddd62023-02-15 19:58:15 -080081 """Starts the services needed for testing the scouting app.
82
83 if notify_fd is set to a non-zero value, the string "READY" is written
84 to that file descriptor once everything is set up.
85 """
86
Philipp Schrader94305722022-03-13 12:59:21 -070087 self.tmpdir = Path(os.environ["TEST_TMPDIR"]) / "servers"
88 self.tmpdir.mkdir(exist_ok=True)
89
Philipp Schrader7365d322022-03-06 16:40:08 -080090 db_config = create_db_config(self.tmpdir)
Philipp Schrader43c730b2023-02-26 20:27:44 -080091 tba_config = create_tba_config(self.tmpdir,
92 year=year,
93 event_code=event_code)
Philipp Schrader94305722022-03-13 12:59:21 -070094
Philipp Schrader7365d322022-03-06 16:40:08 -080095 # The database needs to be running and addressable before the scouting
96 # webserver can start.
Philipp Schraderceaddd62023-02-15 19:58:15 -080097 self.testdb_server = subprocess.Popen([
98 RUNFILES.Rlocation(
99 "org_frc971/scouting/db/testdb_server/testdb_server_/testdb_server"
100 )
101 ])
Philipp Schrader7365d322022-03-06 16:40:08 -0800102 wait_for_server(5432)
103
Philipp Schrader94305722022-03-13 12:59:21 -0700104 self.webserver = subprocess.Popen([
Philipp Schraderceaddd62023-02-15 19:58:15 -0800105 RUNFILES.Rlocation("org_frc971/scouting/scouting"),
Philipp Schrader94305722022-03-13 12:59:21 -0700106 f"--port={port}",
Philipp Schrader7365d322022-03-06 16:40:08 -0800107 f"--db_config={db_config}",
Philipp Schrader94305722022-03-13 12:59:21 -0700108 f"--tba_config={tba_config}",
109 ])
110
111 # Create a fake TBA server to serve the static match list.
Philipp Schrader43c730b2023-02-26 20:27:44 -0800112 set_up_tba_api_dir(self.tmpdir, year, event_code)
Philipp Schrader94305722022-03-13 12:59:21 -0700113 self.fake_tba_api = subprocess.Popen(
114 ["python3", "-m", "http.server", "7000"],
115 cwd=self.tmpdir,
116 )
117
118 # Wait for the TBA server and the scouting webserver to start up.
119 wait_for_server(7000)
120 wait_for_server(port)
121
Philipp Schraderceaddd62023-02-15 19:58:15 -0800122 if notify_fd:
123 with os.fdopen(notify_fd, "w") as file:
124 file.write("READY")
125
Philipp Schrader94305722022-03-13 12:59:21 -0700126 def stop(self):
127 """Stops the services needed for testing the scouting app."""
Philipp Schrader7365d322022-03-06 16:40:08 -0800128 servers = (self.webserver, self.testdb_server, self.fake_tba_api)
Philipp Schrader94305722022-03-13 12:59:21 -0700129 for server in servers:
130 server.terminate()
131 for server in servers:
132 server.wait()
133
134 try:
135 shutil.rmtree(self.tmpdir)
136 except FileNotFoundError:
137 pass
138
Ravago Jones5127ccc2022-07-31 16:32:45 -0700139
Philipp Schraderaa76a692022-05-29 23:55:16 -0700140def discard_signal(signum, frame):
141 """A NOP handler to ignore certain signals.
142
143 We use signal.pause() to wait for a signal. That means we can't use the default handler. The
144 default handler would tear the application down without stopping child processes.
145 """
146 pass
Philipp Schrader94305722022-03-13 12:59:21 -0700147
Ravago Jones5127ccc2022-07-31 16:32:45 -0700148
Philipp Schrader94305722022-03-13 12:59:21 -0700149def main(argv: List[str]):
150 parser = argparse.ArgumentParser()
Ravago Jones5127ccc2022-07-31 16:32:45 -0700151 parser.add_argument("--port",
152 type=int,
153 help="The port for the actual web server.")
Philipp Schraderceaddd62023-02-15 19:58:15 -0800154 parser.add_argument(
155 "--notify_fd",
156 type=int,
157 default=0,
158 help=("If non-zero, indicates a file descriptor to which 'READY' is "
159 "written when everything has started up."),
160 )
Philipp Schrader94305722022-03-13 12:59:21 -0700161 args = parser.parse_args(argv[1:])
162
163 runner = Runner()
Philipp Schraderceaddd62023-02-15 19:58:15 -0800164 runner.start(args.port, args.notify_fd)
Philipp Schrader94305722022-03-13 12:59:21 -0700165
166 # Wait until we're asked to shut down via CTRL-C or SIGTERM.
Philipp Schraderaa76a692022-05-29 23:55:16 -0700167 signal.signal(signal.SIGINT, discard_signal)
168 signal.signal(signal.SIGTERM, discard_signal)
Philipp Schrader94305722022-03-13 12:59:21 -0700169 signal.pause()
170
171 runner.stop()
172
173
174if __name__ == "__main__":
175 sys.exit(main(sys.argv))