blob: 9df23c170a209e3eb058061ce2b3d0709bc4b4fd [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
Ravago Jones5127ccc2022-07-31 16:32:45 -070021
Philipp Schrader94305722022-03-13 12:59:21 -070022def wait_for_server(port: int):
23 """Waits for the server at the specified port to respond to TCP connections."""
24 while True:
25 try:
26 connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
27 connection.connect(("localhost", port))
28 connection.close()
29 break
30 except ConnectionRefusedError:
31 connection.close()
32 time.sleep(0.01)
33
Ravago Jones5127ccc2022-07-31 16:32:45 -070034
Philipp Schrader7365d322022-03-06 16:40:08 -080035def create_db_config(tmpdir: Path) -> Path:
36 config = tmpdir / "db_config.json"
Ravago Jones5127ccc2022-07-31 16:32:45 -070037 config.write_text(
38 json.dumps({
39 "username": "test",
40 "password": "password",
41 "port": 5432,
42 }))
Philipp Schrader7365d322022-03-06 16:40:08 -080043 return config
44
Ravago Jones5127ccc2022-07-31 16:32:45 -070045
Philipp Schrader94305722022-03-13 12:59:21 -070046def create_tba_config(tmpdir: Path) -> Path:
47 # Configure the scouting webserver to scrape data from our fake TBA
48 # server.
49 config = tmpdir / "scouting_config.json"
Ravago Jones5127ccc2022-07-31 16:32:45 -070050 config.write_text(
51 json.dumps({
52 "api_key": "dummy_key_that_is_not_actually_used_in_this_test",
53 "base_url": "http://localhost:7000",
54 }))
Philipp Schrader94305722022-03-13 12:59:21 -070055 return config
56
Ravago Jones5127ccc2022-07-31 16:32:45 -070057
Philipp Schrader94305722022-03-13 12:59:21 -070058def set_up_tba_api_dir(tmpdir: Path, year: int, event_code: str):
59 tba_api_dir = tmpdir / "api" / "v3" / "event" / f"{year}{event_code}"
60 tba_api_dir.mkdir(parents=True, exist_ok=True)
61 (tba_api_dir / "matches").write_text(
Ravago Jones5127ccc2022-07-31 16:32:45 -070062 Path(f"scouting/scraping/test_data/{year}_{event_code}.json").
63 read_text())
64
Philipp Schrader94305722022-03-13 12:59:21 -070065
66class Runner:
67 """Helps manage the services we need for testing the scouting app."""
68
69 def start(self, port: int):
70 """Starts the services needed for testing the scouting app."""
71 self.tmpdir = Path(os.environ["TEST_TMPDIR"]) / "servers"
72 self.tmpdir.mkdir(exist_ok=True)
73
Philipp Schrader7365d322022-03-06 16:40:08 -080074 db_config = create_db_config(self.tmpdir)
Philipp Schrader94305722022-03-13 12:59:21 -070075 tba_config = create_tba_config(self.tmpdir)
76
Philipp Schrader7365d322022-03-06 16:40:08 -080077 # The database needs to be running and addressable before the scouting
78 # webserver can start.
79 self.testdb_server = subprocess.Popen(
80 ["scouting/db/testdb_server/testdb_server_/testdb_server"])
81 wait_for_server(5432)
82
Philipp Schrader94305722022-03-13 12:59:21 -070083 self.webserver = subprocess.Popen([
84 "scouting/scouting",
85 f"--port={port}",
Philipp Schrader7365d322022-03-06 16:40:08 -080086 f"--db_config={db_config}",
Philipp Schrader94305722022-03-13 12:59:21 -070087 f"--tba_config={tba_config}",
88 ])
89
90 # Create a fake TBA server to serve the static match list.
91 set_up_tba_api_dir(self.tmpdir, year=2016, event_code="nytr")
92 set_up_tba_api_dir(self.tmpdir, year=2020, event_code="fake")
93 self.fake_tba_api = subprocess.Popen(
94 ["python3", "-m", "http.server", "7000"],
95 cwd=self.tmpdir,
96 )
97
98 # Wait for the TBA server and the scouting webserver to start up.
99 wait_for_server(7000)
100 wait_for_server(port)
101
102 def stop(self):
103 """Stops the services needed for testing the scouting app."""
Philipp Schrader7365d322022-03-06 16:40:08 -0800104 servers = (self.webserver, self.testdb_server, self.fake_tba_api)
Philipp Schrader94305722022-03-13 12:59:21 -0700105 for server in servers:
106 server.terminate()
107 for server in servers:
108 server.wait()
109
110 try:
111 shutil.rmtree(self.tmpdir)
112 except FileNotFoundError:
113 pass
114
Ravago Jones5127ccc2022-07-31 16:32:45 -0700115
Philipp Schraderaa76a692022-05-29 23:55:16 -0700116def discard_signal(signum, frame):
117 """A NOP handler to ignore certain signals.
118
119 We use signal.pause() to wait for a signal. That means we can't use the default handler. The
120 default handler would tear the application down without stopping child processes.
121 """
122 pass
Philipp Schrader94305722022-03-13 12:59:21 -0700123
Ravago Jones5127ccc2022-07-31 16:32:45 -0700124
Philipp Schrader94305722022-03-13 12:59:21 -0700125def main(argv: List[str]):
126 parser = argparse.ArgumentParser()
Ravago Jones5127ccc2022-07-31 16:32:45 -0700127 parser.add_argument("--port",
128 type=int,
129 help="The port for the actual web server.")
Philipp Schrader94305722022-03-13 12:59:21 -0700130 args = parser.parse_args(argv[1:])
131
132 runner = Runner()
133 runner.start(args.port)
134
135 # Wait until we're asked to shut down via CTRL-C or SIGTERM.
Philipp Schraderaa76a692022-05-29 23:55:16 -0700136 signal.signal(signal.SIGINT, discard_signal)
137 signal.signal(signal.SIGTERM, discard_signal)
Philipp Schrader94305722022-03-13 12:59:21 -0700138 signal.pause()
139
140 runner.stop()
141
142
143if __name__ == "__main__":
144 sys.exit(main(sys.argv))