⛏️ index : haiku.git

/*
 * Copyright 2013, Haiku, Inc.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Ingo Weinhold, ingo_weinhold@gmx.de
 */


#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>

#include <algorithm>
#include <set>
#include <vector>

#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <Looper.h>
#include <ObjectList.h>
#include <Path.h>
#include <String.h>

#include <AutoDeleter.h>
#include <AutoLocker.h>
#include <NotOwningEntryRef.h>
#include <PathMonitor.h>


using BPrivate::BPathMonitor;


static const char* const kTestBasePath = "/tmp/path-monitor-test";
static const bigtime_t kMaxNotificationDelay = 100000;


#define FATAL(...)													\
	do {															\
		throw FatalException(										\
			BString().SetToFormat("%s:%d: ", __FILE__, __LINE__)	\
				<< BString().SetToFormat(__VA_ARGS__));				\
	} while (false)

#define FATAL_IF_ERROR(error, ...)										\
	do {																\
		status_t _fatalError = (error);									\
		if (_fatalError < 0) {											\
			throw FatalException(										\
				BString().SetToFormat("%s:%d: ", __FILE__, __LINE__)	\
					<< BString().SetToFormat(__VA_ARGS__)				\
					<< BString().SetToFormat(							\
						": %s\n", strerror(_fatalError)));				\
		}																\
	} while (false)

#define FATAL_IF_POSIX_ERROR(error, ...)	\
	if ((error) < 0)						\
		FATAL_IF_ERROR(errno, __VA_ARGS__)

#define FAIL(...)	\
	throw TestException(BString().SetToFormat(__VA_ARGS__))


struct TestException {
	TestException(const BString& message)
		:
		fMessage(message)
	{
	}

	const BString& Message() const
	{
		return fMessage;
	}

private:
	BString	fMessage;
};


struct FatalException {
	FatalException(const BString& message)
		:
		fMessage(message)
	{
	}

	const BString& Message() const
	{
		return fMessage;
	}

private:
	BString	fMessage;
};


static BString
test_path(const BString& maybeRelativePath)
{
	if (maybeRelativePath.ByteAt(0) == '/')
		return maybeRelativePath;

	BString path;
	path.SetToFormat("%s/%s", kTestBasePath, maybeRelativePath.String());
	if (path.IsEmpty())
		FATAL_IF_ERROR(B_NO_MEMORY, "Failed to make absolute path");
	return path;
}


static BString
node_ref_to_string(const node_ref& nodeRef)
{
	return BString().SetToFormat("%" B_PRIdDEV ":%" B_PRIdINO, nodeRef.device,
		nodeRef.node);
}


static BString
entry_ref_to_string(const entry_ref& entryRef)
{
	return BString().SetToFormat("%" B_PRIdDEV ":%" B_PRIdINO ":\"%s\"",
		entryRef.device, entryRef.directory, entryRef.name);
}


static BString
indented_string(const char* string, const char* indent,
	const char* firstIndent = NULL)
{
	const char* end = string + strlen(string);
	BString result;
	const char* line = string;
	while (line < end) {
		const char* lineEnd = strchr(line, '\n');
		lineEnd = lineEnd != NULL ? lineEnd + 1 : end;
		result
			<< (line == string && firstIndent != NULL ? firstIndent : indent);
		result.Append(line, lineEnd - line);
		line = lineEnd;
	}

	return result;
}


static BString
message_to_string(const BMessage& message)
{
	BString result;

	char* name;
	type_code typeCode;
	int32 count;
	for (int32 i = 0;
		message.GetInfo(B_ANY_TYPE, i, &name, &typeCode, &count) == B_OK;
		i++) {
		if (i > 0)
			result << '\n';

		result << '"' << name << '"';
		BString type;

		switch (typeCode) {
			case B_UINT8_TYPE:
			case B_INT8_TYPE:
				type << "int8";
				break;

			case B_UINT16_TYPE:
				type = "u";
			case B_INT16_TYPE:
				type << "int16";
				break;

			case B_UINT32_TYPE:
				type = "u";
			case B_INT32_TYPE:
				type << "int32";
				break;

			case B_UINT64_TYPE:
				type = "u";
			case B_INT64_TYPE:
				type << "int64";
				break;

			case B_STRING_TYPE:
				type = "string";
				break;

			default:
			{
				int code = (int)typeCode;
				type.SetToFormat("'%02x%02x%02x%02x'", code >> 24,
					(code >> 16) & 0xff, (code >> 8) & 0xff, code & 0xff);
				break;
			}
		}

		result << " (" << type << "):";

		for (int32 k = 0; k < count; k++) {
			BString value;
			switch (typeCode) {
				case B_UINT8_TYPE:
					value << message.GetUInt8(name, k, 0);
					break;
				case B_INT8_TYPE:
					value << message.GetInt8(name, k, 0);
					break;
				case B_UINT16_TYPE:
					value << message.GetUInt16(name, k, 0);
					break;
				case B_INT16_TYPE:
					value << message.GetInt16(name, k, 0);
					break;
				case B_UINT32_TYPE:
					value << message.GetUInt32(name, k, 0);
					break;
				case B_INT32_TYPE:
					value << message.GetInt32(name, k, 0);
					break;
				case B_UINT64_TYPE:
					value << message.GetUInt64(name, k, 0);
					break;
				case B_INT64_TYPE:
					value << message.GetInt64(name, k, 0);
					break;
				case B_STRING_TYPE:
					value.SetToFormat("\"%s\"", message.GetString(name, k, ""));
					break;
				default:
				{
					const void* data;
					ssize_t size;
					if (message.FindData(name, typeCode, k, &data, &size)
							!= B_OK) {
						value = "???";
						break;
					}

					for (ssize_t l = 0; l < size; l++) {
						uint8 v = ((const uint8*)data)[l];
						value << BString().SetToFormat("%02x", v);
					}
					break;
				}
			}

			if (k == 0 && count == 1) {
				result << ' ' << value;
			} else {
				result << BString().SetToFormat("\n  [%2" B_PRId32 "] ", k)
					<< value;
			}
		}
	}

	return result;
}


static BString
watch_flags_to_string(uint32 flags)
{
	BString result;
	if ((flags & B_WATCH_NAME) != 0)
		result << "name ";
	if ((flags & B_WATCH_STAT) != 0)
		result << "stat ";
	if ((flags & B_WATCH_ATTR) != 0)
		result << "attr ";
	if ((flags & B_WATCH_DIRECTORY) != 0)
		result << "dir ";
	if ((flags & B_WATCH_RECURSIVELY) != 0)
		result << "recursive ";
	if ((flags & B_WATCH_FILES_ONLY) != 0)
		result << "files-only ";
	if ((flags & B_WATCH_DIRECTORIES_ONLY) != 0)
		result << "dirs-only ";

	if (!result.IsEmpty())
		result.Truncate(result.Length() - 1);
	return result;
}


struct MonitoringInfo {
	MonitoringInfo()
	{
	}

	MonitoringInfo(int32 opcode, const char* path)
		:
		fOpcode(opcode)
	{
		_Init(opcode, path);
	}

	MonitoringInfo(int32 opcode, const char* fromPath, const char* toPath)
	{
		_Init(opcode, toPath);

		// init fFromEntryRef
		BEntry entry;
		FATAL_IF_ERROR(entry.SetTo(fromPath),
			"Failed to init BEntry for \"%s\"", fromPath);
		FATAL_IF_ERROR(entry.GetRef(&fFromEntryRef),
			"Failed to get entry_ref for \"%s\"", fromPath);
	}

	BString ToString() const
	{
		switch (fOpcode) {
			case B_ENTRY_CREATED:
			case B_ENTRY_REMOVED:
				return BString().SetToFormat("%s %s at %s",
					fOpcode == B_ENTRY_CREATED ? "created" : "removed",
					node_ref_to_string(fNodeRef).String(),
					entry_ref_to_string(fEntryRef).String());

			case B_ENTRY_MOVED:
				return BString().SetToFormat("moved %s from %s to %s",
					node_ref_to_string(fNodeRef).String(),
					entry_ref_to_string(fFromEntryRef).String(),
					entry_ref_to_string(fEntryRef).String());

			case B_STAT_CHANGED:
				return BString().SetToFormat("stat changed for %s",
					node_ref_to_string(fNodeRef).String());

			case B_ATTR_CHANGED:
				return BString().SetToFormat("attr changed for %s",
					node_ref_to_string(fNodeRef).String());

			case B_DEVICE_MOUNTED:
				return BString().SetToFormat("volume mounted");

			case B_DEVICE_UNMOUNTED:
				return BString().SetToFormat("volume unmounted");
		}

		return BString();
	}

	bool Matches(const BMessage& message) const
	{
		if (fOpcode != message.GetInt32("opcode", -1))
			return false;

		switch (fOpcode) {
			case B_ENTRY_CREATED:
			case B_ENTRY_REMOVED:
			{
				NotOwningEntryRef entryRef;
				node_ref nodeRef;

				if (message.FindInt32("device", &nodeRef.device) != B_OK
					|| message.FindInt64("node", &nodeRef.node) != B_OK
					|| message.FindInt64("directory", &entryRef.directory)
						!= B_OK
					|| message.FindString("name", (const char**)&entryRef.name)
						!= B_OK) {
					return false;
				}
				entryRef.device = nodeRef.device;

				return nodeRef == fNodeRef && entryRef == fEntryRef;
			}

			case B_ENTRY_MOVED:
			{
				NotOwningEntryRef fromEntryRef;
				NotOwningEntryRef toEntryRef;
				node_ref nodeRef;

				if (message.FindInt32("node device", &nodeRef.device) != B_OK
					|| message.FindInt64("node", &nodeRef.node) != B_OK
					|| message.FindInt32("device", &fromEntryRef.device)
						!= B_OK
					|| message.FindInt64("from directory",
						&fromEntryRef.directory) != B_OK
					|| message.FindInt64("to directory", &toEntryRef.directory)
						!= B_OK
					|| message.FindString("from name",
						(const char**)&fromEntryRef.name) != B_OK
					|| message.FindString("name",
						(const char**)&toEntryRef.name) != B_OK) {
					return false;
				}
				toEntryRef.device = fromEntryRef.device;

				return nodeRef == fNodeRef && toEntryRef == fEntryRef
					&& fromEntryRef == fFromEntryRef;
			}

			case B_STAT_CHANGED:
			case B_ATTR_CHANGED:
			{
				node_ref nodeRef;

				if (message.FindInt32("device", &nodeRef.device) != B_OK
					|| message.FindInt64("node", &nodeRef.node) != B_OK) {
					return false;
				}

				return nodeRef == fNodeRef;
			}

			case B_DEVICE_MOUNTED:
			case B_DEVICE_UNMOUNTED:
				return true;
		}

		return false;
	}

private:
	void _Init(int32 opcode, const char* path)
	{
		fOpcode = opcode;
		BEntry entry;
		FATAL_IF_ERROR(entry.SetTo(path), "Failed to init BEntry for \"%s\"",
			path);
		FATAL_IF_ERROR(entry.GetRef(&fEntryRef),
			"Failed to get entry_ref for \"%s\"", path);
		FATAL_IF_ERROR(entry.GetNodeRef(&fNodeRef),
			"Failed to get node_ref for \"%s\"", path);
	}

private:
	int32		fOpcode;
	node_ref	fNodeRef;
	entry_ref	fEntryRef;
	entry_ref	fFromEntryRef;
};


struct MonitoringInfoSet {
	MonitoringInfoSet()
	{
	}

	MonitoringInfoSet& Add(const MonitoringInfo& info, bool expected = true)
	{
		if (expected)
			fInfos.push_back(info);
		return *this;
	}

	MonitoringInfoSet& Add(int32 opcode, const BString& path,
		bool expected = true)
	{
		return Add(MonitoringInfo(opcode, test_path(path)), expected);
	}

	MonitoringInfoSet& Add(int32 opcode, const BString& fromPath,
		const BString& toPath, bool expected = true)
	{
		return Add(MonitoringInfo(opcode, test_path(fromPath),
			test_path(toPath)), expected);
	}

	bool IsEmpty() const
	{
		return fInfos.empty();
	}

	int32 CountInfos() const
	{
		return fInfos.size();
	}

	const MonitoringInfo& InfoAt(int32 index) const
	{
		return fInfos[index];
	}

	void Remove(int32 index)
	{
		fInfos.erase(fInfos.begin() + index);
	}

	BString ToString() const
	{
		BString result;
		for (int32 i = 0; i < CountInfos(); i++) {
			const MonitoringInfo& info = InfoAt(i);
			if (i > 0)
				result << '\n';
			result << info.ToString();
		}
		return result;
	}

private:
	std::vector<MonitoringInfo>	fInfos;
};


struct Test : private BLooper {
	Test(const char* name)
		:
		fName(name),
		fFlags(0),
		fLooperThread(-1),
		fNotifications(10, true),
		fProcessedMonitoringInfos(),
		fIsWatching(false)
	{
	}

	void Init(uint32 flags)
	{
		fFlags = flags;

		// delete and re-create the test directory
		BEntry entry;
		FATAL_IF_ERROR(entry.SetTo(kTestBasePath),
			"Failed to init entry to \"%s\"", kTestBasePath);

		if (entry.Exists())
			_RemoveRecursively(entry);

		_CreateDirectory(kTestBasePath);

		fLooperThread = BLooper::Run();
		if (fLooperThread < 0)
			FATAL_IF_ERROR(fLooperThread, "Failed to init looper");
	}

	void Delete()
	{
		if (fIsWatching)
			BPathMonitor::StopWatching(this);

		if (fLooperThread < 0) {
			delete this;
		} else {
			PostMessage(B_QUIT_REQUESTED);
			wait_for_thread(fLooperThread, NULL);
		}
	}

	void Do()
	{
		bool recursive = (fFlags & B_WATCH_RECURSIVELY) != 0;
		DoInternal(recursive && (fFlags & B_WATCH_DIRECTORIES_ONLY) != 0,
			recursive && (fFlags & B_WATCH_FILES_ONLY) != 0, recursive,
			!recursive && (fFlags & B_WATCH_DIRECTORY) == 0,
			(fFlags & B_WATCH_STAT) != 0);

		// verify that there aren't any spurious notifications
		snooze(kMaxNotificationDelay);

		AutoLocker<BLooper> locker(this);
		if (fNotifications.IsEmpty())
			return;

		BString pendingNotifications
			= "unexpected notification(s) at end of test:";
		for (int32 i = 0; BMessage* message = fNotifications.ItemAt(i); i++) {
			pendingNotifications << '\n'
				<< indented_string(message_to_string(*message), "    ", "  * ");
		}

		FAIL("%s%s", pendingNotifications.String(),
			_ProcessedInfosString().String());
	}

	const BString& Name() const
	{
		return fName;
	}

protected:
	~Test()
	{
	}

	void StartWatching(const char* path)
	{
		BString absolutePath(test_path(path));
		FATAL_IF_ERROR(BPathMonitor::StartWatching(absolutePath, fFlags, this),
			"Failed to start watching \"%s\"", absolutePath.String());
		fIsWatching = true;
	}

	MonitoringInfo CreateDirectory(const char* path)
	{
		BString absolutePath(test_path(path));
		_CreateDirectory(absolutePath);
		return MonitoringInfo(B_ENTRY_CREATED, absolutePath);
	}

	MonitoringInfo CreateFile(const char* path)
	{
		BString absolutePath(test_path(path));
		FATAL_IF_ERROR(
			BFile().SetTo(absolutePath, B_CREATE_FILE | B_READ_WRITE),
			"Failed to create file \"%s\"", absolutePath.String());
		return MonitoringInfo(B_ENTRY_CREATED, absolutePath);
	}

	MonitoringInfo MoveEntry(const char* fromPath, const char* toPath)
	{
		BString absoluteFromPath(test_path(fromPath));
		BString absoluteToPath(test_path(toPath));
		FATAL_IF_POSIX_ERROR(rename(absoluteFromPath, absoluteToPath),
			"Failed to move \"%s\" to \"%s\"", absoluteFromPath.String(),
			absoluteToPath.String());
		return MonitoringInfo(B_ENTRY_MOVED, absoluteFromPath, absoluteToPath);
	}

	MonitoringInfo RemoveEntry(const char* path)
	{
		BString absolutePath(test_path(path));
		MonitoringInfo info(B_ENTRY_REMOVED, absolutePath);
		BEntry entry;
		FATAL_IF_ERROR(entry.SetTo(absolutePath),
			"Failed to init BEntry for \"%s\"", absolutePath.String());
		FATAL_IF_ERROR(entry.Remove(),
			"Failed to remove entry \"%s\"", absolutePath.String());
		return info;
	}

	MonitoringInfo TouchEntry(const char* path)
	{
		BString absolutePath(test_path(path));
		FATAL_IF_POSIX_ERROR(utimes(absolutePath, NULL),
			"Failed to touch \"%s\"", absolutePath.String());
		MonitoringInfo info(B_STAT_CHANGED, absolutePath);
		return info;
	}

	void ExpectNotification(const MonitoringInfo& info, bool expected = true)
	{
		if (!expected)
			return;

		AutoLocker<BLooper> locker(this);
		if (fNotifications.IsEmpty()) {
			locker.Unlock();
			snooze(kMaxNotificationDelay);
			locker.Lock();
		}

		if (fNotifications.IsEmpty()) {
			FAIL("missing notification, expected:\n  %s",
				info.ToString().String());
		}

		BMessage* message = fNotifications.RemoveItemAt(0);
		ObjectDeleter<BMessage> messageDeleter(message);

		if (!info.Matches(*message)) {
			BString processedInfosString(_ProcessedInfosString());
			FAIL("unexpected notification:\n  expected:\n  %s\n  got:\n%s%s",
				info.ToString().String(),
				indented_string(message_to_string(*message), "    ").String(),
				processedInfosString.String());
		}

		fProcessedMonitoringInfos.Add(info);
	}

	void ExpectNotifications(MonitoringInfoSet infos)
	{
		bool waited = false;
		AutoLocker<BLooper> locker(this);

		while (!infos.IsEmpty()) {
			if (fNotifications.IsEmpty()) {
				locker.Unlock();
				if (!waited) {
					snooze(kMaxNotificationDelay);
					waited = true;
				}
				locker.Lock();
			}

			if (fNotifications.IsEmpty()) {
				FAIL("missing notification(s), expected:\n%s",
					indented_string(infos.ToString(), "  ").String());
			}

			BMessage* message = fNotifications.RemoveItemAt(0);
			ObjectDeleter<BMessage> messageDeleter(message);

			bool foundMatch = false;
			for (int32 i = 0; i < infos.CountInfos(); i++) {
				const MonitoringInfo& info = infos.InfoAt(i);
				if (info.Matches(*message)) {
					infos.Remove(i);
					foundMatch = true;
					break;
				}
			}

			if (foundMatch)
				continue;

			BString processedInfosString(_ProcessedInfosString());
			FAIL("unexpected notification:\n  expected:\n%s\n  got:\n%s%s",
				indented_string(infos.ToString(), "    ").String(),
				indented_string(message_to_string(*message), "    ").String(),
				processedInfosString.String());
		}
	}

	virtual void DoInternal(bool directoriesOnly, bool filesOnly,
		bool recursive, bool pathOnly, bool watchStat) = 0;

private:
	typedef BObjectList<BMessage> MessageList;
	typedef BObjectList<MonitoringInfo> MonitoringInfoList;

private:
	virtual void MessageReceived(BMessage* message)
	{
		switch (message->what) {
			case B_PATH_MONITOR:
				if (!fNotifications.AddItem(new BMessage(*message)))
					FATAL_IF_ERROR(B_NO_MEMORY, "Failed to store notification");
				break;

			default:
				BLooper::MessageReceived(message);
				break;
		}
	}

private:
	void _CreateDirectory(const char* path)
	{
		FATAL_IF_ERROR(create_directory(path, 0755),
			"Failed to create directory \"%s\"", path);
	}

	void _RemoveRecursively(BEntry& entry)
	{
		// recurse, if the entry is a directory
		if (entry.IsDirectory()) {
			BDirectory directory;
			FATAL_IF_ERROR(directory.SetTo(&entry),
				"Failed to init BDirectory for \"%s\"",
				BPath(&entry).Path());

			BEntry childEntry;
			while (directory.GetNextEntry(&childEntry) == B_OK)
				_RemoveRecursively(childEntry);
		}

		// remove the entry
		FATAL_IF_ERROR(entry.Remove(), "Failed to remove entry \"%s\"",
			BPath(&entry).Path());
	}

	BString _ProcessedInfosString() const
	{
		BString processedInfosString;
		if (!fProcessedMonitoringInfos.IsEmpty()) {
			processedInfosString << "\nprocessed so far:\n"
				<< indented_string(fProcessedMonitoringInfos.ToString(), "  ");
		}
		return processedInfosString;
	}

protected:
	BString				fName;
	uint32				fFlags;
	thread_id			fLooperThread;
	MessageList			fNotifications;
	MonitoringInfoSet	fProcessedMonitoringInfos;
	bool				fIsWatching;
};


struct TestBase : Test {
protected:
	TestBase(const char* name)
		:
		Test(name)
	{
	}

	void StandardSetup()
	{
		CreateDirectory("base");
		CreateDirectory("base/dir1");
		CreateDirectory("base/dir1/dir0");
		CreateFile("base/file0");
		CreateFile("base/dir1/file0.0");
	}
};


#define CREATE_TEST_WITH_CUSTOM_SETUP(name, code)						\
	struct Test##name : TestBase {										\
		Test##name() : TestBase(#name) {}								\
		virtual void DoInternal(bool directoriesOnly, bool filesOnly,	\
			bool recursive, bool pathOnly, bool watchStat)				\
		{																\
			code														\
		}																\
	};																	\
	tests.push_back(new Test##name);

#define CREATE_TEST(name, code)			\
	CREATE_TEST_WITH_CUSTOM_SETUP(name, \
		StandardSetup();				\
		StartWatching("base");			\
		code							\
	)


static void
create_tests(std::vector<Test*>& tests)
{
	// test coverage:
	// - file/directory outside
	// - file/directory at top level
	// - file/directory at sub level
	// - move file/directory into/within/out of
	// - move non-empty directory into/within/out of
	// - create/move ancestor folder
	// - remove/move ancestor folder
	// - touch path, file/directory at top and sub level
	// - base file instead of directory
	//
	// not covered (yet):
	// - mount/unmount below/in our path
	// - test symlink in watched path
	// - attribute watching (should be similar to stat watching)

	CREATE_TEST(FileOutside,
		CreateFile("file1");
		MoveEntry("file1", "file2");
		RemoveEntry("file2");
	)

	CREATE_TEST(DirectoryOutside,
		CreateDirectory("dir1");
		MoveEntry("dir1", "dir2");
		RemoveEntry("dir2");
	)

	CREATE_TEST(FileTopLevel,
		ExpectNotification(CreateFile("base/file1"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(MoveEntry("base/file1", "base/file2"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/file2"),
			!directoriesOnly && !pathOnly);
	)

	CREATE_TEST(DirectoryTopLevel,
		ExpectNotification(CreateDirectory("base/dir2"),
			!filesOnly && !pathOnly);
		ExpectNotification(MoveEntry("base/dir2", "base/dir3"),
			!filesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/dir3"),
			!filesOnly && !pathOnly);
	)

	CREATE_TEST(FileSubLevel,
		ExpectNotification(CreateFile("base/dir1/file1"),
			recursive && !directoriesOnly);
		ExpectNotification(MoveEntry("base/dir1/file1", "base/dir1/file2"),
			recursive && !directoriesOnly);
		ExpectNotification(RemoveEntry("base/dir1/file2"),
			recursive && !directoriesOnly);
	)

	CREATE_TEST(DirectorySubLevel,
		ExpectNotification(CreateDirectory("base/dir1/dir2"),
			recursive && !filesOnly);
		ExpectNotification(MoveEntry("base/dir1/dir2", "base/dir1/dir3"),
			recursive && !filesOnly);
		ExpectNotification(RemoveEntry("base/dir1/dir3"),
			recursive && !filesOnly);
	)

	CREATE_TEST(FileMoveIntoTopLevel,
		CreateFile("file1");
		ExpectNotification(MoveEntry("file1", "base/file2"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/file2"),
			!directoriesOnly && !pathOnly);
	)

	CREATE_TEST(DirectoryMoveIntoTopLevel,
		CreateDirectory("dir2");
		ExpectNotification(MoveEntry("dir2", "base/dir3"),
			!filesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/dir3"),
			!filesOnly && !pathOnly);
	)

	CREATE_TEST(FileMoveIntoSubLevel,
		CreateFile("file1");
		ExpectNotification(MoveEntry("file1", "base/dir1/file2"),
			recursive && !directoriesOnly);
		ExpectNotification(RemoveEntry("base/dir1/file2"),
			recursive && !directoriesOnly);
	)

	CREATE_TEST(DirectoryMoveIntoSubLevel,
		CreateDirectory("dir2");
		ExpectNotification(MoveEntry("dir2", "base/dir1/dir3"),
			recursive && !filesOnly);
		ExpectNotification(RemoveEntry("base/dir1/dir3"),
			recursive && !filesOnly);
	)

	CREATE_TEST(FileMoveOutOfTopLevel,
		ExpectNotification(CreateFile("base/file1"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(MoveEntry("base/file1", "file2"),
			!directoriesOnly && !pathOnly);
		RemoveEntry("file2");
	)

	CREATE_TEST(DirectoryMoveOutOfTopLevel,
		ExpectNotification(CreateDirectory("base/dir2"),
			!filesOnly && !pathOnly);
		ExpectNotification(MoveEntry("base/dir2", "dir3"),
			!filesOnly && !pathOnly);
		RemoveEntry("dir3");
	)

	CREATE_TEST(FileMoveOutOfSubLevel,
		ExpectNotification(CreateFile("base/dir1/file1"),
			recursive && !directoriesOnly);
		ExpectNotification(MoveEntry("base/dir1/file1", "file2"),
			recursive && !directoriesOnly);
		RemoveEntry("file2");
	)

	CREATE_TEST(DirectoryMoveOutOfSubLevel,
		ExpectNotification(CreateDirectory("base/dir1/dir2"),
			recursive && !filesOnly);
		ExpectNotification(MoveEntry("base/dir1/dir2", "dir3"),
			recursive && !filesOnly);
		RemoveEntry("dir3");
	)

	CREATE_TEST(FileMoveToTopLevel,
		ExpectNotification(CreateFile("base/dir1/file1"),
			!directoriesOnly && recursive);
		ExpectNotification(MoveEntry("base/dir1/file1", "base/file2"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/file2"),
			!directoriesOnly && !pathOnly);
	)

	CREATE_TEST(DirectoryMoveToTopLevel,
		ExpectNotification(CreateDirectory("base/dir1/dir2"),
			!filesOnly && recursive);
		ExpectNotification(MoveEntry("base/dir1/dir2", "base/dir3"),
			!filesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/dir3"),
			!filesOnly && !pathOnly);
	)

	CREATE_TEST(FileMoveToSubLevel,
		ExpectNotification(CreateFile("base/file1"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(MoveEntry("base/file1", "base/dir1/file2"),
			!directoriesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/dir1/file2"),
			!directoriesOnly && recursive);
	)

	CREATE_TEST(DirectoryMoveToSubLevel,
		ExpectNotification(CreateDirectory("base/dir2"),
			!filesOnly && !pathOnly);
		ExpectNotification(MoveEntry("base/dir2", "base/dir1/dir3"),
			!filesOnly && !pathOnly);
		ExpectNotification(RemoveEntry("base/dir1/dir3"),
			!filesOnly && recursive);
	)

	CREATE_TEST(NonEmptyDirectoryMoveIntoTopLevel,
		CreateDirectory("dir2");
		CreateDirectory("dir2/dir3");
		CreateDirectory("dir2/dir4");
		CreateFile("dir2/file1");
		CreateFile("dir2/dir3/file2");
		ExpectNotification(MoveEntry("dir2", "base/dir5"),
			!filesOnly && !pathOnly);
		if (recursive && filesOnly) {
			ExpectNotifications(MonitoringInfoSet()
				.Add(B_ENTRY_CREATED, "base/dir5/file1")
				.Add(B_ENTRY_CREATED, "base/dir5/dir3/file2"));
		}
	)

	CREATE_TEST(NonEmptyDirectoryMoveIntoSubLevel,
		CreateDirectory("dir2");
		CreateDirectory("dir2/dir3");
		CreateDirectory("dir2/dir4");
		CreateFile("dir2/file1");
		CreateFile("dir2/dir3/file2");
		ExpectNotification(MoveEntry("dir2", "base/dir1/dir5"),
			!filesOnly && recursive);
		if (recursive && filesOnly) {
			ExpectNotifications(MonitoringInfoSet()
				.Add(B_ENTRY_CREATED, "base/dir1/dir5/file1")
				.Add(B_ENTRY_CREATED, "base/dir1/dir5/dir3/file2"));
		}
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(NonEmptyDirectoryMoveOutOfTopLevel,
		StandardSetup();
		CreateDirectory("base/dir2");
		CreateDirectory("base/dir2/dir3");
		CreateDirectory("base/dir2/dir4");
		CreateFile("base/dir2/file1");
		CreateFile("base/dir2/dir3/file2");
		StartWatching("base");
		MonitoringInfoSet filesRemoved;
		if (recursive && filesOnly) {
			filesRemoved
				.Add(B_ENTRY_REMOVED, "base/dir2/file1")
				.Add(B_ENTRY_REMOVED, "base/dir2/dir3/file2");
		}
		ExpectNotification(MoveEntry("base/dir2", "dir5"),
			!filesOnly && !pathOnly);
		ExpectNotifications(filesRemoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(NonEmptyDirectoryMoveOutOfSubLevel,
		StandardSetup();
		CreateDirectory("base/dir1/dir2");
		CreateDirectory("base/dir1/dir2/dir3");
		CreateDirectory("base/dir1/dir2/dir4");
		CreateFile("base/dir1/dir2/file1");
		CreateFile("base/dir1/dir2/dir3/file2");
		StartWatching("base");
		MonitoringInfoSet filesRemoved;
		if (recursive && filesOnly) {
			filesRemoved
				.Add(B_ENTRY_REMOVED, "base/dir1/dir2/file1")
				.Add(B_ENTRY_REMOVED, "base/dir1/dir2/dir3/file2");
		}
		ExpectNotification(MoveEntry("base/dir1/dir2", "dir5"),
			!filesOnly && recursive);
		ExpectNotifications(filesRemoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(NonEmptyDirectoryMoveToTopLevel,
		StandardSetup();
		CreateDirectory("base/dir1/dir2");
		CreateDirectory("base/dir1/dir2/dir3");
		CreateDirectory("base/dir1/dir2/dir4");
		CreateFile("base/dir1/dir2/file1");
		CreateFile("base/dir1/dir2/dir3/file2");
		StartWatching("base");
		MonitoringInfoSet filesMoved;
		if (recursive && filesOnly) {
			filesMoved
				.Add(B_ENTRY_REMOVED, "base/dir1/dir2/file1")
				.Add(B_ENTRY_REMOVED, "base/dir1/dir2/dir3/file2");
		}
		ExpectNotification(MoveEntry("base/dir1/dir2", "base/dir5"),
			!filesOnly && !pathOnly);
		if (recursive && filesOnly) {
			filesMoved
				.Add(B_ENTRY_CREATED, "base/dir5/file1")
				.Add(B_ENTRY_CREATED, "base/dir5/dir3/file2");
		}
		ExpectNotifications(filesMoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(NonEmptyDirectoryMoveToSubLevel,
		StandardSetup();
		CreateDirectory("base/dir2");
		CreateDirectory("base/dir2/dir3");
		CreateDirectory("base/dir2/dir4");
		CreateFile("base/dir2/file1");
		CreateFile("base/dir2/dir3/file2");
		StartWatching("base");
		MonitoringInfoSet filesMoved;
		if (recursive && filesOnly) {
			filesMoved
				.Add(B_ENTRY_REMOVED, "base/dir2/file1")
				.Add(B_ENTRY_REMOVED, "base/dir2/dir3/file2");
		}
		ExpectNotification(MoveEntry("base/dir2", "base/dir1/dir5"),
			!filesOnly && !pathOnly);
		if (recursive && filesOnly) {
			filesMoved
				.Add(B_ENTRY_CREATED, "base/dir1/dir5/file1")
				.Add(B_ENTRY_CREATED, "base/dir1/dir5/dir3/file2");
		}
		ExpectNotifications(filesMoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(CreateAncestor,
		StartWatching("ancestor/base");
		CreateDirectory("ancestor");
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateAncestor,
		CreateDirectory("ancestorSibling");
		StartWatching("ancestor/base");
		MoveEntry("ancestorSibling", "ancestor");
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateAncestorWithBase,
		CreateDirectory("ancestorSibling");
		CreateDirectory("ancestorSibling/base");
		StartWatching("ancestor/base");
		MoveEntry("ancestorSibling", "ancestor");
		MonitoringInfoSet entriesCreated;
		if (!filesOnly)
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base");
		ExpectNotifications(entriesCreated);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateAncestorWithBaseAndFile,
		CreateDirectory("ancestorSibling");
		CreateDirectory("ancestorSibling/base");
		CreateFile("ancestorSibling/base/file1");
		StartWatching("ancestor/base");
		MoveEntry("ancestorSibling", "ancestor");
		MonitoringInfoSet entriesCreated;
		if (!filesOnly)
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base");
		else if (!pathOnly)
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base/file1");
		ExpectNotifications(entriesCreated);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateAncestorWithBaseAndDirectory,
		CreateDirectory("ancestorSibling");
		CreateDirectory("ancestorSibling/base");
		CreateDirectory("ancestorSibling/base/dir1");
		CreateFile("ancestorSibling/base/dir1/file1");
		StartWatching("ancestor/base");
		MoveEntry("ancestorSibling", "ancestor");
		MonitoringInfoSet entriesCreated;
		if (!filesOnly) {
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base");
		} else if (recursive)
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base/dir1/file1");
		ExpectNotifications(entriesCreated);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(CreateBase,
		CreateDirectory("ancestor");
		StartWatching("ancestor/base");
		ExpectNotification(CreateDirectory("ancestor/base"),
			!filesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateBase,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/baseSibling");
		StartWatching("ancestor/base");
		ExpectNotification(MoveEntry("ancestor/baseSibling", "ancestor/base"),
			!filesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateBaseWithFile,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/baseSibling");
		CreateFile("ancestor/baseSibling/file1");
		StartWatching("ancestor/base");
		ExpectNotification(MoveEntry("ancestor/baseSibling", "ancestor/base"),
			!filesOnly);
		MonitoringInfoSet entriesCreated;
		if (filesOnly && !pathOnly)
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base/file1");
		ExpectNotifications(entriesCreated);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateBaseWithDirectory,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/baseSibling");
		CreateDirectory("ancestor/baseSibling/dir1");
		CreateFile("ancestor/baseSibling/dir1/file1");
		StartWatching("ancestor/base");
		ExpectNotification(MoveEntry("ancestor/baseSibling", "ancestor/base"),
			!filesOnly);
		MonitoringInfoSet entriesCreated;
		if (filesOnly && recursive)
			entriesCreated.Add(B_ENTRY_CREATED, "ancestor/base/dir1/file1");
		ExpectNotifications(entriesCreated);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveRemoveAncestorWithBaseAndFile,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/base");
		CreateFile("ancestor/base/file1");
		StartWatching("ancestor/base");
		MonitoringInfoSet entriesRemoved;
		if (!filesOnly)
			entriesRemoved.Add(B_ENTRY_REMOVED, "ancestor/base");
		else if (!pathOnly)
			entriesRemoved.Add(B_ENTRY_REMOVED, "ancestor/base/file1");
		MoveEntry("ancestor", "ancestorSibling");
		ExpectNotifications(entriesRemoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveRemoveAncestorWithBaseAndDirectory,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/base");
		CreateDirectory("ancestor/base/dir1");
		CreateFile("ancestor/base/dir1/file1");
		StartWatching("ancestor/base");
		MonitoringInfoSet entriesRemoved;
		if (!filesOnly)
			entriesRemoved.Add(B_ENTRY_REMOVED, "ancestor/base");
		else if (recursive)
			entriesRemoved.Add(B_ENTRY_REMOVED, "ancestor/base/dir1/file1");
		MoveEntry("ancestor", "ancestorSibling");
		ExpectNotifications(entriesRemoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveRemoveBaseWithFile,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/base");
		CreateFile("ancestor/base/file1");
		StartWatching("ancestor/base");
		MonitoringInfoSet entriesRemoved;
		if (filesOnly && !pathOnly)
			entriesRemoved.Add(B_ENTRY_REMOVED, "ancestor/base/file1");
		ExpectNotification(MoveEntry("ancestor/base", "ancestor/baseSibling"),
			!filesOnly);
		ExpectNotifications(entriesRemoved);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveRemoveBaseWithDirectory,
		CreateDirectory("ancestor");
		CreateDirectory("ancestor/base");
		CreateDirectory("ancestor/base/dir1");
		CreateFile("ancestor/base/dir1/file1");
		StartWatching("ancestor/base");
		MonitoringInfoSet entriesRemoved;
		if (filesOnly && recursive)
			entriesRemoved.Add(B_ENTRY_REMOVED, "ancestor/base/dir1/file1");
		ExpectNotification(MoveEntry("ancestor/base", "ancestor/baseSibling"),
			!filesOnly);
		ExpectNotifications(entriesRemoved);
	)

	CREATE_TEST(TouchBase,
		ExpectNotification(TouchEntry("base"), watchStat && !filesOnly);
	)

	CREATE_TEST(TouchFileTopLevel,
		ExpectNotification(TouchEntry("base/file0"),
			watchStat && recursive && !directoriesOnly);
	)

	CREATE_TEST(TouchFileSubLevel,
		ExpectNotification(TouchEntry("base/dir1/file0.0"),
			watchStat && recursive && !directoriesOnly);
	)

	CREATE_TEST(TouchDirectoryTopLevel,
		ExpectNotification(TouchEntry("base/dir1"),
			watchStat && recursive && !filesOnly);
	)

	CREATE_TEST(TouchDirectorySubLevel,
		ExpectNotification(TouchEntry("base/dir1/dir0"),
			watchStat && recursive && !filesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(CreateFileBase,
		StartWatching("file");
		ExpectNotification(CreateFile("file"),
			!directoriesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveCreateFileBase,
		CreateFile("fileSibling");
		StartWatching("file");
		ExpectNotification(MoveEntry("fileSibling", "file"),
			!directoriesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(RemoveFileBase,
		CreateFile("file");
		StartWatching("file");
		ExpectNotification(RemoveEntry("file"),
			!directoriesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(MoveRemoveFileBase,
		CreateFile("file");
		StartWatching("file");
		ExpectNotification(MoveEntry("file", "fileSibling"),
			!directoriesOnly);
	)

	CREATE_TEST_WITH_CUSTOM_SETUP(TouchFileBase,
		CreateFile("file");
		StartWatching("file");
		ExpectNotification(TouchEntry("file"),
			watchStat && !directoriesOnly);
	)
}


static void
run_tests(std::set<BString> testNames, uint32 watchFlags,
	size_t& totalTests, size_t& succeededTests)
{
	std::vector<Test*> tests;
	create_tests(tests);

	// filter the tests, if test names have been specified
	size_t testCount = tests.size();
	if (!testNames.empty()) {
		for (size_t i = 0; i < testCount;) {
			Test* test = tests[i];
			std::set<BString>::iterator it = testNames.find(test->Name());
			if (it != testNames.end()) {
				testNames.erase(it);
				i++;
			} else {
				tests.erase(tests.begin() + i);
				test->Delete();
				testCount--;
			}
		}

		if (!testNames.empty()) {
			printf("no such test(s):\n");
			for (std::set<BString>::iterator it = testNames.begin();
				it != testNames.end(); ++it) {
				printf("  %s\n", it->String());
				exit(1);
			}
		}
	}

	printf("\nrunning tests with flags: %s\n",
		watch_flags_to_string(watchFlags).String());

	int32 longestTestName = 0;

	for (size_t i = 0; i < testCount; i++) {
		Test* test = tests[i];
		longestTestName = std::max(longestTestName, test->Name().Length());
	}

	for (size_t i = 0; i < testCount; i++) {
		Test* test = tests[i];
		bool terminate = false;

		try {
			totalTests++;
			test->Init(watchFlags);
			printf("  %s: %*s", test->Name().String(),
				int(longestTestName - test->Name().Length()), "");
			fflush(stdout);
			test->Do();
			printf("SUCCEEDED\n");
			succeededTests++;
		} catch (FatalException& exception) {
			printf("FAILED FATALLY\n");
			printf("%s\n",
				indented_string(exception.Message(), "    ").String());
			terminate = true;
		} catch (TestException& exception) {
			printf("FAILED\n");
			printf("%s\n",
				indented_string(exception.Message(), "    ").String());
		}

		test->Delete();

		if (terminate)
			exit(1);
	}
}


int
main(int argc, const char* const* argv)
{
	// any args are test names
	std::set<BString> testNames;
	for (int i = 1; i < argc; i++)
		testNames.insert(argv[i]);

	// flags that can be combined arbitrarily
	const uint32 kFlags[] = {
		B_WATCH_NAME,
		B_WATCH_STAT,
		// not that interesting, since similar to B_WATCH_STAT: B_WATCH_ATTR
		B_WATCH_DIRECTORY,
		B_WATCH_RECURSIVELY,
	};
	const size_t kFlagCount = sizeof(kFlags) / sizeof(kFlags[0]);

	size_t totalTests = 0;
	size_t succeededTests = 0;

	for (size_t i = 0; i < 1 << kFlagCount; i++) {
		// construct flags mask
		uint32 flags = 0;
		for (size_t k = 0; k < kFlagCount; k++) {
			if ((i & (1 << k)) != 0)
				flags |= kFlags[k];
		}

		// run tests -- in recursive mode do that additionally for the mutually
		// B_WATCH_FILES_ONLY and B_WATCH_DIRECTORIES_ONLY flags.
		run_tests(testNames, flags, totalTests, succeededTests);
		if ((flags & B_WATCH_RECURSIVELY) != 0) {
			run_tests(testNames, flags | B_WATCH_FILES_ONLY, totalTests,
				succeededTests);
			run_tests(testNames, flags | B_WATCH_DIRECTORIES_ONLY, totalTests,
				succeededTests);
		}
	}

	printf("\n");
	if (succeededTests == totalTests) {
		printf("ALL TESTS SUCCEEDED\n");
	} else {
		printf("%zu of %zu TESTS FAILED\n", totalTests - succeededTests,
			totalTests);
	}

	return 0;
}