2016-06-20 23:38:29 +03:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
#
|
2019-06-20 02:58:25 +03:00
|
|
|
# Copyright (c) Facebook, Inc. and its affiliates.
|
2016-05-26 07:43:44 +03:00
|
|
|
#
|
2019-06-20 02:58:25 +03:00
|
|
|
# This software may be used and distributed according to the terms of the
|
|
|
|
# GNU General Public License version 2.
|
2016-05-26 07:43:44 +03:00
|
|
|
|
2018-03-21 02:34:01 +03:00
|
|
|
import binascii
|
2016-06-20 23:38:29 +03:00
|
|
|
import hashlib
|
2017-08-23 05:43:29 +03:00
|
|
|
import os
|
2019-06-12 04:22:49 +03:00
|
|
|
from pathlib import Path
|
2016-05-26 07:43:44 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
from facebook.eden.ttypes import ScmFileStatus, SHA1Result, TimeSpec
|
|
|
|
|
2016-06-20 23:38:29 +03:00
|
|
|
from .lib import testcase
|
2016-05-26 07:43:44 +03:00
|
|
|
|
|
|
|
|
2016-07-08 21:25:45 +03:00
|
|
|
@testcase.eden_repo_test
|
2018-04-05 03:31:25 +03:00
|
|
|
class ThriftTest(testcase.EdenRepoTest):
|
2018-11-14 23:13:46 +03:00
|
|
|
commit1: str
|
|
|
|
commit2: str
|
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def populate_repo(self) -> None:
|
2018-05-10 07:33:49 +03:00
|
|
|
self.repo.write_file("hello", "hola\n")
|
|
|
|
self.repo.write_file("README", "docs\n")
|
|
|
|
self.repo.write_file("adir/file", "foo!\n")
|
|
|
|
self.repo.write_file("bdir/file", "bar!\n")
|
|
|
|
self.repo.symlink("slink", "hello")
|
|
|
|
self.commit1 = self.repo.commit("Initial commit.")
|
|
|
|
|
|
|
|
self.repo.write_file("bdir/file", "bar?\n")
|
|
|
|
self.repo.write_file("cdir/subdir/new.txt", "and improved")
|
|
|
|
self.repo.remove_file("README")
|
|
|
|
self.commit2 = self.repo.commit("Commit 2.")
|
2016-07-08 21:25:39 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def setUp(self) -> None:
|
2016-07-23 03:31:20 +03:00
|
|
|
super().setUp()
|
|
|
|
self.client = self.get_thrift_client()
|
|
|
|
self.client.open()
|
2018-03-29 08:10:44 +03:00
|
|
|
self.addCleanup(self.client.close)
|
2016-05-26 07:43:44 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def get_loaded_inodes_count(self, path: str) -> int:
|
2018-08-11 11:34:44 +03:00
|
|
|
result = self.client.debugInodeStatus(self.mount_path_bytes, os.fsencode(path))
|
2017-08-23 05:43:29 +03:00
|
|
|
inode_count = 0
|
|
|
|
for item in result:
|
2018-04-05 03:31:28 +03:00
|
|
|
assert item.entries is not None
|
2017-08-23 05:43:29 +03:00
|
|
|
for inode in item.entries:
|
|
|
|
if inode.loaded:
|
|
|
|
inode_count += 1
|
|
|
|
return inode_count
|
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_list_mounts(self) -> None:
|
2016-07-23 03:31:20 +03:00
|
|
|
mounts = self.client.listMounts()
|
2016-05-26 07:43:44 +03:00
|
|
|
self.assertEqual(1, len(mounts))
|
|
|
|
|
|
|
|
mount = mounts[0]
|
2018-08-11 11:34:44 +03:00
|
|
|
self.assertEqual(self.mount_path_bytes, mount.mountPoint)
|
2018-04-05 03:31:28 +03:00
|
|
|
assert mount.edenClientPath is not None
|
2018-03-20 03:01:17 +03:00
|
|
|
# The client path should always be inside the main eden directory
|
2019-06-12 04:22:49 +03:00
|
|
|
# Path.relative_to() will throw a ValueError if self.eden.eden_dir is not a
|
|
|
|
# directory prefix of mount.edenClientPath
|
|
|
|
Path(os.fsdecode(mount.edenClientPath)).relative_to(self.eden.eden_dir)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_get_sha1(self) -> None:
|
2018-05-10 07:33:49 +03:00
|
|
|
expected_sha1_for_hello = hashlib.sha1(b"hola\n").digest()
|
2018-08-04 00:54:10 +03:00
|
|
|
result_for_hello = SHA1Result(expected_sha1_for_hello)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
expected_sha1_for_adir_file = hashlib.sha1(b"foo!\n").digest()
|
2018-08-04 00:54:10 +03:00
|
|
|
result_for_adir_file = SHA1Result(expected_sha1_for_adir_file)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(
|
2018-05-10 07:33:49 +03:00
|
|
|
[result_for_hello, result_for_adir_file],
|
2018-08-11 11:34:44 +03:00
|
|
|
self.client.getSHA1(self.mount_path_bytes, [b"hello", b"adir/file"]),
|
2016-08-18 17:21:36 +03:00
|
|
|
)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2018-12-08 03:32:35 +03:00
|
|
|
def test_get_sha1_throws_for_path_with_dot_components(self) -> None:
|
|
|
|
results = self.client.getSHA1(self.mount_path_bytes, [b"./hello"])
|
|
|
|
self.assertEqual(1, len(results))
|
|
|
|
self.assert_error(
|
|
|
|
results[0], "std::domain_error: PathComponent must not be . or .."
|
|
|
|
)
|
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_get_sha1_throws_for_empty_string(self) -> None:
|
2018-08-11 11:34:44 +03:00
|
|
|
results = self.client.getSHA1(self.mount_path_bytes, [b""])
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(1, len(results))
|
2018-05-10 07:33:49 +03:00
|
|
|
self.assert_error(results[0], "path cannot be the empty string")
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_get_sha1_throws_for_directory(self) -> None:
|
2018-08-11 11:34:44 +03:00
|
|
|
results = self.client.getSHA1(self.mount_path_bytes, [b"adir"])
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(1, len(results))
|
2018-05-10 07:33:49 +03:00
|
|
|
self.assert_error(results[0], "adir: Is a directory")
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_get_sha1_throws_for_non_existent_file(self) -> None:
|
2018-08-11 11:34:44 +03:00
|
|
|
results = self.client.getSHA1(self.mount_path_bytes, [b"i_do_not_exist"])
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(1, len(results))
|
2018-05-10 07:33:49 +03:00
|
|
|
self.assert_error(results[0], "i_do_not_exist: No such file or directory")
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_get_sha1_throws_for_symlink(self) -> None:
|
2018-05-10 07:33:49 +03:00
|
|
|
"""Fails because caller should resolve the symlink themselves."""
|
2018-08-11 11:34:44 +03:00
|
|
|
results = self.client.getSHA1(self.mount_path_bytes, [b"slink"])
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(1, len(results))
|
2018-05-10 07:33:49 +03:00
|
|
|
self.assert_error(results[0], "slink: file is a symlink: Invalid argument")
|
2016-08-18 17:21:36 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def assert_error(self, sha1result: SHA1Result, error_message: str) -> None:
|
2018-05-10 07:33:49 +03:00
|
|
|
self.assertIsNotNone(sha1result, msg="Must pass a SHA1Result")
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(
|
2018-05-10 07:33:49 +03:00
|
|
|
SHA1Result.ERROR, sha1result.getType(), msg="SHA1Result must be an error"
|
2016-08-18 17:21:36 +03:00
|
|
|
)
|
|
|
|
error = sha1result.get_error()
|
|
|
|
self.assertIsNotNone(error)
|
|
|
|
self.assertEqual(error_message, error.message)
|
2017-01-26 23:45:50 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_unload_free_inodes(self) -> None:
|
2017-06-23 08:33:01 +03:00
|
|
|
for i in range(100):
|
2018-05-10 07:33:49 +03:00
|
|
|
self.write_file("testfile%d.txt" % i, "unload test case")
|
2017-06-23 08:33:01 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
inode_count_before_unload = self.get_loaded_inodes_count("")
|
2017-08-23 05:43:29 +03:00
|
|
|
self.assertGreater(
|
2018-05-10 07:33:49 +03:00
|
|
|
inode_count_before_unload, 100, "Number of loaded inodes should increase"
|
2017-08-23 05:43:29 +03:00
|
|
|
)
|
2017-06-23 08:33:01 +03:00
|
|
|
|
2017-08-23 05:43:29 +03:00
|
|
|
age = TimeSpec()
|
|
|
|
age.seconds = 0
|
|
|
|
age.nanoSeconds = 0
|
2018-08-11 11:34:44 +03:00
|
|
|
unload_count = self.client.unloadInodeForPath(self.mount_path_bytes, b"", age)
|
2017-08-23 05:43:29 +03:00
|
|
|
|
2017-08-23 05:43:31 +03:00
|
|
|
self.assertGreaterEqual(
|
2018-05-10 07:33:49 +03:00
|
|
|
unload_count, 100, "Number of loaded inodes should reduce after unload"
|
2018-09-14 00:36:42 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
def test_unload_thrift_api_accepts_single_dot_as_root(self) -> None:
|
|
|
|
self.write_file("testfile.txt", "unload test case")
|
|
|
|
|
|
|
|
age = TimeSpec()
|
|
|
|
age.seconds = 0
|
|
|
|
age.nanoSeconds = 0
|
|
|
|
unload_count = self.client.unloadInodeForPath(self.mount_path_bytes, b".", age)
|
|
|
|
|
|
|
|
self.assertGreater(
|
|
|
|
unload_count, 0, "Number of loaded inodes should reduce after unload"
|
2017-08-23 05:43:29 +03:00
|
|
|
)
|
|
|
|
|
2019-05-29 01:34:55 +03:00
|
|
|
def get_counter(self, name: str) -> float:
|
|
|
|
return self.get_counters()[name]
|
2017-08-22 01:52:55 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_invalidate_inode_cache(self) -> None:
|
2018-05-10 07:33:49 +03:00
|
|
|
filename = "bdir/file"
|
|
|
|
full_dirname = os.path.join(self.mount, "bdir/")
|
2017-08-22 01:52:55 +03:00
|
|
|
|
2018-03-21 01:05:04 +03:00
|
|
|
# Exercise eden a bit to make sure counters are ready
|
2017-08-22 01:52:55 +03:00
|
|
|
for _ in range(20):
|
2018-05-10 07:33:49 +03:00
|
|
|
fn = os.path.join(self.mount, "_tmp_")
|
|
|
|
with open(fn, "w") as f:
|
|
|
|
f.write("foo!\n")
|
2017-08-22 01:52:55 +03:00
|
|
|
os.unlink(fn)
|
|
|
|
|
|
|
|
reads = self.get_counter("fuse.read_us.count")
|
|
|
|
self.read_file(filename)
|
|
|
|
reads_1read = self.get_counter("fuse.read_us.count")
|
|
|
|
self.assertEqual(reads_1read, reads + 1)
|
|
|
|
self.read_file(filename)
|
|
|
|
reads_2read = self.get_counter("fuse.read_us.count")
|
|
|
|
self.assertEqual(reads_1read, reads_2read)
|
2018-08-11 11:34:44 +03:00
|
|
|
self.client.invalidateKernelInodeCache(self.mount_path_bytes, b"bdir/file")
|
2017-08-22 01:52:55 +03:00
|
|
|
self.read_file(filename)
|
|
|
|
reads_3read = self.get_counter("fuse.read_us.count")
|
|
|
|
self.assertEqual(reads_2read + 1, reads_3read)
|
|
|
|
|
|
|
|
lookups = self.get_counter("fuse.lookup_us.count")
|
2018-03-21 01:05:04 +03:00
|
|
|
# -hl makes ls to do a lookup of the file to determine type
|
2018-04-05 03:31:28 +03:00
|
|
|
os.system("ls -hl " + full_dirname + " > /dev/null")
|
2017-08-22 01:52:55 +03:00
|
|
|
lookups_1ls = self.get_counter("fuse.lookup_us.count")
|
2018-03-21 01:05:04 +03:00
|
|
|
# equal, the file was lookup'ed above.
|
2017-08-22 01:52:55 +03:00
|
|
|
self.assertEqual(lookups, lookups_1ls)
|
2018-08-11 11:34:44 +03:00
|
|
|
self.client.invalidateKernelInodeCache(self.mount_path_bytes, b"bdir")
|
2018-04-05 03:31:28 +03:00
|
|
|
os.system("ls -hl " + full_dirname + " > /dev/null")
|
2017-08-22 01:52:55 +03:00
|
|
|
lookups_2ls = self.get_counter("fuse.lookup_us.count")
|
|
|
|
self.assertEqual(lookups_1ls + 1, lookups_2ls)
|
2018-03-21 01:05:04 +03:00
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_diff_revisions(self) -> None:
|
2018-03-21 02:34:01 +03:00
|
|
|
# Convert the commit hashes to binary for the thrift call
|
|
|
|
with self.get_thrift_client() as client:
|
|
|
|
diff = client.getScmStatusBetweenRevisions(
|
2018-11-14 23:13:46 +03:00
|
|
|
os.fsencode(self.mount),
|
2018-03-21 02:34:01 +03:00
|
|
|
binascii.unhexlify(self.commit1),
|
2018-05-10 07:33:49 +03:00
|
|
|
binascii.unhexlify(self.commit2),
|
2018-03-21 02:34:01 +03:00
|
|
|
)
|
|
|
|
|
2018-03-21 02:34:05 +03:00
|
|
|
self.assertDictEqual(diff.errors, {})
|
2018-03-21 02:34:01 +03:00
|
|
|
self.assertDictEqual(
|
2018-05-10 07:33:49 +03:00
|
|
|
diff.entries,
|
|
|
|
{
|
2018-08-11 11:34:44 +03:00
|
|
|
b"cdir/subdir/new.txt": ScmFileStatus.ADDED,
|
|
|
|
b"bdir/file": ScmFileStatus.MODIFIED,
|
|
|
|
b"README": ScmFileStatus.REMOVED,
|
2018-05-10 07:33:49 +03:00
|
|
|
},
|
2018-03-21 02:34:01 +03:00
|
|
|
)
|
|
|
|
|
2018-04-05 03:31:28 +03:00
|
|
|
def test_diff_revisions_hex(self) -> None:
|
2018-11-14 23:13:46 +03:00
|
|
|
# Watchman currently calls getScmStatusBetweenRevisions()
|
2018-03-21 02:34:01 +03:00
|
|
|
# with 40-byte hexadecimal commit IDs, so make sure that works.
|
2018-03-21 01:05:04 +03:00
|
|
|
with self.get_thrift_client() as client:
|
|
|
|
diff = client.getScmStatusBetweenRevisions(
|
2018-11-14 23:13:46 +03:00
|
|
|
os.fsencode(self.mount),
|
|
|
|
self.commit1.encode("utf-8"),
|
|
|
|
self.commit2.encode("utf-8"),
|
2018-03-21 01:05:04 +03:00
|
|
|
)
|
|
|
|
|
2018-03-21 02:34:05 +03:00
|
|
|
self.assertDictEqual(diff.errors, {})
|
2018-03-21 01:05:04 +03:00
|
|
|
self.assertDictEqual(
|
2018-05-10 07:33:49 +03:00
|
|
|
diff.entries,
|
|
|
|
{
|
2018-08-11 11:34:44 +03:00
|
|
|
b"cdir/subdir/new.txt": ScmFileStatus.ADDED,
|
|
|
|
b"bdir/file": ScmFileStatus.MODIFIED,
|
|
|
|
b"README": ScmFileStatus.REMOVED,
|
2018-05-10 07:33:49 +03:00
|
|
|
},
|
2018-03-21 01:05:04 +03:00
|
|
|
)
|