diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespalib/src/tests/io |
Publish
Diffstat (limited to 'vespalib/src/tests/io')
-rw-r--r-- | vespalib/src/tests/io/fileutil/.gitignore | 9 | ||||
-rw-r--r-- | vespalib/src/tests/io/fileutil/CMakeLists.txt | 8 | ||||
-rw-r--r-- | vespalib/src/tests/io/fileutil/DESC | 1 | ||||
-rw-r--r-- | vespalib/src/tests/io/fileutil/FILES | 1 | ||||
-rw-r--r-- | vespalib/src/tests/io/fileutil/fileutiltest.cpp | 686 |
5 files changed, 705 insertions, 0 deletions
diff --git a/vespalib/src/tests/io/fileutil/.gitignore b/vespalib/src/tests/io/fileutil/.gitignore new file mode 100644 index 00000000000..5c3f8a57921 --- /dev/null +++ b/vespalib/src/tests/io/fileutil/.gitignore @@ -0,0 +1,9 @@ +.depend +Makefile +fileutil_test +mydir +myfile +myotherdir +targetfile +/linkeddir +vespalib_fileutil_test_app diff --git a/vespalib/src/tests/io/fileutil/CMakeLists.txt b/vespalib/src/tests/io/fileutil/CMakeLists.txt new file mode 100644 index 00000000000..2b6af2f2dce --- /dev/null +++ b/vespalib/src/tests/io/fileutil/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_fileutil_test_app + SOURCES + fileutiltest.cpp + DEPENDS + vespalib +) +vespa_add_test(NAME vespalib_fileutil_test_app COMMAND vespalib_fileutil_test_app) diff --git a/vespalib/src/tests/io/fileutil/DESC b/vespalib/src/tests/io/fileutil/DESC new file mode 100644 index 00000000000..b3a05049161 --- /dev/null +++ b/vespalib/src/tests/io/fileutil/DESC @@ -0,0 +1 @@ +File utilities test diff --git a/vespalib/src/tests/io/fileutil/FILES b/vespalib/src/tests/io/fileutil/FILES new file mode 100644 index 00000000000..99d2eae90c6 --- /dev/null +++ b/vespalib/src/tests/io/fileutil/FILES @@ -0,0 +1 @@ +fileutiltest.cpp diff --git a/vespalib/src/tests/io/fileutil/fileutiltest.cpp b/vespalib/src/tests/io/fileutil/fileutiltest.cpp new file mode 100644 index 00000000000..4dbb8e0756d --- /dev/null +++ b/vespalib/src/tests/io/fileutil/fileutiltest.cpp @@ -0,0 +1,686 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("fileutil_test"); +#include <vespa/vespalib/io/fileutil.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <iostream> +#include <vector> + +namespace vespalib { + +class Test : public vespalib::TestApp +{ +public: + void testOpen(); + void testIsOpen(); + void testStat(); + void testResize(); + void testDirFunctions(); + void testUnlink(); + void testRename(); + void testCopy(); + void testCopyConstructorAndAssignmentOperator(); + void testLazyFile(); + void testSymlink(); + void testReadAll(); + int Main(); +}; + +int +Test::Main() +{ + TEST_INIT("fileutil_test"); + srandom(1); + std::cerr << "testOpen\n"; + testOpen(); + std::cerr << "testIsOpen\n"; + testIsOpen(); + std::cerr << "testStat\n"; + testStat(); + std::cerr << "testResize\n"; + testResize(); + std::cerr << "testDirFunctions\n"; + testDirFunctions(); + std::cerr << "testUnlink\n"; + testUnlink(); + std::cerr << "testRename\n"; + testRename(); + std::cerr << "testCopy\n"; + testCopy(); + std::cerr << "testCopyConstructorAndAssignmentOperator\n"; + testCopyConstructorAndAssignmentOperator(); + std::cerr << "testLazyFile\n"; + testLazyFile(); + std::cerr << "testSymlink\n"; + testSymlink(); + std::cerr << "testReadAll\n"; + testReadAll(); + TEST_DONE(); +} + +void +Test::testOpen() +{ + // Opening non-existing file for reading should fail. + try{ + unlink("myfile"); // Just in case + File f("myfile"); + f.open(File::READONLY); + TEST_FATAL("Opening non-existing file for reading should fail."); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + } + // Opening non-existing file for writing without CREATE flag fails + try{ + File f("myfile"); + f.open(0); + TEST_FATAL("Opening non-existing file without CREATE flag should fail."); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + } + // Opening file in non-existing subdir should fail. + try{ + rmdir("mydir", true); // Just in case + File f("mydir/myfile"); + f.open(File::CREATE); + TEST_FATAL("Opening non-existing file for reading should fail."); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + } + // Opening file for reading in non-existing subdir should not create + // subdir. + try{ + File f("mydir/myfile"); + f.open(File::READONLY, true); + TEST_FATAL("Read only parameter doesn't work with auto-generate"); + } catch (IllegalArgumentException& e) { + } + // Opening file in non-existing subdir without auto-generating + // directories should not work. + try{ + File f("mydir/myfile"); + f.open(File::CREATE, false); + TEST_FATAL("Need to autogenerate directories for this to work"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + ASSERT_TRUE(!fileExists("mydir")); + } + // Opening file in non-existing subdir works with auto-generate + { + File f("mydir/myfile"); + f.open(File::CREATE, true); + ASSERT_TRUE(fileExists("mydir/myfile")); + f.unlink(); + } + // Opening file in existing subdir works with auto-generate + { + File f("mydir/myfile"); + f.open(File::CREATE, false); + ASSERT_TRUE(fileExists("mydir/myfile")); + f.unlink(); + } + // Opening with direct IO support works. + { + File f("mydir/myfile"); + f.open(File::CREATE | File::DIRECTIO, false); + ASSERT_TRUE(fileExists("mydir/myfile")); + if (!f.isOpenWithDirectIO()) { + std::cerr << "This platform does not support direct IO\n"; + } + } + // Opening plain file works + { + File f("myfile"); + f.open(File::CREATE, false); + ASSERT_TRUE(fileExists("myfile")); + } + // Opening directory does not work. + try{ + File f("mydir"); + f.open(File::CREATE, false); + TEST_FATAL("Can't open directory for reading"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::ILLEGAL_PATH, e.getType()); + } + // Test opening already open file + { + std::unique_ptr<File> f(new File("myfile")); + f->open(File::CREATE, false); + f->closeFileWhenDestructed(false); + File f2(f->getFileDescriptor(), "myfile"); + f.reset(); + ASSERT_TRUE(f2.isOpen()); + f2.write(" ", 1, 0); + } + // Test reopening file in same object + { + File f("myfile"); + f.open(File::CREATE, false); + f.write("a", 1, 0); + f.close(); + f.open(File::CREATE, false); + std::vector<char> vec(10); + size_t read = f.read(&vec[0], 10, 0); + EXPECT_EQUAL(1u, read); + EXPECT_EQUAL('a', vec[0]); + f.write("b", 1, 0); + } +} + +void +Test::testIsOpen() +{ + File f("myfile"); + ASSERT_TRUE(!f.isOpen()); + f.open(File::CREATE, false); + ASSERT_TRUE(f.isOpen()); + f.close(); + ASSERT_TRUE(!f.isOpen()); +} + +void +Test::testStat() +{ + unlink("myfile"); + rmdir("mydir", true); + EXPECT_EQUAL(false, fileExists("myfile")); + EXPECT_EQUAL(false, fileExists("mydir")); + mkdir("mydir"); + FileInfo::UP info = stat("myfile"); + ASSERT_TRUE(info.get() == 0); + File f("myfile"); + f.open(File::CREATE, false); + f.write("foobar", 6, 0); + + info = stat("myfile"); + ASSERT_TRUE(info.get() != 0); + FileInfo info2 = f.stat(); + EXPECT_EQUAL(*info, info2); + EXPECT_EQUAL(6, info->_size); + EXPECT_EQUAL(true, info->_plainfile); + EXPECT_EQUAL(false, info->_directory); + + EXPECT_EQUAL(6, f.getFileSize()); + f.close(); + EXPECT_EQUAL(6, getFileSize("myfile")); + + EXPECT_EQUAL(true, isDirectory("mydir")); + EXPECT_EQUAL(false, isDirectory("myfile")); + EXPECT_EQUAL(false, isPlainFile("mydir")); + EXPECT_EQUAL(true, isPlainFile("myfile")); + EXPECT_EQUAL(true, fileExists("myfile")); + EXPECT_EQUAL(true, fileExists("mydir")); +} + +void +Test::testResize() +{ + unlink("myfile"); + File f("myfile"); + f.open(File::CREATE, false); + f.write("foobar", 6, 0); + EXPECT_EQUAL(6, f.getFileSize()); + f.resize(10); + EXPECT_EQUAL(10, f.getFileSize()); + std::vector<char> vec(20, ' '); + size_t read = f.read(&vec[0], 20, 0); + EXPECT_EQUAL(10u, read); + EXPECT_EQUAL(std::string("foobar"), std::string(&vec[0], 6)); + f.resize(3); + EXPECT_EQUAL(3, f.getFileSize()); + read = f.read(&vec[0], 20, 0); + EXPECT_EQUAL(3u, read); + EXPECT_EQUAL(std::string("foo"), std::string(&vec[0], 3)); +} + +void +Test::testDirFunctions() +{ + rmdir("mydir", true); + ASSERT_TRUE(!fileExists("mydir")); + // Cannot create recursive without recursive option + try{ + mkdir("mydir/otherdir", false); + TEST_FATAL("Should not work without recursive option set"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + } + // Works with recursive option + { + ASSERT_TRUE(mkdir("mydir/otherdir")); + ASSERT_TRUE(!mkdir("mydir/otherdir")); + // Test chdir / getCurrentDirectory + chdir("mydir"); + std::string currDir = getCurrentDirectory(); + std::string::size_type pos = currDir.rfind('/'); + ASSERT_TRUE(pos != std::string::npos); + EXPECT_EQUAL("mydir", currDir.substr(pos + 1)); + EXPECT_EQUAL('/', currDir[0]); + chdir(".."); + currDir = getCurrentDirectory(); + pos = currDir.rfind('/'); + EXPECT_EQUAL("fileutil", currDir.substr(pos + 1)); + } + // rmdir fails with content + try{ + ASSERT_TRUE(mkdir("mydir/otherdir/evenmorestuff")); + rmdir("mydir"); + TEST_FATAL("Should not work without recursive option set"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::DIRECTORY_HAVE_CONTENT, e.getType()); + } + // Works with recursive option + { + ASSERT_TRUE(rmdir("mydir", true)); + ASSERT_TRUE(!fileExists("mydir")); + ASSERT_TRUE(!rmdir("mydir", true)); + } + // Doesn't work on file + try{ + { + File f("myfile"); + f.open(File::CREATE); + f.write("foo", 3, 0); + } + rmdir("myfile"); + TEST_FATAL("Should have failed to run rmdir on file."); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::ILLEGAL_PATH, e.getType()); + } + + // mkdir works when a path component is a symlink which points to + // another directory and the final path component does not exist. + { + rmdir("mydir", true); + rmdir("otherdir", true); + unlink("linkeddir"); // symlink if exists + mkdir("otherdir/stuff"); + symlink("otherdir", "linkeddir"); + // Should now be able to resolve through symlink and create dir + // at the appropriate (linked) location. + ASSERT_TRUE(mkdir("linkeddir/stuff/fluff")); + } + + // mkdir works when the final path component is a symlink which points + // to another directory (causing OS mkdir to fail since something already + // exists at the location). + { + rmdir("mydir", true); + rmdir("otherdir", true); + unlink("linkeddir"); // symlink if exists + mkdir("otherdir/stuff"); + symlink("otherdir", "linkeddir"); + // Should now be able to resolve through symlink and create dir + // at the appropriate (linked) location. + ASSERT_FALSE(mkdir("linkeddir")); + } +} + +void +Test::testUnlink() +{ + // Fails on directory + try{ + mkdir("mydir"); + unlink("mydir"); + TEST_FATAL("Should work on directories."); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::ILLEGAL_PATH, e.getType()); + } + // Works for file + { + { + File f("myfile"); + f.open(File::CREATE); + f.write("foo", 3, 0); + } + ASSERT_TRUE(fileExists("myfile")); + ASSERT_TRUE(unlink("myfile")); + ASSERT_TRUE(!fileExists("myfile")); + ASSERT_TRUE(!unlink("myfile")); + } +} + +void +Test::testRename() +{ + rmdir("mydir", true); + File f("myfile"); + f.open(File::CREATE | File::TRUNC); + f.write("Hello World!\n", 13, 0); + f.close(); + // Renaming to non-existing dir doesn't work + try{ + rename("myfile", "mydir/otherfile"); + TEST_FATAL("This shouldn't work when mydir doesn't exist"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + } + // Renaming to non-existing dir works if autocreating dirs + { + ASSERT_TRUE(rename("myfile", "mydir/otherfile", true, true)); + ASSERT_TRUE(!fileExists("myfile")); + ASSERT_TRUE(fileExists("mydir/otherfile")); + + File f2("mydir/otherfile"); + f2.open(File::READONLY); + std::vector<char> vec(20, ' '); + size_t read = f2.read(&vec[0], 20, 0); + EXPECT_EQUAL(13u, read); + EXPECT_EQUAL(std::string("Hello World!\n"), std::string(&vec[0], 13)); + } + // Renaming non-existing returns false + ASSERT_TRUE(!rename("myfile", "mydir/otherfile", true)); + // Rename to overwrite works + { + f.open(File::CREATE | File::TRUNC); + f.write("Bah\n", 4, 0); + f.close(); + ASSERT_TRUE(rename("myfile", "mydir/otherfile", true, true)); + + File f2("mydir/otherfile"); + f2.open(File::READONLY); + std::vector<char> vec(20, ' '); + size_t read = f2.read(&vec[0], 20, 0); + EXPECT_EQUAL(4u, read); + EXPECT_EQUAL(std::string("Bah\n"), std::string(&vec[0], 4)); + } + // Overwriting directory fails (does not put inside dir) + try{ + mkdir("mydir"); + f.open(File::CREATE | File::TRUNC); + f.write("Bah\n", 4, 0); + f.close(); + ASSERT_TRUE(rename("myfile", "mydir")); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::ILLEGAL_PATH, e.getType()); + } + // Moving directory works + { + ASSERT_TRUE(isDirectory("mydir")); + rmdir("myotherdir", true); + ASSERT_TRUE(rename("mydir", "myotherdir")); + ASSERT_TRUE(isDirectory("myotherdir")); + ASSERT_TRUE(!isDirectory("mydir")); + ASSERT_TRUE(!rename("mydir", "myotherdir")); + } + // Overwriting directory fails + try{ + File f2("mydir/yetanotherfile"); + f2.open(File::CREATE, true); + f2.write("foo", 3, 0); + f2.open(File::READONLY); + f2.close(); + rename("mydir", "myotherdir"); + TEST_FATAL("Should fail trying to overwrite directory"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_TRUE((IoException::DIRECTORY_HAVE_CONTENT == e.getType()) || + (IoException::ALREADY_EXISTS == e.getType())); + } +} + +void +Test::testCopy() +{ + rmdir("mydir", true); + File f("myfile"); + f.open(File::CREATE | File::TRUNC); + + MallocAutoPtr buffer = getAlignedBuffer(5000); + memset(buffer.get(), 0, 5000); + strncpy(static_cast<char*>(buffer.get()), "Hello World!\n", 13); + f.write(buffer.get(), 4096, 0); + f.close(); + std::cerr << "Simple copy\n"; + // Simple copy works (512b dividable file) + copy("myfile", "targetfile"); + ASSERT_TRUE(system("diff myfile targetfile") == 0); + std::cerr << "Overwriting\n"; + // Overwriting works (may not be able to use direct IO writing on all + // systems, so will always use cached IO) + { + f.open(File::CREATE | File::TRUNC); + f.write("Bah\n", 4, 0); + f.close(); + + ASSERT_TRUE(system("diff myfile targetfile > /dev/null") != 0); + copy("myfile", "targetfile"); + ASSERT_TRUE(system("diff myfile targetfile > /dev/null") == 0); + } + // Fails if target is directory + try{ + mkdir("mydir"); + copy("myfile", "mydir"); + TEST_FATAL("Should fail trying to overwrite directory"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::ILLEGAL_PATH, e.getType()); + } + // Fails if source is directory + try{ + mkdir("mydir"); + copy("mydir", "myfile"); + TEST_FATAL("Should fail trying to copy directory"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::ILLEGAL_PATH, e.getType()); + } +} + +void +Test::testCopyConstructorAndAssignmentOperator() +{ + // Copy file not opened. + { + File f("myfile"); + File f2(f); + EXPECT_EQUAL(f.getFilename(), f2.getFilename()); + } + // Copy file opened + { + File f("myfile"); + f.open(File::CREATE); + File f2(f); + EXPECT_EQUAL(f.getFilename(), f2.getFilename()); + ASSERT_TRUE(f2.isOpen()); + ASSERT_TRUE(!f.isOpen()); + } + // Assign file opened to another file opened + { + File f("myfile"); + f.open(File::CREATE); + int fd = f.getFileDescriptor(); + File f2("targetfile"); + f2.open(File::CREATE); + f = f2; + EXPECT_EQUAL(std::string("targetfile"), f2.getFilename()); + EXPECT_EQUAL(f.getFilename(), f2.getFilename()); + ASSERT_TRUE(!f2.isOpen()); + ASSERT_TRUE(f.isOpen()); + try{ + File f3(fd, "myfile"); + f3.closeFileWhenDestructed(false); // Already closed + f3.write("foo", 3, 0); + TEST_FATAL("This file descriptor should have been closed"); + } catch (IoException& e) { + //std::cerr << e.what() << "\n"; + EXPECT_EQUAL(IoException::INTERNAL_FAILURE, e.getType()); + } + } +} + +void +Test::testLazyFile() +{ + // Copy constructor + { + LazyFile file("myfile", File::CREATE, true); + LazyFile file2(file); + EXPECT_EQUAL(file.getFlags(), file2.getFlags()); + EXPECT_EQUAL(file.autoCreateDirectories(), file2.autoCreateDirectories()); + } + // Assignment + { + LazyFile file("myfile", File::CREATE, true); + LazyFile file2("targetfile", File::READONLY); + file = file2; + EXPECT_EQUAL(file.getFlags(), file2.getFlags()); + EXPECT_EQUAL(file.autoCreateDirectories(), file2.autoCreateDirectories()); + } + // Lazily write + { + LazyFile file("myfile", File::CREATE, true); + file.write("foo", 3, 0); + } + // Lazy stat + { + LazyFile file("myfile", File::CREATE, true); + EXPECT_EQUAL(3, file.getFileSize()); + file.close(); + + LazyFile file2("myfile", File::CREATE, true); + FileInfo info = file2.stat(); + EXPECT_EQUAL(3, info._size); + EXPECT_EQUAL(true, info._plainfile); + } + + // Lazy read + { + LazyFile file("myfile", File::CREATE, true); + std::vector<char> buf(10, ' '); + EXPECT_EQUAL(3u, file.read(&buf[0], 10, 0)); + EXPECT_EQUAL(std::string("foo"), std::string(&buf[0], 3)); + } + // Lazy resize + { + LazyFile file("myfile", File::CREATE, true); + file.resize(5); + EXPECT_EQUAL(5, file.getFileSize()); + } + // Lazy get file descriptor + { + LazyFile file("myfile", File::CREATE, true); + int fd = file.getFileDescriptor(); + ASSERT_TRUE(fd != -1); + } +} + +void +Test::testSymlink() +{ + // Target exists + { + rmdir("mydir", true); + mkdir("mydir"); + + File f("mydir/myfile"); + f.open(File::CREATE | File::TRUNC); + f.write("Hello World!\n", 13, 0); + f.close(); + + symlink("myfile", "mydir/linkyfile"); + EXPECT_TRUE(fileExists("mydir/linkyfile")); + + File f2("mydir/linkyfile"); + f2.open(File::READONLY); + std::vector<char> vec(20, ' '); + size_t read = f2.read(&vec[0], 20, 0); + EXPECT_EQUAL(13u, read); + EXPECT_EQUAL(std::string("Hello World!\n"), std::string(&vec[0], 13)); + } + + // POSIX symlink() fails + { + rmdir("mydir", true); + mkdir("mydir/a", true); + mkdir("mydir/b"); + try { + // Link already exists + symlink("a", "mydir/b"); + TEST_FATAL("Exception not thrown on already existing link"); + } catch (IoException& e) { + EXPECT_EQUAL(IoException::ALREADY_EXISTS, e.getType()); + } + } + + { + rmdir("mydir", true); + mkdir("mydir"); + + File f("mydir/myfile"); + f.open(File::CREATE | File::TRUNC); + f.write("Hello World!\n", 13, 0); + f.close(); + } + + // readLink success + { + symlink("myfile", "mydir/linkyfile"); + EXPECT_EQUAL("myfile", readLink("mydir/linkyfile")); + } + // readLink failure + { + try { + readLink("no/such/link"); + } catch (IoException& e) { + EXPECT_EQUAL(IoException::NOT_FOUND, e.getType()); + } + } +} + +void +Test::testReadAll() +{ + // Write text into a file. + unlink("myfile"); + File fileForWriting("myfile"); + fileForWriting.open(File::CREATE); + vespalib::string text = "This is some text. "; + fileForWriting.write(text.data(), text.size(), 0); + fileForWriting.close(); + + // Read contents of file, and verify it's identical. + File file("myfile"); + file.open(File::READONLY); + vespalib::string content = file.readAll(); + file.close(); + ASSERT_EQUAL(content, text); + + // Write lots of text into file. + off_t offset = 0; + fileForWriting.open(File::TRUNC); + while (offset < 10000) { + offset += fileForWriting.write(text.data(), text.size(), offset); + } + fileForWriting.close(); + + // Read it all and verify. + file.open(File::READONLY); + content = file.readAll(); + file.close(); + ASSERT_EQUAL(offset, static_cast<off_t>(content.size())); + + vespalib::string chunk; + for (offset = 0; offset < 10000; offset += text.size()) { + chunk.assign(content.begin() + offset, text.size()); + ASSERT_EQUAL(text, chunk); + } +} + +} // vespalib + +TEST_APPHOOK(vespalib::Test) + |