sapling/eden/integration/lib/testcase.py
Adam Simpkins 78b42e4208 make vlog settings controllable on a per-test basis
Summary:
Update the integration test code to allow TestCase classes to control the
--vmodule settings used when starting the edenfs daemon.

D3851805 initially set the vlog level for RequestData to 5 to help debug
issues.  However, this log level doesn't really make sense for most of the
mercurial integration tests: most mercurial commands checks for the existence
of lots of files, causing the logs to be filled with useless ENOENT messages
when the RequestData log level is this high.

Reviewed By: wez

Differential Revision: D4814402

fbshipit-source-id: 1127c0a25b656ea1a710ca54a59a9407d66a5659
2017-03-31 18:21:44 -07:00

299 lines
10 KiB
Python

#!/usr/bin/env python3
#
# Copyright (c) 2016-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.
import atexit
import inspect
import os
import shutil
import tempfile
import unittest
from hypothesis import settings, HealthCheck
import hypothesis.strategies as st
from hypothesis.internal.detection import is_hypothesis_test
from hypothesis.configuration import (
set_hypothesis_home_dir,
hypothesis_home_dir)
from . import edenclient
from . import hgrepo
from . import gitrepo
def is_sandcastle():
return 'SANDCASTLE' in os.environ
default_settings = settings(
# Turn off the health checks because setUp/tearDown are too slow
suppress_health_check=[HealthCheck.too_slow],
# Turn off the example database; we don't have a way to persist this
# or share this across runs, so we don't derive any benefit from it at
# this time.
database=None,
)
# Configure Hypothesis to run faster when iterating locally
settings.register_profile("dev", settings(default_settings, max_examples=5))
# ... and use the defaults (which have more combinations) when running
# on CI, which we want to be more deterministic.
settings.register_profile("ci", settings(default_settings, derandomize=True))
# Use the dev profile by default, but use the ci profile on sandcastle.
settings.load_profile('ci' if is_sandcastle()
else os.getenv('HYPOTHESIS_PROFILE', 'dev'))
# Some helpers for Hypothesis decorators
FILENAME_STRATEGY = st.text(
st.characters(min_codepoint=1,
max_codepoint=1000,
blacklist_characters="/:\\",
),
min_size=1)
# We need to set a global (but non-conflicting) path to store some state
# during hypothesis example runs. We want to avoid putting this state in
# the repo.
set_hypothesis_home_dir(tempfile.mkdtemp(prefix='eden_hypothesis.'))
atexit.register(shutil.rmtree, hypothesis_home_dir())
if is_sandcastle() and not edenclient.can_run_eden():
# This is avoiding a reporting noise issue in our CI that files
# tasks about skipped tests. Let's just skip defining most of them
# to avoid the noise if we know that they won't work anyway.
TestParent = object
else:
TestParent = unittest.TestCase
@unittest.skipIf(not edenclient.can_run_eden(), "unable to run edenfs")
class EdenTestCase(TestParent):
'''
Base class for eden integration test cases.
This starts an eden daemon during setUp(), and cleans it up during
tearDown().
'''
def run(self, report=None):
''' Some slightly awful magic here to arrange for setUp and
tearDown to be called at the appropriate times when hypothesis
is enabled for a test case.
This can be removed once a future version of hypothesis
ships with support for this baked in. '''
if is_hypothesis_test(getattr(self, self._testMethodName)):
try:
old_setUp = self.setUp
old_tearDown = self.tearDown
self.setUp = lambda: None
self.tearDown = lambda: None
self.setup_example = old_setUp
self.teardown_example = lambda _: old_tearDown()
return super(EdenTestCase, self).run(report)
finally:
self.setUp = old_setUp
self.tearDown = old_tearDown
del self.setup_example
del self.teardown_example
else:
return super(EdenTestCase, self).run(report)
def setUp(self):
self.tmp_dir = None
self.eden = None
self.old_home = None
# Call setup_eden_test() to do most of the setup work, and call
# tearDown() on any error. tearDown() won't be called by default if
# setUp() throws.
try:
self.setup_eden_test()
except Exception as ex:
self.tearDown()
raise
def setup_eden_test(self):
self.tmp_dir = tempfile.mkdtemp(prefix='eden_test.')
# The home directory, to make sure eden looks at this rather than the
# real home directory of the user running the tests.
self.home_dir = os.path.join(self.tmp_dir, 'homedir')
os.mkdir(self.home_dir)
self.old_home = os.getenv('HOME')
os.environ['HOME'] = self.home_dir
# TODO: Make this configurable via ~/.edenrc.
# The eden config directory.
self.eden_dir = os.path.join(self.home_dir, 'local/.eden')
os.makedirs(self.eden_dir)
self.etc_eden_dir = os.path.join(self.tmp_dir, 'etc-eden')
os.mkdir(self.etc_eden_dir)
# The directory holding the system configuration files
self.system_config_dir = os.path.join(self.etc_eden_dir, 'config.d')
os.mkdir(self.system_config_dir)
# Parent directory for any git/hg repositories created during the test
self.repos_dir = os.path.join(self.tmp_dir, 'repos')
os.mkdir(self.repos_dir)
# Parent directory for eden mount points
self.mounts_dir = os.path.join(self.tmp_dir, 'mounts')
os.mkdir(self.mounts_dir)
vmodule_settings = self.edenfs_vmodule_settings()
self.eden = edenclient.EdenFS(self.eden_dir,
etc_eden_dir=self.etc_eden_dir,
home_dir=self.home_dir,
vmodule_settings=vmodule_settings)
self.eden.start()
def tearDown(self):
error = None
try:
if self.eden is not None:
self.eden.cleanup()
except Exception as ex:
error = ex
if self.old_home is not None:
os.environ['HOME'] = self.old_home
self.old_home = None
if self.tmp_dir is not None:
shutil.rmtree(self.tmp_dir, ignore_errors=True)
self.tmp_dir = None
# Re-raise any error that occurred, after we finish
# trying to clean up our directories.
if error is not None:
raise error
def get_thrift_client(self):
'''
Get a thrift client to the edenfs daemon.
'''
return self.eden.get_thrift_client()
def edenfs_vmodule_settings(self):
'''
Get the log settings to pass to edenfs via the --vmodule argument.
This should return a dictionary of {module_name: level}
- module_name is the basename of the C++ file in question, without the
file extension (e.g., "TreeInode")
- level is the integer vlog level to use for that module.
You can return None if you do not want any extra verbose logging
enabled.
'''
return None
def create_repo(self, name, repo_class):
'''
Create a new repository.
Arguments:
- name
The repository name. This determines the repository location inside
the self.repos_dir directory. The full repository path can be
accessed as repo.path on the returned repo object.
- repo_class
The repository class object, such as hgrepo.HgRepository or
gitrepo.GitRepository.
'''
repo_path = os.path.join(self.repos_dir, name)
os.mkdir(repo_path)
repo = repo_class(repo_path)
repo.init()
return repo
class EdenRepoTestBase(EdenTestCase):
'''
Base class for EdenHgTest and EdenGitTest.
This sets up a repository and mounts it before starting each test function.
'''
def setup_eden_test(self):
super().setup_eden_test()
self.repo_name = 'main'
self.mount = os.path.join(self.mounts_dir, self.repo_name)
self.repo = self.create_repo(self.repo_name, self.get_repo_class())
self.populate_repo()
self.eden.add_repository(self.repo_name, self.repo.path)
self.eden.clone(self.repo_name, self.mount)
def populate_repo(self):
raise NotImplementedError('individual test classes must implement '
'populate_repo()')
class EdenHgTest(EdenRepoTestBase):
'''
Subclass of EdenTestCase which uses a single mercurial repository and
eden mount.
The repository is available as self.repo, and the client mount path is
available as self.mount
'''
def get_repo_class(self):
return hgrepo.HgRepository
class EdenGitTest(EdenRepoTestBase):
'''
Subclass of EdenTestCase which uses a single mercurial repository and
eden mount.
The repository is available as self.repo, and the client mount path is
available as self.mount
'''
def get_repo_class(self):
return gitrepo.GitRepository
def eden_repo_test(test_class):
'''
A decorator function used to create EdenHgTest and EdenGitTest
subclasses from a given input test class.
Given an input test class named "MyTest", this will create two separate
classes named "MyTestHg" and "MyTestGit", which run the tests with
mercurial and git repositories, respectively.
'''
repo_types = [
(EdenHgTest, 'Hg'),
(EdenGitTest, 'Git'),
]
# We do some rather hacky things here to define new test class types
# in our caller's scope. This is needed so that the unittest TestLoader
# will find the subclasses we define.
caller_scope = inspect.currentframe().f_back.f_locals
for (parent_class, suffix) in repo_types:
subclass_name = test_class.__name__ + suffix
# Define a new class that derives from the input class
# as well as the repo-specific parent class type
class RepoSpecificTest(test_class, parent_class):
pass
# Set the name and module information on our new subclass
RepoSpecificTest.__name__ = subclass_name
RepoSpecificTest.__qualname__ = subclass_name
RepoSpecificTest.__module__ = test_class.__module__
caller_scope[subclass_name] = RepoSpecificTest
return None