mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 09:17:30 +03:00
93d6f0a3e9
Summary: I was looking in the `edenfs_events` table and saw that sandcastle was logging to this table. Rice was able to identify that the reason was because the integration tests were logging. So if we're on running integration tests, we should return a `NullTelemetryLogger`. The daemon currently does not log on sandcastle AFAIK. Reviewed By: simpkins Differential Revision: D20203556 fbshipit-source-id: e09175347631478cb366d4fa2c6092d976504dd8
443 lines
15 KiB
Python
443 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) Facebook, Inc. and its affiliates.
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2.
|
|
|
|
import configparser
|
|
import errno
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import time
|
|
import typing
|
|
import unittest
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Dict,
|
|
Iterable,
|
|
List,
|
|
Optional,
|
|
Sequence,
|
|
Set,
|
|
Tuple,
|
|
Type,
|
|
Union,
|
|
)
|
|
|
|
from eden.test_support.environment_variable import EnvironmentVariableMixin
|
|
from eden.test_support.temporary_directory import TemporaryDirectoryMixin
|
|
from eden.thrift import EdenClient
|
|
|
|
from . import edenclient, gitrepo, hgrepo, repobase
|
|
from .find_executables import FindExe
|
|
|
|
|
|
if not FindExe.is_buck_build() or os.environ.get("EDENFS_SUFFIX", "") != "":
|
|
_build_flavor = "open_source"
|
|
else:
|
|
_build_flavor = "facebook"
|
|
|
|
|
|
@unittest.skipIf(not edenclient.can_run_eden(), "unable to run edenfs")
|
|
class EdenTestCase(
|
|
unittest.TestCase, EnvironmentVariableMixin, TemporaryDirectoryMixin
|
|
):
|
|
"""
|
|
Base class for eden integration test cases.
|
|
|
|
This starts an eden daemon during setUp(), and cleans it up during
|
|
tearDown().
|
|
"""
|
|
|
|
mount: str
|
|
eden: edenclient.EdenFS
|
|
start: float
|
|
last_event: float
|
|
|
|
# Override enable_fault_injection to True in subclasses to enable Eden's fault
|
|
# injection framework when starting edenfs
|
|
enable_fault_injection: bool = False
|
|
|
|
enable_logview: bool = True
|
|
|
|
def report_time(self, event: str) -> None:
|
|
"""
|
|
report_time() is a helper function for logging how long different
|
|
parts of the test took.
|
|
|
|
Each time it is called it logs a message containing the time since the
|
|
test started and the time since the last time report_time() was called.
|
|
"""
|
|
now = time.time()
|
|
since_last = now - self.last_event
|
|
since_start = now - self.start
|
|
logging.info("=== %s at %.03fs (+%0.3fs)", event, since_start, since_last)
|
|
self.last_event = now
|
|
|
|
def setUp(self) -> None:
|
|
self.start = time.time()
|
|
self.last_event = self.start
|
|
self.system_hgrc: Optional[str] = None
|
|
|
|
# Set an environment variable to prevent telemetry logging
|
|
# during integration tests
|
|
os.environ["INTEGRATION_TEST"] = "1"
|
|
|
|
# Add a cleanup event just to log once the other cleanup
|
|
# actions have completed.
|
|
self.addCleanup(self.report_time, "clean up done")
|
|
|
|
self.setup_eden_test()
|
|
self.report_time("test setup done")
|
|
|
|
self.addCleanup(self.report_time, "clean up started")
|
|
|
|
def setup_eden_test(self) -> None:
|
|
self.tmp_dir = self.make_temporary_directory()
|
|
|
|
# Place scratch configuration somewhere deterministic for the tests
|
|
scratch_config_file = os.path.join(self.tmp_dir, "scratch.toml")
|
|
with open(scratch_config_file, "w") as f:
|
|
f.write('template = "%s/scratch"\n' % self.tmp_dir)
|
|
f.write("overrides = {}\n")
|
|
self.set_environment_variable("SCRATCH_CONFIG_PATH", scratch_config_file)
|
|
|
|
# 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)
|
|
self.report_time("temporary directory creation done")
|
|
|
|
logging_settings = self.edenfs_logging_settings()
|
|
extra_args = self.edenfs_extra_args()
|
|
if self.enable_fault_injection:
|
|
extra_args.append("--enable_fault_injection")
|
|
|
|
if _build_flavor == "facebook" and not self.enable_logview:
|
|
# add option to disable logview
|
|
# we set `EDENFS_SUFFIX` when running our tests with OSS build
|
|
extra_args.append("--eden_logview=false")
|
|
|
|
storage_engine = self.select_storage_engine()
|
|
self.eden = edenclient.EdenFS(
|
|
base_dir=pathlib.Path(self.tmp_dir),
|
|
logging_settings=logging_settings,
|
|
extra_args=extra_args,
|
|
storage_engine=storage_engine,
|
|
)
|
|
# Just to better reflect normal user environments, update $HOME
|
|
# to point to our test home directory for the duration of the test.
|
|
self.set_environment_variable("HOME", str(self.eden.home_dir))
|
|
self.eden.start()
|
|
self.addCleanup(self.eden.cleanup)
|
|
self.report_time("eden daemon started")
|
|
|
|
self.mount = os.path.join(self.mounts_dir, "main")
|
|
|
|
@property
|
|
def eden_dir(self) -> str:
|
|
return str(self.eden.eden_dir)
|
|
|
|
@property
|
|
def home_dir(self) -> str:
|
|
return str(self.eden.home_dir)
|
|
|
|
@property
|
|
def etc_eden_dir(self) -> str:
|
|
return str(self.eden.etc_eden_dir)
|
|
|
|
@property
|
|
def mount_path(self) -> pathlib.Path:
|
|
return pathlib.Path(self.mount)
|
|
|
|
@property
|
|
def mount_path_bytes(self) -> bytes:
|
|
return bytes(self.mount_path)
|
|
|
|
def get_thrift_client(self) -> EdenClient:
|
|
"""
|
|
Get a thrift client to the edenfs daemon.
|
|
"""
|
|
return self.eden.get_thrift_client()
|
|
|
|
def get_counters(self) -> typing.Mapping[str, float]:
|
|
with self.get_thrift_client() as thrift_client:
|
|
thrift_client.flushStatsNow()
|
|
return thrift_client.getCounters()
|
|
|
|
def edenfs_logging_settings(self) -> Optional[Dict[str, str]]:
|
|
"""
|
|
Get the log settings to pass to edenfs via the --logging argument.
|
|
|
|
This should return a dictionary of {category_name: level}
|
|
- module_name is the C++ log category name. e.g., "eden.fs.store"
|
|
or "eden.fs.inodes.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 edenfs_extra_args(self) -> List[str]:
|
|
"""
|
|
Get additional arguments to pass to edenfs
|
|
"""
|
|
return []
|
|
|
|
def create_hg_repo(
|
|
self, name: str, hgrc: Optional[configparser.ConfigParser] = None
|
|
) -> hgrepo.HgRepository:
|
|
repo_path = os.path.join(self.repos_dir, name)
|
|
os.mkdir(repo_path)
|
|
|
|
if self.system_hgrc is None:
|
|
system_hgrc_path = os.path.join(self.repos_dir, "hgrc")
|
|
with open(system_hgrc_path, "w") as f:
|
|
f.write(hgrepo.HgRepository.get_system_hgrc_contents())
|
|
self.system_hgrc = system_hgrc_path
|
|
|
|
repo = hgrepo.HgRepository(repo_path, system_hgrc=self.system_hgrc)
|
|
repo.init(hgrc=hgrc)
|
|
|
|
return repo
|
|
|
|
def create_git_repo(self, name: str) -> gitrepo.GitRepository:
|
|
repo_path = os.path.join(self.repos_dir, name)
|
|
os.mkdir(repo_path)
|
|
repo = gitrepo.GitRepository(repo_path)
|
|
repo.init()
|
|
|
|
return repo
|
|
|
|
def get_path(self, path: str) -> str:
|
|
"""Resolves the path against self.mount."""
|
|
return os.path.join(self.mount, path)
|
|
|
|
def touch(self, path: str) -> None:
|
|
"""Touch the file at the specified path relative to the clone."""
|
|
fullpath = self.get_path(path)
|
|
with open(fullpath, "a"):
|
|
os.utime(fullpath)
|
|
|
|
def write_file(self, path: str, contents: str, mode: int = 0o644) -> None:
|
|
"""Create or overwrite a file with the given contents."""
|
|
fullpath = self.get_path(path)
|
|
self.make_parent_dir(fullpath)
|
|
with open(fullpath, "w") as f:
|
|
f.write(contents)
|
|
os.chmod(fullpath, mode)
|
|
|
|
def read_file(self, path: str) -> str:
|
|
"""Read the file with the specified path inside the eden repository,
|
|
and return its contents.
|
|
"""
|
|
fullpath = self.get_path(path)
|
|
with open(fullpath, "r") as f:
|
|
return f.read()
|
|
|
|
def mkdir(self, path: str) -> None:
|
|
"""Call mkdir for the specified path relative to the clone."""
|
|
full_path = self.get_path(path)
|
|
try:
|
|
os.makedirs(full_path)
|
|
except OSError as ex:
|
|
if ex.errno != errno.EEXIST:
|
|
raise
|
|
|
|
def make_parent_dir(self, path: str) -> None:
|
|
dirname = os.path.dirname(path)
|
|
if dirname:
|
|
self.mkdir(dirname)
|
|
|
|
def rm(self, path: str) -> None:
|
|
"""Unlink the file at the specified path relative to the clone."""
|
|
os.unlink(self.get_path(path))
|
|
|
|
def select_storage_engine(self) -> str:
|
|
"""
|
|
Prefer to use memory in the integration tests, but allow
|
|
the tests that restart to override this and pick something else.
|
|
"""
|
|
return "memory"
|
|
|
|
|
|
# pyre-ignore[13]: T62487924
|
|
class EdenRepoTest(EdenTestCase):
|
|
"""
|
|
Base class for EdenHgTest and EdenGitTest.
|
|
|
|
This sets up a repository and mounts it before starting each test function.
|
|
|
|
You normally should put the @eden_repo_test decorator on your test
|
|
when subclassing from EdenRepoTest. @eden_repo_test will automatically run
|
|
your tests once per supported repository type.
|
|
"""
|
|
|
|
repo: repobase.Repository
|
|
repo_name: str
|
|
|
|
enable_logview: bool = False
|
|
|
|
def setup_eden_test(self) -> None:
|
|
super().setup_eden_test()
|
|
|
|
self.repo_name = "main"
|
|
self.repo = self.create_repo(self.repo_name)
|
|
self.populate_repo()
|
|
self.report_time("repository setup done")
|
|
|
|
self.eden.add_repository(self.repo_name, self.repo.path)
|
|
self.eden.clone(self.repo_name, self.mount)
|
|
self.report_time("eden clone done")
|
|
|
|
def populate_repo(self) -> None:
|
|
raise NotImplementedError(
|
|
"individual test classes must implement " "populate_repo()"
|
|
)
|
|
|
|
def create_repo(self, name: str) -> repobase.Repository:
|
|
"""
|
|
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.
|
|
"""
|
|
raise NotImplementedError(
|
|
"test subclasses must implement "
|
|
"create_repo(). This is normally "
|
|
"implemented automatically by "
|
|
"@eden_repo_test"
|
|
)
|
|
|
|
def get_thrift_client(self) -> EdenClient:
|
|
# get_thrift_client() is also defined in our parent class, but for some reason
|
|
# mypy gets confused when get_thrift_client() is used in our subclasses unless
|
|
# we define it here. (mypy knows that the method exists but cannot figure out
|
|
# its return type for some reason.)
|
|
return super().get_thrift_client()
|
|
|
|
def assert_checkout_root_entries(
|
|
self,
|
|
expected_entries: Set[str],
|
|
path: Union[str, pathlib.Path, None] = None,
|
|
scm_type: Optional[str] = None,
|
|
) -> None:
|
|
"""Verify that the contents of a checkout root directory are what we expect.
|
|
|
|
This automatically expects to find a ".hg" directory in the root of hg
|
|
checkouts.
|
|
"""
|
|
checkout_root = pathlib.Path(path if path is not None else self.mount)
|
|
real_scm_type = scm_type if scm_type is not None else self.repo.get_type()
|
|
if real_scm_type == "hg":
|
|
expected_entries = expected_entries | {".hg"}
|
|
actual_entries = set(os.listdir(checkout_root))
|
|
self.assertEqual(
|
|
expected_entries, actual_entries, f"incorrect entries in {checkout_root}"
|
|
)
|
|
|
|
|
|
def _replicate_test(
|
|
caller_scope: Dict[str, Any],
|
|
replicate: Callable[..., Iterable[Tuple[str, Type[unittest.TestCase]]]],
|
|
test_class: Type[unittest.TestCase],
|
|
args: Sequence[Any],
|
|
kwargs: Dict[str, Any],
|
|
) -> None:
|
|
for suffix, new_class in replicate(test_class, *args, **kwargs):
|
|
# Set the name and module information on our new subclass
|
|
name = test_class.__name__ + suffix
|
|
new_class.__name__ = name
|
|
new_class.__qualname__ = name
|
|
new_class.__module__ = test_class.__module__
|
|
|
|
# Add the class to our caller's scope
|
|
caller_scope[name] = new_class
|
|
|
|
|
|
def test_replicator(
|
|
replicate: Callable[..., Iterable[Tuple[str, Type[unittest.TestCase]]]]
|
|
) -> Callable[..., Any]:
|
|
"""
|
|
A helper function for implementing decorators that replicate TestCase
|
|
classes so that the same test function can be run multiple times with
|
|
several different settings.
|
|
|
|
See the @eden_repo_test decorator for an example of how this is used.
|
|
"""
|
|
|
|
def decorator(
|
|
*args: Any, **kwargs: Any
|
|
) -> Optional[Callable[[Type[unittest.TestCase]], None]]:
|
|
# 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.
|
|
current_frame = inspect.currentframe()
|
|
if current_frame is None:
|
|
raise Exception(
|
|
"we require a python interpreter with " "stack frame support"
|
|
)
|
|
caller_scope = current_frame.f_back.f_locals
|
|
|
|
if len(args) == 1 and not kwargs and isinstance(args[0], type):
|
|
# The decorator was invoked directly with the test class,
|
|
# with no arguments or keyword arguments
|
|
_replicate_test(caller_scope, replicate, args[0], args=(), kwargs={})
|
|
return None
|
|
else:
|
|
|
|
def inner_decorator(test_class: Type[unittest.TestCase]) -> None:
|
|
_replicate_test(caller_scope, replicate, test_class, args, kwargs)
|
|
|
|
return inner_decorator
|
|
|
|
return decorator
|
|
|
|
|
|
def _replicate_eden_repo_test(
|
|
test_class: Type[EdenRepoTest]
|
|
) -> Iterable[Tuple[str, Type[EdenRepoTest]]]:
|
|
class HgRepoTest(HgRepoTestMixin, test_class): # type: ignore
|
|
pass
|
|
|
|
class GitRepoTest(GitRepoTestMixin, test_class): # type: ignore
|
|
pass
|
|
|
|
return [
|
|
("Hg", typing.cast(Type[EdenRepoTest], HgRepoTest)),
|
|
("Git", typing.cast(Type[EdenRepoTest], GitRepoTest)),
|
|
]
|
|
|
|
|
|
# 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.
|
|
eden_repo_test = test_replicator(_replicate_eden_repo_test)
|
|
|
|
|
|
class HgRepoTestMixin:
|
|
def create_repo(self, name: str) -> repobase.Repository:
|
|
# HgRepoTestMixin is always used in classes that derive from EdenRepoTest,
|
|
# but it is difficult to make the type checkers aware of that. We can't
|
|
# add an abstract create_hg_repo() method to this class since the MRO would find
|
|
# it before the real create_hg_repo() name. We can't change the MRO without
|
|
# breaking resolution of create_repo().
|
|
return self.create_hg_repo(name) # type: ignore
|
|
|
|
|
|
class GitRepoTestMixin:
|
|
def create_repo(self, name: str) -> repobase.Repository:
|
|
return self.create_git_repo(name) # type: ignore
|