| 1 | """ |
|---|
| 2 | Ported to Python 3 |
|---|
| 3 | """ |
|---|
| 4 | |
|---|
| 5 | import os.path, re, sys |
|---|
| 6 | from os import linesep |
|---|
| 7 | import locale |
|---|
| 8 | |
|---|
| 9 | from testtools.matchers import ( |
|---|
| 10 | MatchesListwise, |
|---|
| 11 | MatchesAny, |
|---|
| 12 | Contains, |
|---|
| 13 | Equals, |
|---|
| 14 | Always, |
|---|
| 15 | ) |
|---|
| 16 | from testtools.twistedsupport import ( |
|---|
| 17 | succeeded, |
|---|
| 18 | ) |
|---|
| 19 | from eliot import ( |
|---|
| 20 | log_call, |
|---|
| 21 | ) |
|---|
| 22 | |
|---|
| 23 | from twisted.trial import unittest |
|---|
| 24 | |
|---|
| 25 | from twisted.internet import reactor |
|---|
| 26 | from twisted.python import usage |
|---|
| 27 | from twisted.python.runtime import platform |
|---|
| 28 | from twisted.internet.defer import ( |
|---|
| 29 | inlineCallbacks, |
|---|
| 30 | DeferredList, |
|---|
| 31 | ) |
|---|
| 32 | from twisted.internet.testing import ( |
|---|
| 33 | MemoryReactorClock, |
|---|
| 34 | ) |
|---|
| 35 | from twisted.python.filepath import FilePath |
|---|
| 36 | from allmydata.util import fileutil, pollmixin |
|---|
| 37 | from allmydata.util.encodingutil import unicode_to_argv |
|---|
| 38 | from allmydata.util.pid import ( |
|---|
| 39 | check_pid_process, |
|---|
| 40 | _pidfile_to_lockpath, |
|---|
| 41 | ProcessInTheWay, |
|---|
| 42 | ) |
|---|
| 43 | from allmydata.test import common_util |
|---|
| 44 | import allmydata |
|---|
| 45 | from allmydata.scripts.tahoe_run import ( |
|---|
| 46 | on_stdin_close, |
|---|
| 47 | ) |
|---|
| 48 | |
|---|
| 49 | from .common import ( |
|---|
| 50 | PIPE, |
|---|
| 51 | Popen, |
|---|
| 52 | ) |
|---|
| 53 | from .common_util import ( |
|---|
| 54 | parse_cli, |
|---|
| 55 | run_cli, |
|---|
| 56 | run_cli_unicode, |
|---|
| 57 | ) |
|---|
| 58 | from .cli_node_api import ( |
|---|
| 59 | CLINodeAPI, |
|---|
| 60 | Expect, |
|---|
| 61 | on_stdout, |
|---|
| 62 | on_stdout_and_stderr, |
|---|
| 63 | ) |
|---|
| 64 | from ..util.eliotutil import ( |
|---|
| 65 | inline_callbacks, |
|---|
| 66 | ) |
|---|
| 67 | from .common import ( |
|---|
| 68 | SyncTestCase, |
|---|
| 69 | ) |
|---|
| 70 | |
|---|
| 71 | def get_root_from_file(src): |
|---|
| 72 | srcdir = os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(src)))) |
|---|
| 73 | |
|---|
| 74 | root = os.path.dirname(srcdir) |
|---|
| 75 | if os.path.basename(srcdir) == 'site-packages': |
|---|
| 76 | if re.search(r'python.+\..+', os.path.basename(root)): |
|---|
| 77 | root = os.path.dirname(root) |
|---|
| 78 | root = os.path.dirname(root) |
|---|
| 79 | elif os.path.basename(root) == 'src': |
|---|
| 80 | root = os.path.dirname(root) |
|---|
| 81 | |
|---|
| 82 | return root |
|---|
| 83 | |
|---|
| 84 | srcfile = allmydata.__file__ |
|---|
| 85 | rootdir = get_root_from_file(srcfile) |
|---|
| 86 | |
|---|
| 87 | |
|---|
| 88 | class ParseOrExitTests(SyncTestCase): |
|---|
| 89 | """ |
|---|
| 90 | Tests for ``parse_or_exit``. |
|---|
| 91 | """ |
|---|
| 92 | def test_nonascii_error_content(self): |
|---|
| 93 | """ |
|---|
| 94 | ``parse_or_exit`` can report errors that include non-ascii content. |
|---|
| 95 | """ |
|---|
| 96 | tricky = u"\u00F6" |
|---|
| 97 | self.assertThat( |
|---|
| 98 | run_cli_unicode(tricky, [], encoding="utf-8"), |
|---|
| 99 | succeeded( |
|---|
| 100 | MatchesListwise([ |
|---|
| 101 | # returncode |
|---|
| 102 | Equals(1), |
|---|
| 103 | # stdout |
|---|
| 104 | MatchesAny( |
|---|
| 105 | # Python 2 |
|---|
| 106 | Contains(u"Unknown command: \\xf6"), |
|---|
| 107 | # Python 3 |
|---|
| 108 | Contains(u"Unknown command: \xf6"), |
|---|
| 109 | ), |
|---|
| 110 | # stderr, |
|---|
| 111 | Always() |
|---|
| 112 | ]), |
|---|
| 113 | ), |
|---|
| 114 | ) |
|---|
| 115 | |
|---|
| 116 | |
|---|
| 117 | @log_call(action_type="run-bin-tahoe") |
|---|
| 118 | def run_bintahoe(extra_argv, python_options=None): |
|---|
| 119 | """ |
|---|
| 120 | Run the main Tahoe entrypoint in a child process with the given additional |
|---|
| 121 | arguments. |
|---|
| 122 | |
|---|
| 123 | :param [unicode] extra_argv: More arguments for the child process argv. |
|---|
| 124 | |
|---|
| 125 | :return: A three-tuple of stdout (unicode), stderr (unicode), and the |
|---|
| 126 | child process "returncode" (int). |
|---|
| 127 | """ |
|---|
| 128 | argv = [sys.executable] |
|---|
| 129 | if python_options is not None: |
|---|
| 130 | argv.extend(python_options) |
|---|
| 131 | argv.extend([u"-b", u"-m", u"allmydata.scripts.runner"]) |
|---|
| 132 | argv.extend(extra_argv) |
|---|
| 133 | argv = list(unicode_to_argv(arg) for arg in argv) |
|---|
| 134 | p = Popen(argv, stdout=PIPE, stderr=PIPE) |
|---|
| 135 | encoding = locale.getpreferredencoding(False) |
|---|
| 136 | out = p.stdout.read().decode(encoding) |
|---|
| 137 | err = p.stderr.read().decode(encoding) |
|---|
| 138 | returncode = p.wait() |
|---|
| 139 | return (out, err, returncode) |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | class BinTahoe(common_util.SignalMixin, unittest.TestCase): |
|---|
| 143 | def test_unicode_arguments_and_output(self): |
|---|
| 144 | """ |
|---|
| 145 | The runner script receives unmangled non-ASCII values in argv. |
|---|
| 146 | """ |
|---|
| 147 | tricky = u"\u00F6" |
|---|
| 148 | out, err, returncode = run_bintahoe([tricky]) |
|---|
| 149 | expected = u"Unknown command: \xf6" |
|---|
| 150 | self.assertEqual(returncode, 1) |
|---|
| 151 | self.assertIn( |
|---|
| 152 | expected, |
|---|
| 153 | out, |
|---|
| 154 | "expected {!r} not found in {!r}\nstderr: {!r}".format(expected, out, err), |
|---|
| 155 | ) |
|---|
| 156 | |
|---|
| 157 | def test_with_python_options(self): |
|---|
| 158 | """ |
|---|
| 159 | Additional options for the Python interpreter don't prevent the runner |
|---|
| 160 | script from receiving the arguments meant for it. |
|---|
| 161 | """ |
|---|
| 162 | # This seems like a redundant test for someone else's functionality |
|---|
| 163 | # but on Windows we parse the whole command line string ourselves so |
|---|
| 164 | # we have to have our own implementation of skipping these options. |
|---|
| 165 | |
|---|
| 166 | # -B is a harmless option that prevents writing bytecode so we can add it |
|---|
| 167 | # without impacting other behavior noticably. |
|---|
| 168 | out, err, returncode = run_bintahoe([u"--version"], python_options=[u"-B"]) |
|---|
| 169 | self.assertEqual(returncode, 0, f"Out:\n{out}\nErr:\n{err}") |
|---|
| 170 | self.assertTrue(out.startswith(allmydata.__appname__ + '/')) |
|---|
| 171 | |
|---|
| 172 | def test_help_eliot_destinations(self): |
|---|
| 173 | out, err, returncode = run_bintahoe([u"--help-eliot-destinations"]) |
|---|
| 174 | self.assertIn(u"\tfile:<path>", out) |
|---|
| 175 | self.assertEqual(returncode, 0) |
|---|
| 176 | |
|---|
| 177 | def test_eliot_destination(self): |
|---|
| 178 | out, err, returncode = run_bintahoe([ |
|---|
| 179 | # Proves little but maybe more than nothing. |
|---|
| 180 | u"--eliot-destination=file:-", |
|---|
| 181 | # Throw in *some* command or the process exits with error, making |
|---|
| 182 | # it difficult for us to see if the previous arg was accepted or |
|---|
| 183 | # not. |
|---|
| 184 | u"--help", |
|---|
| 185 | ]) |
|---|
| 186 | self.assertEqual(returncode, 0) |
|---|
| 187 | |
|---|
| 188 | def test_unknown_eliot_destination(self): |
|---|
| 189 | out, err, returncode = run_bintahoe([ |
|---|
| 190 | u"--eliot-destination=invalid:more", |
|---|
| 191 | ]) |
|---|
| 192 | self.assertEqual(1, returncode) |
|---|
| 193 | self.assertIn(u"Unknown destination description", out) |
|---|
| 194 | self.assertIn(u"invalid:more", out) |
|---|
| 195 | |
|---|
| 196 | def test_malformed_eliot_destination(self): |
|---|
| 197 | out, err, returncode = run_bintahoe([ |
|---|
| 198 | u"--eliot-destination=invalid", |
|---|
| 199 | ]) |
|---|
| 200 | self.assertEqual(1, returncode) |
|---|
| 201 | self.assertIn(u"must be formatted like", out) |
|---|
| 202 | |
|---|
| 203 | def test_escape_in_eliot_destination(self): |
|---|
| 204 | out, err, returncode = run_bintahoe([ |
|---|
| 205 | u"--eliot-destination=file:@foo", |
|---|
| 206 | ]) |
|---|
| 207 | self.assertEqual(1, returncode) |
|---|
| 208 | self.assertIn(u"Unsupported escape character", out) |
|---|
| 209 | |
|---|
| 210 | |
|---|
| 211 | class CreateNode(unittest.TestCase): |
|---|
| 212 | # exercise "tahoe create-node" and "tahoe create-introducer" by calling |
|---|
| 213 | # the corresponding code as a subroutine. |
|---|
| 214 | |
|---|
| 215 | def workdir(self, name): |
|---|
| 216 | basedir = os.path.join("test_runner", "CreateNode", name) |
|---|
| 217 | fileutil.make_dirs(basedir) |
|---|
| 218 | return basedir |
|---|
| 219 | |
|---|
| 220 | @inlineCallbacks |
|---|
| 221 | def do_create(self, kind, *args): |
|---|
| 222 | basedir = self.workdir("test_" + kind) |
|---|
| 223 | command = "create-" + kind |
|---|
| 224 | is_client = kind in ("node", "client") |
|---|
| 225 | tac = is_client and "tahoe-client.tac" or ("tahoe-" + kind + ".tac") |
|---|
| 226 | |
|---|
| 227 | n1 = os.path.join(basedir, command + "-n1") |
|---|
| 228 | argv = ["--quiet", command, "--basedir", n1] + list(args) |
|---|
| 229 | rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) |
|---|
| 230 | self.failUnlessEqual(err, "") |
|---|
| 231 | self.failUnlessEqual(out, "") |
|---|
| 232 | self.failUnlessEqual(rc, 0) |
|---|
| 233 | self.failUnless(os.path.exists(n1)) |
|---|
| 234 | self.failUnless(os.path.exists(os.path.join(n1, tac))) |
|---|
| 235 | |
|---|
| 236 | if is_client: |
|---|
| 237 | # tahoe.cfg should exist, and should have storage enabled for |
|---|
| 238 | # 'create-node', and disabled for 'create-client'. |
|---|
| 239 | tahoe_cfg = os.path.join(n1, "tahoe.cfg") |
|---|
| 240 | self.failUnless(os.path.exists(tahoe_cfg)) |
|---|
| 241 | content = fileutil.read(tahoe_cfg).decode('utf-8').replace('\r\n', '\n') |
|---|
| 242 | if kind == "client": |
|---|
| 243 | self.failUnless(re.search(r"\n\[storage\]\n#.*\nenabled = false\n", content), content) |
|---|
| 244 | else: |
|---|
| 245 | self.failUnless(re.search(r"\n\[storage\]\n#.*\nenabled = true\n", content), content) |
|---|
| 246 | self.failUnless("\nreserved_space = 1G\n" in content) |
|---|
| 247 | |
|---|
| 248 | # creating the node a second time should be rejected |
|---|
| 249 | rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) |
|---|
| 250 | self.failIfEqual(rc, 0, str((out, err, rc))) |
|---|
| 251 | self.failUnlessEqual(out, "") |
|---|
| 252 | self.failUnless("is not empty." in err) |
|---|
| 253 | |
|---|
| 254 | # Fail if there is a non-empty line that doesn't end with a |
|---|
| 255 | # punctuation mark. |
|---|
| 256 | for line in err.splitlines(): |
|---|
| 257 | self.failIf(re.search(r"[\S][^\.!?]$", line), (line,)) |
|---|
| 258 | |
|---|
| 259 | # test that the non --basedir form works too |
|---|
| 260 | n2 = os.path.join(basedir, command + "-n2") |
|---|
| 261 | argv = ["--quiet", command] + list(args) + [n2] |
|---|
| 262 | rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) |
|---|
| 263 | self.failUnlessEqual(err, "") |
|---|
| 264 | self.failUnlessEqual(out, "") |
|---|
| 265 | self.failUnlessEqual(rc, 0) |
|---|
| 266 | self.failUnless(os.path.exists(n2)) |
|---|
| 267 | self.failUnless(os.path.exists(os.path.join(n2, tac))) |
|---|
| 268 | |
|---|
| 269 | # test the --node-directory form |
|---|
| 270 | n3 = os.path.join(basedir, command + "-n3") |
|---|
| 271 | argv = ["--quiet", "--node-directory", n3, command] + list(args) |
|---|
| 272 | rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) |
|---|
| 273 | self.failUnlessEqual(err, "") |
|---|
| 274 | self.failUnlessEqual(out, "") |
|---|
| 275 | self.failUnlessEqual(rc, 0) |
|---|
| 276 | self.failUnless(os.path.exists(n3)) |
|---|
| 277 | self.failUnless(os.path.exists(os.path.join(n3, tac))) |
|---|
| 278 | |
|---|
| 279 | if kind in ("client", "node", "introducer"): |
|---|
| 280 | # test that the output (without --quiet) includes the base directory |
|---|
| 281 | n4 = os.path.join(basedir, command + "-n4") |
|---|
| 282 | argv = [command] + list(args) + [n4] |
|---|
| 283 | rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) |
|---|
| 284 | self.failUnlessEqual(err, "") |
|---|
| 285 | self.failUnlessIn(" created in ", out) |
|---|
| 286 | self.failUnlessIn(n4, out) |
|---|
| 287 | self.failIfIn("\\\\?\\", out) |
|---|
| 288 | self.failUnlessEqual(rc, 0) |
|---|
| 289 | self.failUnless(os.path.exists(n4)) |
|---|
| 290 | self.failUnless(os.path.exists(os.path.join(n4, tac))) |
|---|
| 291 | |
|---|
| 292 | # make sure it rejects too many arguments |
|---|
| 293 | self.failUnlessRaises(usage.UsageError, parse_cli, |
|---|
| 294 | command, "basedir", "extraarg") |
|---|
| 295 | |
|---|
| 296 | # when creating a non-client, there is no default for the basedir |
|---|
| 297 | if not is_client: |
|---|
| 298 | argv = [command] |
|---|
| 299 | self.failUnlessRaises(usage.UsageError, parse_cli, |
|---|
| 300 | command) |
|---|
| 301 | |
|---|
| 302 | def test_node(self): |
|---|
| 303 | self.do_create("node", "--hostname=127.0.0.1") |
|---|
| 304 | |
|---|
| 305 | def test_client(self): |
|---|
| 306 | # create-client should behave like create-node --no-storage. |
|---|
| 307 | self.do_create("client") |
|---|
| 308 | |
|---|
| 309 | def test_introducer(self): |
|---|
| 310 | self.do_create("introducer", "--hostname=127.0.0.1") |
|---|
| 311 | |
|---|
| 312 | def test_subcommands(self): |
|---|
| 313 | # no arguments should trigger a command listing, via UsageError |
|---|
| 314 | self.failUnlessRaises(usage.UsageError, parse_cli, |
|---|
| 315 | ) |
|---|
| 316 | |
|---|
| 317 | |
|---|
| 318 | class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): |
|---|
| 319 | """ |
|---|
| 320 | exercise "tahoe run" for both introducer and client node, by spawning |
|---|
| 321 | "tahoe run" as a subprocess. This doesn't get us line-level coverage, but |
|---|
| 322 | it does a better job of confirming that the user can actually run |
|---|
| 323 | "./bin/tahoe run" and expect it to work. This verifies that bin/tahoe sets |
|---|
| 324 | up PYTHONPATH and the like correctly. |
|---|
| 325 | """ |
|---|
| 326 | |
|---|
| 327 | def workdir(self, name): |
|---|
| 328 | basedir = os.path.join("test_runner", "RunNode", name) |
|---|
| 329 | fileutil.make_dirs(basedir) |
|---|
| 330 | return basedir |
|---|
| 331 | |
|---|
| 332 | @inline_callbacks |
|---|
| 333 | def test_introducer(self): |
|---|
| 334 | """ |
|---|
| 335 | The introducer furl is stable across restarts. |
|---|
| 336 | """ |
|---|
| 337 | basedir = self.workdir("test_introducer") |
|---|
| 338 | c1 = os.path.join(basedir, u"c1") |
|---|
| 339 | tahoe = CLINodeAPI(reactor, FilePath(c1)) |
|---|
| 340 | self.addCleanup(tahoe.stop_and_wait) |
|---|
| 341 | |
|---|
| 342 | out, err, returncode = run_bintahoe([ |
|---|
| 343 | u"--quiet", |
|---|
| 344 | u"create-introducer", |
|---|
| 345 | u"--basedir", c1, |
|---|
| 346 | u"--hostname", u"127.0.0.1", |
|---|
| 347 | ]) |
|---|
| 348 | |
|---|
| 349 | self.assertEqual( |
|---|
| 350 | returncode, |
|---|
| 351 | 0, |
|---|
| 352 | "stdout: {!r}\n" |
|---|
| 353 | "stderr: {!r}\n", |
|---|
| 354 | ) |
|---|
| 355 | |
|---|
| 356 | # This makes sure that node.url is written, which allows us to |
|---|
| 357 | # detect when the introducer restarts in _node_has_restarted below. |
|---|
| 358 | config = fileutil.read(tahoe.config_file.path).decode('utf-8') |
|---|
| 359 | self.assertIn('{}web.port = {}'.format(linesep, linesep), config) |
|---|
| 360 | fileutil.write( |
|---|
| 361 | tahoe.config_file.path, |
|---|
| 362 | config.replace( |
|---|
| 363 | '{}web.port = {}'.format(linesep, linesep), |
|---|
| 364 | '{}web.port = 0{}'.format(linesep, linesep), |
|---|
| 365 | ) |
|---|
| 366 | ) |
|---|
| 367 | |
|---|
| 368 | p = Expect() |
|---|
| 369 | tahoe.run(on_stdout(p)) |
|---|
| 370 | yield p.expect(b"introducer running") |
|---|
| 371 | tahoe.active() |
|---|
| 372 | |
|---|
| 373 | yield self.poll(tahoe.introducer_furl_file.exists) |
|---|
| 374 | |
|---|
| 375 | # read the introducer.furl file so we can check that the contents |
|---|
| 376 | # don't change on restart |
|---|
| 377 | furl = fileutil.read(tahoe.introducer_furl_file.path) |
|---|
| 378 | |
|---|
| 379 | tahoe.active() |
|---|
| 380 | |
|---|
| 381 | self.assertTrue(tahoe.twistd_pid_file.exists()) |
|---|
| 382 | self.assertTrue(tahoe.node_url_file.exists()) |
|---|
| 383 | |
|---|
| 384 | # rm this so we can detect when the second incarnation is ready |
|---|
| 385 | tahoe.node_url_file.remove() |
|---|
| 386 | |
|---|
| 387 | yield tahoe.stop_and_wait() |
|---|
| 388 | |
|---|
| 389 | p = Expect() |
|---|
| 390 | tahoe.run(on_stdout(p)) |
|---|
| 391 | yield p.expect(b"introducer running") |
|---|
| 392 | |
|---|
| 393 | # Again, the second incarnation of the node might not be ready yet, so |
|---|
| 394 | # poll until it is. This time introducer_furl_file already exists, so |
|---|
| 395 | # we check for the existence of node_url_file instead. |
|---|
| 396 | yield self.poll(tahoe.node_url_file.exists) |
|---|
| 397 | |
|---|
| 398 | # The point of this test! After starting the second time the |
|---|
| 399 | # introducer furl file must exist and contain the same contents as it |
|---|
| 400 | # did before. |
|---|
| 401 | self.assertTrue(tahoe.introducer_furl_file.exists()) |
|---|
| 402 | self.assertEqual(furl, fileutil.read(tahoe.introducer_furl_file.path)) |
|---|
| 403 | |
|---|
| 404 | @inline_callbacks |
|---|
| 405 | def test_client(self): |
|---|
| 406 | """ |
|---|
| 407 | Test too many things. |
|---|
| 408 | |
|---|
| 409 | 0) Verify that "tahoe create-node" takes a --webport option and writes |
|---|
| 410 | the value to the configuration file. |
|---|
| 411 | |
|---|
| 412 | 1) Verify that "tahoe run" writes a pid file and a node url file (on POSIX). |
|---|
| 413 | |
|---|
| 414 | 2) Verify that the storage furl file has a stable value across a |
|---|
| 415 | "tahoe run" / stop / "tahoe run" sequence. |
|---|
| 416 | |
|---|
| 417 | 3) Verify that the pid file is removed after SIGTERM (on POSIX). |
|---|
| 418 | """ |
|---|
| 419 | basedir = self.workdir("test_client") |
|---|
| 420 | c1 = os.path.join(basedir, u"c1") |
|---|
| 421 | |
|---|
| 422 | tahoe = CLINodeAPI(reactor, FilePath(c1)) |
|---|
| 423 | # Set this up right now so we don't forget later. |
|---|
| 424 | self.addCleanup(tahoe.cleanup) |
|---|
| 425 | |
|---|
| 426 | out, err, returncode = run_bintahoe([ |
|---|
| 427 | u"--quiet", u"create-node", u"--basedir", c1, |
|---|
| 428 | u"--webport", u"0", |
|---|
| 429 | u"--hostname", u"localhost", |
|---|
| 430 | ]) |
|---|
| 431 | self.failUnlessEqual(returncode, 0) |
|---|
| 432 | |
|---|
| 433 | # Check that the --webport option worked. |
|---|
| 434 | config = fileutil.read(tahoe.config_file.path).decode('utf-8') |
|---|
| 435 | self.assertIn( |
|---|
| 436 | '{}web.port = 0{}'.format(linesep, linesep), |
|---|
| 437 | config, |
|---|
| 438 | ) |
|---|
| 439 | |
|---|
| 440 | # After this it's safe to start the node |
|---|
| 441 | tahoe.active() |
|---|
| 442 | |
|---|
| 443 | p = Expect() |
|---|
| 444 | # This will run until we stop it. |
|---|
| 445 | tahoe.run(on_stdout(p)) |
|---|
| 446 | # Wait for startup to have proceeded to a reasonable point. |
|---|
| 447 | yield p.expect(b"client running") |
|---|
| 448 | tahoe.active() |
|---|
| 449 | |
|---|
| 450 | # read the storage.furl file so we can check that its contents don't |
|---|
| 451 | # change on restart |
|---|
| 452 | storage_furl = fileutil.read(tahoe.storage_furl_file.path) |
|---|
| 453 | |
|---|
| 454 | self.assertTrue(tahoe.twistd_pid_file.exists()) |
|---|
| 455 | |
|---|
| 456 | # rm this so we can detect when the second incarnation is ready |
|---|
| 457 | tahoe.node_url_file.remove() |
|---|
| 458 | yield tahoe.stop_and_wait() |
|---|
| 459 | |
|---|
| 460 | p = Expect() |
|---|
| 461 | # We don't have to add another cleanup for this one, the one from |
|---|
| 462 | # above is still registered. |
|---|
| 463 | tahoe.run(on_stdout(p)) |
|---|
| 464 | yield p.expect(b"client running") |
|---|
| 465 | tahoe.active() |
|---|
| 466 | |
|---|
| 467 | self.assertEqual( |
|---|
| 468 | storage_furl, |
|---|
| 469 | fileutil.read(tahoe.storage_furl_file.path), |
|---|
| 470 | ) |
|---|
| 471 | |
|---|
| 472 | self.assertTrue( |
|---|
| 473 | tahoe.twistd_pid_file.exists(), |
|---|
| 474 | "PID file ({}) didn't exist when we expected it to. " |
|---|
| 475 | "These exist: {}".format( |
|---|
| 476 | tahoe.twistd_pid_file, |
|---|
| 477 | tahoe.twistd_pid_file.parent().listdir(), |
|---|
| 478 | ), |
|---|
| 479 | ) |
|---|
| 480 | yield tahoe.stop_and_wait() |
|---|
| 481 | |
|---|
| 482 | # twistd.pid should be gone by now -- except on Windows, where |
|---|
| 483 | # killing a subprocess immediately exits with no chance for |
|---|
| 484 | # any shutdown code (that is, no Twisted shutdown hooks can |
|---|
| 485 | # run). |
|---|
| 486 | if not platform.isWindows(): |
|---|
| 487 | self.assertFalse(tahoe.twistd_pid_file.exists()) |
|---|
| 488 | |
|---|
| 489 | def _remove(self, res, file): |
|---|
| 490 | fileutil.remove(file) |
|---|
| 491 | return res |
|---|
| 492 | |
|---|
| 493 | def test_run_bad_directory(self): |
|---|
| 494 | """ |
|---|
| 495 | If ``tahoe run`` is pointed at a non-node directory, it reports an error |
|---|
| 496 | and exits. |
|---|
| 497 | """ |
|---|
| 498 | return self._bad_directory_test( |
|---|
| 499 | u"test_run_bad_directory", |
|---|
| 500 | "tahoe run", |
|---|
| 501 | lambda tahoe, p: tahoe.run(p), |
|---|
| 502 | "is not a recognizable node directory", |
|---|
| 503 | ) |
|---|
| 504 | |
|---|
| 505 | def test_run_bogus_directory(self): |
|---|
| 506 | """ |
|---|
| 507 | If ``tahoe run`` is pointed at a non-directory, it reports an error and |
|---|
| 508 | exits. |
|---|
| 509 | """ |
|---|
| 510 | return self._bad_directory_test( |
|---|
| 511 | u"test_run_bogus_directory", |
|---|
| 512 | "tahoe run", |
|---|
| 513 | lambda tahoe, p: CLINodeAPI( |
|---|
| 514 | tahoe.reactor, |
|---|
| 515 | tahoe.basedir.sibling(u"bogus"), |
|---|
| 516 | ).run(p), |
|---|
| 517 | "does not look like a directory at all" |
|---|
| 518 | ) |
|---|
| 519 | |
|---|
| 520 | @inline_callbacks |
|---|
| 521 | def _bad_directory_test(self, workdir, description, operation, expected_message): |
|---|
| 522 | """ |
|---|
| 523 | Verify that a certain ``tahoe`` CLI operation produces a certain expected |
|---|
| 524 | message and then exits. |
|---|
| 525 | |
|---|
| 526 | :param unicode workdir: A distinct path name for this test to operate |
|---|
| 527 | on. |
|---|
| 528 | |
|---|
| 529 | :param unicode description: A description of the operation being |
|---|
| 530 | performed. |
|---|
| 531 | |
|---|
| 532 | :param operation: A two-argument callable implementing the operation. |
|---|
| 533 | The first argument is a ``CLINodeAPI`` instance to use to perform |
|---|
| 534 | the operation. The second argument is an ``IProcessProtocol`` to |
|---|
| 535 | which the operations output must be delivered. |
|---|
| 536 | |
|---|
| 537 | :param unicode expected_message: Some text that is expected in the |
|---|
| 538 | stdout or stderr of the operation in the successful case. |
|---|
| 539 | |
|---|
| 540 | :return: A ``Deferred`` that fires when the assertions have been made. |
|---|
| 541 | """ |
|---|
| 542 | basedir = self.workdir(workdir) |
|---|
| 543 | fileutil.make_dirs(basedir) |
|---|
| 544 | |
|---|
| 545 | tahoe = CLINodeAPI(reactor, FilePath(basedir)) |
|---|
| 546 | # If tahoe ends up thinking it should keep running, make sure it stops |
|---|
| 547 | # promptly when the test is done. |
|---|
| 548 | self.addCleanup(tahoe.cleanup) |
|---|
| 549 | |
|---|
| 550 | p = Expect() |
|---|
| 551 | operation(tahoe, on_stdout_and_stderr(p)) |
|---|
| 552 | |
|---|
| 553 | client_running = p.expect(b"client running") |
|---|
| 554 | |
|---|
| 555 | result, index = yield DeferredList([ |
|---|
| 556 | p.expect(expected_message.encode('utf-8')), |
|---|
| 557 | client_running, |
|---|
| 558 | ], fireOnOneCallback=True, consumeErrors=True, |
|---|
| 559 | ) |
|---|
| 560 | |
|---|
| 561 | self.assertEqual( |
|---|
| 562 | index, |
|---|
| 563 | 0, |
|---|
| 564 | "Expected error message from '{}', got something else: {}".format( |
|---|
| 565 | description, |
|---|
| 566 | str(p.get_buffered_output(), "utf-8"), |
|---|
| 567 | ), |
|---|
| 568 | ) |
|---|
| 569 | |
|---|
| 570 | # It should not be running (but windows shutdown can't run |
|---|
| 571 | # code so the PID file still exists there). |
|---|
| 572 | if not platform.isWindows(): |
|---|
| 573 | self.assertFalse(tahoe.twistd_pid_file.exists()) |
|---|
| 574 | |
|---|
| 575 | # Wait for the operation to *complete*. If we got this far it's |
|---|
| 576 | # because we got the expected message so we can expect the "tahoe ..." |
|---|
| 577 | # child process to exit very soon. This other Deferred will fail when |
|---|
| 578 | # it eventually does but DeferredList above will consume the error. |
|---|
| 579 | # What's left is a perfect indicator that the process has exited and |
|---|
| 580 | # we won't get blamed for leaving the reactor dirty. |
|---|
| 581 | yield client_running |
|---|
| 582 | |
|---|
| 583 | |
|---|
| 584 | def _simulate_windows_stdin_close(stdio): |
|---|
| 585 | """ |
|---|
| 586 | on Unix we can just close all the readers, correctly "simulating" |
|---|
| 587 | a stdin close .. of course, Windows has to be difficult |
|---|
| 588 | """ |
|---|
| 589 | stdio.writeConnectionLost() |
|---|
| 590 | stdio.readConnectionLost() |
|---|
| 591 | |
|---|
| 592 | |
|---|
| 593 | class OnStdinCloseTests(SyncTestCase): |
|---|
| 594 | """ |
|---|
| 595 | Tests for on_stdin_close |
|---|
| 596 | """ |
|---|
| 597 | |
|---|
| 598 | def test_close_called(self): |
|---|
| 599 | """ |
|---|
| 600 | our on-close method is called when stdin closes |
|---|
| 601 | """ |
|---|
| 602 | reactor = MemoryReactorClock() |
|---|
| 603 | called = [] |
|---|
| 604 | |
|---|
| 605 | def onclose(): |
|---|
| 606 | called.append(True) |
|---|
| 607 | transport = on_stdin_close(reactor, onclose) |
|---|
| 608 | self.assertEqual(called, []) |
|---|
| 609 | |
|---|
| 610 | if platform.isWindows(): |
|---|
| 611 | _simulate_windows_stdin_close(transport) |
|---|
| 612 | else: |
|---|
| 613 | for reader in reactor.getReaders(): |
|---|
| 614 | reader.loseConnection() |
|---|
| 615 | reactor.advance(1) # ProcessReader does a callLater(0, ..) |
|---|
| 616 | |
|---|
| 617 | self.assertEqual(called, [True]) |
|---|
| 618 | |
|---|
| 619 | def test_exception_ignored(self): |
|---|
| 620 | """ |
|---|
| 621 | An exception from our on-close function is discarded. |
|---|
| 622 | """ |
|---|
| 623 | reactor = MemoryReactorClock() |
|---|
| 624 | called = [] |
|---|
| 625 | |
|---|
| 626 | def onclose(): |
|---|
| 627 | called.append(True) |
|---|
| 628 | raise RuntimeError("unexpected error") |
|---|
| 629 | transport = on_stdin_close(reactor, onclose) |
|---|
| 630 | self.assertEqual(called, []) |
|---|
| 631 | |
|---|
| 632 | if platform.isWindows(): |
|---|
| 633 | _simulate_windows_stdin_close(transport) |
|---|
| 634 | else: |
|---|
| 635 | for reader in reactor.getReaders(): |
|---|
| 636 | reader.loseConnection() |
|---|
| 637 | reactor.advance(1) # ProcessReader does a callLater(0, ..) |
|---|
| 638 | |
|---|
| 639 | self.assertEqual(called, [True]) |
|---|
| 640 | |
|---|
| 641 | |
|---|
| 642 | class PidFileLocking(SyncTestCase): |
|---|
| 643 | """ |
|---|
| 644 | Direct tests for allmydata.util.pid functions |
|---|
| 645 | """ |
|---|
| 646 | |
|---|
| 647 | def test_locking(self): |
|---|
| 648 | """ |
|---|
| 649 | Fail to create a pidfile if another process has the lock already. |
|---|
| 650 | """ |
|---|
| 651 | # this can't just be "our" process because the locking library |
|---|
| 652 | # allows the same process to acquire a lock multiple times. |
|---|
| 653 | pidfile = FilePath(self.mktemp()) |
|---|
| 654 | lockfile = _pidfile_to_lockpath(pidfile) |
|---|
| 655 | |
|---|
| 656 | with open("other_lock.py", "w") as f: |
|---|
| 657 | f.write( |
|---|
| 658 | "\n".join([ |
|---|
| 659 | "import filelock, time, sys", |
|---|
| 660 | "with filelock.FileLock(sys.argv[1], timeout=1):", |
|---|
| 661 | " sys.stdout.write('.\\n')", |
|---|
| 662 | " sys.stdout.flush()", |
|---|
| 663 | " time.sleep(10)", |
|---|
| 664 | ]) |
|---|
| 665 | ) |
|---|
| 666 | proc = Popen( |
|---|
| 667 | [sys.executable, "other_lock.py", lockfile.path], |
|---|
| 668 | stdout=PIPE, |
|---|
| 669 | stderr=PIPE, |
|---|
| 670 | ) |
|---|
| 671 | # make sure our subprocess has had time to acquire the lock |
|---|
| 672 | # for sure (from the "." it prints) |
|---|
| 673 | proc.stdout.read(2) |
|---|
| 674 | |
|---|
| 675 | # acquiring the same lock should fail; it is locked by the subprocess |
|---|
| 676 | with self.assertRaises(ProcessInTheWay): |
|---|
| 677 | check_pid_process(pidfile) |
|---|
| 678 | proc.terminate() |
|---|