source: trunk/src/allmydata/test/cli/test_run.py

Last change on this file was 53084f7, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-27T23:49:07Z

remove more Python2 compatibility

  • Property mode set to 100644
File size: 7.9 KB
Line 
1"""
2Tests for ``allmydata.scripts.tahoe_run``.
3"""
4
5from __future__ import annotations
6
7import re
8from io import (
9    StringIO,
10)
11
12from hypothesis.strategies import text
13from hypothesis import given, assume
14
15from testtools.matchers import (
16    Contains,
17    Equals,
18)
19
20from twisted.python.filepath import (
21    FilePath,
22)
23from twisted.internet.testing import (
24    MemoryReactor,
25)
26from twisted.python.failure import (
27    Failure,
28)
29from twisted.internet.error import (
30    ConnectionDone,
31)
32from twisted.internet.test.modulehelpers import (
33    AlternateReactor,
34)
35
36from ...scripts.tahoe_run import (
37    DaemonizeTheRealService,
38    RunOptions,
39    run,
40)
41from ...util.pid import (
42    check_pid_process,
43    InvalidPidFile,
44)
45
46from ...scripts.runner import (
47    parse_options
48)
49from ..common import (
50    SyncTestCase,
51)
52
53class 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
148class 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
233class 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
Note: See TracBrowser for help on using the repository browser.