blob: d1e4e326c4e6e74b032ec9e50534604e30528e03 [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 Schrader94305722022-03-13 12:59:21 -070050def create_tba_config(tmpdir: Path) -> Path:
51 # Configure the scouting webserver to scrape data from our fake TBA
52 # server.
53 config = tmpdir / "scouting_config.json"
Ravago Jones5127ccc2022-07-31 16:32:45 -070054 config.write_text(
55 json.dumps({
56 "api_key": "dummy_key_that_is_not_actually_used_in_this_test",
57 "base_url": "http://localhost:7000",
58 }))
Philipp Schrader94305722022-03-13 12:59:21 -070059 return config
60
Ravago Jones5127ccc2022-07-31 16:32:45 -070061
Philipp Schrader94305722022-03-13 12:59:21 -070062def set_up_tba_api_dir(tmpdir: Path, year: int, event_code: str):
63 tba_api_dir = tmpdir / "api" / "v3" / "event" / f"{year}{event_code}"
64 tba_api_dir.mkdir(parents=True, exist_ok=True)
65 (tba_api_dir / "matches").write_text(
Philipp Schraderceaddd62023-02-15 19:58:15 -080066 Path(
67 RUNFILES.Rlocation(
68 f"org_frc971/scouting/scraping/test_data/{year}_{event_code}.json"
69 )).read_text())
Ravago Jones5127ccc2022-07-31 16:32:45 -070070
Philipp Schrader94305722022-03-13 12:59:21 -070071
72class Runner:
73 """Helps manage the services we need for testing the scouting app."""
74
Philipp Schraderceaddd62023-02-15 19:58:15 -080075 def start(self, port: int, notify_fd: int = 0):
76 """Starts the services needed for testing the scouting app.
77
78 if notify_fd is set to a non-zero value, the string "READY" is written
79 to that file descriptor once everything is set up.
80 """
81
Philipp Schrader94305722022-03-13 12:59:21 -070082 self.tmpdir = Path(os.environ["TEST_TMPDIR"]) / "servers"
83 self.tmpdir.mkdir(exist_ok=True)
84
Philipp Schrader7365d322022-03-06 16:40:08 -080085 db_config = create_db_config(self.tmpdir)
Philipp Schrader94305722022-03-13 12:59:21 -070086 tba_config = create_tba_config(self.tmpdir)
87
Philipp Schrader7365d322022-03-06 16:40:08 -080088 # The database needs to be running and addressable before the scouting
89 # webserver can start.
Philipp Schraderceaddd62023-02-15 19:58:15 -080090 self.testdb_server = subprocess.Popen([
91 RUNFILES.Rlocation(
92 "org_frc971/scouting/db/testdb_server/testdb_server_/testdb_server"
93 )
94 ])
Philipp Schrader7365d322022-03-06 16:40:08 -080095 wait_for_server(5432)
96
Philipp Schrader94305722022-03-13 12:59:21 -070097 self.webserver = subprocess.Popen([
Philipp Schraderceaddd62023-02-15 19:58:15 -080098 RUNFILES.Rlocation("org_frc971/scouting/scouting"),
Philipp Schrader94305722022-03-13 12:59:21 -070099 f"--port={port}",
Philipp Schrader7365d322022-03-06 16:40:08 -0800100 f"--db_config={db_config}",
Philipp Schrader94305722022-03-13 12:59:21 -0700101 f"--tba_config={tba_config}",
102 ])
103
104 # Create a fake TBA server to serve the static match list.
105 set_up_tba_api_dir(self.tmpdir, year=2016, event_code="nytr")
106 set_up_tba_api_dir(self.tmpdir, year=2020, event_code="fake")
107 self.fake_tba_api = subprocess.Popen(
108 ["python3", "-m", "http.server", "7000"],
109 cwd=self.tmpdir,
110 )
111
112 # Wait for the TBA server and the scouting webserver to start up.
113 wait_for_server(7000)
114 wait_for_server(port)
115
Philipp Schraderceaddd62023-02-15 19:58:15 -0800116 if notify_fd:
117 with os.fdopen(notify_fd, "w") as file:
118 file.write("READY")
119
Philipp Schrader94305722022-03-13 12:59:21 -0700120 def stop(self):
121 """Stops the services needed for testing the scouting app."""
Philipp Schrader7365d322022-03-06 16:40:08 -0800122 servers = (self.webserver, self.testdb_server, self.fake_tba_api)
Philipp Schrader94305722022-03-13 12:59:21 -0700123 for server in servers:
124 server.terminate()
125 for server in servers:
126 server.wait()
127
128 try:
129 shutil.rmtree(self.tmpdir)
130 except FileNotFoundError:
131 pass
132
Ravago Jones5127ccc2022-07-31 16:32:45 -0700133
Philipp Schraderaa76a692022-05-29 23:55:16 -0700134def discard_signal(signum, frame):
135 """A NOP handler to ignore certain signals.
136
137 We use signal.pause() to wait for a signal. That means we can't use the default handler. The
138 default handler would tear the application down without stopping child processes.
139 """
140 pass
Philipp Schrader94305722022-03-13 12:59:21 -0700141
Ravago Jones5127ccc2022-07-31 16:32:45 -0700142
Philipp Schrader94305722022-03-13 12:59:21 -0700143def main(argv: List[str]):
144 parser = argparse.ArgumentParser()
Ravago Jones5127ccc2022-07-31 16:32:45 -0700145 parser.add_argument("--port",
146 type=int,
147 help="The port for the actual web server.")
Philipp Schraderceaddd62023-02-15 19:58:15 -0800148 parser.add_argument(
149 "--notify_fd",
150 type=int,
151 default=0,
152 help=("If non-zero, indicates a file descriptor to which 'READY' is "
153 "written when everything has started up."),
154 )
Philipp Schrader94305722022-03-13 12:59:21 -0700155 args = parser.parse_args(argv[1:])
156
157 runner = Runner()
Philipp Schraderceaddd62023-02-15 19:58:15 -0800158 runner.start(args.port, args.notify_fd)
Philipp Schrader94305722022-03-13 12:59:21 -0700159
160 # Wait until we're asked to shut down via CTRL-C or SIGTERM.
Philipp Schraderaa76a692022-05-29 23:55:16 -0700161 signal.signal(signal.SIGINT, discard_signal)
162 signal.signal(signal.SIGTERM, discard_signal)
Philipp Schrader94305722022-03-13 12:59:21 -0700163 signal.pause()
164
165 runner.stop()
166
167
168if __name__ == "__main__":
169 sys.exit(main(sys.argv))