root/tests/sys/fs/fusefs/cache.cc
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2020 Alan Somers
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

extern "C" {
#include <sys/param.h>
#include <fcntl.h>
}

#include "mockfs.hh"
#include "utils.hh"

/*
 * Tests for thorny cache problems not specific to any one opcode
 */

using namespace testing;

/*
 * Parameters
 * - reopen file        - If true, close and reopen the file between reads
 * - cache lookups      - If true, allow lookups to be cached
 * - cache attrs        - If true, allow file attributes to be cached
 * - cache_mode         - uncached, writeback, or writethrough
 * - initial size       - File size before truncation
 * - truncated size     - File size after truncation
 */
typedef tuple<tuple<bool, bool, bool>, cache_mode, ssize_t, ssize_t> CacheParam;

class Cache: public FuseTest, public WithParamInterface<CacheParam> {
public:
bool m_direct_io;

Cache(): m_direct_io(false) {};

virtual void SetUp() {
        int cache_mode = get<1>(GetParam());
        switch (cache_mode) {
                case Uncached:
                        m_direct_io = true;
                        break;
                case WritebackAsync:
                        m_async = true;
                        /* FALLTHROUGH */
                case Writeback:
                        m_init_flags |= FUSE_WRITEBACK_CACHE;
                        /* FALLTHROUGH */
                case Writethrough:
                        break;
                default:
                        FAIL() << "Unknown cache mode";
        }
        m_noatime = true;       // To prevent SETATTR for atime on close

        FuseTest::SetUp();
        if (IsSkipped())
                return;
}

void expect_getattr(uint64_t ino, int times, uint64_t size, uint64_t attr_valid)
{
        EXPECT_CALL(*m_mock, process(
                ResultOf([=](auto in) {
                        return (in.header.opcode == FUSE_GETATTR &&
                                in.header.nodeid == ino);
                }, Eq(true)),
                _)
        ).Times(times)
        .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, attr);
                out.body.attr.attr_valid = attr_valid;
                out.body.attr.attr.ino = ino;
                out.body.attr.attr.mode = S_IFREG | 0644;
                out.body.attr.attr.size = size;
        })));
}

void expect_lookup(const char *relpath, uint64_t ino,
        uint64_t size, uint64_t entry_valid, uint64_t attr_valid)
{
        EXPECT_LOOKUP(FUSE_ROOT_ID, relpath)
        .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFREG | 0644;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.nlink = 1;
                out.body.entry.attr_valid = attr_valid;
                out.body.entry.attr.size = size;
                out.body.entry.entry_valid = entry_valid;
        })));
}

void expect_open(uint64_t ino, int times)
{
        FuseTest::expect_open(ino, m_direct_io ? FOPEN_DIRECT_IO: 0, times);
}

void expect_release(uint64_t ino, ProcessMockerT r)
{
        EXPECT_CALL(*m_mock, process(
                ResultOf([=](auto in) {
                        return (in.header.opcode == FUSE_RELEASE &&
                                in.header.nodeid == ino);
                }, Eq(true)),
                _)
        ).WillRepeatedly(Invoke(r));
}

};

// If the server truncates the file behind the kernel's back, the kernel should
// invalidate cached pages beyond the new EOF
TEST_P(Cache, truncate_by_surprise_invalidates_cache)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
        uint64_t ino = 42;
        uint64_t attr_valid, entry_valid;
        int fd;
        ssize_t bufsize = strlen(CONTENTS);
        uint8_t buf[bufsize];
        bool reopen = get<0>(get<0>(GetParam()));
        bool cache_lookups = get<1>(get<0>(GetParam()));
        bool cache_attrs = get<2>(get<0>(GetParam()));
        ssize_t osize = get<2>(GetParam());
        ssize_t nsize = get<3>(GetParam());

        ASSERT_LE(osize, bufsize);
        ASSERT_LE(nsize, bufsize);
        if (cache_attrs)
                attr_valid = UINT64_MAX;
        else
                attr_valid = 0;
        if (cache_lookups)
                entry_valid = UINT64_MAX;
        else
                entry_valid = 0;

        expect_lookup(RELPATH, ino, osize, entry_valid, attr_valid);
        expect_open(ino, 1);
        if (!cache_attrs)
                expect_getattr(ino, 2, osize, attr_valid);
        expect_read(ino, 0, osize, osize, CONTENTS);

        fd = open(FULLPATH, O_RDONLY);
        ASSERT_LE(0, fd) << strerror(errno);

        ASSERT_EQ(osize, read(fd, buf, bufsize)) << strerror(errno);
        ASSERT_EQ(0, memcmp(buf, CONTENTS, osize));

        // Now truncate the file behind the kernel's back.  The next read
        // should discard cache and fetch from disk again.
        if (reopen) {
                // Close and reopen the file
                expect_flush(ino, 1, ReturnErrno(ENOSYS));
                expect_release(ino, ReturnErrno(0));
                ASSERT_EQ(0, close(fd));
                expect_lookup(RELPATH, ino, nsize, entry_valid, attr_valid);
                expect_open(ino, 1);
                fd = open(FULLPATH, O_RDONLY);
                ASSERT_LE(0, fd) << strerror(errno);
        }

        if (!cache_attrs)
                expect_getattr(ino, 1, nsize, attr_valid);
        expect_read(ino, 0, nsize, nsize, CONTENTS);
        ASSERT_EQ(0, lseek(fd, 0, SEEK_SET));
        ASSERT_EQ(nsize, read(fd, buf, bufsize)) << strerror(errno);
        ASSERT_EQ(0, memcmp(buf, CONTENTS, nsize));

        leak(fd);
}

INSTANTIATE_TEST_SUITE_P(Cache, Cache,
        Combine(
                /* Test every combination that:
                 * - does not cache at least one of entries and attrs
                 * - either doesn't cache attrs, or reopens the file
                 * In the other combinations, the kernel will never learn that
                 * the file's size has changed.
                 */
                Values(
                        std::make_tuple(false, false, false),
                        std::make_tuple(false, true, false),
                        std::make_tuple(true, false, false),
                        std::make_tuple(true, false, true),
                        std::make_tuple(true, true, false)
                ),
                Values(Writethrough, Writeback),
                /* Test both reductions and extensions to file size */
                Values(20),
                Values(10, 25)
        )
);