Commit f289c392 authored by Ramon Nou's avatar Ramon Nou
Browse files

uploaded some testing

parent 4a42e5f1
Loading
Loading
Loading
Loading
+20 −1
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ from pathlib import Path
from harness.logger import logger, initialize_logging, finalize_logging
from harness.cli import add_cli_options, set_default_log_formatter
from harness.workspace import Workspace, FileCreator
from harness.gkfs import Daemon, Client, Proxy, ShellClient, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator
from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator
from harness.reporter import report_test_status, report_test_headline, report_assertion_pass

def pytest_configure(config):
@@ -159,6 +159,16 @@ def gkfs_client_proxy(test_workspace):

    return Client(test_workspace, True)

@pytest.fixture
def gkfs_clientLibc(test_workspace):
    """
    Sets up a gekkofs client environment so that
    operations (system calls, library calls, ...) can
    be requested from a co-running daemon.
    """

    return ClientLibc(test_workspace)

@pytest.fixture
def gkfs_shell(test_workspace):
    """
@@ -177,6 +187,15 @@ def gkfs_shell_proxy(test_workspace):

    return ShellClient(test_workspace,True)

@pytest.fixture
def gkfs_shellLibc(test_workspace):
    """
    Sets up a gekkofs environment so that shell commands
    (stat, ls, mkdir, etc.) can be issued to a co-running daemon.
    """

    return ShellClientLibc(test_workspace)

@pytest.fixture
def file_factory(test_workspace):
    """
+20 −1
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ from pathlib import Path
from harness.logger import logger, initialize_logging, finalize_logging
from harness.cli import add_cli_options, set_default_log_formatter
from harness.workspace import Workspace, FileCreator
from harness.gkfs import Daemon, Client, Proxy, ShellClient, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator
from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator
from harness.reporter import report_test_status, report_test_headline, report_assertion_pass

def pytest_configure(config):
@@ -159,6 +159,16 @@ def gkfs_client_proxy(test_workspace):

    return Client(test_workspace, True)
    
@pytest.fixture
def gkfs_clientLibc(test_workspace):
    """
    Sets up a gekkofs client environment so that
    operations (system calls, library calls, ...) can
    be requested from a co-running daemon.
    """

    return ClientLibc(test_workspace)

@pytest.fixture
def gkfs_shell(test_workspace):
    """
@@ -177,6 +187,15 @@ def gkfs_shell_proxy(test_workspace):

    return ShellClient(test_workspace,True)

@pytest.fixture
def gkfs_shellLibc(test_workspace):
    """
    Sets up a gekkofs environment so that shell commands
    (stat, ls, mkdir, etc.) can be issued to a co-running daemon.
    """

    return ShellClientLibc(test_workspace)

@pytest.fixture
def file_factory(test_workspace):
    """
+312 −3
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ from harness.cmd import CommandParser
gkfs_daemon_cmd = 'gkfs_daemon'
gkfs_client_cmd = 'gkfs.io'
gkfs_client_lib_file = 'libgkfs_intercept.so'
gkfs_client_lib_libc_file = 'libgkfs_libc_intercept.so'
gkfs_hosts_file = 'gkfs_hosts.txt'
gkfs_daemon_log_file = 'gkfs_daemon.log'
gkfs_daemon_log_level = '100'
@@ -255,10 +256,10 @@ class Daemon:

    def run(self):

        args = ['--mountdir', self.mountdir,
                '--rootdir', self.rootdir,
        args = ['--mountdir', self.mountdir.as_posix(),
                '--rootdir', self.rootdir.as_posix(),
                '-l', self._address,
                '--metadir', self._metadir,
                '--metadir', self._metadir.as_posix(),
                '--dbbackend', self._database,
                '--output-stats', self.logdir / 'stats.log',
                '--enable-collection',
@@ -602,6 +603,87 @@ class Client:
    def cwd(self):
        return self._workspace.twd
    
class ClientLibc:
    """
    A class to represent a GekkoFS client process with a patched LD_PRELOAD.
    This class allows tests to interact with the file system using I/O-related
    function calls, be them system calls (e.g. read()) or glibc I/O functions
    (e.g. opendir()).
    """
    def __init__(self, workspace):
        self._parser = IOParser()
        self._workspace = workspace
        self._cmd = sh.Command(gkfs_client_cmd, self._workspace.bindirs)
        self._env = os.environ.copy()

        libdirs = ':'.join(
                filter(None, [os.environ.get('LD_LIBRARY_PATH', '')] +
                             [str(p) for p in self._workspace.libdirs]))

        # ensure the client interception library is available:
        # to avoid running code with potentially installed libraries,
        # it must be found in one (and only one) of the workspace's bindirs
        preloads = []
        for d in self._workspace.bindirs:
            search_path = Path(d) / gkfs_client_lib_libc_file
            if search_path.exists():
                preloads.append(search_path)

        if len(preloads) == 0:
            logger.error(f'No client libraries found in the test\'s binary directories:')
            pytest.exit("Aborted due to initialization error. Check test logs.")

        if len(preloads) != 1:
            logger.error(f'Multiple client libraries found in the test\'s binary directories:')
            for p in preloads:
                logger.error(f'  {p}')
            logger.error(f'Make sure that only one copy of the client library is available.')
            pytest.exit("Aborted due to initialization error. Check test logs.")

        self._preload_library = preloads[0]

        self._patched_env = {
            'LD_LIBRARY_PATH': libdirs,
            'LD_PRELOAD': str(self._preload_library),
            'LIBGKFS_HOSTS_FILE': str(self.cwd / gkfs_hosts_file),
            'LIBGKFS_LOG': gkfs_client_log_level,
            'LIBGKFS_LOG_OUTPUT': str(self._workspace.logdir / gkfs_client_log_file),
            'LIBGKFS_LOG_SYSCALL_FILTER': gkfs_client_log_syscall_filter
        }

        self._env.update(self._patched_env)

    @property
    def preload_library(self):
        """
        Return the preload library detected for this client
        """

        return self._preload_library

    def run(self, cmd, *args):

        logger.debug(f"running client")
        logger.debug(f"cmdline: {self._cmd} " + " ".join(map(str, list(args))))
        logger.debug(f"patched env: {pformat(self._patched_env)}")

        out = self._cmd(
            [ cmd ] + list(args),
            _env = self._env,
    #        _out=sys.stdout,
    #        _err=sys.stderr,
            )

        logger.debug(f"command output: {out.stdout}")
        return self._parser.parse(cmd, out.stdout)

    def __getattr__(self, name):
        return _proxy_exec(self, name)

    @property
    def cwd(self):
        return self._workspace.twd

class ShellCommand:
    """
    A wrapper class for sh.RunningCommand that allows seamlessly using all
@@ -814,6 +896,233 @@ class ShellClient:

       

        found_cmd = shutil.which(cmd,
            path=':'.join(str(p) for p in self._search_paths)
            )

        if not found_cmd:
            raise sh.CommandNotFound(cmd)

        self._cmd = sh.Command(found_cmd)

        logger.debug(f"running program")
        logger.debug(f"cmd: {cmd} {' '.join(str(a) for a in args)}")
        logger.debug(f"search_paths: {':'.join(str(p) for p in self._search_paths)}")
        logger.debug(f"timeout: {timeout} seconds")
        logger.debug(f"timeout_signal: {signal.Signals(timeout_signal).name}")
        logger.debug(f"patched env:\n{pformat(self._patched_env)}")


        # 'sh' raises an exception if the return code is not zero;
        # since we'd rather check for return codes explictly, we
        # whitelist all exit codes from 1 to 255 as 'ok' using the
        # _ok_code argument
        proc = self._cmd(
            args,
            _env = self._env,
    #        _out=sys.stdout,
    #        _err=sys.stderr,
            _timeout=timeout,
            _timeout_signal=timeout_signal,
    #        _ok_code=list(range(0, 256))
            )

        logger.debug(f"program stdout: {proc.stdout}")
        logger.debug(f"program stderr: {proc.stderr}")

        return ShellCommand(cmd, proc)

    def __getattr__(self, name):
        return _proxy_exec(self, name)

    @property
    def cwd(self):
        return self._workspace.twd
    

class ShellClientLibc:
    """
    A class to represent a GekkoFS shell client process.
    This class allows tests to execute shell commands or scripts via bash -c
    on a GekkoFS instance.
    """

    def __init__(self, workspace):
        self._workspace = workspace
        self._search_paths = _find_search_paths(self._workspace.bindirs)
        self._env = os.environ.copy()

        libdirs = ':'.join(
                filter(None, [os.environ.get('LD_LIBRARY_PATH', '')] +
                             [str(p) for p in self._workspace.libdirs]))

        # ensure the client interception library is available:
        # to avoid running code with potentially installed libraries,
        # it must be found in one (and only one) of the workspace's bindirs
        preloads = []
        for d in self._workspace.bindirs:
            search_path = Path(d) / gkfs_client_lib_libc_file
            if search_path.exists():
                preloads.append(search_path)

        if len(preloads) != 1:
            logger.error(f'Multiple client libraries found in the test\'s binary directories:')
            for p in preloads:
                logger.error(f'  {p}')
            logger.error(f'Make sure that only one copy of the client library is available.')
            pytest.exit("Aborted due to initialization error")

        self._preload_library = preloads[0]

        self._patched_env = {
            'LD_LIBRARY_PATH'      : libdirs,
            'LD_PRELOAD'           : str(self._preload_library),
            'LIBGKFS_HOSTS_FILE'   : str(self.cwd / gkfs_hosts_file),
            'LIBGKFS_LOG'          : gkfs_client_log_level,
            'LIBGKFS_LOG_OUTPUT'   : str(self._workspace.logdir / gkfs_client_log_file),
            'LIBGKFS_LOG_SYSCALL_FILTER': gkfs_client_log_syscall_filter
        }

        self._env.update(self._patched_env)

    @property
    def patched_environ(self):
        """
        Return the patched environment required to run a test as a string that
        can be prepended to a shell command.
        """

        return ' '.join(f'{k}="{v}"' for k,v in self._patched_env.items())

    def script(self, code, intercept_shell=True, timeout=60, timeout_signal=signal.SIGKILL):
        """
        Execute a shell script passed as an argument in bash.

        For instance, the following snippet:

            mountdir = pathlib.Path('/tmp')
            file01 = 'file01'

            ShellClient().script(
                f'''
                    expected_pathname={mountdir / file01}
                    if [[ -e ${{expected_pathname}} ]];
                    then
                        exit 0
                    fi
                    exit 1
                ''')

        transforms into:

            bash -c '
                expected_pathname=/tmp/file01
                if [[ -e ${expected_pathname} ]];
                then
                    exit 0
                fi
                exit 1
            '

        Note that since we are using Python's f-strings, for variable
        expansions to work correctly, they need to be defined with double
        braces, e.g.  ${{expected_pathname}}.

        Parameters
        ----------
        code: `str`
            The script code to be passed to 'bash -c'.

        intercept_shell: `bool`
            Controls whether the shell executing the script should be
            executed with LD_PRELOAD=libgkfs_intercept.so (default: True).

        timeout: `int`
            How much time, in seconds, we should give the process to complete.
            If the process does not finish within the timeout, it will be sent
            the signal defined by `timeout_signal`.

            Default value: 60

        timeout_signal: `int`
            The signal to be sent to the process if `timeout` is not None.

            Default value: signal.SIGKILL

        Returns
        -------
        A sh.RunningCommand instance that allows interacting with
        the finished process.
        """

        logger.debug(f"running bash")
        logger.debug(f"cmd: bash -c '{code}'")
        logger.debug(f"timeout: {timeout} seconds")
        logger.debug(f"timeout_signal: {signal.Signals(timeout_signal).name}")

        if intercept_shell:
            logger.debug(f"patched env: {self._patched_env}")

        self._cmd = sh.Command("bash")

        # 'sh' raises an exception if the return code is not zero;
        # since we'd rather check for return codes explictly, we
        # whitelist all exit codes from 1 to 255 as 'ok' using the
        # _ok_code argument
        return self._cmd('-c',
            code,
            _env = (self._env if intercept_shell else os.environ),
    #        _out=sys.stdout,
    #        _err=sys.stderr,
            _timeout=timeout,
            _timeout_signal=timeout_signal,
    #        _ok_code=list(range(0, 256))
            )

    def run(self, cmd, *args, timeout=60, timeout_signal=signal.SIGKILL):
        """
        Execute a shell command  with arguments.

        For example, the following snippet:

            mountdir = pathlib.Path('/tmp')
            file01 = 'file01'

            ShellClient().stat('--terse', mountdir / file01)

        transforms into:

            bash -c 'stat --terse /tmp/file01'

        Parameters:
        -----------
        cmd: `str`
            The command to execute.

        args: `list`
            The list of arguments for the command.

        timeout: `number`
            How much time, in seconds, we should give the process to complete.
            If the process does not finish within the timeout, it will be sent
            the signal defined by `timeout_signal`.

            Default value: 60

        timeout_signal: `int`
            The signal to be sent to the process if `timeout` is not None.

            Default value: signal.SIGKILL

        Returns
        -------
        A ShellCommand instance that allows interacting with the finished
        process. Note that ShellCommand wraps sh.RunningCommand and adds s
        extra properties to it.
        """

       

        found_cmd = shutil.which(cmd,
            path=':'.join(str(p) for p in self._search_paths)
            )
+128 −0
Original line number Diff line number Diff line
@@ -192,3 +192,131 @@ def test_preadv(gkfs_daemon, gkfs_client):
    assert ret.buf_0 == buf_0
    assert ret.buf_1 == buf_1
    assert ret.retval == len(buf_0) + len(buf_1) # Return the number of read bytes



def test_read_libc(gkfs_daemon, gkfs_clientLibc):

    file = gkfs_daemon.mountdir / "file"

    # create a file in gekkofs
    ret = gkfs_clientLibc.open(file,
                           os.O_CREAT | os.O_WRONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # write a buffer we know
    buf = b'42'
    ret = gkfs_clientLibc.write(file, buf, len(buf))

    assert ret.retval == len(buf) # Return the number of written bytes

    # open the file to read
    ret = gkfs_clientLibc.open(file,
                           os.O_RDONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # read the file
    ret = gkfs_clientLibc.read(file, len(buf))

    assert ret.buf == buf
    assert ret.retval == len(buf) # Return the number of read bytes

def test_pread_libc(gkfs_daemon, gkfs_clientLibc):

    file = gkfs_daemon.mountdir / "file"

    # create a file in gekkofs
    ret = gkfs_clientLibc.open(file,
                           os.O_CREAT | os.O_WRONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # write a buffer we know
    buf = b'42'
    ret = gkfs_clientLibc.pwrite(file, buf, len(buf), 1024)

    assert ret.retval == len(buf) # Return the number of written bytes

    # open the file to read
    ret = gkfs_clientLibc.open(file,
                           os.O_RDONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # read the file at offset 1024
    ret = gkfs_clientLibc.pread(file, len(buf), 1024)

    assert ret.buf == buf
    assert ret.retval == len(buf) # Return the number of read bytes

@pytest.mark.skip(reason="readv not implemented in libc")
def test_readv_libc(gkfs_daemon, gkfs_clientLibc):

    file = gkfs_daemon.mountdir / "file"

    # create a file in gekkofs
    ret = gkfs_clientLibc.open(file,
                           os.O_CREAT | os.O_WRONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # write a buffer we know
    buf_0 = b'42'
    buf_1 = b'24'
    ret = gkfs_clientLibc.writev(file, buf_0, buf_1, 2)

    assert ret.retval == len(buf_0) + len(buf_1) # Return the number of written bytes

    # open the file to read
    ret = gkfs_clientLibc.open(file,
                           os.O_RDONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # read the file
    ret = gkfs_clientLibc.readv(file, len(buf_0), len(buf_1))

    assert ret.buf_0 == buf_0
    assert ret.buf_1 == buf_1
    assert ret.retval == len(buf_0) + len(buf_1) # Return the number of read bytes

@pytest.mark.skip(reason="preadv not implemented in libc")
def test_preadv_libc(gkfs_daemon, gkfs_clientLibc):

    file = gkfs_daemon.mountdir / "file"

    # create a file in gekkofs
    ret = gkfs_clientLibc.open(file,
                           os.O_CREAT | os.O_WRONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # write a buffer we know
    buf_0 = b'42'
    buf_1 = b'24'
    ret = gkfs_clientLibc.pwritev(file, buf_0, buf_1, 2, 1024)

    assert ret.retval == len(buf_0) + len(buf_1) # Return the number of written bytes

    # open the file to read
    ret = gkfs_clientLibc.open(file,
                           os.O_RDONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    # read the file
    ret = gkfs_clientLibc.preadv(file, len(buf_0), len(buf_1), 1024)

    assert ret.buf_0 == buf_0
    assert ret.buf_1 == buf_1
    assert ret.retval == len(buf_0) + len(buf_1) # Return the number of read bytes
 No newline at end of file
+47 −0
Original line number Diff line number Diff line
@@ -129,6 +129,53 @@ def test_write_proxy(gkfs_daemon_proxy, gkfs_proxy, gkfs_client_proxy):
    assert ret.retval == 0
    assert ret.statbuf.st_size == (len(str1) + len(str2) + len(str3))

def test_write(gkfs_daemon, gkfs_clientLibc):

    file = gkfs_daemon.mountdir / "file"

    ret = gkfs_clientLibc.open(file,
                           os.O_CREAT | os.O_WRONLY,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    buf = b'42'
    ret = gkfs_clientLibc.write(file, buf, len(buf))

    assert ret.retval == len(buf)  # Return the number of written bytes

    file_append = gkfs_daemon.mountdir / "file_append"

    ret = gkfs_clientLibc.open(file_append,
                           os.O_CREAT | os.O_WRONLY | os.O_APPEND,
                           stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    assert ret.retval != -1

    str1 = b'Hello'
    str2 = b', World!'
    str3 = b' This is a test.\n'

    ret = gkfs_clientLibc.write(file_append, str1, len(str1), True)
    assert ret.retval == len(str1)
    ret = gkfs_clientLibc.stat(file_append)
    assert ret.retval == 0
    assert ret.statbuf.st_size == len(str1)

    ret = gkfs_clientLibc.write(file_append, str2, len(str2), True)
    assert ret.retval == len(str2)
    ret = gkfs_clientLibc.stat(file_append)
    assert ret.retval == 0
    assert ret.statbuf.st_size == (len(str1) + len(str2))

    ret = gkfs_clientLibc.write(file_append, str3, len(str3), True)
    assert ret.retval == len(str3)
    ret = gkfs_clientLibc.stat(file_append)
    assert ret.retval == 0
    assert ret.statbuf.st_size == (len(str1) + len(str2) + len(str3))



def test_pwrite(gkfs_daemon, gkfs_client):
    file = gkfs_daemon.mountdir / "file"

Loading