/* * Copyright (C) 2005-2009 Patrick Ohly * Copyright (C) 2009 Intel Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "config.h" #include #include #include #include #include #include #include "CmdlineSyncClient.h" #include #include #include #include #include SE_BEGIN_CXX /* * always provide this test class, even if not used: * that way the test scripts can unconditionally * invoke "client-test SyncEvolution" */ CPPUNIT_REGISTRY_ADD_TO_DEFAULT("SyncEvolution"); class EvolutionLocalTests : public LocalTests { public: EvolutionLocalTests(const std::string &name, ClientTest &cl, int sourceParam, ClientTest::Config &co) : LocalTests(name, cl, sourceParam, co) {} virtual void addTests() { LocalTests::addTests(); } }; /** * This is a workaround for libecal/libebook in Evolution >= 2.30. * The storage daemons shut down after only 10 seconds of no client * being attached. Due to limitations in libecal/libebook this is not * detected when only using the synchronous API ("destroyed" signal * not delivered, see e-cal.c), which then leads to D-Bus errors. * * The workaround consists of keeping one open SyncEvolution backend * around for each of eds_event and eds_contact/30, if they ever were used * during testing. */ static map > lockEvolution; static void CleanupSources() { lockEvolution.clear(); } /** * This code uses the ClientTest and and information provided by * the backends in their RegisterSyncSourceTest instances to test * real synchronization with a server. * * Configuration is done by environment variables which indicate which * part below the root node "client-test" of the the configuration tree to use; * beyond that everything needed for synchronization is read from the * configuration tree. * * - CLIENT_TEST_SERVER = maps to name of root node in configuration tree * - CLIENT_TEST_EVOLUTION_PREFIX = a common "evolutionsource" prefix for *all* * sources; the source name followed by "_[12]" * is appended to get unique names * - CLIENT_TEST_EVOLUTION_USER = sets the "evolutionuser" property of all sources * - CLIENT_TEST_EVOLUTION_PASSWORD = sets the "evolutionpassword" property of all sources * - CLIENT_TEST_SOURCES = comma separated list of active sources, * names as selected in their RegisterSyncSourceTest * instances * - CLIENT_TEST_DELAY = number of seconds to sleep between syncs, required * by some servers * - CLIENT_TEST_LOG = logfile name of a server, can be empty: * if given, then the content of that file will be * copied and stored together with the client log * (only works on Unix) * - CLIENT_TEST_NUM_ITEMS = numbers of contacts/events/... to use during * local and sync tests which create artificial * items * * The CLIENT_TEST_SERVER also has another meaning: it is used as hint * by the synccompare.pl script and causes it to automatically ignore * known, acceptable data modifications caused by sending an item to * a server and back again. Currently the script recognizes "funambol", * "scheduleworld", "synthesis" and "egroupware" as special server * names. */ class TestEvolution : public ClientTest { public: /** * can be instantiated as client A with id == "1" and client B with id == "2" */ TestEvolution(const string &id) : ClientTest(atoi(getEnv("CLIENT_TEST_DELAY", "0")), getEnv("CLIENT_TEST_LOG", "")), m_initialized(false), m_clientID(id), m_configs(SyncSource::getTestRegistry()) { } virtual std::string getClientID() const { return m_clientID; } /** * code depends on other global constructors to run first, execute it after constructor but before * any other methods */ void init() { if (m_initialized) { return; } else { m_initialized = true; } // allow each backend test to create more backend tests size_t count = 0; while (count != m_configs.size()) { count = m_configs.size(); BOOST_FOREACH (const RegisterSyncSourceTest *test, TestRegistry(m_configs)) { test->init(); } } const char *server = getenv("CLIENT_TEST_SERVER"); if (m_clientID == "1") { m_clientB.reset(new TestEvolution("2")); } /* check server */ if (!server) { server = "funambol"; setenv("CLIENT_TEST_SERVER", "funambol", 1); } /* override Evolution database names? */ const char *evoprefix = getenv("CLIENT_TEST_EVOLUTION_PREFIX"); m_evoPrefix = evoprefix ? evoprefix : "SyncEvolution_Test_"; const char *evouser = getenv("CLIENT_TEST_EVOLUTION_USER"); if (evouser) { m_evoUser = evouser; } const char *evopasswd = getenv("CLIENT_TEST_EVOLUTION_PASSWORD"); if (evopasswd) { m_evoPassword = evopasswd; } /* check sources */ const char *sourcelist = getenv("CLIENT_TEST_SOURCES"); set sources; if (sourcelist) { boost::split(sources, sourcelist, boost::is_any_of(",")); } else { BOOST_FOREACH(const RegisterSyncSourceTest *test, m_configs) { if (!test->m_configName.empty()) { sources.insert(test->m_configName); } } } BOOST_FOREACH(const RegisterSyncSourceTest *test, m_configs) { if (sources.find(test->m_configName) != sources.end() && !test->m_configName.empty()) { m_syncSource2Config.push_back(test->m_configName); } } /* Local Test SyncSource : remove all virtual datastores, inserting the * sub datastores*/ ClientTest::Config conf; BOOST_FOREACH (string source, sources) { getSourceConfig (source, conf); if (!conf.m_subConfigs.empty()) { vector subs; boost::split (subs, conf.m_subConfigs, boost::is_any_of(",")); BOOST_FOREACH (string sub, subs) { pushLocalSource2Config(sub); } } else { pushLocalSource2Config(source); } } // get configuration and set obligatory fields Logger::instance().setLevel(Logger::DEBUG); std::string root = std::string("evolution/") + server + "_" + m_clientID; boost::shared_ptr config(new SyncConfig(string(server) + "_" + m_clientID)); boost::shared_ptr from = boost::shared_ptr (); if (!config->exists()) { // no configuration yet, create in different contexts because // device ID is different config.reset(new SyncConfig(string(server) + "_" + m_clientID + "@client-test-" + m_clientID)); config->setDefaults(); from = SyncConfig::createPeerTemplate(server); if(from) { set filter; config->copy(*from, &filter); } config->setDevID(m_clientID == "1" ? "sc-api-nat" : "sc-pim-ppc"); } BOOST_FOREACH(const RegisterSyncSourceTest *test, m_configs) { if (test->m_configName.empty()) { continue; } ClientTest::Config testconfig; getSourceConfig(test, testconfig); CPPUNIT_ASSERT(!testconfig.m_type.empty()); boost::shared_ptr sc = config->getSyncSourceConfig(testconfig.m_sourceName); if (!sc || !sc->exists()) { // no configuration yet config->setSourceDefaults(testconfig.m_sourceName); sc = config->getSyncSourceConfig(testconfig.m_sourceName); CPPUNIT_ASSERT(sc); sc->setURI(testconfig.m_uri); if(from && !testconfig.m_sourceNameServerTemplate.empty()) { boost::shared_ptr scServerTemplate = from->getSyncSourceConfig(testconfig.m_sourceNameServerTemplate); sc->setURI(scServerTemplate->getURI()); } } // Set these properties if not set yet: that means the env // variables are used when creating the config initially, // but then no longer can be used to change the config. // This prevents accidentally running a test with default // values, for example for the database. if (!sc->getDatabaseID().wasSet()) { string database = getDatabaseName(test->m_configName); sc->setDatabaseID(database); } if (!sc->getUser().wasSet() && !m_evoUser.empty()) { sc->setUser(m_evoUser); } if (!sc->getPassword().wasSet() && !m_evoPassword.empty()) { sc->setPassword(m_evoPassword); } // Always set this one, to ensure the config matches the test. sc->setBackend(SourceType(testconfig.m_type).m_backend); } config->flush(); } virtual LocalTests *createLocalTests(const std::string &name, int sourceParam, ClientTest::Config &co) { init(); return new EvolutionLocalTests(name, *this, sourceParam, co); } virtual int getNumLocalSources() { init(); return m_localSource2Config.size(); } virtual int getNumSyncSources() { init(); return m_syncSource2Config.size(); } virtual void getLocalSourceConfig(int source, Config &config) { init(); getSourceConfig(m_configs[m_localSource2Config[source]], config); } virtual void getSyncSourceConfig(int source, Config &config) { init(); getSourceConfig(m_configs[m_syncSource2Config[source]], config); } virtual int getLocalSourcePosition(const string &configName) { for (size_t i=0; i< m_localSource2Config.size(); i++) { if(m_localSource2Config[i] == configName) { return i; break; } } return -1; } virtual void getSourceConfig (const string &configName, Config &config) { init(); return getSourceConfig (m_configs[configName], config); } static void getSourceConfig(const RegisterSyncSourceTest *test, Config &config) { ClientTest::getTestData(test->m_testCaseName.c_str(), config); config.m_createSourceA = createSource; config.m_createSourceB = createSource; config.m_sourceName = test->m_configName.c_str(); config.m_linkedSources = test->m_linkedSources; test->updateConfig(config); } virtual ClientTest *getClientB() { init(); return m_clientB.get(); } virtual bool isB64Enabled() { return false; } virtual SyncMLStatus doSync(const int *sources, const std::string &logbase, const SyncOptions &options) { init(); // Let "Client_Sync_Current" symlink point to a new, empty // directory logbase + ".server". Can be used by SyncEvolution // server as per-test logdir. std::string current = logbase + ".server"; rm_r(current); mkdir_p(current); rm_r("Client_Sync_Current"); symlink(current.c_str(), "Client_Sync_Current"); string server = getenv("CLIENT_TEST_SERVER") ? getenv("CLIENT_TEST_SERVER") : "funambol"; server += "_"; server += m_clientID; class ClientTest : public CmdlineSyncClient { public: ClientTest(const string &server, const string &logbase, const SyncOptions &options) : CmdlineSyncClient(server, false), m_logbase(logbase), m_options(options), m_started(false) {} protected: virtual void prepare() { setLogDir(m_logbase, true); setMaxLogDirs(0, true); setMaxObjSize(m_options.m_maxObjSize, true); setMaxMsgSize(m_options.m_maxMsgSize, true); setWBXML(m_options.m_isWBXML, true); setRetryDuration(m_options.m_retryDuration, true); setRetryInterval(m_options.m_retryInterval, true); if (m_options.m_syncMode == SYNC_TWO_WAY && m_options.m_checkReport.syncMode == SYNC_NONE) { // For this test, any kind of final sync mode is // acceptable. Disable slow sync prevention // temporarily. The check for the requested sync // mode is perhaps too conservative, but in // practice the only test where slow sync // prevention caused a test failure was // Client::Sync::eds_contact::testTwoWaySync after // some other failed test, so let's be conservative... setPreventSlowSync(false); } SyncContext::prepare(); if (m_options.m_prepareCallback && m_options.m_prepareCallback(*this, m_options)) { m_options.m_isAborted = SuspendFlags::getSuspendFlags().abort(); } } virtual void displaySyncProgress(sysync::TProgressEventEnum type, int32_t extra1, int32_t extra2, int32_t extra3) { if (!m_started) { m_started = true; if (m_options.m_startCallback(*this, m_options)) { m_options.m_isAborted = SuspendFlags::getSuspendFlags().abort(); } } } virtual boost::shared_ptr createTransportAgent() { boost::shared_ptrwrapper = m_options.m_transport; boost::shared_ptragent =SyncContext::createTransportAgent(); if (!wrapper.get()) return agent; dynamic_cast(wrapper.get())->setAgent(agent); dynamic_cast(wrapper.get())->setSyncOptions(&m_options); return wrapper; } private: const string m_logbase; SyncOptions m_options; bool m_started; } client(server, logbase, options); // configure active sources with the desired sync mode, // disable the rest FilterConfigNode::ConfigFilter filter; filter["sync"] = InitStateString("none", true); client.setConfigFilter(false, "", filter); filter["sync"] = InitStateString(PrettyPrintSyncMode(options.m_syncMode), true); for(int i = 0; sources[i] >= 0; i++) { std::string &name = m_syncSource2Config[sources[i]]; client.setConfigFilter(false, name, filter); checkEvolutionSource(name); } SyncReport report; SyncMLStatus status = client.sync(&report); options.m_checkReport.check(status, report); return status; } private: bool m_initialized; string m_clientID; std::auto_ptr m_clientB; const TestRegistry &m_configs; /** prefix, username, password to be used for local databases */ string m_evoPrefix, m_evoUser, m_evoPassword; /** * The ClientTest framework identifies active configs with an integer. * This is the mapping to the corresponding config name, created when * constructing this instance. */ vector m_localSource2Config; vector m_syncSource2Config; /** returns the name of the Evolution database */ string getDatabaseName(const string &configName) { if (configName == "calendar+todo") { return "eds_event,eds_task"; } else if (configName == "file_calendar+todo") { return "file_event,file_task"; } return m_evoPrefix + configName + "_" + m_clientID; } /** called by test frame work */ static TestingSyncSource *createSource(ClientTest &client, const std::string &clientID, int source, bool isSourceA) { TestEvolution &evClient((TestEvolution &)client); string name = evClient.m_localSource2Config[source]; // implement Evolution shutdown workaround (see lockEvolution above) evClient.checkEvolutionSource(name); return evClient.createNamedSource(name, isSourceA); } /** called internally in this class */ TestingSyncSource *createNamedSource(const string &name, bool isSourceA) { string database = getDatabaseName(name); std::string config = "target-config@client-test"; const char *server = getenv("CLIENT_TEST_SERVER"); if (server) { config += "-"; config += server; } std::string tracking = string("_") + m_clientID + "_" + (isSourceA ? "A" : "B"); SE_LOG_DEBUG(NULL, "instantiating testing source %s in config %s, with tracking name %s", name.c_str(), config.c_str(), tracking.c_str()); boost::shared_ptr context(new SyncConfig(config)); SyncSourceNodes nodes = context->getSyncSourceNodes(name, tracking); // The user of client-test must have configured the source // @_/ when doing // Client::Sync testing. Our testing source must use the same // properties, but different change tracking. std::string peerName = server ? (std::string(server) + "_" + m_clientID) : "@default"; boost::shared_ptr peer(new SyncConfig(peerName)); SyncSourceNodes peerNodes = peer->getSyncSourceNodes(name); SE_LOG_DEBUG(NULL, "overriding testing source %s properties with the ones from config %s = %s", name.c_str(), peerName.c_str(), peer->getRootPath().c_str()); BOOST_FOREACH(const ConfigProperty *prop, SyncSourceConfig::getRegistry()) { if (prop->isHidden()) { continue; } boost::shared_ptr node = peerNodes.getNode(*prop); InitStateString value = prop->getProperty(*node); SE_LOG_DEBUG(NULL, " %s = %s (%s)", prop->getMainName().c_str(), value.c_str(), value.wasSet() ? "set" : "default"); node = nodes.getNode(*prop); node->setProperty(prop->getMainName(), value); } context->flush(); // Same as in init() above: set values if still empty, but don't // overwrite anything. boost::shared_ptr props = nodes.getProperties(); std::string value; if (!props->getProperty("database", value)) { props->setProperty("database", database); } if (!props->getProperty("databaseUser", value) && !m_evoUser.empty()) { props->setProperty("databaseUser", m_evoUser); } if (!props->getProperty("databasePassword", value) && !m_evoPassword.empty()) { props->setProperty("databasePassword", m_evoPassword); } SyncSourceParams params(name, nodes, context); const RegisterSyncSourceTest *test = m_configs[name]; ClientTestConfig testConfig; getSourceConfig(test, testConfig); PersistentSyncSourceConfig sourceConfig(params.m_name, params.m_nodes); sourceConfig.setSourceType(SourceType(testConfig.m_type)); // downcasting here: anyone who registers his sources for testing // must ensure that they are indeed TestingSyncSource instances SyncSource *ss = SyncSource::createSource(params); return static_cast(ss); } // push source into localsource2config if it doesn't exist in the vector void pushLocalSource2Config(const string &source) { bool finded = false; BOOST_FOREACH(string element, m_localSource2Config) { if (boost::iequals(element, source)) { finded = true; break; } } if (!finded) { m_localSource2Config.push_back (source); } } void checkEvolutionSource(std::string &name) { string basename; // hard-coded names as used by src/backends/evolution; // if some other backend reuses them, it gets the // same treatment, which shouldn't cause any harm if (name == "eds_contact") { basename = "ebook"; } else if (name == "eds_event" || name == "text") { basename = "ecal"; } if (!basename.empty() && lockEvolution.find(basename) == lockEvolution.end()) { lockEvolution[basename].reset(createNamedSource(name, true)); lockEvolution[basename]->open(); ClientTest::registerCleanup(CleanupSources); } } }; static class RegisterTestEvolution { public: RegisterTestEvolution() : testClient("1") { testClient.registerTests(); } private: TestEvolution testClient; } testEvolution; SE_END_CXX