2016-06-20 23:38:29 +03:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
#
|
2017-01-21 09:02:33 +03:00
|
|
|
# Copyright (c) 2016-present, Facebook, Inc.
|
2016-05-26 07:43:44 +03:00
|
|
|
# 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.
|
|
|
|
|
2016-06-20 23:38:29 +03:00
|
|
|
import hashlib
|
2017-08-23 05:43:29 +03:00
|
|
|
import os
|
|
|
|
import time
|
2016-05-26 07:43:44 +03:00
|
|
|
|
2018-03-21 01:05:04 +03:00
|
|
|
from facebook.eden.ttypes import EdenError, ScmFileStatus, SHA1Result
|
2017-08-23 05:43:29 +03:00
|
|
|
from facebook.eden.ttypes import 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
|
2016-07-08 21:25:39 +03:00
|
|
|
class ThriftTest:
|
|
|
|
def populate_repo(self):
|
|
|
|
self.repo.write_file('hello', 'hola\n')
|
2018-03-21 01:05:04 +03:00
|
|
|
self.repo.write_file('README', 'docs\n')
|
2016-07-08 21:25:39 +03:00
|
|
|
self.repo.write_file('adir/file', 'foo!\n')
|
2017-08-22 01:52:55 +03:00
|
|
|
self.repo.write_file('bdir/file', 'bar!\n')
|
2016-07-08 21:25:39 +03:00
|
|
|
self.repo.symlink('slink', 'hello')
|
2018-03-21 01:05:04 +03:00
|
|
|
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
|
|
|
|
2016-07-23 03:31:20 +03:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
|
|
|
self.client = self.get_thrift_client()
|
|
|
|
self.client.open()
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
self.client.close()
|
|
|
|
super().tearDown()
|
2016-05-26 07:43:44 +03:00
|
|
|
|
2017-08-23 05:43:29 +03:00
|
|
|
def get_loaded_inodes_count(self, path):
|
|
|
|
result = self.client.debugInodeStatus(self.mount, path)
|
|
|
|
inode_count = 0
|
|
|
|
for item in result:
|
|
|
|
for inode in item.entries:
|
|
|
|
if inode.loaded:
|
|
|
|
inode_count += 1
|
|
|
|
return inode_count
|
|
|
|
|
2016-07-23 03:31:20 +03:00
|
|
|
def test_list_mounts(self):
|
|
|
|
mounts = self.client.listMounts()
|
2016-05-26 07:43:44 +03:00
|
|
|
self.assertEqual(1, len(mounts))
|
|
|
|
|
|
|
|
mount = mounts[0]
|
2016-07-08 21:25:39 +03:00
|
|
|
self.assertEqual(self.mount, mount.mountPoint)
|
2018-03-20 03:01:17 +03:00
|
|
|
# The client path should always be inside the main eden directory
|
|
|
|
self.assertTrue(mount.edenClientPath.startswith(self.eden.eden_dir),
|
|
|
|
msg='unexpected client path: %r' % mount.edenClientPath)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
|
|
|
def test_get_sha1(self):
|
2016-06-20 23:38:29 +03:00
|
|
|
expected_sha1_for_hello = hashlib.sha1(b'hola\n').digest()
|
2016-08-18 17:21:36 +03:00
|
|
|
result_for_hello = SHA1Result()
|
|
|
|
result_for_hello.set_sha1(expected_sha1_for_hello)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2016-06-20 23:38:29 +03:00
|
|
|
expected_sha1_for_adir_file = hashlib.sha1(b'foo!\n').digest()
|
2016-08-18 17:21:36 +03:00
|
|
|
result_for_adir_file = SHA1Result()
|
|
|
|
result_for_adir_file.set_sha1(expected_sha1_for_adir_file)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2016-08-18 17:21:36 +03:00
|
|
|
self.assertEqual(
|
|
|
|
[
|
|
|
|
result_for_hello,
|
|
|
|
result_for_adir_file,
|
|
|
|
], self.client.getSHA1(self.mount, ['hello', 'adir/file'])
|
|
|
|
)
|
2016-05-28 04:16:27 +03:00
|
|
|
|
2016-08-18 17:21:36 +03:00
|
|
|
def test_get_sha1_throws_for_empty_string(self):
|
|
|
|
results = self.client.getSHA1(self.mount, [''])
|
|
|
|
self.assertEqual(1, len(results))
|
|
|
|
self.assert_error(results[0], 'path cannot be the empty string')
|
2016-05-28 04:16:27 +03:00
|
|
|
|
|
|
|
def test_get_sha1_throws_for_directory(self):
|
2016-08-18 17:21:36 +03:00
|
|
|
results = self.client.getSHA1(self.mount, ['adir'])
|
|
|
|
self.assertEqual(1, len(results))
|
2016-12-15 02:35:05 +03:00
|
|
|
self.assert_error(results[0], 'adir: Is a directory')
|
2016-05-28 04:16:27 +03:00
|
|
|
|
|
|
|
def test_get_sha1_throws_for_non_existent_file(self):
|
2016-08-18 17:21:36 +03:00
|
|
|
results = self.client.getSHA1(self.mount, ['i_do_not_exist'])
|
|
|
|
self.assertEqual(1, len(results))
|
2016-12-15 02:35:01 +03:00
|
|
|
self.assert_error(results[0],
|
|
|
|
'i_do_not_exist: No such file or directory')
|
2016-05-28 04:16:27 +03:00
|
|
|
|
|
|
|
def test_get_sha1_throws_for_symlink(self):
|
|
|
|
'''Fails because caller should resolve the symlink themselves.'''
|
2016-08-18 17:21:36 +03:00
|
|
|
results = self.client.getSHA1(self.mount, ['slink'])
|
|
|
|
self.assertEqual(1, len(results))
|
2016-12-15 02:35:05 +03:00
|
|
|
self.assert_error(results[0],
|
|
|
|
'slink: file is a symlink: Invalid argument')
|
2016-08-18 17:21:36 +03:00
|
|
|
|
|
|
|
def assert_error(self, sha1result, error_message):
|
|
|
|
self.assertIsNotNone(sha1result, msg='Must pass a SHA1Result')
|
|
|
|
self.assertEqual(
|
|
|
|
SHA1Result.ERROR,
|
|
|
|
sha1result.getType(),
|
|
|
|
msg='SHA1Result must be an error'
|
|
|
|
)
|
|
|
|
error = sha1result.get_error()
|
|
|
|
self.assertIsNotNone(error)
|
|
|
|
self.assertEqual(error_message, error.message)
|
2017-01-26 23:45:50 +03:00
|
|
|
|
|
|
|
def test_glob(self):
|
|
|
|
self.assertEqual(
|
|
|
|
['adir/file'], self.client.glob(self.mount, ['a*/file']))
|
2017-08-22 01:52:55 +03:00
|
|
|
self.assertCountEqual(
|
2018-03-21 01:05:04 +03:00
|
|
|
['adir/file', 'bdir/file'],
|
|
|
|
self.client.glob(self.mount, ['**/file'])
|
|
|
|
)
|
2017-01-26 23:45:50 +03:00
|
|
|
self.assertEqual(
|
|
|
|
['adir/file'], self.client.glob(self.mount, ['adir/*']))
|
2017-08-22 01:52:55 +03:00
|
|
|
self.assertCountEqual(
|
|
|
|
['adir/file', 'bdir/file'],
|
|
|
|
self.client.glob(self.mount, ['adir/*', '**/file']),
|
2017-01-26 23:45:50 +03:00
|
|
|
msg='De-duplicate results from multiple globs')
|
|
|
|
self.assertEqual(
|
|
|
|
['hello'], self.client.glob(self.mount, ['hello']))
|
|
|
|
self.assertEqual(
|
|
|
|
[], self.client.glob(self.mount, ['hell']),
|
|
|
|
msg="No accidental substring match")
|
|
|
|
self.assertEqual(
|
|
|
|
['hello'], self.client.glob(self.mount, ['hel*']))
|
|
|
|
self.assertEqual(
|
|
|
|
['adir'], self.client.glob(self.mount, ['ad*']))
|
|
|
|
self.assertEqual(
|
|
|
|
['adir/file'], self.client.glob(self.mount, ['adir/**/*']))
|
|
|
|
self.assertEqual(
|
|
|
|
['adir/file'], self.client.glob(self.mount, ['adir/**']))
|
|
|
|
|
|
|
|
with self.assertRaises(EdenError) as ctx:
|
|
|
|
self.client.glob(self.mount, ['adir['])
|
|
|
|
self.assertIn('unterminated bracket sequence',
|
|
|
|
str(ctx.exception))
|
2017-06-23 08:33:01 +03:00
|
|
|
|
|
|
|
def test_unload_free_inodes(self):
|
|
|
|
for i in range(100):
|
|
|
|
self.write_file('testfile%d.txt' % i, 'unload test case')
|
|
|
|
|
2017-08-23 05:43:29 +03:00
|
|
|
inode_count_before_unload = self.get_loaded_inodes_count('')
|
|
|
|
self.assertGreater(
|
|
|
|
inode_count_before_unload, 100,
|
|
|
|
'Number of loaded inodes should increase'
|
|
|
|
)
|
2017-06-23 08:33:01 +03:00
|
|
|
|
2017-08-23 05:43:29 +03:00
|
|
|
age = TimeSpec()
|
|
|
|
age.seconds = 0
|
|
|
|
age.nanoSeconds = 0
|
2017-08-23 05:43:31 +03:00
|
|
|
unload_count = self.client.unloadInodeForPath(self.mount, '', age)
|
2017-08-23 05:43:29 +03:00
|
|
|
|
2017-08-23 05:43:31 +03:00
|
|
|
self.assertGreaterEqual(
|
|
|
|
unload_count, 100,
|
2017-08-23 05:43:29 +03:00
|
|
|
'Number of loaded inodes should reduce after unload'
|
|
|
|
)
|
|
|
|
|
2017-08-22 01:52:55 +03:00
|
|
|
def read_file(self, filename):
|
|
|
|
with open(filename, 'r') as f:
|
|
|
|
f.read()
|
|
|
|
|
|
|
|
def get_counter(self, name):
|
|
|
|
self.client.flushStatsNow()
|
|
|
|
return self.client.getCounters()[name]
|
|
|
|
|
|
|
|
def test_invalidate_inode_cache(self):
|
|
|
|
filename = os.path.join(self.mount, 'bdir/file')
|
|
|
|
dirname = os.path.join(self.mount, 'bdir/')
|
|
|
|
|
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):
|
|
|
|
fn = os.path.join(self.mount, '_tmp_')
|
|
|
|
with open(fn, 'w') as f:
|
|
|
|
f.write('foo!\n')
|
|
|
|
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)
|
|
|
|
self.client.invalidateKernelInodeCache(self.mount, 'bdir/file')
|
|
|
|
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
|
2017-08-22 01:52:55 +03:00
|
|
|
os.system("ls -hl " + dirname + " > /dev/null")
|
|
|
|
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)
|
|
|
|
self.client.invalidateKernelInodeCache(self.mount, 'bdir')
|
|
|
|
os.system("ls -hl " + dirname + " > /dev/null")
|
|
|
|
lookups_2ls = self.get_counter("fuse.lookup_us.count")
|
|
|
|
self.assertEqual(lookups_1ls + 1, lookups_2ls)
|
2018-03-21 01:05:04 +03:00
|
|
|
|
|
|
|
def test_diff_revisions(self):
|
|
|
|
with self.get_thrift_client() as client:
|
|
|
|
diff = client.getScmStatusBetweenRevisions(
|
|
|
|
self.mount, self.commit1, self.commit2
|
|
|
|
)
|
|
|
|
|
|
|
|
# FIXME: getScmStatusBetweenRevisions() currently reports
|
|
|
|
# the ADDED and REMOVED statuses backwards.
|
|
|
|
self.assertDictEqual(
|
|
|
|
diff.entries, {
|
|
|
|
'cdir/subdir/new.txt': ScmFileStatus.REMOVED, # should be ADDED
|
|
|
|
'bdir/file': ScmFileStatus.MODIFIED,
|
|
|
|
'README': ScmFileStatus.ADDED, # should be REMOVED
|
|
|
|
}
|
|
|
|
)
|