blob: 95c146e37555834fbe5619bc2b926b02a4b18faa [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 Schraderfbec6a92023-03-04 15:24:25 -0800104 # Create a fake TBA server to serve the static match list.
105 # Make sure it starts up first so that the webserver successfully
106 # scrapes the TBA data on startup.
107 set_up_tba_api_dir(self.tmpdir, year, event_code)
108 self.fake_tba_api = subprocess.Popen(
109 ["python3", "-m", "http.server", "7000"],
110 cwd=self.tmpdir,
111 )
112 wait_for_server(7000)
113
114 # Wait for the scouting webserver to start up.
Philipp Schrader94305722022-03-13 12:59:21 -0700115 self.webserver = subprocess.Popen([
Philipp Schraderceaddd62023-02-15 19:58:15 -0800116 RUNFILES.Rlocation("org_frc971/scouting/scouting"),
Philipp Schrader94305722022-03-13 12:59:21 -0700117 f"--port={port}",
Philipp Schrader7365d322022-03-06 16:40:08 -0800118 f"--db_config={db_config}",
Philipp Schrader94305722022-03-13 12:59:21 -0700119 f"--tba_config={tba_config}",
120 ])
Philipp Schrader94305722022-03-13 12:59:21 -0700121 wait_for_server(port)
122
Philipp Schraderceaddd62023-02-15 19:58:15 -0800123 if notify_fd:
124 with os.fdopen(notify_fd, "w") as file:
125 file.write("READY")
126
Philipp Schrader94305722022-03-13 12:59:21 -0700127 def stop(self):
128 """Stops the services needed for testing the scouting app."""
Philipp Schrader7365d322022-03-06 16:40:08 -0800129 servers = (self.webserver, self.testdb_server, self.fake_tba_api)
Philipp Schrader94305722022-03-13 12:59:21 -0700130 for server in servers:
131 server.terminate()
132 for server in servers:
133 server.wait()
134
135 try:
136 shutil.rmtree(self.tmpdir)
137 except FileNotFoundError:
138 pass
139
Ravago Jones5127ccc2022-07-31 16:32:45 -0700140
Philipp Schraderaa76a692022-05-29 23:55:16 -0700141def discard_signal(signum, frame):
142 """A NOP handler to ignore certain signals.
143
144 We use signal.pause() to wait for a signal. That means we can't use the default handler. The
145 default handler would tear the application down without stopping child processes.
146 """
147 pass
Philipp Schrader94305722022-03-13 12:59:21 -0700148
Ravago Jones5127ccc2022-07-31 16:32:45 -0700149
Philipp Schrader94305722022-03-13 12:59:21 -0700150def main(argv: List[str]):
151 parser = argparse.ArgumentParser()
Ravago Jones5127ccc2022-07-31 16:32:45 -0700152 parser.add_argument("--port",
153 type=int,
154 help="The port for the actual web server.")
Philipp Schraderceaddd62023-02-15 19:58:15 -0800155 parser.add_argument(
156 "--notify_fd",
157 type=int,
158 default=0,
159 help=("If non-zero, indicates a file descriptor to which 'READY' is "
160 "written when everything has started up."),
161 )
Philipp Schrader94305722022-03-13 12:59:21 -0700162 args = parser.parse_args(argv[1:])
163
164 runner = Runner()
Philipp Schraderceaddd62023-02-15 19:58:15 -0800165 runner.start(args.port, args.notify_fd)
Philipp Schrader94305722022-03-13 12:59:21 -0700166
167 # Wait until we're asked to shut down via CTRL-C or SIGTERM.
Philipp Schraderaa76a692022-05-29 23:55:16 -0700168 signal.signal(signal.SIGINT, discard_signal)
169 signal.signal(signal.SIGTERM, discard_signal)
Philipp Schrader94305722022-03-13 12:59:21 -0700170 signal.pause()
171
172 runner.stop()
173
174
175if __name__ == "__main__":
176 sys.exit(main(sys.argv))