[reprotest] 01/02: Improve logging to try to get to the bottom of the jenkins failures
Ximin Luo
infinity0 at debian.org
Fri Oct 27 00:18:51 UTC 2017
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch master
in repository reprotest.
commit a9c7d6baadd1d2417d447d6b04bacb9837257201
Author: Ximin Luo <infinity0 at debian.org>
Date: Fri Oct 27 02:06:03 2017 +0200
Improve logging to try to get to the bottom of the jenkins failures
---
reprotest/__init__.py | 55 +++++++++++++++++++++++++------------------------
reprotest/build.py | 14 +++++++------
tests/mock_build.py | 31 ++++++++++++++--------------
tests/test_reprotest.py | 29 +++++++++++++++++++++++++-
4 files changed, 80 insertions(+), 49 deletions(-)
diff --git a/reprotest/__init__.py b/reprotest/__init__.py
index 1c85d71..7ac4ff0 100644
--- a/reprotest/__init__.py
+++ b/reprotest/__init__.py
@@ -24,6 +24,7 @@ from reprotest.lib import adt_testbed
from reprotest.build import Build, VariationSpec, Variations, tool_missing
from reprotest import environ, presets, shell_syn
+logger = logging.getLogger(__name__)
VIRT_PREFIX = "autopkgtest-virt-"
@@ -75,7 +76,7 @@ def start_testbed(args, temp_dir, no_clean_on_error=False, host_distro='debian')
# Find the location of reprotest using setuptools and then get the
# path for the correct virt-server script.
server_path = get_server_path(args[0])
- logging.info('STARTING VIRTUAL SERVER %r', [server_path] + args[1:])
+ logger.info('STARTING VIRTUAL SERVER %r', [server_path] + args[1:])
# TODO: make the user configurable, like autopkgtest
testbed = Testbed([server_path] + args[1:], temp_dir,
getpass.getuser(), host_distro=host_distro)
@@ -88,7 +89,7 @@ def start_testbed(args, temp_dir, no_clean_on_error=False, host_distro='debian')
pass
except BaseException as e:
if no_clean_on_error:
- logging.warn("preserving temporary files in: %s", testbed.scratch)
+ logger.warn("preserving temporary files in: %s", testbed.scratch)
should_clean = False
raise
finally:
@@ -170,7 +171,7 @@ class BuildContext(collections.namedtuple('_BuildContext',
def plan_variations(self, build):
actions = self.variations.spec.actions()
- logging.info('build "%s": %s',
+ logger.info('build "%s": %s',
self.build_name,
", ".join("%s %s" % ("FIX" if not vary else "vary", v) for v, vary, action in actions))
for v, vary, action in actions:
@@ -178,15 +179,15 @@ class BuildContext(collections.namedtuple('_BuildContext',
return build
def copydown(self, testbed):
- logging.info("copying %s over to virtual server's %s", self.local_src, self.testbed_src)
+ logger.info("copying %s over to virtual server's %s", self.local_src, self.testbed_src)
testbed.command('copydown', (os.path.join(self.local_src, ''), self.testbed_src))
def copyup(self, testbed):
- logging.info("copying %s back from virtual server's %s", self.testbed_dist, self.local_dist)
+ logger.info("copying %s back from virtual server's %s", self.testbed_dist, self.local_dist)
testbed.command('copyup', (self.testbed_dist, os.path.join(self.local_dist, '')))
def run_build(self, testbed, build, old_env, artifact_pattern, testbed_build_pre, no_clean_on_error):
- logging.info("starting build with source directory: %s, artifact pattern: %s",
+ logger.info("starting build with source directory: %s, artifact pattern: %s",
self.testbed_src, artifact_pattern)
# we remove existing artifacts in case the build doesn't overwrite it
# e.g. like how make(1) sometimes works
@@ -194,12 +195,12 @@ class BuildContext(collections.namedtuple('_BuildContext',
['sh', '-ec', 'cd "%s" && rm -rf %s && %s' %
(self.testbed_src, artifact_pattern, testbed_build_pre or "true")])
build_script = build.to_script(no_clean_on_error)
- logging.info("executing build in %s", build.tree)
- logging.debug("#### REPROTEST BUILD ENVVARS ###################################################\n" +
+ logger.info("executing build in %s", build.tree)
+ logger.debug("#### REPROTEST BUILD ENVVARS ###################################################\n" +
"\n".join(environ.env_diff(old_env, build.env)))
- logging.debug("#### BEGIN REPROTEST BUILD SCRIPT ##############################################\n" +
+ logger.debug("#### BEGIN REPROTEST BUILD SCRIPT ##############################################\n" +
build_script)
- logging.debug("#### END REPROTEST BUILD SCRIPT ################################################")
+ logger.debug("#### END REPROTEST BUILD SCRIPT ################################################")
if 'root-on-testbed' in testbed.caps:
build_argv = ['su', '-s', '/bin/sh', testbed.user, '-c', 'set -e; ' + build_script]
@@ -209,7 +210,7 @@ class BuildContext(collections.namedtuple('_BuildContext',
testbed.check_exec2(build_argv,
xenv=['-i'] + ['%s=%s' % (k, v) for k, v in build.env.items()],
kind='build')
- logging.info("build successful, copying artifacts")
+ logger.info("build successful, copying artifacts")
dist_base = os.path.join(self.testbed_dist, VSRC_DIR)
testbed.check_exec2(shell_copy_pattern(dist_base, self.testbed_src, artifact_pattern))
# FIXME: `touch` is needed because of the FIXME in build.faketime(). we can rm it after that is fixed
@@ -229,14 +230,14 @@ def run_or_tee(progargs, filename, store_dir, *args, **kwargs):
def run_diff(dist_0, dist_1, diffoscope_args, store_dir):
if diffoscope_args is None: # don't run diffoscope
diffprogram = ['diff', '-ru', dist_0, dist_1]
- logging.info("Running diff: %r", diffprogram)
+ logger.info("Running diff: %r", diffprogram)
else:
diffprogram = ['diffoscope', dist_0, dist_1] + diffoscope_args
- logging.info("Running diffoscope: %r", diffprogram)
+ logger.info("Running diffoscope: %r", diffprogram)
retcode = run_or_tee(diffprogram, 'diffoscope.out', store_dir).returncode
if retcode == 0:
- logging.info("No differences between %s, %s", dist_0, dist_1)
+ logger.info("No differences between %s, %s", dist_0, dist_1)
if store_dir:
shutil.rmtree(dist_1)
os.symlink(os.path.basename(dist_0), dist_1)
@@ -256,11 +257,11 @@ class TestArgs(collections.namedtuple('_Test',
def of(cls, build_command, source_root, artifact_pattern, result_dir=None,
source_pattern=None, no_clean_on_error=False, diffoscope_args=[]):
artifact_pattern = shell_syn.sanitize_globs(artifact_pattern)
- logging.debug("artifact_pattern sanitized to: %s", artifact_pattern)
+ logger.debug("artifact_pattern sanitized to: %s", artifact_pattern)
if source_pattern:
source_pattern = shell_syn.sanitize_globs(source_pattern)
- logging.debug("source_pattern sanitized to: %s", source_pattern)
+ logger.debug("source_pattern sanitized to: %s", source_pattern)
return cls(build_command, source_root, artifact_pattern, result_dir,
source_pattern, no_clean_on_error, diffoscope_args)
@@ -282,7 +283,7 @@ class TestArgs(collections.namedtuple('_Test',
source_root = os.path.normpath(os.path.dirname(source_root))
source_root = str(source_root)
- logging.debug("virtual_server_args: %r", virtual_server_args)
+ logger.debug("virtual_server_args: %r", virtual_server_args)
# TODO: if no_clean_on_error then this shouldn't be rm'd
with tempfile.TemporaryDirectory() as temp_dir:
@@ -292,7 +293,7 @@ class TestArgs(collections.namedtuple('_Test',
source_root = new_source_root
if testbed_pre:
subprocess.check_call(["sh", "-ec", testbed_pre], cwd=new_source_root)
- logging.debug("source_root: %s", source_root)
+ logger.debug("source_root: %s", source_root)
# TODO: an alternative strategy is to run the testbed many times, one for each build
# not sure if it's worth implementing at this stage, but perhaps in the future.
@@ -661,7 +662,7 @@ def command_line(parser, argv):
def get_main_spec(parsed_args):
variations = [parsed_args.variations] + parsed_args.vary
if parsed_args.dont_vary:
- logging.warn("--dont-vary is deprecated; use --vary=-$variation instead")
+ logger.warn("--dont-vary is deprecated; use --vary=-$variation instead")
variations += ["-%s" % a for x in parsed_args.dont_vary for a in x.split(",")]
return VariationSpec().extend(variations)
@@ -679,7 +680,7 @@ def run(argv, dry_run=None):
verbosity = parsed_args.verbosity
adtlog.verbosity = verbosity - 1
logging.basicConfig(level=30-10*verbosity)
- logging.debug('%r', parsed_args)
+ logger.debug('%r', parsed_args)
if not dry_run and parsed_args.print_sudoers:
build.print_sudoers(get_main_spec(parsed_args))
@@ -703,9 +704,9 @@ def run(argv, dry_run=None):
elif first_arg == "auto":
build_command = first_arg
if parsed_args.artifact_pattern:
- logging.warn("old CLI form `reprotest auto <source_root>` detected, "
+ logger.warn("old CLI form `reprotest auto <source_root>` detected, "
"setting source_root to the second argument: %s", parsed_args.artifact_pattern)
- logging.warn("to avoid this warning, use instead `reprotest <source_root>` "
+ logger.warn("to avoid this warning, use instead `reprotest <source_root>` "
"or (if really necessary) `reprotest -s <source_root> auto <artifact>`")
source_root = parsed_args.artifact_pattern
parsed_args.artifact_pattern = None
@@ -715,7 +716,7 @@ def run(argv, dry_run=None):
parts = shlex.split(first_arg)
if len(parts) == 1:
if shutil.which(parts[0]) is None:
- logging.warn("XXX")
+ logger.warn("XXX")
raise RuntimeError("Not found, neither as a file nor as a command: %s" % first_arg)
# if len(parts) > 1 then it could be something like '( command )'
# which is valid despite '(' not existing.
@@ -741,7 +742,7 @@ def run(argv, dry_run=None):
auto_preset_expr = parsed_args.auto_preset_expr
values = presets.get_presets(source_root, virtual_server_args[0])
values = eval(auto_preset_expr, {'_': values}, {})
- logging.info("preset auto-selected: %r", values)
+ logger.info("preset auto-selected: %r", values)
build_command = values.build_command
artifact_pattern = artifact_pattern or values.artifact_pattern
testbed_pre = testbed_pre or values.testbed_pre
@@ -773,10 +774,10 @@ def run(argv, dry_run=None):
missing = [(var, tools) for var, tools in missing if tools]
for var, tools in missing:
if tools:
- logging.warn("Varying '%s' requires these program(s): %s", var, ", ".join(tools))
+ logger.warn("Varying '%s' requires these program(s): %s", var, ", ".join(tools))
if missing:
- logging.warn("Your build will probably fail, either install them or disable the variations.")
- logging.warn("(From a system package manager, simply install the 'optional' or 'recommended' "
+ logger.warn("Your build will probably fail, either install them or disable the variations.")
+ logger.warn("(From a system package manager, simply install the 'optional' or 'recommended' "
"dependencies of reprotest.)")
# Remaining args
diff --git a/reprotest/build.py b/reprotest/build.py
index c9b796c..cd3a09c 100644
--- a/reprotest/build.py
+++ b/reprotest/build.py
@@ -18,6 +18,8 @@ from reprotest import mdiffconf
from reprotest import shell_syn
from reprotest.utils import AttributeReplacer
+logger = logging.getLogger(__name__)
+
def tool_required(*tools):
def wrap(f):
@@ -238,10 +240,10 @@ def domain_host(ctx, build, vary):
# wrap our build command
_ = _.prepend_to_build_command('sudo', '-E', 'nsenter', *ns_args, *make_sudo_command(*current_user_group()))
else:
- logging.warn("Not using sudo for domain_host; it is recommended. Your build may fail.")
- logging.warn("Be sure to `echo 1 > /proc/sys/kernel/unprivileged_userns_clone` if on a Debian system.")
+ logger.warn("Not using sudo for domain_host; it is recommended. Your build may fail.")
+ logger.warn("Be sure to `echo 1 > /proc/sys/kernel/unprivileged_userns_clone` if on a Debian system.")
if "user_group" in ctx.spec and ctx.spec.user_group.available:
- logging.error("Incompatible variations: domain_host.use_sudo False, user_group.available non-empty.")
+ logger.error("Incompatible variations: domain_host.use_sudo False, user_group.available non-empty.")
raise ValueError("Incompatible variations; check the log for details.")
_ = _.prepend_to_build_command(*"unshare -r --uts".split(),
"sh", "-ec", r"""
@@ -405,7 +407,7 @@ def user_group(ctx, build, vary):
return build
if not ctx.spec.user_group.available:
- logging.warn("IGNORING user_group variation; supply more usergroups "
+ logger.warn("IGNORING user_group variation; supply more usergroups "
"with --variations=user_group.available+=USER1:GROUP1;USER2:GROUP2 or "
"alternatively, suppress this warning with --variations=-user_group")
return build
@@ -565,8 +567,8 @@ class Variations(collections.namedtuple('_Variations', 'spec verbosity')):
def print_sudoers(spec):
- logging.warn("This feature is EXPERIMENTAL, use at your own risk.")
- logging.warn("The output may be out-of-date, please file bugs if it doesn't work...")
+ logger.warn("This feature is EXPERIMENTAL, use at your own risk.")
+ logger.warn("The output may be out-of-date, please file bugs if it doesn't work...")
user, group = current_user_group()
a = "[a-zA-Z0-9]"
diff --git a/tests/mock_build.py b/tests/mock_build.py
index 35977d6..efe24c3 100755
--- a/tests/mock_build.py
+++ b/tests/mock_build.py
@@ -2,6 +2,7 @@
# For details: reprotest/debian/copyright
import argparse
+import binascii
import locale
import os
import pathlib
@@ -18,18 +19,18 @@ if __name__ == '__main__':
arg_parser.add_argument('captures', nargs='*',
help='Reproducibility properties.')
captures = set(arg_parser.parse_args().captures)
- output = [b'']
+ output = ['']
# This test can theoretically fail by producing the same
# random bits in both runs, but it is extremely unlikely.
if 'irreproducible' in captures:
- output.append(os.urandom(1024))
+ output.append(binascii.b2a_hex(os.urandom(1024)))
# Like the above test, this test can theoretically fail by
# producing the same file order, but this is unlikely, if not
# as unlikely as in the above test.
if 'environment' in captures:
- output.append("\n".join(os.environ.keys()).encode("ascii"))
+ output.append("\n".join(os.environ.keys()))
if 'build_path' in captures:
- output.append(os.getcwd().encode("ascii"))
+ output.append(os.getcwd())
if 'fileordering' in captures:
# Ensure this temporary directory is created in the disorders
# mount point by passing the dir argument.
@@ -37,26 +38,26 @@ if __name__ == '__main__':
test_file_order = pathlib.Path(temp)
for i in range(20):
str((test_file_order/str(i)).touch())
- output.extend(p.name.encode('ascii') for p in test_file_order.iterdir())
+ output.extend(p.name for p in test_file_order.iterdir())
if 'home' in captures:
- output.append(os.path.expanduser('~').encode('ascii'))
+ output.append(os.path.expanduser('~'))
if 'kernel' in captures:
- output.append(subprocess.check_output(['uname', '-r']))
+ output.append(subprocess.check_output(['uname', '-r']).decode("utf-8"))
if 'locales' in captures:
- output.extend(l.encode('ascii') if l else b'(None)' for l in locale.getlocale())
- output.append(subprocess.check_output(['locale']))
+ output.extend(l or '(None)' for l in locale.getlocale())
+ output.append(subprocess.check_output(['locale']).decode("utf-8"))
if 'exec_path' in captures:
- output.extend(p.encode('ascii') for p in os.get_exec_path())
+ output.extend(p for p in os.get_exec_path())
if 'time' in captures:
- output.append(str(time.time()).encode('ascii'))
+ output.append(str(time.time()))
if 'timezone' in captures:
- output.append(str(time.timezone).encode('ascii'))
+ output.append(str(time.timezone))
if 'umask' in captures:
with tempfile.TemporaryDirectory(dir=str(pathlib.Path.cwd())) as temp:
test_permissions = pathlib.Path(temp)/'test_permissions'
test_permissions.touch()
- output.append(stat.filemode(test_permissions.stat().st_mode).encode('ascii'))
+ output.append(stat.filemode(test_permissions.stat().st_mode))
else:
os.umask(0o0022) # otherwise open() will capture the surrounding one in its file metadata
- with open('artifact', 'wb') as artifact:
- artifact.write(b''.join(output))
+ with open('artifact', 'w') as artifact:
+ artifact.write(''.join(output))
diff --git a/tests/test_reprotest.py b/tests/test_reprotest.py
index b631259..c2062c8 100644
--- a/tests/test_reprotest.py
+++ b/tests/test_reprotest.py
@@ -1,6 +1,8 @@
# Licensed under the GPL: https://www.gnu.org/licenses/gpl-3.0.en.html
# For details: reprotest/debian/copyright
+import contextlib
+import logging
import os
import subprocess
import sys
@@ -56,11 +58,36 @@ def test_simple_builds(virtual_server):
check_reproducibility('python3 mock_failure.py', virtual_server)
check_reproducibility('python3 mock_build.py irreproducible', virtual_server, False)
+ at contextlib.contextmanager
+def setup_logging(debug):
+ logger = logging.getLogger()
+ oldLevel = logger.getEffectiveLevel()
+ logger.setLevel(logging.DEBUG if debug else logging.WARNING)
+
+ ch = logging.StreamHandler()
+ ch.setLevel(logging.DEBUG)
+ logger.addHandler(ch)
+
+ formatter = logging.Formatter(
+ '%(asctime)s %(levelname).1s: %(name)s: %(message)s',
+ '%Y-%m-%d %H:%M:%S',
+ )
+ ch.setFormatter(formatter)
+ try:
+ yield logger
+ finally:
+ # restore old logging settings. this helps pytest not spew out errors
+ # like "ValueError: I/O operation on closed file", see
+ # https://github.com/pytest-dev/pytest/issues/14#issuecomment-272243656
+ logger.removeHandler(ch)
+ logger.setLevel(oldLevel)
+
# TODO: test all variations that we support
@pytest.mark.parametrize('captures', list(VARIATIONS.keys()))
def test_variations(virtual_server, captures):
expected = captures not in TEST_VARIATIONS
- check_reproducibility('python3 mock_build.py ' + captures, virtual_server, expected)
+ with setup_logging(True):
+ check_reproducibility('python3 mock_build.py ' + captures, virtual_server, expected)
@pytest.mark.need_builddeps
def test_self_build(virtual_server):
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/reprotest.git
More information about the Reproducible-commits
mailing list