/////////////////////////////////////////////////////////////////////////////// // Name: tests/fswatcher/fswatchertest.cpp // Purpose: wxFileSystemWatcher unit test // Author: Bartosz Bekier // Created: 2009-06-11 // RCS-ID: $Id$ // Copyright: (c) 2009 Bartosz Bekier /////////////////////////////////////////////////////////////////////////////// // ---------------------------------------------------------------------------- // headers // ---------------------------------------------------------------------------- #include "testprec.h" #ifdef __BORLANDC__ #pragma hdrstop #endif #ifndef WX_PRECOMP #include "wx/timer.h" #endif #include "wx/evtloop.h" #include "wx/filename.h" #include "wx/filefn.h" #include "wx/stdpaths.h" #include "wx/fswatcher.h" #include "testfile.h" // ---------------------------------------------------------------------------- // local functions // ---------------------------------------------------------------------------- // class generating file system events class EventGenerator { public: static EventGenerator& Get() { if (!ms_instance) ms_instance = new EventGenerator(GetWatchDir()); return *ms_instance; } EventGenerator(const wxFileName& path) : m_base(path) { m_old = wxFileName(); m_file = RandomName(); m_new = RandomName(); } // operations bool CreateFile() { wxFile file(m_file.GetFullPath(), wxFile::write); return file.IsOpened() && m_file.FileExists(); } bool RenameFile() { CPPUNIT_ASSERT(m_file.FileExists()); wxLogDebug("Renaming %s=>%s", m_file.GetFullPath(), m_new.GetFullPath()); bool ret = wxRenameFile(m_file.GetFullPath(), m_new.GetFullPath()); if (ret) { m_old = m_file; m_file = m_new; m_new = RandomName(); } return ret; } bool DeleteFile() { CPPUNIT_ASSERT(m_file.FileExists()); bool ret = wxRemoveFile(m_file.GetFullPath()); if (ret) { m_old = m_file; m_file = m_new; m_new = RandomName(); } return ret; } bool TouchFile() { return m_file.Touch(); } bool ReadFile() { wxFile f(m_file.GetFullPath()); CPPUNIT_ASSERT(f.IsOpened()); char buf[1]; ssize_t count = f.Read(buf, sizeof(buf)); CPPUNIT_ASSERT(count > 0); return true; } bool ModifyFile() { CPPUNIT_ASSERT(m_file.FileExists()); wxFile file(m_file.GetFullPath(), wxFile::write_append); CPPUNIT_ASSERT(file.IsOpened()); CPPUNIT_ASSERT(file.Write("Words of Wisdom, Lloyd. Words of wisdom\n")); return file.Close(); } // helpers wxFileName RandomName(int length = 10) { return RandomName(m_base, length); } // static helpers static const wxFileName& GetWatchDir() { static wxFileName dir; if (dir.DirExists()) return dir; wxString tmp = wxStandardPaths::Get().GetTempDir(); dir.AssignDir(tmp); // XXX look for more unique name? there is no function to generate // unique filename, the file always get created... dir.AppendDir("fswatcher_test"); CPPUNIT_ASSERT(!dir.DirExists()); CPPUNIT_ASSERT(dir.Mkdir()); return dir; } static void RemoveWatchDir() { wxFileName dir = GetWatchDir(); CPPUNIT_ASSERT(dir.DirExists()); // just to be really sure we know what we remove CPPUNIT_ASSERT_EQUAL( "fswatcher_test", dir.GetDirs().Last() ); // FIXME-VC6: using non-static Rmdir() results in ICE CPPUNIT_ASSERT( wxFileName::Rmdir(dir.GetFullPath(), wxPATH_RMDIR_RECURSIVE) ); } static wxFileName RandomName(const wxFileName& base, int length = 10) { static int ALFA_CNT = 'z' - 'a'; wxString s; for (int i = 0 ; i < length; ++i) { char c = 'a' + (rand() % ALFA_CNT); s += c; } return wxFileName(base.GetFullPath(), s); } public: wxFileName m_base; // base dir for doing operations wxFileName m_file; // current file name wxFileName m_old; // previous file name wxFileName m_new; // name after renaming protected: static EventGenerator* ms_instance; }; EventGenerator* EventGenerator::ms_instance = 0; // custom event handler class EventHandler : public wxEvtHandler { public: enum { WAIT_DURATION = 3 }; EventHandler() : eg(EventGenerator::Get()), m_loop(0), m_count(0), m_watcher(0) { m_loop = new wxEventLoop(); Connect(wxEVT_IDLE, wxIdleEventHandler(EventHandler::OnIdle)); Connect(wxEVT_FSWATCHER, wxFileSystemWatcherEventHandler( EventHandler::OnFileSystemEvent)); } virtual ~EventHandler() { delete m_watcher; if (m_loop) { if (m_loop->IsRunning()) m_loop->Exit(); delete m_loop; } } void Exit() { m_loop->Exit(); } // sends idle event, so we get called in a moment void SendIdle() { wxIdleEvent* e = new wxIdleEvent(); QueueEvent(e); } void Run() { SendIdle(); m_loop->Run(); } void OnIdle(wxIdleEvent& /*evt*/) { bool more = Action(); m_count++; if (more) { SendIdle(); } } // returns whether we should produce more idle events virtual bool Action() { switch (m_count) { case 0: CPPUNIT_ASSERT(Init()); break; case 1: GenerateEvent(); break; case 2: // actual test CheckResult(); Exit(); break; // TODO a mechanism that will break the loop in case we // don't receive a file system event // this below doesn't quite work, so all tests must pass :-) #if 0 case 2: m_loop.Yield(); m_loop.WakeUp(); CPPUNIT_ASSERT(KeepWaiting()); m_loop.Yield(); break; case 3: break; case 4: CPPUNIT_ASSERT(AfterWait()); break; #endif } // switch (m_count) return m_count <= 0; } virtual bool Init() { // test we're good to go CPPUNIT_ASSERT(wxEventLoopBase::GetActive()); // XXX only now can we construct Watcher, because we need // active loop here m_watcher = new wxFileSystemWatcher(); m_watcher->SetOwner(this); // add dir to be watched wxFileName dir = EventGenerator::GetWatchDir(); CPPUNIT_ASSERT(m_watcher->Add(dir, wxFSW_EVENT_ALL)); return true; } virtual bool KeepWaiting() { // did we receive event already? if (!tested) { // well, let's wait a bit more wxSleep(WAIT_DURATION); } return true; } virtual bool AfterWait() { // fail if still no events WX_ASSERT_MESSAGE ( ("No events during %d seconds!", static_cast(WAIT_DURATION)), tested ); return true; } virtual void OnFileSystemEvent(wxFileSystemWatcherEvent& evt) { wxLogDebug("--- %s ---", evt.ToString()); m_lastEvent = wxDynamicCast(evt.Clone(), wxFileSystemWatcherEvent); m_events.Add(m_lastEvent); // test finished SendIdle(); tested = true; } virtual void CheckResult() { CPPUNIT_ASSERT_MESSAGE( "No events received", !m_events.empty() ); const wxFileSystemWatcherEvent * const e = m_events.front(); // this is our "reference event" const wxFileSystemWatcherEvent expected = ExpectedEvent(); CPPUNIT_ASSERT_EQUAL( expected.GetChangeType(), e->GetChangeType() ); CPPUNIT_ASSERT_EQUAL((int)wxEVT_FSWATCHER, e->GetEventType()); // XXX this needs change CPPUNIT_ASSERT_EQUAL(wxEVT_CATEGORY_UNKNOWN, e->GetEventCategory()); CPPUNIT_ASSERT_EQUAL(expected.GetPath(), e->GetPath()); CPPUNIT_ASSERT_EQUAL(expected.GetNewPath(), e->GetNewPath()); // Under MSW extra modification events are sometimes reported after a // rename and we just can't get rid of them, so ignore them in this // test if they do happen. if ( e->GetChangeType() == wxFSW_EVENT_RENAME && m_events.size() == 2 ) { const wxFileSystemWatcherEvent* const e2 = m_events.back(); if ( e2->GetChangeType() == wxFSW_EVENT_MODIFY && e2->GetPath() == e->GetNewPath() ) { // This is a modify event for the new file, ignore it. return; } } WX_ASSERT_EQUAL_MESSAGE ( ( "Extra events received, last one is of type %x, path=\"%s\" " "(the original event was for \"%s\" (\"%s\")", m_events.back()->GetChangeType(), m_events.back()->GetPath().GetFullPath(), e->GetPath().GetFullPath(), e->GetNewPath().GetFullPath() ), 1, m_events.size() ); } virtual void GenerateEvent() = 0; virtual wxFileSystemWatcherEvent ExpectedEvent() = 0; protected: EventGenerator& eg; wxEventLoopBase* m_loop; // loop reference int m_count; // idle events count wxFileSystemWatcher* m_watcher; bool tested; // indicates, whether we have already passed the test #include "wx/arrimpl.cpp" WX_DEFINE_ARRAY_PTR(wxFileSystemWatcherEvent*, wxArrayEvent); wxArrayEvent m_events; wxFileSystemWatcherEvent* m_lastEvent; }; // ---------------------------------------------------------------------------- // test class // ---------------------------------------------------------------------------- class FileSystemWatcherTestCase : public CppUnit::TestCase { public: FileSystemWatcherTestCase() { } virtual void setUp(); virtual void tearDown(); protected: wxEventLoopBase* m_loop; private: CPPUNIT_TEST_SUITE( FileSystemWatcherTestCase ); CPPUNIT_TEST( TestEventCreate ); CPPUNIT_TEST( TestEventDelete ); // FIXME: Currently this test fails under Windows. #ifndef __WINDOWS__ CPPUNIT_TEST( TestTrees ); #endif // __WINDOWS__ // kqueue-based implementation doesn't collapse create/delete pairs in // renames and doesn't detect neither modifications nor access to the // files reliably currently so disable these tests // // FIXME: fix the code and reenable them #ifndef wxHAS_KQUEUE CPPUNIT_TEST( TestEventRename ); CPPUNIT_TEST( TestEventModify ); // MSW implementation doesn't detect file access events currently #ifndef __WINDOWS__ CPPUNIT_TEST( TestEventAccess ); #endif // __WINDOWS__ #endif // !wxHAS_KQUEUE CPPUNIT_TEST( TestNoEventsAfterRemove ); CPPUNIT_TEST_SUITE_END(); void TestEventCreate(); void TestEventDelete(); void TestEventRename(); void TestEventModify(); void TestEventAccess(); #ifndef __WINDOWS__ void TestTrees(); #endif // __WINDOWS__ void TestNoEventsAfterRemove(); DECLARE_NO_COPY_CLASS(FileSystemWatcherTestCase) }; // the test currently hangs under OS X for some reason and this prevents tests // ran by buildbot from completing so disable it until someone has time to // debug it // // FIXME: debug and fix this! #ifndef __WXOSX__ // register in the unnamed registry so that these tests are run by default CPPUNIT_TEST_SUITE_REGISTRATION( FileSystemWatcherTestCase ); #endif // also include in its own registry so that these tests can be run alone CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( FileSystemWatcherTestCase, "FileSystemWatcherTestCase" ); void FileSystemWatcherTestCase::setUp() { wxLog::AddTraceMask(wxTRACE_FSWATCHER); EventGenerator::Get().GetWatchDir(); } void FileSystemWatcherTestCase::tearDown() { EventGenerator::Get().RemoveWatchDir(); } // ---------------------------------------------------------------------------- // TestEventCreate // ---------------------------------------------------------------------------- void FileSystemWatcherTestCase::TestEventCreate() { wxLogDebug("TestEventCreate()"); class EventTester : public EventHandler { public: virtual void GenerateEvent() { CPPUNIT_ASSERT(eg.CreateFile()); } virtual wxFileSystemWatcherEvent ExpectedEvent() { wxFileSystemWatcherEvent event(wxFSW_EVENT_CREATE); event.SetPath(eg.m_file); event.SetNewPath(eg.m_file); return event; } }; EventTester tester; wxLogTrace(wxTRACE_FSWATCHER, "TestEventCreate tester created()"); tester.Run(); } // ---------------------------------------------------------------------------- // TestEventDelete // ---------------------------------------------------------------------------- void FileSystemWatcherTestCase::TestEventDelete() { wxLogDebug("TestEventDelete()"); class EventTester : public EventHandler { public: virtual void GenerateEvent() { CPPUNIT_ASSERT(eg.DeleteFile()); } virtual wxFileSystemWatcherEvent ExpectedEvent() { wxFileSystemWatcherEvent event(wxFSW_EVENT_DELETE); event.SetPath(eg.m_old); // CHECK maybe new path here could be NULL or sth? event.SetNewPath(eg.m_old); return event; } }; // we need to create a file now, so we can delete it EventGenerator::Get().CreateFile(); EventTester tester; tester.Run(); } // ---------------------------------------------------------------------------- // TestEventRename // ---------------------------------------------------------------------------- void FileSystemWatcherTestCase::TestEventRename() { wxLogDebug("TestEventRename()"); class EventTester : public EventHandler { public: virtual void GenerateEvent() { CPPUNIT_ASSERT(eg.RenameFile()); } virtual wxFileSystemWatcherEvent ExpectedEvent() { wxFileSystemWatcherEvent event(wxFSW_EVENT_RENAME); event.SetPath(eg.m_old); event.SetNewPath(eg.m_file); return event; } }; // need a file to rename later EventGenerator::Get().CreateFile(); EventTester tester; tester.Run(); } // ---------------------------------------------------------------------------- // TestEventModify // ---------------------------------------------------------------------------- void FileSystemWatcherTestCase::TestEventModify() { wxLogDebug("TestEventModify()"); class EventTester : public EventHandler { public: virtual void GenerateEvent() { CPPUNIT_ASSERT(eg.ModifyFile()); } virtual wxFileSystemWatcherEvent ExpectedEvent() { wxFileSystemWatcherEvent event(wxFSW_EVENT_MODIFY); event.SetPath(eg.m_file); event.SetNewPath(eg.m_file); return event; } }; // we need to create a file to modify EventGenerator::Get().CreateFile(); EventTester tester; tester.Run(); } // ---------------------------------------------------------------------------- // TestEventAccess // ---------------------------------------------------------------------------- void FileSystemWatcherTestCase::TestEventAccess() { wxLogDebug("TestEventAccess()"); class EventTester : public EventHandler { public: virtual void GenerateEvent() { CPPUNIT_ASSERT(eg.ReadFile()); } virtual wxFileSystemWatcherEvent ExpectedEvent() { wxFileSystemWatcherEvent event(wxFSW_EVENT_ACCESS); event.SetPath(eg.m_file); event.SetNewPath(eg.m_file); return event; } }; // we need to create a file to read from it and write sth to it EventGenerator::Get().CreateFile(); EventGenerator::Get().ModifyFile(); EventTester tester; tester.Run(); } // ---------------------------------------------------------------------------- // TestTrees // ---------------------------------------------------------------------------- #ifndef __WINDOWS__ void FileSystemWatcherTestCase::TestTrees() { class TreeTester : public EventHandler { const size_t subdirs; const size_t files; public: TreeTester() : subdirs(5), files(3) {} void GrowTree(wxFileName dir) { CPPUNIT_ASSERT(dir.Mkdir()); // Create a branch of 5 numbered subdirs, each containing 3 // numbered files for ( unsigned d = 0; d < subdirs; ++d ) { dir.AppendDir(wxString::Format("subdir%u", d+1)); CPPUNIT_ASSERT(dir.Mkdir()); const wxString prefix = dir.GetPathWithSep(); for ( unsigned f = 0; f < files; ++f ) { // Just create the files. wxFile(prefix + wxString::Format("file%u", f+1), wxFile::write); } } } void RmDir(wxFileName dir) { CPPUNIT_ASSERT(dir.DirExists()); CPPUNIT_ASSERT(dir.Rmdir(wxPATH_RMDIR_RECURSIVE)); } void WatchDir(wxFileName dir) { CPPUNIT_ASSERT(m_watcher); // Store the initial count; there may already be some watches const int initial = m_watcher->GetWatchedPathsCount(); m_watcher->Add(dir); CPPUNIT_ASSERT_EQUAL(initial + 1, m_watcher->GetWatchedPathsCount()); } void RemoveSingleWatch(wxFileName dir) { CPPUNIT_ASSERT(m_watcher); const int initial = m_watcher->GetWatchedPathsCount(); m_watcher->Remove(dir); CPPUNIT_ASSERT_EQUAL(initial - 1, m_watcher->GetWatchedPathsCount()); } void WatchTree(const wxFileName& dir) { CPPUNIT_ASSERT(m_watcher); const size_t treeitems = (subdirs*files) + subdirs + 1; // +1 for the trunk // Store the initial count; there may already be some watches const int initial = m_watcher->GetWatchedPathsCount(); GrowTree(dir); m_watcher->AddTree(dir); const int plustree = m_watcher->GetWatchedPathsCount(); CPPUNIT_ASSERT_EQUAL(initial + treeitems, plustree); m_watcher->RemoveTree(dir); CPPUNIT_ASSERT_EQUAL(initial, m_watcher->GetWatchedPathsCount()); } void RemoveAllWatches() { CPPUNIT_ASSERT(m_watcher); m_watcher->RemoveAll(); CPPUNIT_ASSERT_EQUAL(0, m_watcher->GetWatchedPathsCount()); } virtual void GenerateEvent() { // We don't use this function for events. Just run the tests wxFileName watchdir = EventGenerator::GetWatchDir(); CPPUNIT_ASSERT(watchdir.DirExists()); wxFileName treedir(watchdir); treedir.AppendDir("treetrunk"); CPPUNIT_ASSERT(!treedir.DirExists()); wxFileName singledir(watchdir); singledir.AppendDir("single"); CPPUNIT_ASSERT(!singledir.DirExists()); CPPUNIT_ASSERT(singledir.Mkdir()); WatchDir(singledir); WatchTree(treedir); RemoveSingleWatch(singledir); // Add it back again, ready to test RemoveAll() WatchDir(singledir); RemoveAllWatches(); // Clean up RmDir(singledir); RmDir(treedir); Exit(); } virtual wxFileSystemWatcherEvent ExpectedEvent() { CPPUNIT_FAIL("Shouldn't be called"); return wxFileSystemWatcherEvent(wxFSW_EVENT_ERROR); } virtual void CheckResult() { // Do nothing. We override this to prevent receiving events in // ExpectedEvent() } }; TreeTester tester; tester.Run(); } #endif // __WINDOWS__ namespace { // We can't define this class locally inside TestNoEventsAfterRemove() for some // reason with g++ 4.0 under OS X 10.5, it results in the following mysterious // error: // // /var/tmp//ccTkNCkc.s:unknown:Non-global symbol: // __ZThn80_ZN25FileSystemWatcherTestCase23TestNoEventsAfterRemoveEvEN11EventTester6NotifyEv.eh // can't be a weak_definition // // So define this class outside the function instead. class NoEventsAfterRemoveEventTester : public EventHandler, public wxTimer { public: NoEventsAfterRemoveEventTester() { // We need to use an inactivity timer as we never get any file // system events in this test, so we consider that the test is // finished when this 1s timeout expires instead of, as usual, // stopping after getting the file system events. Start(1000, true); } virtual void GenerateEvent() { m_watcher->Remove(EventGenerator::GetWatchDir()); CPPUNIT_ASSERT(eg.CreateFile()); } virtual void CheckResult() { CPPUNIT_ASSERT( m_events.empty() ); } virtual wxFileSystemWatcherEvent ExpectedEvent() { CPPUNIT_FAIL( "Shouldn't be called" ); return wxFileSystemWatcherEvent(wxFSW_EVENT_ERROR); } virtual void Notify() { SendIdle(); } }; } // anonymous namespace void FileSystemWatcherTestCase::TestNoEventsAfterRemove() { NoEventsAfterRemoveEventTester tester; tester.Run(); }