tahoe-lafs/src/allmydata/test/test_runner.py

656 lines
24 KiB
Python
Raw Normal View History

from __future__ import (
absolute_import,
)
import os.path, re, sys
2019-05-03 16:09:03 +00:00
from os import linesep
from twisted.trial import unittest
from twisted.internet import reactor
2019-05-03 16:09:21 +00:00
from twisted.python import usage
from twisted.internet.defer import (
inlineCallbacks,
returnValue,
DeferredList,
)
from twisted.python.filepath import FilePath
2019-05-13 10:28:57 +00:00
from twisted.python.runtime import (
platform,
)
from allmydata.util import fileutil, pollmixin
from allmydata.util.encodingutil import unicode_to_argv, unicode_to_output, \
get_filesystem_encoding
from allmydata.test import common_util
from allmydata.version_checks import normalized_version
import allmydata
from allmydata import __appname__
from .common_util import parse_cli, run_cli
from .cli_node_api import (
CLINodeAPI,
Expect,
on_stdout,
on_stdout_and_stderr,
)
from ._twisted_9607 import (
getProcessOutputAndValue,
)
from ..util.eliotutil import (
inline_callbacks,
)
def get_root_from_file(src):
srcdir = os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(src))))
root = os.path.dirname(srcdir)
if os.path.basename(srcdir) == 'site-packages':
if re.search(r'python.+\..+', os.path.basename(root)):
root = os.path.dirname(root)
root = os.path.dirname(root)
elif os.path.basename(root) == 'src':
root = os.path.dirname(root)
return root
srcfile = allmydata.__file__
rootdir = get_root_from_file(srcfile)
2019-05-15 06:17:44 +00:00
class RunBinTahoeMixin(object):
@inlineCallbacks
def find_import_location(self):
res = yield self.run_bintahoe(["--version-and-path"])
out, err, rc_or_sig = res
self.assertEqual(rc_or_sig, 0, res)
lines = out.splitlines()
tahoe_pieces = lines[0].split()
self.assertEqual(tahoe_pieces[0], "%s:" % (__appname__,), (tahoe_pieces, res))
returnValue(tahoe_pieces[-1].strip("()"))
def run_bintahoe(self, args, stdin=None, python_options=[], env=None):
command = sys.executable
argv = python_options + ["-m", "allmydata.scripts.runner"] + args
if env is None:
env = os.environ
d = getProcessOutputAndValue(command, argv, env, stdinBytes=stdin)
def fix_signal(result):
# Mirror subprocess.Popen.returncode structure
(out, err, signal) = result
return (out, err, -signal)
d.addErrback(fix_signal)
return d
class BinTahoe(common_util.SignalMixin, unittest.TestCase, RunBinTahoeMixin):
@inlineCallbacks
def test_the_right_code(self):
# running "tahoe" in a subprocess should find the same code that
# holds this test file, else something is weird
test_path = os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile))))
bintahoe_import_path = yield self.find_import_location()
same = (bintahoe_import_path == test_path)
if not same:
msg = ("My tests and my 'tahoe' executable are using different paths.\n"
"tahoe: %r\n"
"tests: %r\n"
"( according to the test source filename %r)\n" %
(bintahoe_import_path, test_path, srcfile))
if (not isinstance(rootdir, unicode) and
rootdir.decode(get_filesystem_encoding(), 'replace') != rootdir):
msg += ("However, this may be a false alarm because the import path\n"
"is not representable in the filesystem encoding.")
raise unittest.SkipTest(msg)
else:
msg += "Please run the tests in a virtualenv that includes both the Tahoe-LAFS library and the 'tahoe' executable."
self.fail(msg)
def test_path(self):
d = self.run_bintahoe(["--version-and-path"])
def _cb(res):
out, err, rc_or_sig = res
self.failUnlessEqual(rc_or_sig, 0, str(res))
# Fail unless the __appname__ package is *this* version *and*
# was loaded from *this* source directory.
required_verstr = str(allmydata.__version__)
self.failIfEqual(required_verstr, "unknown",
"We don't know our version, because this distribution didn't come "
"with a _version.py and 'setup.py update_version' hasn't been run.")
srcdir = os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile))))
info = repr((res, allmydata.__appname__, required_verstr, srcdir))
appverpath = out.split(')')[0]
(appverfull, path) = appverpath.split('] (')
(appver, comment) = appverfull.split(' [')
(branch, full_version) = comment.split(': ')
(app, ver) = appver.split(': ')
self.failUnlessEqual(app, allmydata.__appname__, info)
norm_ver = normalized_version(ver)
norm_required = normalized_version(required_verstr)
self.failUnlessEqual(norm_ver, norm_required, info)
self.failUnlessEqual(path, srcdir, info)
self.failUnlessEqual(branch, allmydata.branch)
self.failUnlessEqual(full_version, allmydata.full_version)
d.addCallback(_cb)
return d
def test_unicode_arguments_and_output(self):
tricky = u"\u2621"
try:
tricky_arg = unicode_to_argv(tricky, mangle=True)
tricky_out = unicode_to_output(tricky)
except UnicodeEncodeError:
raise unittest.SkipTest("A non-ASCII argument/output could not be encoded on this platform.")
d = self.run_bintahoe([tricky_arg])
def _cb(res):
out, err, rc_or_sig = res
self.failUnlessEqual(rc_or_sig, 1, str(res))
self.failUnlessIn("Unknown command: "+tricky_out, out)
d.addCallback(_cb)
return d
def test_run_with_python_options(self):
# -t is a harmless option that warns about tabs.
d = self.run_bintahoe(["--version"], python_options=["-t"])
def _cb(res):
out, err, rc_or_sig = res
self.failUnlessEqual(rc_or_sig, 0, str(res))
self.failUnless(out.startswith(allmydata.__appname__+':'), str(res))
d.addCallback(_cb)
return d
def test_version_no_noise(self):
d = self.run_bintahoe(["--version"])
def _cb(res):
out, err, rc_or_sig = res
self.failUnlessEqual(rc_or_sig, 0, str(res))
self.failUnless(out.startswith(allmydata.__appname__+':'), str(res))
self.failIfIn("DeprecationWarning", out, str(res))
errlines = err.split("\n")
self.failIf([True for line in errlines if (line != "" and "UserWarning: Unbuilt egg for setuptools" not in line
and "from pkg_resources import load_entry_point" not in line)], str(res))
if err != "":
raise unittest.SkipTest("This test is known not to pass on Ubuntu Lucid; see #1235.")
d.addCallback(_cb)
return d
@inlineCallbacks
def test_help_eliot_destinations(self):
out, err, rc_or_sig = yield self.run_bintahoe(["--help-eliot-destinations"])
self.assertIn("\tfile:<path>", out)
self.assertEqual(rc_or_sig, 0)
@inlineCallbacks
def test_eliot_destination(self):
out, err, rc_or_sig = yield self.run_bintahoe([
# Proves little but maybe more than nothing.
"--eliot-destination=file:-",
# Throw in *some* command or the process exits with error, making
# it difficult for us to see if the previous arg was accepted or
# not.
"--help",
])
self.assertEqual(rc_or_sig, 0)
@inlineCallbacks
def test_unknown_eliot_destination(self):
out, err, rc_or_sig = yield self.run_bintahoe([
"--eliot-destination=invalid:more",
])
self.assertEqual(1, rc_or_sig)
self.assertIn("Unknown destination description", out)
self.assertIn("invalid:more", out)
@inlineCallbacks
def test_malformed_eliot_destination(self):
out, err, rc_or_sig = yield self.run_bintahoe([
"--eliot-destination=invalid",
])
self.assertEqual(1, rc_or_sig)
self.assertIn("must be formatted like", out)
@inlineCallbacks
def test_escape_in_eliot_destination(self):
out, err, rc_or_sig = yield self.run_bintahoe([
"--eliot-destination=file:@foo",
])
self.assertEqual(1, rc_or_sig)
self.assertIn("Unsupported escape character", out)
class CreateNode(unittest.TestCase):
# exercise "tahoe create-node", create-introducer,
# create-key-generator, and create-stats-gatherer, by calling the
# corresponding code as a subroutine.
def workdir(self, name):
basedir = os.path.join("test_runner", "CreateNode", name)
fileutil.make_dirs(basedir)
return basedir
@inlineCallbacks
def do_create(self, kind, *args):
basedir = self.workdir("test_" + kind)
command = "create-" + kind
is_client = kind in ("node", "client")
tac = is_client and "tahoe-client.tac" or ("tahoe-" + kind + ".tac")
n1 = os.path.join(basedir, command + "-n1")
argv = ["--quiet", command, "--basedir", n1] + list(args)
rc, out, err = yield run_cli(*argv)
self.failUnlessEqual(err, "")
self.failUnlessEqual(out, "")
self.failUnlessEqual(rc, 0)
self.failUnless(os.path.exists(n1))
self.failUnless(os.path.exists(os.path.join(n1, tac)))
if is_client:
# tahoe.cfg should exist, and should have storage enabled for
# 'create-node', and disabled for 'create-client'.
tahoe_cfg = os.path.join(n1, "tahoe.cfg")
self.failUnless(os.path.exists(tahoe_cfg))
content = fileutil.read(tahoe_cfg).replace('\r\n', '\n')
if kind == "client":
self.failUnless(re.search(r"\n\[storage\]\n#.*\nenabled = false\n", content), content)
else:
self.failUnless(re.search(r"\n\[storage\]\n#.*\nenabled = true\n", content), content)
self.failUnless("\nreserved_space = 1G\n" in content)
# creating the node a second time should be rejected
rc, out, err = yield run_cli(*argv)
self.failIfEqual(rc, 0, str((out, err, rc)))
self.failUnlessEqual(out, "")
self.failUnless("is not empty." in err)
# Fail if there is a non-empty line that doesn't end with a
# punctuation mark.
for line in err.splitlines():
self.failIf(re.search("[\S][^\.!?]$", line), (line,))
# test that the non --basedir form works too
n2 = os.path.join(basedir, command + "-n2")
argv = ["--quiet", command] + list(args) + [n2]
rc, out, err = yield run_cli(*argv)
self.failUnlessEqual(err, "")
self.failUnlessEqual(out, "")
self.failUnlessEqual(rc, 0)
self.failUnless(os.path.exists(n2))
self.failUnless(os.path.exists(os.path.join(n2, tac)))
# test the --node-directory form
n3 = os.path.join(basedir, command + "-n3")
argv = ["--quiet", "--node-directory", n3, command] + list(args)
rc, out, err = yield run_cli(*argv)
self.failUnlessEqual(err, "")
self.failUnlessEqual(out, "")
self.failUnlessEqual(rc, 0)
self.failUnless(os.path.exists(n3))
self.failUnless(os.path.exists(os.path.join(n3, tac)))
if kind in ("client", "node", "introducer"):
# test that the output (without --quiet) includes the base directory
n4 = os.path.join(basedir, command + "-n4")
argv = [command] + list(args) + [n4]
rc, out, err = yield run_cli(*argv)
self.failUnlessEqual(err, "")
self.failUnlessIn(" created in ", out)
self.failUnlessIn(n4, out)
self.failIfIn("\\\\?\\", out)
self.failUnlessEqual(rc, 0)
self.failUnless(os.path.exists(n4))
self.failUnless(os.path.exists(os.path.join(n4, tac)))
# make sure it rejects too many arguments
self.failUnlessRaises(usage.UsageError, parse_cli,
command, "basedir", "extraarg")
# when creating a non-client, there is no default for the basedir
if not is_client:
argv = [command]
self.failUnlessRaises(usage.UsageError, parse_cli,
command)
def test_node(self):
self.do_create("node", "--hostname=127.0.0.1")
def test_client(self):
# create-client should behave like create-node --no-storage.
self.do_create("client")
def test_introducer(self):
self.do_create("introducer", "--hostname=127.0.0.1")
def test_stats_gatherer(self):
self.do_create("stats-gatherer", "--hostname=127.0.0.1")
def test_subcommands(self):
# no arguments should trigger a command listing, via UsageError
self.failUnlessRaises(usage.UsageError, parse_cli,
)
@inlineCallbacks
def test_stats_gatherer_good_args(self):
rc,out,err = yield run_cli("create-stats-gatherer", "--hostname=foo",
self.mktemp())
self.assertEqual(rc, 0)
rc,out,err = yield run_cli("create-stats-gatherer",
"--location=tcp:foo:1234",
"--port=tcp:1234", self.mktemp())
self.assertEqual(rc, 0)
def test_stats_gatherer_bad_args(self):
def _test(args):
argv = args.split()
self.assertRaises(usage.UsageError, parse_cli, *argv)
# missing hostname/location/port
_test("create-stats-gatherer D")
# missing port
_test("create-stats-gatherer --location=foo D")
# missing location
_test("create-stats-gatherer --port=foo D")
# can't provide both
_test("create-stats-gatherer --hostname=foo --port=foo D")
# can't provide both
_test("create-stats-gatherer --hostname=foo --location=foo D")
# can't provide all three
_test("create-stats-gatherer --hostname=foo --location=foo --port=foo D")
class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin,
RunBinTahoeMixin):
"""
2019-05-03 16:09:10 +00:00
exercise "tahoe run" for both introducer, client node, and key-generator,
by spawning "tahoe run" (or "tahoe start") as a subprocess. This doesn't
get us line-level coverage, but it does a better job of confirming that
the user can actually run "./bin/tahoe run" and expect it to work. This
verifies that bin/tahoe sets up PYTHONPATH and the like correctly.
This doesn't work on cygwin (it hangs forever), so we skip this test
when we're on cygwin. It is likely that "tahoe start" itself doesn't
work on cygwin: twisted seems unable to provide a version of
spawnProcess which really works there.
"""
def workdir(self, name):
basedir = os.path.join("test_runner", "RunNode", name)
fileutil.make_dirs(basedir)
return basedir
@inline_callbacks
def test_introducer(self):
"""
The introducer furl is stable across restarts.
"""
basedir = self.workdir("test_introducer")
c1 = os.path.join(basedir, "c1")
tahoe = CLINodeAPI(reactor, FilePath(c1))
self.addCleanup(tahoe.stop_and_wait)
out, err, rc_or_sig = yield self.run_bintahoe([
"--quiet",
"create-introducer",
"--basedir", c1,
"--hostname", "127.0.0.1",
])
self.assertEqual(rc_or_sig, 0)
# This makes sure that node.url is written, which allows us to
# detect when the introducer restarts in _node_has_restarted below.
config = fileutil.read(tahoe.config_file.path)
2019-05-03 16:09:03 +00:00
self.assertIn('{}web.port = {}'.format(linesep, linesep), config)
fileutil.write(
tahoe.config_file.path,
2019-05-03 16:09:03 +00:00
config.replace(
'{}web.port = {}'.format(linesep, linesep),
'{}web.port = 0{}'.format(linesep, linesep),
)
)
p = Expect()
tahoe.run(on_stdout(p))
yield p.expect("introducer running")
tahoe.active()
yield self.poll(tahoe.introducer_furl_file.exists)
# read the introducer.furl file so we can check that the contents
# don't change on restart
furl = fileutil.read(tahoe.introducer_furl_file.path)
tahoe.active()
2019-05-13 10:28:57 +00:00
# We don't keep track of PIDs in files on Windows.
if not platform.isWindows():
self.assertTrue(tahoe.twistd_pid_file.exists())
self.assertTrue(tahoe.node_url_file.exists())
# rm this so we can detect when the second incarnation is ready
tahoe.node_url_file.remove()
yield tahoe.stop_and_wait()
p = Expect()
tahoe.run(on_stdout(p))
yield p.expect("introducer running")
# Again, the second incarnation of the node might not be ready yet, so
# poll until it is. This time introducer_furl_file already exists, so
# we check for the existence of node_url_file instead.
yield self.poll(tahoe.node_url_file.exists)
# The point of this test! After starting the second time the
# introducer furl file must exist and contain the same contents as it
# did before.
self.assertTrue(tahoe.introducer_furl_file.exists())
self.assertEqual(furl, fileutil.read(tahoe.introducer_furl_file.path))
@inline_callbacks
def test_client(self):
"""
Test many things.
0) Verify that "tahoe create-node" takes a --webport option and writes
the value to the configuration file.
2019-05-13 10:28:57 +00:00
1) Verify that "tahoe run" writes a pid file and a node url file (on POSIX).
2) Verify that the storage furl file has a stable value across a
"tahoe run" / "tahoe stop" / "tahoe run" sequence.
2019-05-13 10:28:57 +00:00
3) Verify that the pid file is removed after "tahoe stop" succeeds (on POSIX).
"""
basedir = self.workdir("test_client")
c1 = os.path.join(basedir, "c1")
tahoe = CLINodeAPI(reactor, FilePath(c1))
# Set this up right now so we don't forget later.
2019-05-03 11:27:58 +00:00
self.addCleanup(tahoe.cleanup)
out, err, rc_or_sig = yield self.run_bintahoe([
"--quiet", "create-node", "--basedir", c1,
"--webport", "0",
"--hostname", "localhost",
])
self.failUnlessEqual(rc_or_sig, 0)
# Check that the --webport option worked.
config = fileutil.read(tahoe.config_file.path)
2019-05-03 16:09:03 +00:00
self.assertIn(
'{}web.port = 0{}'.format(linesep, linesep),
config,
)
# After this it's safe to start the node
tahoe.active()
p = Expect()
# This will run until we stop it.
tahoe.run(on_stdout(p))
# Wait for startup to have proceeded to a reasonable point.
yield p.expect("client running")
tahoe.active()
# read the storage.furl file so we can check that its contents don't
# change on restart
storage_furl = fileutil.read(tahoe.storage_furl_file.path)
2019-05-13 10:28:57 +00:00
# We don't keep track of PIDs in files on Windows.
if not platform.isWindows():
self.assertTrue(tahoe.twistd_pid_file.exists())
# rm this so we can detect when the second incarnation is ready
tahoe.node_url_file.remove()
yield tahoe.stop_and_wait()
p = Expect()
# We don't have to add another cleanup for this one, the one from
# above is still registered.
tahoe.run(on_stdout(p))
yield p.expect("client running")
tahoe.active()
self.assertEqual(
storage_furl,
fileutil.read(tahoe.storage_furl_file.path),
)
2019-05-13 10:28:57 +00:00
if not platform.isWindows():
self.assertTrue(
tahoe.twistd_pid_file.exists(),
"PID file ({}) didn't exist when we expected it to. "
"These exist: {}".format(
tahoe.twistd_pid_file,
tahoe.twistd_pid_file.parent().listdir(),
),
)
yield tahoe.stop_and_wait()
2019-05-13 10:28:57 +00:00
if not platform.isWindows():
# twistd.pid should be gone by now.
self.assertFalse(tahoe.twistd_pid_file.exists())
def _remove(self, res, file):
fileutil.remove(file)
return res
def test_run_bad_directory(self):
"""
If ``tahoe run`` is pointed at a non-node directory, it reports an error
and exits.
"""
return self._bad_directory_test(
u"test_run_bad_directory",
"tahoe run",
lambda tahoe, p: tahoe.run(p),
"is not a recognizable node directory",
)
def test_run_bogus_directory(self):
"""
If ``tahoe run`` is pointed at a non-directory, it reports an error and
exits.
"""
return self._bad_directory_test(
u"test_run_bogus_directory",
"tahoe run",
lambda tahoe, p: CLINodeAPI(
tahoe.reactor,
tahoe.basedir.sibling(u"bogus"),
).run(p),
"does not look like a directory at all"
)
def test_stop_bad_directory(self):
"""
If ``tahoe run`` is pointed at a directory where no node is running, it
reports an error and exits.
"""
return self._bad_directory_test(
u"test_stop_bad_directory",
"tahoe stop",
lambda tahoe, p: tahoe.stop(p),
"does not look like a running node directory",
)
@inline_callbacks
def _bad_directory_test(self, workdir, description, operation, expected_message):
"""
Verify that a certain ``tahoe`` CLI operation produces a certain expected
message and then exits.
:param unicode workdir: A distinct path name for this test to operate
on.
:param unicode description: A description of the operation being
performed.
:param operation: A two-argument callable implementing the operation.
The first argument is a ``CLINodeAPI`` instance to use to perform
the operation. The second argument is an ``IProcessProtocol`` to
which the operations output must be delivered.
:param unicode expected_message: Some text that is expected in the
stdout or stderr of the operation in the successful case.
:return: A ``Deferred`` that fires when the assertions have been made.
"""
basedir = self.workdir(workdir)
fileutil.make_dirs(basedir)
tahoe = CLINodeAPI(reactor, FilePath(basedir))
# If tahoe ends up thinking it should keep running, make sure it stops
# promptly when the test is done.
self.addCleanup(tahoe.cleanup)
p = Expect()
operation(tahoe, on_stdout_and_stderr(p))
client_running = p.expect(b"client running")
result, index = yield DeferredList([
p.expect(expected_message),
client_running,
], fireOnOneCallback=True, consumeErrors=True,
)
self.assertEqual(
index,
0,
"Expected error message from '{}', got something else: {}".format(
description,
p.get_buffered_output(),
),
)
2019-05-13 10:28:57 +00:00
if not platform.isWindows():
# It should not be running.
self.assertFalse(tahoe.twistd_pid_file.exists())
# Wait for the operation to *complete*. If we got this far it's
# because we got the expected message so we can expect the "tahoe ..."
# child process to exit very soon. This other Deferred will fail when
# it eventually does but DeferredList above will consume the error.
# What's left is a perfect indicator that the process has exited and
# we won't get blamed for leaving the reactor dirty.
yield client_running