blob: b6e5c7af3209ac9abb426108626c2375f2f235d5 [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
21def wait_for_server(port: int):
22 """Waits for the server at the specified port to respond to TCP connections."""
23 while True:
24 try:
25 connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
26 connection.connect(("localhost", port))
27 connection.close()
28 break
29 except ConnectionRefusedError:
30 connection.close()
31 time.sleep(0.01)
32
Philipp Schrader7365d322022-03-06 16:40:08 -080033def create_db_config(tmpdir: Path) -> Path:
34 config = tmpdir / "db_config.json"
35 config.write_text(json.dumps({
36 "username": "test",
37 "password": "password",
38 "port": 5432,
39 }))
40 return config
41
Philipp Schrader94305722022-03-13 12:59:21 -070042def create_tba_config(tmpdir: Path) -> Path:
43 # Configure the scouting webserver to scrape data from our fake TBA
44 # server.
45 config = tmpdir / "scouting_config.json"
46 config.write_text(json.dumps({
47 "api_key": "dummy_key_that_is_not_actually_used_in_this_test",
48 "base_url": "http://localhost:7000",
49 }))
50 return config
51
52def set_up_tba_api_dir(tmpdir: Path, year: int, event_code: str):
53 tba_api_dir = tmpdir / "api" / "v3" / "event" / f"{year}{event_code}"
54 tba_api_dir.mkdir(parents=True, exist_ok=True)
55 (tba_api_dir / "matches").write_text(
56 Path(f"scouting/scraping/test_data/{year}_{event_code}.json").read_text()
57 )
58
59class Runner:
60 """Helps manage the services we need for testing the scouting app."""
61
62 def start(self, port: int):
63 """Starts the services needed for testing the scouting app."""
64 self.tmpdir = Path(os.environ["TEST_TMPDIR"]) / "servers"
65 self.tmpdir.mkdir(exist_ok=True)
66
Philipp Schrader7365d322022-03-06 16:40:08 -080067 db_config = create_db_config(self.tmpdir)
Philipp Schrader94305722022-03-13 12:59:21 -070068 tba_config = create_tba_config(self.tmpdir)
69
Philipp Schrader7365d322022-03-06 16:40:08 -080070 # The database needs to be running and addressable before the scouting
71 # webserver can start.
72 self.testdb_server = subprocess.Popen(
73 ["scouting/db/testdb_server/testdb_server_/testdb_server"])
74 wait_for_server(5432)
75
Philipp Schrader94305722022-03-13 12:59:21 -070076 self.webserver = subprocess.Popen([
77 "scouting/scouting",
78 f"--port={port}",
Philipp Schrader7365d322022-03-06 16:40:08 -080079 f"--db_config={db_config}",
Philipp Schrader94305722022-03-13 12:59:21 -070080 f"--tba_config={tba_config}",
81 ])
82
83 # Create a fake TBA server to serve the static match list.
84 set_up_tba_api_dir(self.tmpdir, year=2016, event_code="nytr")
85 set_up_tba_api_dir(self.tmpdir, year=2020, event_code="fake")
86 self.fake_tba_api = subprocess.Popen(
87 ["python3", "-m", "http.server", "7000"],
88 cwd=self.tmpdir,
89 )
90
91 # Wait for the TBA server and the scouting webserver to start up.
92 wait_for_server(7000)
93 wait_for_server(port)
94
95 def stop(self):
96 """Stops the services needed for testing the scouting app."""
Philipp Schrader7365d322022-03-06 16:40:08 -080097 servers = (self.webserver, self.testdb_server, self.fake_tba_api)
Philipp Schrader94305722022-03-13 12:59:21 -070098 for server in servers:
99 server.terminate()
100 for server in servers:
101 server.wait()
102
103 try:
104 shutil.rmtree(self.tmpdir)
105 except FileNotFoundError:
106 pass
107
Philipp Schraderaa76a692022-05-29 23:55:16 -0700108def discard_signal(signum, frame):
109 """A NOP handler to ignore certain signals.
110
111 We use signal.pause() to wait for a signal. That means we can't use the default handler. The
112 default handler would tear the application down without stopping child processes.
113 """
114 pass
Philipp Schrader94305722022-03-13 12:59:21 -0700115
116def main(argv: List[str]):
117 parser = argparse.ArgumentParser()
118 parser.add_argument("--port", type=int, help="The port for the actual web server.")
119 args = parser.parse_args(argv[1:])
120
121 runner = Runner()
122 runner.start(args.port)
123
124 # Wait until we're asked to shut down via CTRL-C or SIGTERM.
Philipp Schraderaa76a692022-05-29 23:55:16 -0700125 signal.signal(signal.SIGINT, discard_signal)
126 signal.signal(signal.SIGTERM, discard_signal)
Philipp Schrader94305722022-03-13 12:59:21 -0700127 signal.pause()
128
129 runner.stop()
130
131
132if __name__ == "__main__":
133 sys.exit(main(sys.argv))