1 | """ |
---|
2 | Tests for ``allmydata.scripts.tahoe_run``. |
---|
3 | """ |
---|
4 | |
---|
5 | from __future__ import annotations |
---|
6 | |
---|
7 | import re |
---|
8 | from io import ( |
---|
9 | StringIO, |
---|
10 | ) |
---|
11 | |
---|
12 | from hypothesis.strategies import text |
---|
13 | from hypothesis import given, assume |
---|
14 | |
---|
15 | from testtools.matchers import ( |
---|
16 | Contains, |
---|
17 | Equals, |
---|
18 | ) |
---|
19 | |
---|
20 | from twisted.python.filepath import ( |
---|
21 | FilePath, |
---|
22 | ) |
---|
23 | from twisted.internet.testing import ( |
---|
24 | MemoryReactor, |
---|
25 | ) |
---|
26 | from twisted.python.failure import ( |
---|
27 | Failure, |
---|
28 | ) |
---|
29 | from twisted.internet.error import ( |
---|
30 | ConnectionDone, |
---|
31 | ) |
---|
32 | from twisted.internet.test.modulehelpers import ( |
---|
33 | AlternateReactor, |
---|
34 | ) |
---|
35 | |
---|
36 | from ...scripts.tahoe_run import ( |
---|
37 | DaemonizeTheRealService, |
---|
38 | RunOptions, |
---|
39 | run, |
---|
40 | ) |
---|
41 | from ...util.pid import ( |
---|
42 | check_pid_process, |
---|
43 | InvalidPidFile, |
---|
44 | ) |
---|
45 | |
---|
46 | from ...scripts.runner import ( |
---|
47 | parse_options |
---|
48 | ) |
---|
49 | from ..common import ( |
---|
50 | SyncTestCase, |
---|
51 | ) |
---|
52 | |
---|
53 | class DaemonizeTheRealServiceTests(SyncTestCase): |
---|
54 | """ |
---|
55 | Tests for ``DaemonizeTheRealService``. |
---|
56 | """ |
---|
57 | def _verify_error(self, config, expected): |
---|
58 | """ |
---|
59 | Assert that when ``DaemonizeTheRealService`` is started using the given |
---|
60 | configuration it writes the given message to stderr and stops the |
---|
61 | reactor. |
---|
62 | |
---|
63 | :param bytes config: The contents of a ``tahoe.cfg`` file to give to |
---|
64 | the service. |
---|
65 | |
---|
66 | :param bytes expected: A string to assert appears in stderr after the |
---|
67 | service starts. |
---|
68 | """ |
---|
69 | nodedir = FilePath(self.mktemp()) |
---|
70 | nodedir.makedirs() |
---|
71 | nodedir.child("tahoe.cfg").setContent(config.encode("ascii")) |
---|
72 | nodedir.child("tahoe-client.tac").touch() |
---|
73 | |
---|
74 | options = parse_options(["run", nodedir.path]) |
---|
75 | stdout = options.stdout = StringIO() |
---|
76 | stderr = options.stderr = StringIO() |
---|
77 | run_options = options.subOptions |
---|
78 | |
---|
79 | reactor = MemoryReactor() |
---|
80 | with AlternateReactor(reactor): |
---|
81 | service = DaemonizeTheRealService( |
---|
82 | "client", |
---|
83 | nodedir.path, |
---|
84 | run_options, |
---|
85 | ) |
---|
86 | service.startService() |
---|
87 | |
---|
88 | # We happen to know that the service uses reactor.callWhenRunning |
---|
89 | # to schedule all its work (though I couldn't tell you *why*). |
---|
90 | # Make sure those scheduled calls happen. |
---|
91 | waiting = reactor.whenRunningHooks[:] |
---|
92 | del reactor.whenRunningHooks[:] |
---|
93 | for f, a, k in waiting: |
---|
94 | f(*a, **k) |
---|
95 | |
---|
96 | self.assertThat( |
---|
97 | reactor.hasStopped, |
---|
98 | Equals(True), |
---|
99 | ) |
---|
100 | |
---|
101 | self.assertThat( |
---|
102 | stdout.getvalue(), |
---|
103 | Equals(""), |
---|
104 | ) |
---|
105 | |
---|
106 | self.assertThat( |
---|
107 | stderr.getvalue(), |
---|
108 | Contains(expected), |
---|
109 | ) |
---|
110 | |
---|
111 | def test_unknown_config(self): |
---|
112 | """ |
---|
113 | If there are unknown items in the node configuration file then a short |
---|
114 | message introduced with ``"Configuration error:"`` is written to |
---|
115 | stderr. |
---|
116 | """ |
---|
117 | self._verify_error("[invalid-section]\n", "Configuration error:") |
---|
118 | |
---|
119 | def test_port_assignment_required(self): |
---|
120 | """ |
---|
121 | If ``tub.port`` is configured to use port 0 then a short message rejecting |
---|
122 | this configuration is written to stderr. |
---|
123 | """ |
---|
124 | self._verify_error( |
---|
125 | """ |
---|
126 | [node] |
---|
127 | tub.port = 0 |
---|
128 | """, |
---|
129 | "tub.port cannot be 0", |
---|
130 | ) |
---|
131 | |
---|
132 | def test_privacy_error(self): |
---|
133 | """ |
---|
134 | If ``reveal-IP-address`` is set to false and the tub is not configured in |
---|
135 | a way that avoids revealing the node's IP address, a short message |
---|
136 | about privacy is written to stderr. |
---|
137 | """ |
---|
138 | self._verify_error( |
---|
139 | """ |
---|
140 | [node] |
---|
141 | tub.port = AUTO |
---|
142 | reveal-IP-address = false |
---|
143 | """, |
---|
144 | "Privacy requested", |
---|
145 | ) |
---|
146 | |
---|
147 | |
---|
148 | class DaemonizeStopTests(SyncTestCase): |
---|
149 | """ |
---|
150 | Tests relating to stopping the daemon |
---|
151 | """ |
---|
152 | def setUp(self): |
---|
153 | self.nodedir = FilePath(self.mktemp()) |
---|
154 | self.nodedir.makedirs() |
---|
155 | config = "" |
---|
156 | self.nodedir.child("tahoe.cfg").setContent(config.encode("ascii")) |
---|
157 | self.nodedir.child("tahoe-client.tac").touch() |
---|
158 | |
---|
159 | # arrange to know when reactor.stop() is called |
---|
160 | self.reactor = MemoryReactor() |
---|
161 | self.stop_calls = [] |
---|
162 | |
---|
163 | def record_stop(): |
---|
164 | self.stop_calls.append(object()) |
---|
165 | self.reactor.stop = record_stop |
---|
166 | |
---|
167 | super().setUp() |
---|
168 | |
---|
169 | def _make_daemon(self, extra_argv: list[str]) -> DaemonizeTheRealService: |
---|
170 | """ |
---|
171 | Create the daemonization service. |
---|
172 | |
---|
173 | :param extra_argv: Extra arguments to pass between ``run`` and the |
---|
174 | node path. |
---|
175 | """ |
---|
176 | options = parse_options(["run"] + extra_argv + [self.nodedir.path]) |
---|
177 | options.stdout = StringIO() |
---|
178 | options.stderr = StringIO() |
---|
179 | options.stdin = StringIO() |
---|
180 | run_options = options.subOptions |
---|
181 | return DaemonizeTheRealService( |
---|
182 | "client", |
---|
183 | self.nodedir.path, |
---|
184 | run_options, |
---|
185 | ) |
---|
186 | |
---|
187 | def _run_daemon(self) -> None: |
---|
188 | """ |
---|
189 | Simulate starting up the reactor so the daemon plugin can do its |
---|
190 | stuff. |
---|
191 | """ |
---|
192 | # We happen to know that the service uses reactor.callWhenRunning |
---|
193 | # to schedule all its work (though I couldn't tell you *why*). |
---|
194 | # Make sure those scheduled calls happen. |
---|
195 | waiting = self.reactor.whenRunningHooks[:] |
---|
196 | del self.reactor.whenRunningHooks[:] |
---|
197 | for f, a, k in waiting: |
---|
198 | f(*a, **k) |
---|
199 | |
---|
200 | def _close_stdin(self) -> None: |
---|
201 | """ |
---|
202 | Simulate closing the daemon plugin's stdin. |
---|
203 | """ |
---|
204 | # there should be a single reader: our StandardIO process |
---|
205 | # reader for stdin. Simulate it closing. |
---|
206 | for r in self.reactor.getReaders(): |
---|
207 | r.connectionLost(Failure(ConnectionDone())) |
---|
208 | |
---|
209 | def test_stop_on_stdin_close(self): |
---|
210 | """ |
---|
211 | We stop when stdin is closed. |
---|
212 | """ |
---|
213 | with AlternateReactor(self.reactor): |
---|
214 | service = self._make_daemon([]) |
---|
215 | service.startService() |
---|
216 | self._run_daemon() |
---|
217 | self._close_stdin() |
---|
218 | self.assertEqual(len(self.stop_calls), 1) |
---|
219 | |
---|
220 | def test_allow_stdin_close(self): |
---|
221 | """ |
---|
222 | If --allow-stdin-close is specified then closing stdin doesn't |
---|
223 | stop the process |
---|
224 | """ |
---|
225 | with AlternateReactor(self.reactor): |
---|
226 | service = self._make_daemon(["--allow-stdin-close"]) |
---|
227 | service.startService() |
---|
228 | self._run_daemon() |
---|
229 | self._close_stdin() |
---|
230 | self.assertEqual(self.stop_calls, []) |
---|
231 | |
---|
232 | |
---|
233 | class RunTests(SyncTestCase): |
---|
234 | """ |
---|
235 | Tests for ``run``. |
---|
236 | """ |
---|
237 | |
---|
238 | def test_non_numeric_pid(self): |
---|
239 | """ |
---|
240 | If the pidfile exists but does not contain a numeric value, a complaint to |
---|
241 | this effect is written to stderr. |
---|
242 | """ |
---|
243 | basedir = FilePath(self.mktemp()).asTextMode() |
---|
244 | basedir.makedirs() |
---|
245 | basedir.child(u"running.process").setContent(b"foo") |
---|
246 | basedir.child(u"tahoe-client.tac").setContent(b"") |
---|
247 | |
---|
248 | config = RunOptions() |
---|
249 | config.stdout = StringIO() |
---|
250 | config.stderr = StringIO() |
---|
251 | config['basedir'] = basedir.path |
---|
252 | config.twistd_args = [] |
---|
253 | |
---|
254 | reactor = MemoryReactor() |
---|
255 | |
---|
256 | runs = [] |
---|
257 | result_code = run(reactor, config, runApp=runs.append) |
---|
258 | self.assertThat( |
---|
259 | config.stderr.getvalue(), |
---|
260 | Contains("found invalid PID file in"), |
---|
261 | ) |
---|
262 | # because the pidfile is invalid we shouldn't get to the |
---|
263 | # .run() call itself. |
---|
264 | self.assertThat(runs, Equals([])) |
---|
265 | self.assertThat(result_code, Equals(1)) |
---|
266 | |
---|
267 | good_file_content_re = re.compile(r"\s*[0-9]*\s[0-9]*\s*", re.M) |
---|
268 | |
---|
269 | @given(text()) |
---|
270 | def test_pidfile_contents(self, content): |
---|
271 | """ |
---|
272 | invalid contents for a pidfile raise errors |
---|
273 | """ |
---|
274 | assume(not self.good_file_content_re.match(content)) |
---|
275 | pidfile = FilePath("pidfile") |
---|
276 | pidfile.setContent(content.encode("utf8")) |
---|
277 | |
---|
278 | with self.assertRaises(InvalidPidFile): |
---|
279 | with check_pid_process(pidfile): |
---|
280 | pass |
---|