Browse Source

Rework open file resource management

openfiles
Antonio SJ Musumeci 1 month ago
parent
commit
e32985e490
  1. 10
      libfuse/include/fuse.h
  2. 5
      libfuse/lib/fuse.cpp
  3. 2
      libfuse/lib/fuse_lowlevel.cpp
  4. 15
      src/fileinfo.hpp
  5. 124
      src/fuse_create.cpp
  6. 242
      src/fuse_open.cpp
  7. 84
      src/fuse_release.cpp
  8. 12
      src/state.hpp
  9. 103
      tests/TEST_io_passthrough_create_open
  10. 132
      tests/TEST_io_passthrough_open_race
  11. 5
      tests/tests.cpp

10
libfuse/include/fuse.h

@ -29,6 +29,16 @@ EXTERN_C_BEGIN
* Basic FUSE API *
* ----------------------------------------------------------- */
#define INVALID_BACKING_ID (0)
static
inline
int
fuse_backing_id_is_valid(const int backing_id_)
{
return (backing_id_ > INVALID_BACKING_ID);
}
struct fuse_dirents_t;
typedef struct fuse_dirents_t fuse_dirents_t;

5
libfuse/lib/fuse.cpp

@ -3878,7 +3878,7 @@ fuse_passthrough_open(const int fd_)
rv = ::ioctl(dev_fuse_fd,FUSE_DEV_IOC_BACKING_OPEN,&bm);
return rv;
return ((rv < 0) ? INVALID_BACKING_ID : rv);
}
int
@ -3886,6 +3886,9 @@ fuse_passthrough_close(const int backing_id_)
{
int dev_fuse_fd;
if(!fuse_backing_id_is_valid(backing_id_))
return 0;
dev_fuse_fd = fuse_chan_fd(f.se->ch);
return ::ioctl(dev_fuse_fd,FUSE_DEV_IOC_BACKING_CLOSE,&backing_id_);

2
libfuse/lib/fuse_lowlevel.cpp

@ -281,7 +281,7 @@ fill_open(struct fuse_open_out *arg_,
arg_->open_flags |= FOPEN_PARALLEL_DIRECT_WRITES;
if(ffi_->noflush)
arg_->open_flags |= FOPEN_NOFLUSH;
if(ffi_->passthrough && (ffi_->backing_id > 0))
if(ffi_->passthrough && fuse_backing_id_is_valid(ffi_->backing_id))
{
arg_->open_flags |= FOPEN_PASSTHROUGH;
arg_->backing_id = ffi_->backing_id;

15
src/fileinfo.hpp

@ -22,13 +22,15 @@
#include "base_types.h"
#include <cassert>
#include <mutex>
class FileInfo : public FH
{
public:
static FileInfo *from_fh(const u64 fh);
static FileInfo *from_fh(const u64);
static u64 to_fh(const FileInfo*);
public:
FileInfo(const int fd_,
@ -71,16 +73,25 @@ public:
std::mutex mutex;
};
inline
u64
FileInfo::to_fh(const FileInfo *fi_)
{
assert(fi_ != nullptr);
return reinterpret_cast<u64>(fi_);
}
inline
u64
FileInfo::to_fh() const
{
return reinterpret_cast<u64>(this);
return FileInfo::to_fh(this);
}
inline
FileInfo*
FileInfo::from_fh(const u64 fh_)
{
assert(fh_ != 0);
return reinterpret_cast<FileInfo*>(fh_);
}

124
src/fuse_create.cpp

@ -36,6 +36,7 @@
#include "fuse.h"
#include <cassert>
#include <string>
#include <vector>
@ -225,29 +226,26 @@ _(const PassthroughIOEnum e_,
static
int
_create_for_insert_lambda(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
const mode_t mode_,
fuse_file_info_t *ffi_,
State::OpenFile *of_)
_create(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
mode_t mode_,
fuse_file_info_t *ffi_)
{
int rv;
FileInfo *fi;
auto &of = state.open_files;
::_config_to_ffi_flags(cfg,ctx_->pid,ffi_);
if(cfg.cache_writeback)
::_tweak_flags_cache_writeback(&ffi_->flags);
ffi_->noflush = !::_calculate_flush(cfg.flushonclose,
ffi_->flags);
rv = ::_create(ctx_,
cfg.func.getattr.policy,
cfg.func.create.policy,
cfg.branches,
fusepath_,
ffi_,
mode_,
ctx_->umask);
ffi_->noflush = !::_calculate_flush(cfg.flushonclose,ffi_->flags);
int rv = ::_create(ctx_,
cfg.func.getattr.policy,
cfg.func.create.policy,
cfg.branches,
fusepath_,
ffi_,
mode_,
ctx_->umask);
if(rv == -EROFS)
{
cfg.branches.find_and_set_mode_ro();
@ -264,10 +262,8 @@ _create_for_insert_lambda(const fuse_req_ctx_t *ctx_,
if(rv < 0)
return rv;
fi = FileInfo::from_fh(ffi_->fh);
of_->ref_count = 1;
of_->fi = fi;
FileInfo *fi = FileInfo::from_fh(ffi_->fh);
int backing_id = INVALID_BACKING_ID;
switch(_(cfg.passthrough_io,ffi_->flags))
{
@ -276,85 +272,29 @@ _create_for_insert_lambda(const fuse_req_ctx_t *ctx_,
case _(PassthroughIO::ENUM::RW,O_RDONLY):
case _(PassthroughIO::ENUM::RW,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDWR):
backing_id = FUSE::passthrough_open(fi->fd);
if(fuse_backing_id_is_valid(backing_id))
{
ffi_->backing_id = backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
}
break;
default:
return 0;
break;
}
of_->backing_id = FUSE::passthrough_open(fi->fd);
if(of_->backing_id < 0)
return 0;
bool inserted = of.try_emplace(ctx_->nodeid,
backing_id,
fi);
ffi_->backing_id = of_->backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
// Legit... this should never happen.
assert(inserted);
(void)inserted;
return 0;
}
static
inline
auto
_create_insert_lambda(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
const mode_t mode_,
fuse_file_info_t *ffi_,
int *_rv_)
{
return
[=](auto &val_)
{
*_rv_ = ::_create_for_insert_lambda(ctx_,
fusepath_,
mode_,
ffi_,
&val_.second);
};
}
// This function should never be called?
static
inline
auto
_create_update_lambda()
{
return
[](const auto &val_)
{
fmt::println(stderr,"CREATE_UPDATE_LAMBDA: THIS SHOULD NOT HAPPEN");
SysLog::crit("CREATE_UPDATE_LAMBDA: THIS SHOULD NOT HAPPEN");
abort();
};
}
static
int
_create(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
mode_t mode_,
fuse_file_info_t *ffi_)
{
int rv;
auto &of = state.open_files;
rv = -EINVAL;
of.try_emplace_and_visit(ctx_->nodeid,
::_create_insert_lambda(ctx_,fusepath_,mode_,ffi_,&rv),
::_create_update_lambda());
// Can't abort an emplace_and_visit and can't assume another thread
// hasn't created an entry since this failure so erase only if
// ref_count is default (0).
if(rv < 0)
of.erase_if(ctx_->nodeid,
[](const auto &val_)
{
return (val_.second.ref_count <= 0);
});
return rv;
}
int
FUSE::create(const fuse_req_ctx_t *ctx_,
const char *fusepath_,

242
src/fuse_open.cpp

@ -21,6 +21,7 @@
#include "config.hpp"
#include "errno.hpp"
#include "fileinfo.hpp"
#include "fs_close.hpp"
#include "fs_cow.hpp"
#include "fs_fchmod.hpp"
#include "fs_lchmod.hpp"
@ -274,169 +275,126 @@ _(const PassthroughIOEnum e_,
static
int
_open_for_insert_lambda(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_,
State::OpenFile *of_)
_open(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_)
{
int rv;
FileInfo *fi;
auto &of = state.open_files;
::_config_to_ffi_flags(cfg,ctx_->pid,ffi_);
if(cfg.cache_writeback)
::_tweak_flags_cache_writeback(&ffi_->flags);
ffi_->noflush = !::_calculate_flush(cfg.flushonclose,ffi_->flags);
ffi_->noflush = !::_calculate_flush(cfg.flushonclose,
ffi_->flags);
rv = ::_open(cfg.func.open.policy,
cfg.branches,
fusepath_,
ffi_,
cfg.link_cow,
cfg.nfsopenhack);
if(rv < 0)
return rv;
fi = FileInfo::from_fh(ffi_->fh);
of_->ref_count = 1;
of_->fi = fi;
switch(_(cfg.passthrough_io,ffi_->flags))
while(true)
{
case _(PassthroughIO::ENUM::RO,O_RDONLY):
case _(PassthroughIO::ENUM::WO,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDONLY):
case _(PassthroughIO::ENUM::RW,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDWR):
break;
default:
return 0;
}
of_->backing_id = FUSE::passthrough_open(fi->fd);
if(of_->backing_id <= 0)
return 0;
ffi_->backing_id = of_->backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
return 0;
}
static
int
_open_for_update_lambda(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_,
State::OpenFile *of_)
{
int rv;
::_config_to_ffi_flags(cfg,ctx_->pid,ffi_);
if(cfg.cache_writeback)
::_tweak_flags_cache_writeback(&ffi_->flags);
FileInfo *fi = nullptr;
int backing_id = INVALID_BACKING_ID;
// The ref increment keeps fuse_release.cpp from erasing and
// freeing the entry.
of.visit(ctx_->nodeid,
[&](auto &v_)
{
v_.second.ref_count++;
fi = v_.second.fi;
backing_id = v_.second.backing_id;
});
// If the file is already open...
if(fi)
{
rv = ::_open_fd(fi->fd,
&fi->branch,
fusepath_,
ffi_);
// If we fail to open the already open file we need to treat it
// similarly to fuse_release.
if(rv < 0)
{
fi = nullptr;
backing_id = INVALID_BACKING_ID;
of.erase_if(ctx_->nodeid,
[&](auto &v_)
{
v_.second.ref_count--;
if(v_.second.ref_count > 0)
return false;
fi = v_.second.fi;
backing_id = v_.second.backing_id;
return true;
});
FUSE::passthrough_close(backing_id);
if(fi)
{
fs::close(fi->fd);
delete fi;
}
ffi_->noflush = !::_calculate_flush(cfg.flushonclose,
ffi_->flags);
return rv;
}
rv = ::_open_fd(of_->fi->fd,
&of_->fi->branch,
fusepath_,
ffi_);
if(rv < 0)
return rv;
if(!fuse_backing_id_is_valid(backing_id))
return 0;
of_->ref_count++;
ffi_->backing_id = backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
if(of_->backing_id <= 0)
return 0;
return 0;
}
ffi_->backing_id = of_->backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
// Was not open, do first open, try to insert, if someone beat us
// to it in another thread then throw it away and try again.
rv = ::_open(cfg.func.open.policy,
cfg.branches,
fusepath_,
ffi_,
cfg.link_cow,
cfg.nfsopenhack);
if(rv < 0)
return rv;
return rv;
}
fi = FileInfo::from_fh(ffi_->fh);
static
inline
auto
_open_insert_lambda(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_,
int *rv_)
{
return
[=](auto &val_)
{
*rv_ = ::_open_for_insert_lambda(ctx_,
fusepath_,
ffi_,
&val_.second);
};
}
static
inline
auto
_open_update_lambda(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_,
int *rv_)
{
return
[=](auto &val_)
{
// For the edge case where insert succeeded but the open failed
// and hadn't been cleaned up yet. There unfortunately is no way
// to abort an insert.
if(val_.second.ref_count <= 0)
switch(_(cfg.passthrough_io,ffi_->flags))
{
*rv_ = ::_open_for_insert_lambda(ctx_,
fusepath_,
ffi_,
&val_.second);
return;
case _(PassthroughIO::ENUM::RO,O_RDONLY):
case _(PassthroughIO::ENUM::WO,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDONLY):
case _(PassthroughIO::ENUM::RW,O_WRONLY):
case _(PassthroughIO::ENUM::RW,O_RDWR):
backing_id = FUSE::passthrough_open(fi->fd);
if(fuse_backing_id_is_valid(backing_id))
{
ffi_->backing_id = backing_id;
ffi_->passthrough = true;
ffi_->keep_cache = false;
}
break;
default:
break;
}
*rv_ = ::_open_for_update_lambda(ctx_,
fusepath_,
ffi_,
&val_.second);
};
}
bool inserted;
static
int
_open(const fuse_req_ctx_t *ctx_,
const fs::path &fusepath_,
fuse_file_info_t *ffi_)
{
int rv;
auto &of = state.open_files;
rv = -EINVAL;
of.try_emplace_and_visit(ctx_->nodeid,
::_open_insert_lambda(ctx_,fusepath_,ffi_,&rv),
::_open_update_lambda(ctx_,fusepath_,ffi_,&rv));
inserted = of.try_emplace(ctx_->nodeid,
backing_id,
fi);
if(inserted)
return 0;
// Can't abort an emplace_and_visit and can't assume another thread
// hasn't created an entry since this failure so erase only if
// ref_count is default (0).
if(rv < 0)
of.erase_if(ctx_->nodeid,
[](auto &val_)
{
return (val_.second.ref_count <= 0);
});
FUSE::passthrough_close(backing_id);
fs::close(fi->fd);
delete fi;
}
return rv;
return 0;
}

84
src/fuse_release.cpp

@ -26,35 +26,6 @@
#include "fuse.h"
static
constexpr
auto
_erase_if_lambda(FileInfo *fi_,
bool *existed_in_map_)
{
return
[=](auto &val_)
{
*existed_in_map_ = true;
if(fi_ != val_.second.fi)
{
fs::close(fi_->fd);
delete fi_;
}
val_.second.ref_count--;
if(val_.second.ref_count > 0)
return false;
if(val_.second.backing_id > 0)
FUSE::passthrough_close(val_.second.backing_id);
fs::close(val_.second.fi->fd);
delete val_.second.fi;
return true;
};
}
static
int
@ -62,7 +33,10 @@ _release(const fuse_req_ctx_t *ctx_,
FileInfo *fi_,
const bool dropcacheonclose_)
{
bool existed_in_map;
int backing_id = INVALID_BACKING_ID;
FileInfo *map_fi = nullptr;
bool fi_in_map = false;
auto &of = state.open_files;
// according to Feh of nocache calling it once doesn't always work
// https://github.com/Feh/nocache
@ -72,34 +46,46 @@ _release(const fuse_req_ctx_t *ctx_,
fs::fadvise_dontneed(fi_->fd);
}
// Because of course the API doesn't tell you if the key
// existed. Just how many it erased and in this case I only want to
// erase if there are no more open files.
existed_in_map = false;
state.open_files.erase_if(ctx_->nodeid,
::_erase_if_lambda(fi_,&existed_in_map));
if(existed_in_map)
u64 erased;
erased = of.erase_if(ctx_->nodeid,
[&](auto &v_)
{
fi_in_map = (fi_ == v_.second.fi);
v_.second.ref_count--;
if(v_.second.ref_count > 0)
return false;
backing_id = v_.second.backing_id;
map_fi = v_.second.fi;
return true;
});
if(not fi_in_map)
{
fs::close(fi_->fd);
delete fi_;
}
if(not erased)
return 0;
fs::close(fi_->fd);
delete fi_;
FUSE::passthrough_close(backing_id);
if(map_fi)
{
fs::close(map_fi->fd);
delete map_fi;
}
return 0;
}
static
int
_release(const fuse_req_ctx_t *ctx_,
const fuse_file_info_t *ffi_)
FUSE::release(const fuse_req_ctx_t *ctx_,
const fuse_file_info_t *ffi_)
{
FileInfo *fi = FileInfo::from_fh(ffi_->fh);
return ::_release(ctx_,fi,cfg.dropcacheonclose);
}
int
FUSE::release(const fuse_req_ctx_t *ctx_,
const fuse_file_info_t *ffi_)
{
return ::_release(ctx_,ffi_);
}

12
src/state.hpp

@ -4,13 +4,13 @@
#include "fileinfo.hpp"
#include "fuse.h"
#include <functional>
#include <map>
#include <string>
constexpr int INVALID_BACKING_ID = -1;
class State
{
public:
@ -26,6 +26,14 @@ public:
{
}
OpenFile(const int backing_id_,
FileInfo * const fi_)
: ref_count(1),
backing_id(backing_id_),
fi(fi_)
{
}
int ref_count;
int backing_id;
FileInfo *fi;

103
tests/TEST_io_passthrough_create_open

@ -0,0 +1,103 @@
#!/usr/bin/env python3
import os
import sys
import tempfile
import time
# Test create + write + read (passthrough.io=rw)
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
test_data = b"passthrough io test data for create\n" * 100
# Write to newly created file
bytes_written = os.write(fd, test_data)
if bytes_written != len(test_data):
print("create write failed: expected {} bytes, wrote {}".format(len(test_data), bytes_written))
sys.exit(1)
# Seek and read back
os.lseek(fd, 0, os.SEEK_SET)
read_data = os.read(fd, len(test_data))
if read_data != test_data:
print("create read failed: data mismatch")
sys.exit(1)
os.close(fd)
# Test open existing file + write + read
fd = os.open(filepath, os.O_RDWR)
# Read existing data
os.lseek(fd, 0, os.SEEK_SET)
read_data = os.read(fd, len(test_data))
if read_data != test_data:
print("open read failed: data mismatch")
sys.exit(1)
# Write more data at end
os.lseek(fd, 0, os.SEEK_END)
more_data = b"additional passthrough data for open\n" * 50
bytes_written = os.write(fd, more_data)
if bytes_written != len(more_data):
print("open write failed: expected {} bytes, wrote {}".format(len(more_data), bytes_written))
sys.exit(1)
# Verify all data
os.lseek(fd, 0, os.SEEK_SET)
all_data = os.read(fd, len(test_data) + len(more_data))
if all_data != test_data + more_data:
print("open final read failed: data mismatch")
sys.exit(1)
# Test multiple opens of same file (single backing_id feature)
# When a file is already open with passthrough, subsequent opens
# must reuse the same backing_id rather than creating new ones
fd2 = os.open(filepath, os.O_RDWR)
# Read from second fd - should see same data
os.lseek(fd2, 0, os.SEEK_SET)
read_data2 = os.read(fd2, len(test_data) + len(more_data))
if read_data2 != test_data + more_data:
print("second open read failed: data mismatch")
os.close(fd)
os.close(fd2)
sys.exit(1)
# Write from second fd
os.lseek(fd2, 0, os.SEEK_END)
extra_data = b"data from second file descriptor\n" * 25
bytes_written = os.write(fd2, extra_data)
if bytes_written != len(extra_data):
print("second open write failed: expected {} bytes, wrote {}".format(len(extra_data), bytes_written))
os.close(fd)
os.close(fd2)
sys.exit(1)
# Verify write is visible from first fd (shared backing)
os.lseek(fd, 0, os.SEEK_SET)
combined_data = os.read(fd, len(test_data) + len(more_data) + len(extra_data))
expected_data = test_data + more_data + extra_data
if combined_data != expected_data:
print("cross-fd read failed: data mismatch (writes from fd2 not visible on fd)")
os.close(fd)
os.close(fd2)
sys.exit(1)
# Open a third fd while others are still open
fd3 = os.open(filepath, os.O_RDONLY)
os.lseek(fd3, 0, os.SEEK_SET)
read_data3 = os.read(fd3, len(expected_data))
if read_data3 != expected_data:
print("third open read failed: data mismatch")
os.close(fd)
os.close(fd2)
os.close(fd3)
sys.exit(1)
os.close(fd3)
os.close(fd2)
os.close(fd)
# Cleanup
os.unlink(filepath)

132
tests/TEST_io_passthrough_open_race

@ -0,0 +1,132 @@
#!/usr/bin/env python3
import os
import sys
import tempfile
import threading
import time
NUM_THREADS = 50
TEST_DATA = b"race condition test data\n"
def open_and_operate(filepath, barrier, results, index):
"""
Wait at barrier, then open file, write, read, and store result.
"""
try:
# Wait for all threads to be ready
barrier.wait()
# All threads try to open simultaneously
fd = os.open(filepath, os.O_RDWR)
# Write thread-specific data at a unique offset
offset = index * len(TEST_DATA)
os.lseek(fd, offset, os.SEEK_SET)
bytes_written = os.write(fd, TEST_DATA)
if bytes_written != len(TEST_DATA):
results[index] = ("write_error", "expected {} bytes, wrote {}".format(len(TEST_DATA), bytes_written))
os.close(fd)
return
# Read back what we wrote
os.lseek(fd, offset, os.SEEK_SET)
read_data = os.read(fd, len(TEST_DATA))
if read_data != TEST_DATA:
results[index] = ("read_error", "data mismatch at offset {}".format(offset))
os.close(fd)
return
results[index] = ("success", fd)
except Exception as e:
results[index] = ("exception", str(e))
# Create test file with enough space for all threads
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
# Pre-allocate file to avoid issues with concurrent writes extending file
total_size = NUM_THREADS * len(TEST_DATA)
os.ftruncate(fd, total_size)
os.close(fd)
# Set up synchronization
barrier = threading.Barrier(NUM_THREADS)
results = [None] * NUM_THREADS
threads = []
# Create and start all threads
for i in range(NUM_THREADS):
t = threading.Thread(target=open_and_operate, args=(filepath, barrier, results, i))
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
# Check results
failed = False
fds_to_close = []
for i, result in enumerate(results):
if result is None:
print("thread {} returned no result".format(i))
failed = True
elif result[0] == "success":
fds_to_close.append(result[1])
else:
print("thread {} failed: {} - {}".format(i, result[0], result[1]))
failed = True
if failed:
for fd in fds_to_close:
os.close(fd)
os.unlink(filepath)
sys.exit(1)
# Verify all data is consistent by reading through one fd
if fds_to_close:
verify_fd = fds_to_close[0]
os.lseek(verify_fd, 0, os.SEEK_SET)
all_data = os.read(verify_fd, total_size)
expected_data = TEST_DATA * NUM_THREADS
if all_data != expected_data:
print("final verification failed: data mismatch")
print("expected {} bytes, got {} bytes".format(len(expected_data), len(all_data)))
for fd in fds_to_close:
os.close(fd)
os.unlink(filepath)
sys.exit(1)
# Test cross-fd visibility: write from one fd, read from another
if len(fds_to_close) >= 2:
fd_a = fds_to_close[0]
fd_b = fds_to_close[1]
# Write new data from fd_a
new_data = b"cross-fd visibility test\n"
os.lseek(fd_a, 0, os.SEEK_SET)
os.write(fd_a, new_data)
# Read from fd_b - should see the new data
os.lseek(fd_b, 0, os.SEEK_SET)
read_back = os.read(fd_b, len(new_data))
if read_back != new_data:
print("cross-fd visibility failed: write from fd_a not visible on fd_b")
for fd in fds_to_close:
os.close(fd)
os.unlink(filepath)
sys.exit(1)
# Close all file descriptors
for fd in fds_to_close:
os.close(fd)
# Cleanup
os.unlink(filepath)

5
tests/tests.cpp

@ -123,12 +123,11 @@ test_str_stuff()
void
test_config_branches()
{
uint64_t minfreespace;
Branches b(minfreespace);
Branches b;
Branches::Ptr bcp0;
Branches::Ptr bcp1;
minfreespace = 1234;
b.minfreespace = 1234;
TEST_CHECK(b->minfreespace() == 1234);
TEST_CHECK(b.to_string() == "");

Loading…
Cancel
Save