Imported Upstream version 0.8.1~0.9~beta3~20090723
[platform/upstream/syncevolution.git] / src / core / SyncEvolutionCmdline.cpp
1 /*
2  * Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
3  * Copyright (C) 2009 Intel Corporation
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) version 3.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18  * 02110-1301  USA
19  */
20
21 #include "SyncEvolutionCmdline.h"
22 #include "FilterConfigNode.h"
23 #include "VolatileConfigNode.h"
24 #include "EvolutionSyncSource.h"
25 #include "EvolutionSyncClient.h"
26 #include "SyncEvolutionUtil.h"
27
28 #include <unistd.h>
29 #include <errno.h>
30
31 #include <iostream>
32 #include <iomanip>
33 #include <sstream>
34 #include <memory>
35 #include <set>
36 #include <algorithm>
37 using namespace std;
38
39 #include <boost/shared_ptr.hpp>
40 #include <boost/algorithm/string/join.hpp>
41 #include <boost/algorithm/string.hpp>
42 #include <boost/foreach.hpp>
43
44 SyncEvolutionCmdline::SyncEvolutionCmdline(int argc, const char * const * argv, ostream &out, ostream &err) :
45     m_argc(argc),
46     m_argv(argv),
47     m_out(out),
48     m_err(err),
49     m_validSyncProps(EvolutionSyncConfig::getRegistry()),
50     m_validSourceProps(EvolutionSyncSourceConfig::getRegistry())
51 {}
52
53 bool SyncEvolutionCmdline::parse()
54 {
55     int opt = 1;
56     while (opt < m_argc) {
57         if (m_argv[opt][0] != '-') {
58             break;
59         }
60         if (boost::iequals(m_argv[opt], "--sync") ||
61             boost::iequals(m_argv[opt], "-s")) {
62             opt++;
63             string param;
64             string cmdopt(m_argv[opt - 1]);
65             if (!parseProp(m_validSourceProps, m_sourceProps,
66                            m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt],
67                            EvolutionSyncSourceConfig::m_sourcePropSync.getName().c_str())) {
68                 return false;
69             }
70
71             // disable requirement to add --run explicitly in order to
72             // be compatible with traditional command lines
73             m_run = true;
74         } else if(boost::iequals(m_argv[opt], "--sync-property") ||
75                   boost::iequals(m_argv[opt], "-y")) {
76                 opt++;
77                 if (!parseProp(m_validSyncProps, m_syncProps,
78                                m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt])) {
79                     return false;
80                 }
81         } else if(boost::iequals(m_argv[opt], "--source-property") ||
82                   boost::iequals(m_argv[opt], "-z")) {
83             opt++;
84             if (!parseProp(m_validSourceProps, m_sourceProps,
85                            m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt])) {
86                 return false;
87             }
88         } else if(boost::iequals(m_argv[opt], "--template") ||
89                   boost::iequals(m_argv[opt], "-l")) {
90             opt++;
91             if (opt >= m_argc) {
92                 usage(true, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
93                 return false;
94             }
95             m_template = m_argv[opt];
96             m_configure = true;
97             if (boost::trim_copy(m_template) == "?") {
98                 dumpServers("Available configuration templates:",
99                             EvolutionSyncConfig::getServerTemplates());
100                 m_dontrun = true;
101             }
102         } else if(boost::iequals(m_argv[opt], "--print-servers")) {
103             m_printServers = true;
104         } else if(boost::iequals(m_argv[opt], "--print-config") ||
105                   boost::iequals(m_argv[opt], "-p")) {
106             m_printConfig = true;
107         } else if(boost::iequals(m_argv[opt], "--print-sessions")) {
108             m_printSessions = true;
109         } else if(boost::iequals(m_argv[opt], "--configure") ||
110                   boost::iequals(m_argv[opt], "-c")) {
111             m_configure = true;
112         } else if(boost::iequals(m_argv[opt], "--remove")) {
113             m_remove = true;
114         } else if(boost::iequals(m_argv[opt], "--run") ||
115                   boost::iequals(m_argv[opt], "-r")) {
116             m_run = true;
117         } else if(boost::iequals(m_argv[opt], "--restore")) {
118             opt++;
119             if (opt >= m_argc) {
120                 usage(true, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
121                 return false;
122             }
123             m_restore = m_argv[opt];
124             if (m_restore.empty()) {
125                 usage(true, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
126                 return false;
127             }
128             if (!isDir(m_restore)) {
129                 usage(true, string("parameter '") + m_restore + "' for " + cmdOpt(m_argv[opt - 1]) + " must be log directory");
130                 return false;
131             }
132         } else if(boost::iequals(m_argv[opt], "--before")) {
133             m_before = true;
134         } else if(boost::iequals(m_argv[opt], "--after")) {
135             m_after = true;
136         } else if(boost::iequals(m_argv[opt], "--dry-run")) {
137             m_dryrun = true;
138         } else if(boost::iequals(m_argv[opt], "--migrate")) {
139             m_migrate = true;
140         } else if(boost::iequals(m_argv[opt], "--status") ||
141                   boost::iequals(m_argv[opt], "-t")) {
142             m_status = true;
143         } else if(boost::iequals(m_argv[opt], "--quiet") ||
144                   boost::iequals(m_argv[opt], "-q")) {
145             m_quiet = true;
146         } else if(boost::iequals(m_argv[opt], "--help") ||
147                   boost::iequals(m_argv[opt], "-h")) {
148             m_usage = true;
149         } else if(boost::iequals(m_argv[opt], "--version")) {
150             m_version = true;
151         } else {
152             usage(false, string(m_argv[opt]) + ": unknown parameter");
153             return false;
154         }
155         opt++;
156     }
157
158     if (opt < m_argc) {
159         m_server = m_argv[opt++];
160         while (opt < m_argc) {
161             m_sources.insert(m_argv[opt++]);
162         }
163     }
164
165     return true;
166 }
167
168 bool SyncEvolutionCmdline::run() {
169     // --dry-run is only supported by some operations.
170     // Be very strict about it and make sure it is off in all
171     // potentially harmful operations, otherwise users might
172     // expect it to have an effect when it doesn't.
173
174     if (m_usage) {
175         usage(true);
176     } else if (m_version) {
177         printf("SyncEvolution %s\n", VERSION);
178         printf("%s", EDSAbiWrapperInfo());
179     } else if (m_printServers || boost::trim_copy(m_server) == "?") {
180         dumpServers("Configured servers:",
181                     EvolutionSyncConfig::getServers());
182     } else if (m_dontrun) {
183         // user asked for information
184     } else if (m_argc == 1) {
185         // no parameters: list databases and short usage
186         const SourceRegistry &registry(EvolutionSyncSource::getSourceRegistry());
187         boost::shared_ptr<FilterConfigNode> configNode(new VolatileConfigNode());
188         boost::shared_ptr<FilterConfigNode> hiddenNode(new VolatileConfigNode());
189         boost::shared_ptr<FilterConfigNode> trackingNode(new VolatileConfigNode());
190         SyncSourceNodes nodes(configNode, hiddenNode, trackingNode);
191         EvolutionSyncSourceParams params("list", nodes, "");
192         
193         BOOST_FOREACH(const RegisterSyncSource *source, registry) {
194             BOOST_FOREACH(const Values::value_type &alias, source->m_typeValues) {
195                 if (!alias.empty() && source->m_enabled) {
196                     configNode->setProperty("type", *alias.begin());
197                     auto_ptr<EvolutionSyncSource> source(EvolutionSyncSource::createSource(params, false));
198                     if (source.get() != NULL) {
199                         listSources(*source, boost::join(alias, " = "));
200                         m_out << "\n";
201                     }
202                 }
203             }
204         }
205
206         usage(false);
207     } else if (m_printConfig) {
208         boost::shared_ptr<EvolutionSyncConfig> config;
209
210         if (m_template.empty()) {
211             if (m_server.empty()) {
212                 m_err << "ERROR: --print-config requires either a --template or a server name." << endl;
213                 return false;
214             }
215             config.reset(new EvolutionSyncConfig(m_server));
216             if (!config->exists()) {
217                 m_err << "ERROR: server '" << m_server << "' has not been configured yet." << endl;
218                 return false;
219             }
220         } else {
221             config = EvolutionSyncConfig::createServerTemplate(m_template);
222             if (!config.get()) {
223                 m_err << "ERROR: no configuration template for '" << m_template << "' available." << endl;
224                 return false;
225             }
226         }
227
228         if (m_sources.empty() ||
229             m_sources.find("main") != m_sources.end()) {
230             boost::shared_ptr<FilterConfigNode> syncProps(config->getProperties());
231             syncProps->setFilter(m_syncProps);
232             dumpProperties(*syncProps, config->getRegistry());
233         }
234
235         list<string> sources = config->getSyncSources();
236         sources.sort();
237         BOOST_FOREACH(const string &name, sources) {
238             if (m_sources.empty() ||
239                 m_sources.find(name) != m_sources.end()) {
240                 m_out << endl << "[" << name << "]" << endl;
241                 ConstSyncSourceNodes nodes = config->getSyncSourceNodes(name);
242                 boost::shared_ptr<FilterConfigNode> sourceProps(new FilterConfigNode(boost::shared_ptr<const ConfigNode>(nodes.m_configNode)));
243                 sourceProps->setFilter(m_sourceProps);
244                 dumpProperties(*sourceProps, EvolutionSyncSourceConfig::getRegistry());
245             }
246         }
247     } else if (m_server == "" && m_argc > 1) {
248         // Options given, but no server - not sure what the user wanted?!
249         usage(true, "server name missing");
250         return false;
251     } else if (m_configure || m_migrate) {
252         if (m_dryrun) {
253             EvolutionSyncClient::throwError("--dry-run not supported for configuration changes");
254         }
255
256         bool fromScratch = false;
257
258         // Both config changes and migration are implemented as copying from
259         // another config (template resp. old one). Migration also moves
260         // the old config.
261         boost::shared_ptr<EvolutionSyncConfig> from;
262         if (m_migrate) {
263             from.reset(new EvolutionSyncConfig(m_server));
264             if (!from->exists()) {
265                 m_err << "ERROR: server '" << m_server << "' has not been configured yet." << endl;
266                 return false;
267             }
268
269             int counter = 0;
270             string oldRoot = from->getRootPath();
271             string suffix;
272             while (true) {
273                 string newname;
274                 ostringstream newsuffix;
275                 newsuffix << ".old";
276                 if (counter) {
277                     newsuffix << "." << counter;
278                 }
279                 suffix = newsuffix.str();
280                 newname = oldRoot + suffix;
281                 if (!rename(oldRoot.c_str(),
282                             newname.c_str())) {
283                     break;
284                 } else if (errno != EEXIST && errno != ENOTEMPTY) {
285                     m_err << "ERROR: renaming " << oldRoot << " to " <<
286                         newname << ": " << strerror(errno) << endl;
287                     return false;
288                 }
289                 counter++;
290             }
291
292             from.reset(new EvolutionSyncConfig(m_server + suffix));
293         } else {
294             from.reset(new EvolutionSyncConfig(m_server));
295             if (!from->exists()) {
296                 // creating from scratch, look for template
297                 fromScratch = true;
298                 string configTemplate = m_template.empty() ? m_server : m_template;
299                 from = EvolutionSyncConfig::createServerTemplate(configTemplate);
300                 if (!from.get()) {
301                     m_err << "ERROR: no configuration template for '" << configTemplate << "' available." << endl;
302                     dumpServers("Available configuration templates:",
303                                 EvolutionSyncConfig::getServerTemplates());
304                     return false;
305                 }
306             }
307         }
308
309         // apply config changes on-the-fly
310         from->setConfigFilter(true, m_syncProps);
311         from->setConfigFilter(false, m_sourceProps);
312
313         // write into the requested configuration, creating it if necessary
314         boost::shared_ptr<EvolutionSyncConfig> to(new EvolutionSyncConfig(m_server));
315         to->copy(*from, !fromScratch && !m_sources.empty() ? &m_sources : NULL);
316
317         // Sources are active now according to the server default.
318         // Disable all sources not selected by user (if any selected)
319         // and those which have no database.
320         if (fromScratch) {
321             list<string> configuredSources = to->getSyncSources();
322             set<string> sources = m_sources;
323             
324             BOOST_FOREACH(const string &source, configuredSources) {
325                 boost::shared_ptr<PersistentEvolutionSyncSourceConfig> sourceConfig(to->getSyncSourceConfig(source));
326                 string disable = "";
327                 set<string>::iterator entry = sources.find(source);
328                 bool selected = entry != sources.end();
329
330                 if (!m_sources.empty() &&
331                     !selected) {
332                     disable = "not selected";
333                 } else {
334                     if (entry != sources.end()) {
335                         // The command line parameter matched a valid source.
336                         // All entries left afterwards must have been typos.
337                         sources.erase(entry);
338                     }
339
340                     // check whether the sync source works
341                     EvolutionSyncSourceParams params("list", to->getSyncSourceNodes(source), "");
342                     auto_ptr<EvolutionSyncSource> syncSource(EvolutionSyncSource::createSource(params, false));
343                     if (syncSource.get() == NULL) {
344                         disable = "no backend available";
345                     } else {
346                         try {
347                             EvolutionSyncSource::Databases databases = syncSource->getDatabases();
348                             if (databases.empty()) {
349                                 disable = "no database to synchronize";
350                             }
351                         } catch (...) {
352                             disable = "backend failed";
353                         }
354                     }
355                 }
356
357                 if (!disable.empty()) {
358                     // abort if the user explicitly asked for the sync source
359                     // and it cannot be enabled, otherwise disable it silently
360                     if (selected) {
361                         EvolutionSyncClient::throwError(source + ": " + disable);
362                     }
363                     sourceConfig->setSync("disabled");
364                 } else if (selected) {
365                     // user absolutely wants it: enable even if off by default
366                     FilterConfigNode::ConfigFilter::const_iterator sync =
367                         m_sourceProps.find(EvolutionSyncSourceConfig::m_sourcePropSync.getName());
368                     sourceConfig->setSync(sync == m_sourceProps.end() ? "two-way" : sync->second);
369                 }
370             }
371
372             if (!sources.empty()) {
373                 EvolutionSyncClient::throwError(string("no such source(s): ") + boost::join(sources, " "));
374             }
375         }
376
377         // done, now write it
378         to->flush();
379     } else if (m_remove) {
380         if (m_dryrun) {
381             EvolutionSyncClient::throwError("--dry-run not supported for removing configurations");
382         }
383
384         // extra sanity check
385         if (!m_sources.empty() ||
386             !m_syncProps.empty() ||
387             !m_sourceProps.empty()) {
388             usage(true, "too many parameters for --remove");
389             return false;
390         } else {
391             boost::shared_ptr<EvolutionSyncConfig> config;
392             config.reset(new EvolutionSyncConfig(m_server));
393             config->remove();
394             return true;
395         }
396     } else {
397         EvolutionSyncClient client(m_server, true, m_sources);
398         client.setQuiet(m_quiet);
399         client.setDryRun(m_dryrun);
400         client.setConfigFilter(true, m_syncProps);
401         client.setConfigFilter(false, m_sourceProps);
402         if (m_status) {
403             client.status();
404         } else if (m_printSessions) {
405             vector<string> dirs;
406             client.getSessions(dirs);
407             bool first = true;
408             BOOST_FOREACH(const string &dir, dirs) {
409                 if (first) {
410                     first = false;
411                 } else if(!m_quiet) {
412                     cout << endl;
413                 }
414                 cout << dir << endl;
415                 if (!m_quiet) {
416                     SyncReport report;
417                     client.readSessionInfo(dir, report);
418                     cout << report;
419                 }
420             }
421         } else if (!m_restore.empty()) {
422             // sanity checks: either --after or --before must be given, sources must be selected
423             if ((!m_after && !m_before) ||
424                 (m_after && m_before)) {
425                 usage(false, "--restore <log dir> must be used with either --after (restore database as it was after that sync) or --before (restore data from before sync)");
426                 return false;
427             }
428             if (m_sources.empty()) {
429                 usage(false, "Sources must be selected explicitly for --restore to prevent accidental restore.");
430                 return false;
431             }
432             client.restore(m_restore,
433                            m_after ?
434                            EvolutionSyncClient::DATABASE_AFTER_SYNC :
435                            EvolutionSyncClient::DATABASE_BEFORE_SYNC);
436         } else {
437             if (m_dryrun) {
438                 EvolutionSyncClient::throwError("--dry-run not supported for running a synchronization");
439             }
440
441             // safety catch: if props are given, then --run
442             // is required
443             if (!m_run &&
444                 (m_syncProps.size() || m_sourceProps.size())) {
445                 usage(false, "Properties specified, but neither '--configure' nor '--run' - what did you want?");
446                 return false;
447             }
448
449             return ( client.sync() == STATUS_OK);
450         }
451     }
452
453     return true;
454 }
455
456 string SyncEvolutionCmdline::cmdOpt(const char *opt, const char *param)
457 {
458     string res = "'";
459     res += opt;
460     if (param) {
461         res += " ";
462         res += param;
463     }
464     res += "'";
465     return res;
466 }
467
468 bool SyncEvolutionCmdline::parseProp(const ConfigPropertyRegistry &validProps,
469                                      FilterConfigNode::ConfigFilter &props,
470                                      const char *opt,
471                                      const char *param,
472                                      const char *propname)
473 {
474     if (!param) {
475         usage(true, string("missing parameter for ") + cmdOpt(opt, param));
476         return false;
477     } else if (boost::trim_copy(string(param)) == "?") {
478         m_dontrun = true;
479         if (propname) {
480             return listPropValues(validProps, propname, opt);
481         } else {
482             return listProperties(validProps, opt);
483         }
484     } else {
485         string propstr;
486         string paramstr;
487         if (propname) {
488             propstr = propname;
489             paramstr = param;
490         } else {
491             const char *equal = strchr(param, '=');
492             if (!equal) {
493                 usage(true, string("the '=<value>' part is missing in: ") + cmdOpt(opt, param));
494                 return false;
495             }
496             propstr.assign(param, equal - param);
497             paramstr.assign(equal + 1);
498         }
499
500         boost::trim(propstr);
501         boost::trim_left(paramstr);
502
503         if (boost::trim_copy(paramstr) == "?") {
504             m_dontrun = true;
505             return listPropValues(validProps, propstr, cmdOpt(opt, param));
506         } else {
507             const ConfigProperty *prop = validProps.find(propstr);
508             if (!prop) {
509                 m_err << "ERROR: " << cmdOpt(opt, param) << ": no such property" << endl;
510                 return false;
511             } else {
512                 string error;
513                 if (!prop->checkValue(paramstr, error)) {
514                     m_err << "ERROR: " << cmdOpt(opt, param) << ": " << error << endl;
515                     return false;
516                 } else {
517                     props[propstr] = paramstr;
518                     return true;                        
519                 }
520             }
521         }
522     }
523 }
524
525 bool SyncEvolutionCmdline::listPropValues(const ConfigPropertyRegistry &validProps,
526                                           const string &propName,
527                                           const string &opt)
528 {
529     const ConfigProperty *prop = validProps.find(propName);
530     if (!prop) {
531         m_err << "ERROR: "<< opt << ": no such property" << endl;
532         return false;
533     } else {
534         m_out << opt << endl;
535         string comment = prop->getComment();
536
537         if (comment != "") {
538             list<string> commentLines;
539             ConfigProperty::splitComment(comment, commentLines);
540             BOOST_FOREACH(const string &line, commentLines) {
541                 m_out << "   " << line << endl;
542             }
543         } else {
544             m_out << "   no documentation available" << endl;
545         }
546         return true;
547     }
548 }
549
550 bool SyncEvolutionCmdline::listProperties(const ConfigPropertyRegistry &validProps,
551                                           const string &opt)
552 {
553     // The first of several related properties has a comment.
554     // Remember that comment and print it as late as possible,
555     // that way related properties preceed their comment.
556     string comment;
557     BOOST_FOREACH(const ConfigProperty *prop, validProps) {
558         if (!prop->isHidden()) {
559             string newComment = prop->getComment();
560
561             if (newComment != "") {
562                 if (!comment.empty()) {
563                     dumpComment(m_out, "   ", comment);
564                     m_out << endl;
565                 }
566                 comment = newComment;
567             }
568             m_out << prop->getName() << ":" << endl;
569         }
570     }
571     dumpComment(m_out, "   ", comment);
572     return true;
573 }
574
575 void SyncEvolutionCmdline::listSources(EvolutionSyncSource &syncSource, const string &header)
576 {
577     m_out << header << ":\n";
578     EvolutionSyncSource::Databases databases = syncSource.getDatabases();
579
580     BOOST_FOREACH(const EvolutionSyncSource::Database &database, databases) {
581         m_out << "   " << database.m_name << " (" << database.m_uri << ")";
582         if (database.m_isDefault) {
583             m_out << " <default>";
584         }
585         m_out << endl;
586     }
587 }
588
589 void SyncEvolutionCmdline::dumpServers(const string &preamble,
590                                        const EvolutionSyncConfig::ServerList &servers)
591 {
592     m_out << preamble << endl;
593     BOOST_FOREACH(const EvolutionSyncConfig::ServerList::value_type &server,servers) {
594         m_out << "   "  << server.first << " = " << server.second << endl;
595     }
596     if (!servers.size()) {
597         m_out << "   none" << endl;
598     }
599 }
600
601 void SyncEvolutionCmdline::dumpProperties(const ConfigNode &configuredProps,
602                                           const ConfigPropertyRegistry &allProps)
603 {
604     BOOST_FOREACH(const ConfigProperty *prop, allProps) {
605         if (prop->isHidden()) {
606             continue;
607         }
608         if (!m_quiet) {
609             string comment = prop->getComment();
610             if (!comment.empty()) {
611                 m_out << endl;
612                 dumpComment(m_out, "# ", comment);
613             }
614         }
615         bool isDefault;
616         prop->getProperty(configuredProps, &isDefault);
617         if (isDefault) {
618             m_out << "# ";
619         }
620         m_out << prop->getName() << " = " << prop->getProperty(configuredProps) << endl;
621     }
622 }
623
624 void SyncEvolutionCmdline::dumpComment(ostream &stream,
625                                        const string &prefix,
626                                        const string &comment)
627 {
628     list<string> commentLines;
629     ConfigProperty::splitComment(comment, commentLines);
630     BOOST_FOREACH(const string &line, commentLines) {
631         stream << prefix << line << endl;
632     }
633 }
634
635 void SyncEvolutionCmdline::usage(bool full, const string &error, const string &param)
636 {
637     ostream &out(error.empty() ? m_out : m_err);
638
639     out << "Show available sources:" << endl;
640     out << "  " << m_argv[0] << endl;
641     out << "Show information about configuration(s) and sync sessions:" << endl;
642     out << "  " << m_argv[0] << " --print-servers" << endl;
643     out << "  " << m_argv[0] << " --print-config [--quiet] <server> [sync|<source ...]" << endl;
644     out << "  " << m_argv[0] << " --print-sessions [--quiet] <server>" << endl;
645     out << "Show information about SyncEvolution:" << endl;
646     out << "  " << m_argv[0] << " --help|-h" << endl;
647     out << "  " << m_argv[0] << " --version" << endl;
648     out << "Run a synchronization:" << endl;
649     out << "  " << m_argv[0] << " <server> [<source> ...]" << endl;
650     out << "  " << m_argv[0] << " --run <options for run> <server> [<source> ...]" << endl;
651     out << "Restore data from the automatic backups:" << endl;
652     out << "  " << m_argv[0] << " --restore <session directory> --before|--after [--dry-run] <server> <source> ..." << endl;
653     out << "Remove a configuration:" << endl;
654     out << "  " << m_argv[0] << " --remove <server>" << endl;
655     out << "Modify configuration:" << endl;
656     out << "  " << m_argv[0] << " --configure <options for configuration> <server> [<source> ...]" << endl;
657     out << "  " << m_argv[0] << " --migrate <server>" << endl;
658     if (full) {
659         out << endl <<
660             "Options:" << endl <<
661             "--sync|-s <mode>" << endl <<
662             "--sync|-s ?" << endl <<
663             "  Temporarily synchronize the active sources in that mode. Useful" << endl <<
664             "  for a \"refresh-from-server\" or \"refresh-from-client\" sync which" << endl <<
665             "  clears all data at one end and copies all items from the other." << endl <<
666             "" << endl <<
667             "--print-servers" << endl <<
668             "  Prints the names of all configured servers to stdout." << endl <<
669             "" << endl <<
670             "--print-config|-p" << endl <<
671             "  Prints the complete configuration for the selected server" << endl <<
672             "  to stdout, including up-to-date comments for all properties. The" << endl <<
673             "  format is the normal .ini format with source configurations in" << endl <<
674             "  different sections introduced with [<source>] lines. Can be combined" << endl <<
675             "  with --sync-property and --source-property to modify the configuration" << endl <<
676             "  on-the-fly. When one or more sources are listed after the <server>" << endl <<
677             "  name on the command line, then only the configs of those sources are" << endl <<
678             "  printed. Using --quiet suppresses the comments for each property." << endl <<
679             "  When setting a --template, then the reference configuration for" << endl <<
680             "  that server is printed instead of an existing configuration." << endl <<
681             "" << endl <<
682             "--print-sessions" << endl <<
683             "  Prints a list of all previous log directories. Unless --quiet is used, each" << endl <<
684             "  file name is followed by the original sync report." << endl <<
685             "" << endl <<
686             "-–configure|-c" << endl <<
687             "  Modify the configuration files for the selected server. If no such" << endl <<
688             "  configuration exists, then a new one is created using one of the" << endl <<
689             "  template configurations (see --template option). When creating" << endl <<
690             "  a new configuration only the active sources will be set to active" << endl <<
691             "  in the new configuration, i.e. \"syncevolution -c scheduleworld addressbook\"" << endl <<
692             "  followed by \"syncevolution scheduleworld\" will only synchronize the" << endl <<
693             "  address book. The other sources are created in a disabled state." << endl <<
694             "  When modifying an existing configuration and sources are specified," << endl <<
695             "  then the source properties of only those sources are modified." << endl <<
696             "" << endl <<
697             "--migrate" << endl <<
698             "  In SyncEvolution <= 0.7 a different layout of configuration files" << endl <<
699             "  was used. Using --migrate will automatically migrate to the new" << endl <<
700             "  layout and rename the old directory $HOME/.sync4j/evolution/<server> " << endl <<
701             "  into $HOME/.sync4j/evolution/<server>.old to prevent accidental use" << endl <<
702             "  of the old configuration. WARNING: old SyncEvolution releases cannot" << endl <<
703             "  use the new configuration!" << endl <<
704             "  The switch can also be used to migrate a configuration in the current" << endl <<
705             "  configuration directory: this preserves all property values, discards" << endl <<
706             "  obsolete properties and sets all comments exactly as if the configuration" << endl <<
707             "  had been created from scratch. WARNING: custom comments in the" << endl <<
708             "  configuration are not preserved." << endl <<
709             "  --migrate implies --configure and can be combined with modifying" << endl <<
710             "  properties." << endl <<
711             "" << endl <<
712             "--restore" << endl <<
713             "  Restores the data of the selected sources to the state from before or after the" << endl <<
714             "  selected synchronization. The synchronization is selected via its log directory" << endl <<
715             "  (see --print-sessions). Other directories can also be given as long as" << endl <<
716             "  they contain database dumps in the format created by SyncEvolution." << endl <<
717             "  The output includes information about the changes made during the" << endl <<
718             "  restore, both in terms of item changes and content changes (which is" << endl <<
719             "  not always the same, see manual for details). This output can be suppressed" << endl <<
720             "  with --quiet." << endl <<
721             "  In combination with --dry-run, the changes to local data are only simulated." << endl <<
722             "  This can be used to check that --restore will not remove valuable information." << endl <<
723             "" << endl <<
724             "--remove" << endl <<
725             "  This removes only the configuration files and related meta information." << endl <<
726             "  If other files were added to the config directory of the server, then" << endl <<
727             "  those and the directory will not be removed. Log directories will also" << endl <<
728             "  not be removed." << endl <<
729             "" << endl <<
730             "--sync-property|-y <property>=<value>" << endl <<
731             "--sync-property|-y ?" << endl <<
732             "--sync-property|-y <property>=?" << endl <<
733             "  Overrides a configuration property in the <server>/config.ini file" << endl <<
734             "  for the current synchronization run or permanently when --configure" << endl <<
735             "  is used to update the configuration. Can be used multiple times." << endl <<
736             "  Specifying an unused property will trigger an error message." << endl <<
737             "" << endl <<
738             "--source-property|-z <property>=<value>" << endl <<
739             "--source-property|-z ?" << endl <<
740             "--source-property|-z <property>=?" << endl <<
741             "  Same as --sync-option, but applies to the configuration of all active" << endl <<
742             "  sources. \"--sync <mode>\" is a shortcut for \"--source-option sync=<mode>\"." << endl <<
743             "" << endl <<
744             "--template|-l <server name>|default|?" << endl <<
745             "  Can be used to select from one of the built-in default configurations" << endl <<
746             "  for known SyncML servers. Defaults to the <server> name, so --template" << endl <<
747             "  only has to be specified when creating multiple different configurations" << endl <<
748             "  for the same server. \"default\" is an alias for \"scheduleworld\" and can be" << endl <<
749             "  used as the starting point for servers which do not have a built-in" << endl <<
750             "  configuration." << endl <<
751             "  Each template contains a pseudo-random device ID. Therefore setting the" << endl <<
752             "  \"deviceId\" sync property is only necessary when manually recreating a" << endl <<
753             "  configuration or when a more descriptive name is desired." << endl <<
754             "" << endl <<
755             "--status|-t" << endl <<
756             "  The changes made to local data since the last synchronization are" << endl <<
757             "  shown without starting a new one. This can be used to see in advance" << endl <<
758             "  whether the local data needs to be synchronized with the server." << endl <<
759             "" << endl <<
760             "--quiet|-q" << endl <<
761             "  Suppresses most of the normal output during a synchronization. The" << endl <<
762             "  log file still contains all the information." << endl <<
763             "" << endl <<
764             "--help|-h" << endl <<
765             "  Prints usage information." << endl <<
766             "" << endl <<
767             "--version" << endl <<
768             "  Prints the SyncEvolution version." << endl;
769     }
770
771     if (error != "") {
772         out << endl << "ERROR: " << error << endl;
773     }
774     if (param != "") {
775         out << "INFO: use '" << param << (param[param.size() - 1] == '=' ? "" : " ") <<
776             "?' to get a list of valid parameters" << endl;
777     }
778 }
779
780 #ifdef ENABLE_UNIT_TESTS
781 #include "test.h"
782
783 /** simple line-by-line diff */
784 static string diffStrings(const string &lhs, const string &rhs)
785 {
786     ostringstream res;
787
788     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
789     string_split_iterator lit =
790         boost::make_split_iterator(lhs, boost::first_finder("\n", boost::is_iequal()));
791     string_split_iterator rit =
792         boost::make_split_iterator(rhs, boost::first_finder("\n", boost::is_iequal()));
793     while (lit != string_split_iterator() &&
794            rit != string_split_iterator()) {
795         if (*lit != *rit) {
796             res << "< " << *lit << endl;
797             res << "> " << *rit << endl;
798         }
799         ++lit;
800         ++rit;
801     }
802
803     while (lit != string_split_iterator()) {
804         res << "< " << *lit << endl;
805         ++lit;
806     }
807
808     while (rit != string_split_iterator()) {
809         res << "> " << *rit << endl;
810         ++rit;
811     }
812
813     return res.str();
814 }
815
816 # define CPPUNIT_ASSERT_EQUAL_DIFF( expected, actual )      \
817     do { \
818         string expected_ = (expected);                                  \
819         string actual_ = (actual);                                      \
820         if (expected_ != actual_) {                                     \
821             CPPUNIT_NS::Message cpputMsg_(string("expected:\n") +       \
822                                           expected_);                   \
823             cpputMsg_.addDetail(string("actual:\n") +                   \
824                                 actual_);                               \
825             cpputMsg_.addDetail(string("diff:\n") +                     \
826                                 diffStrings(expected_, actual_));       \
827             CPPUNIT_NS::Asserter::fail( cpputMsg_,                      \
828                                         CPPUNIT_SOURCELINE() );         \
829         } \
830     } while ( false )
831
832 // returns last line, including trailing line break, empty if input is empty
833 static string lastLine(const string &buffer)
834 {
835     if (buffer.size() < 2) {
836         return buffer;
837     }
838
839     size_t line = buffer.rfind("\n", buffer.size() - 2);
840     if (line == buffer.npos) {
841         return buffer;
842     }
843
844     return buffer.substr(line + 1);
845 }
846
847 // true if <word> =
848 static bool isPropAssignment(const string &buffer) {
849     size_t start = 0;
850     while (start < buffer.size() &&
851            !isspace(buffer[start])) {
852         start++;
853     }
854     if (start + 3 <= buffer.size() &&
855         buffer.substr(start, 3) == " = ") {
856         return true;
857     } else {
858         return false;
859     }
860 }
861
862 // remove pure comment lines from buffer,
863 // also empty lines
864 static string filterConfig(const string &buffer)
865 {
866     ostringstream res;
867
868     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
869     for (string_split_iterator it =
870              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
871          it != string_split_iterator();
872          ++it) {
873         string line = boost::copy_range<string>(*it);
874         if (!line.empty() &&
875             (!boost::starts_with(line, "# ") ||
876              isPropAssignment(line.substr(2)))) {
877             res << line << endl;
878         }
879     }
880
881     return res.str();
882 }
883
884 // remove lines indented with spaces
885 static string filterIndented(const string &buffer)
886 {
887     ostringstream res;
888     bool first = true;
889
890     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
891     for (string_split_iterator it =
892              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
893          it != string_split_iterator();
894          ++it) {
895         if (!boost::starts_with(*it, " ")) {
896             if (!first) {
897                 res << endl;
898             } else {
899                 first = false;
900             }
901             res << *it;
902         }
903     }
904
905     return res.str();
906 }
907
908 // convert the internal config dump to .ini style
909 static string internalToIni(const string &config)
910 {
911     ostringstream res;
912
913     string section;
914     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
915     for (string_split_iterator it =
916              boost::make_split_iterator(config, boost::first_finder("\n", boost::is_iequal()));
917          it != string_split_iterator();
918          ++it) {
919         string line(it->begin(), it->end());
920         if (line.empty()) {
921             continue;
922         }
923
924         size_t colon = line.find(':');
925         string prefix = line.substr(0, colon);
926         if (boost::contains(prefix, ".internal.ini") ||
927             boost::contains(line, "= internal value")) {
928             continue;
929         }
930         // sources/<name>/config.ini or
931         // spds/sources/<name>/config.ini
932         size_t endslash = prefix.rfind('/');
933         if (endslash != line.npos && endslash > 1) {
934             size_t slash = prefix.rfind('/', endslash - 1);
935             if (slash != line.npos) {
936                 string newsource = prefix.substr(slash + 1, endslash - slash - 1);
937                 if (newsource != section &&
938                     newsource != "syncml") {
939                     res << endl << "[" << newsource << "]" << endl;
940                     section = newsource;
941                 }
942             }
943         }
944         string assignment = line.substr(colon + 1);
945         // substitude aliases with generic values
946         boost::replace_first(assignment, "= F", "= 0");
947         boost::replace_first(assignment, "= T", "= 1");
948         boost::replace_first(assignment, "= syncml:auth-md5", "= md5");
949         boost::replace_first(assignment, "= syncml:auth-basix", "= basic");
950         res << assignment << endl;
951     }
952
953     return res.str();
954 }
955
956
957 /**
958  * Testing is based on a text representation of a directory
959  * hierarchy where each line is of the format
960  * <file path>:<line in file>
961  *
962  * The order of files is alphabetical, of lines in the file as
963  * in the file. Lines in the file without line break cannot
964  * be represented.
965  *
966  * The root of the hierarchy is not part of the representation
967  * itself.
968  */
969 class SyncEvolutionCmdlineTest : public CppUnit::TestFixture {
970     CPPUNIT_TEST_SUITE(SyncEvolutionCmdlineTest);
971     CPPUNIT_TEST(testFramework);
972     CPPUNIT_TEST(testSetupScheduleWorld);
973     CPPUNIT_TEST(testSetupDefault);
974     CPPUNIT_TEST(testSetupRenamed);
975     CPPUNIT_TEST(testSetupFunambol);
976     CPPUNIT_TEST(testSetupSynthesis);
977     CPPUNIT_TEST(testPrintServers);
978     CPPUNIT_TEST(testPrintConfig);
979     CPPUNIT_TEST(testTemplate);
980     CPPUNIT_TEST(testSync);
981     CPPUNIT_TEST(testConfigure);
982     CPPUNIT_TEST(testOldConfigure);
983     CPPUNIT_TEST(testListSources);
984     CPPUNIT_TEST(testMigrate);
985     CPPUNIT_TEST_SUITE_END();
986     
987 public:
988     SyncEvolutionCmdlineTest() :
989         m_testDir("SyncEvolutionCmdlineTest"),
990         m_scheduleWorldConfig(".internal.ini:# HashCode = 0\n"
991                               ".internal.ini:# ConfigDate = \n"
992                               "config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
993                               "config.ini:username = your SyncML server account name\n"
994                               "config.ini:password = your SyncML server password\n"
995                               "config.ini:# logdir = \n"
996                               "config.ini:# loglevel = 0\n"
997                               "config.ini:# printChanges = 1\n"
998                               "config.ini:# maxlogdirs = 10\n"
999                               "config.ini:# useProxy = 0\n"
1000                               "config.ini:# proxyHost = \n"
1001                               "config.ini:# proxyUsername = \n"
1002                               "config.ini:# proxyPassword = \n"
1003                               "config.ini:# clientAuthType = md5\n"
1004                               "config.ini:deviceId = fixed-devid\n" /* this is not the default! */
1005                               "config.ini:# enableWBXML = 1\n"
1006                               "config.ini:# maxMsgSize = 20000\n"
1007                               "config.ini:# maxObjSize = 4000000\n"
1008                               "config.ini:# enableCompression = 0\n"
1009                               "config.ini:# SSLServerCertificates = \n"
1010                               "config.ini:# SSLVerifyServer = 1\n"
1011                               "config.ini:# SSLVerifyHost = 1\n"
1012                               "config.ini:WebURL = http://sync.scheduleworld.com\n"
1013                               "config.ini:# IconURI = \n"
1014                               "config.ini:ConsumerReady = 1\n"
1015                               "sources/addressbook/.internal.ini:# last = 0\n"
1016                               "sources/addressbook/config.ini:sync = two-way\n"
1017                               "sources/addressbook/config.ini:type = addressbook:text/vcard\n"
1018                               "sources/addressbook/config.ini:# evolutionsource = \n"
1019                               "sources/addressbook/config.ini:uri = card3\n"
1020                               "sources/addressbook/config.ini:# evolutionuser = \n"
1021                               "sources/addressbook/config.ini:# evolutionpassword = \n"
1022                               "sources/calendar/.internal.ini:# last = 0\n"
1023                               "sources/calendar/config.ini:sync = two-way\n"
1024                               "sources/calendar/config.ini:type = calendar\n"
1025                               "sources/calendar/config.ini:# evolutionsource = \n"
1026                               "sources/calendar/config.ini:uri = cal2\n"
1027                               "sources/calendar/config.ini:# evolutionuser = \n"
1028                               "sources/calendar/config.ini:# evolutionpassword = \n"
1029                               "sources/memo/.internal.ini:# last = 0\n"
1030                               "sources/memo/config.ini:sync = two-way\n"
1031                               "sources/memo/config.ini:type = memo\n"
1032                               "sources/memo/config.ini:# evolutionsource = \n"
1033                               "sources/memo/config.ini:uri = note\n"
1034                               "sources/memo/config.ini:# evolutionuser = \n"
1035                               "sources/memo/config.ini:# evolutionpassword = \n"
1036                               "sources/todo/.internal.ini:# last = 0\n"
1037                               "sources/todo/config.ini:sync = two-way\n"
1038                               "sources/todo/config.ini:type = todo\n"
1039                               "sources/todo/config.ini:# evolutionsource = \n"
1040                               "sources/todo/config.ini:uri = task2\n"
1041                               "sources/todo/config.ini:# evolutionuser = \n"
1042                               "sources/todo/config.ini:# evolutionpassword = \n")
1043     {
1044 #ifdef ENABLE_LIBSOUP
1045         // path to SSL certificates has to be set only for libsoup
1046         boost::replace_first(m_scheduleWorldConfig,
1047                              "SSLServerCertificates = ",
1048                              "SSLServerCertificates = /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:/usr/share/ssl/certs/ca-bundle.crt");
1049 #endif
1050     }
1051
1052 protected:
1053
1054     /** verify that createFiles/scanFiles themselves work */
1055     void testFramework() {
1056         const string root(m_testDir);
1057         const string content("baz:line\n"
1058                              "caz/subdir:booh\n"
1059                              "caz/subdir2/sub:# comment\n"
1060                              "caz/subdir2/sub:# foo = bar\n"
1061                              "caz/subdir2/sub:# empty = \n"
1062                              "caz/subdir2/sub:# another comment\n"
1063                              "foo:bar1\n"
1064                              "foo:\n"
1065                              "foo: \n"
1066                              "foo:bar2\n");
1067         const string filtered("baz:line\n"
1068                               "caz/subdir:booh\n"
1069                               "caz/subdir2/sub:# foo = bar\n"
1070                               "caz/subdir2/sub:# empty = \n"
1071                               "foo:bar1\n"
1072                               "foo: \n"
1073                               "foo:bar2\n");
1074         createFiles(root, content);
1075         string res = scanFiles(root);
1076         CPPUNIT_ASSERT_EQUAL_DIFF(filtered, res);
1077     }
1078
1079     void removeRandomUUID(string &buffer) {
1080         string uuidstr = "deviceId = sc-pim-";
1081         size_t uuid = buffer.find(uuidstr);
1082         CPPUNIT_ASSERT(uuid != buffer.npos);
1083         size_t end = buffer.find("\n", uuid + uuidstr.size());
1084         CPPUNIT_ASSERT(end != buffer.npos);
1085         buffer.replace(uuid, end - uuid, "deviceId = fixed-devid");
1086     }
1087
1088     /** create new configurations */
1089     void testSetupScheduleWorld() {
1090         string root;
1091         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1092         ScopedEnvChange home("HOME", m_testDir);
1093
1094         root = m_testDir;
1095         root += "/syncevolution/scheduleworld";
1096
1097         {
1098             rm_r(root);
1099             TestCmdline cmdline("--configure",
1100                                 "--sync-property", "proxyHost = proxy",
1101                                 "scheduleworld",
1102                                 "addressbook",
1103                                 NULL);
1104             cmdline.doit();
1105             string res = scanFiles(root);
1106             removeRandomUUID(res);
1107             string expected = ScheduleWorldConfig();
1108             boost::replace_first(expected,
1109                                  "# proxyHost = ",
1110                                  "proxyHost = proxy");
1111             boost::replace_all(expected,
1112                                "sync = two-way",
1113                                "sync = disabled");
1114             boost::replace_first(expected,
1115                                  "addressbook/config.ini:sync = disabled",
1116                                  "addressbook/config.ini:sync = two-way");
1117             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1118         }
1119
1120         {
1121             rm_r(root);
1122             TestCmdline cmdline("--configure",
1123                                 "--sync-property", "deviceID = fixed-devid",
1124                                 "scheduleworld",
1125                                 NULL);
1126             cmdline.doit();
1127             string res = scanFiles(root);
1128             CPPUNIT_ASSERT_EQUAL_DIFF(ScheduleWorldConfig(), res);
1129         }
1130     }
1131
1132     void testSetupDefault() {
1133         string root;
1134         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1135         ScopedEnvChange home("HOME", m_testDir);
1136
1137         root = m_testDir;
1138         root += "/syncevolution/some-other-server";
1139         rm_r(root);
1140         TestCmdline cmdline("--configure",
1141                             "--template", "default",
1142                             "--sync-property", "deviceID = fixed-devid",
1143                             "some-other-server",
1144                             NULL);
1145         cmdline.doit();
1146         string res = scanFiles(root);
1147         CPPUNIT_ASSERT_EQUAL_DIFF(ScheduleWorldConfig(), res);
1148     }
1149     void testSetupRenamed() {
1150         string root;
1151         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1152         ScopedEnvChange home("HOME", m_testDir);
1153
1154         root = m_testDir;
1155         root += "/syncevolution/scheduleworld2";
1156         rm_r(root);
1157         TestCmdline cmdline("--configure",
1158                             "--template", "scheduleworld",
1159                             "--sync-property", "deviceID = fixed-devid",
1160                             "scheduleworld2",
1161                             NULL);
1162         cmdline.doit();
1163         string res = scanFiles(root);
1164         CPPUNIT_ASSERT_EQUAL_DIFF(ScheduleWorldConfig(), res);
1165     }
1166     void testSetupFunambol() {
1167         string root;
1168         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1169         ScopedEnvChange home("HOME", m_testDir);
1170
1171         root = m_testDir;
1172         root += "/syncevolution/funambol";
1173         rm_r(root);
1174         TestCmdline cmdline("--configure",
1175                             "--sync-property", "deviceID = fixed-devid",
1176                             // templates are case-insensitive
1177                             "FunamBOL",
1178                             NULL);
1179         cmdline.doit();
1180         string res = scanFiles(root);
1181         CPPUNIT_ASSERT_EQUAL_DIFF(FunambolConfig(), res);
1182     }
1183
1184     void testSetupSynthesis() {
1185         string root;
1186         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1187         ScopedEnvChange home("HOME", m_testDir);
1188
1189         root = m_testDir;
1190         root += "/syncevolution/synthesis";
1191         rm_r(root);
1192         TestCmdline cmdline("--configure",
1193                             "--sync-property", "deviceID = fixed-devid",
1194                             "synthesis",
1195                             NULL);
1196         cmdline.doit();
1197         string res = scanFiles(root);
1198         CPPUNIT_ASSERT_EQUAL_DIFF(SynthesisConfig(), res);
1199     }
1200
1201     void testTemplate() {
1202         TestCmdline failure("--template", NULL);
1203         CPPUNIT_ASSERT(!failure.m_cmdline->parse());
1204         CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1205         CPPUNIT_ASSERT_EQUAL(string("ERROR: missing parameter for '--template'\n"), lastLine(failure.m_err.str()));
1206
1207         TestCmdline help("--template", "? ", NULL);
1208         help.doit();
1209         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates:\n"
1210                                   "   Funambol = http://my.funambol.com\n"
1211                                   "   Google = http://m.google.com/sync\n"
1212                                   "   Memotoo = http://www.memotoo.com\n"
1213                                   "   ScheduleWorld = http://sync.scheduleworld.com\n"
1214                                   "   Synthesis = http://www.synthesis.ch\n"
1215                                   "   ZYB = http://www.zyb.com\n",
1216                                   help.m_out.str());
1217         CPPUNIT_ASSERT_EQUAL_DIFF("", help.m_err.str());
1218     }
1219
1220     void testPrintServers() {
1221         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1222         ScopedEnvChange home("HOME", m_testDir);
1223
1224         rm_r(m_testDir);
1225         testSetupScheduleWorld();
1226         testSetupSynthesis();
1227         testSetupFunambol();
1228
1229         TestCmdline cmdline("--print-servers", NULL);
1230         cmdline.doit();
1231         CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
1232                                   "   scheduleworld = SyncEvolutionCmdlineTest/syncevolution/scheduleworld\n"
1233                                   "   synthesis = SyncEvolutionCmdlineTest/syncevolution/synthesis\n"
1234                                   "   funambol = SyncEvolutionCmdlineTest/syncevolution/funambol\n",
1235                                   cmdline.m_out.str());
1236         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1237     }
1238
1239     void testPrintConfig() {
1240         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1241         ScopedEnvChange home("HOME", m_testDir);
1242
1243         rm_r(m_testDir);
1244         testSetupFunambol();
1245
1246         {
1247             TestCmdline failure("--print-config", NULL);
1248             CPPUNIT_ASSERT(failure.m_cmdline->parse());
1249             CPPUNIT_ASSERT(!failure.m_cmdline->run());
1250             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1251             CPPUNIT_ASSERT_EQUAL(string("ERROR: --print-config requires either a --template or a server name.\n"),
1252                                  lastLine(failure.m_err.str()));
1253         }
1254
1255         {
1256             TestCmdline failure("--print-config", "foo", NULL);
1257             CPPUNIT_ASSERT(failure.m_cmdline->parse());
1258             CPPUNIT_ASSERT(!failure.m_cmdline->run());
1259             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1260             CPPUNIT_ASSERT_EQUAL(string("ERROR: server 'foo' has not been configured yet.\n"),
1261                                  lastLine(failure.m_err.str()));
1262         }
1263
1264         {
1265             TestCmdline failure("--print-config", "--template", "foo", NULL);
1266             CPPUNIT_ASSERT(failure.m_cmdline->parse());
1267             CPPUNIT_ASSERT(!failure.m_cmdline->run());
1268             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1269             CPPUNIT_ASSERT_EQUAL(string("ERROR: no configuration template for 'foo' available.\n"),
1270                                  lastLine(failure.m_err.str()));
1271         }
1272
1273         {
1274             TestCmdline cmdline("--print-config", "--template", "scheduleworld", NULL);
1275             cmdline.doit();
1276             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1277             string actual = cmdline.m_out.str();
1278             removeRandomUUID(actual);
1279             string filtered = filterConfig(actual);
1280             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
1281                                       filtered);
1282             // there should have been comments
1283             CPPUNIT_ASSERT(actual.size() > filtered.size());
1284         }
1285
1286         {
1287             TestCmdline cmdline("--print-config", "--template", "Default", NULL);
1288             cmdline.doit();
1289             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1290             string actual = filterConfig(cmdline.m_out.str());
1291             removeRandomUUID(actual);
1292             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
1293                                       actual);
1294         }
1295
1296         {
1297             TestCmdline cmdline("--print-config", "funambol", NULL);
1298             cmdline.doit();
1299             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1300             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(FunambolConfig())),
1301                                       filterConfig(cmdline.m_out.str()));
1302         }
1303
1304         {
1305             TestCmdline cmdline("--print-config", "--template", "scheduleworld",
1306                                 "--sync-property", "syncURL=foo",
1307                                 "--source-property", "sync=disabled",
1308                                 NULL);
1309             cmdline.doit();
1310             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1311             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
1312             boost::replace_first(expected,
1313                                  "syncURL = http://sync.scheduleworld.com/funambol/ds",
1314                                  "syncURL = foo");
1315             boost::replace_all(expected,
1316                                "sync = two-way",
1317                                "sync = disabled");
1318             string actual = filterConfig(cmdline.m_out.str());
1319             removeRandomUUID(actual);
1320             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
1321                                       actual);
1322         }
1323
1324         {
1325             TestCmdline cmdline("--print-config", "--quiet",
1326                                 "--template", "scheduleworld",
1327                                 "funambol",
1328                                 NULL);
1329             cmdline.doit();
1330             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1331             string actual = cmdline.m_out.str();
1332             removeRandomUUID(actual);
1333             CPPUNIT_ASSERT_EQUAL_DIFF(internalToIni(ScheduleWorldConfig()),
1334                                       actual);
1335         }
1336         
1337     }
1338
1339     void testSync() {
1340         TestCmdline failure("--sync", NULL);
1341         CPPUNIT_ASSERT(!failure.m_cmdline->parse());
1342         CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1343         CPPUNIT_ASSERT_EQUAL(string("ERROR: missing parameter for '--sync'\n"), lastLine(failure.m_err.str()));
1344
1345         TestCmdline failure2("--sync", "foo", NULL);
1346         CPPUNIT_ASSERT(!failure2.m_cmdline->parse());
1347         CPPUNIT_ASSERT_EQUAL_DIFF("", failure2.m_out.str());
1348         CPPUNIT_ASSERT_EQUAL(string("ERROR: '--sync foo': not one of the valid values (two-way, slow, refresh-from-client = refresh-client, refresh-from-server = refresh-server = refresh, one-way-from-client = one-way-client, one-way-from-server = one-way-server = one-way, disabled = none)\n"), lastLine(failure2.m_err.str()));
1349
1350         TestCmdline help("--sync", " ?", NULL);
1351         help.doit();
1352         CPPUNIT_ASSERT_EQUAL_DIFF("--sync\n"
1353                                   "   requests a certain synchronization mode:\n"
1354                                   "     two-way             = only send/receive changes since last sync\n"
1355                                   "     slow                = exchange all items\n"
1356                                   "     refresh-from-client = discard all remote items and replace with\n"
1357                                   "                           the items on the client\n"
1358                                   "     refresh-from-server = discard all local items and replace with\n"
1359                                   "                           the items on the server\n"
1360                                   "     one-way-from-client = transmit changes from client\n"
1361                                   "     one-way-from-server = transmit changes from server\n"
1362                                   "     none (or disabled)  = synchronization disabled\n",
1363                                   help.m_out.str());
1364         CPPUNIT_ASSERT_EQUAL_DIFF("", help.m_err.str());
1365
1366         TestCmdline filter("--sync", "refresh-from-server", NULL);
1367         CPPUNIT_ASSERT(filter.m_cmdline->parse());
1368         CPPUNIT_ASSERT(!filter.m_cmdline->run());
1369         CPPUNIT_ASSERT_EQUAL_DIFF("", filter.m_out.str());
1370         CPPUNIT_ASSERT_EQUAL_DIFF("sync = refresh-from-server",
1371                                   string(filter.m_cmdline->m_sourceProps));
1372         CPPUNIT_ASSERT_EQUAL_DIFF("",
1373                                   string(filter.m_cmdline->m_syncProps));
1374
1375         TestCmdline filter2("--source-property", "sync=refresh", NULL);
1376         CPPUNIT_ASSERT(filter2.m_cmdline->parse());
1377         CPPUNIT_ASSERT(!filter2.m_cmdline->run());
1378         CPPUNIT_ASSERT_EQUAL_DIFF("", filter2.m_out.str());
1379         CPPUNIT_ASSERT_EQUAL_DIFF("sync = refresh",
1380                                   string(filter2.m_cmdline->m_sourceProps));
1381         CPPUNIT_ASSERT_EQUAL_DIFF("",
1382                                   string(filter2.m_cmdline->m_syncProps));
1383     }
1384
1385     void testConfigure() {
1386         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1387         ScopedEnvChange home("HOME", m_testDir);
1388
1389         rm_r(m_testDir);
1390         testSetupScheduleWorld();
1391         doConfigure(ScheduleWorldConfig(), "sources/addressbook/config.ini:");
1392
1393         string syncProperties("syncURL:\n"
1394                               "\n"
1395                               "username:\n"
1396                               "\n"
1397                               "password:\n"
1398                               "\n"
1399                               "logdir:\n"
1400                               "\n"
1401                               "loglevel:\n"
1402                               "\n"
1403                               "printChanges:\n"
1404                               "\n"
1405                               "maxlogdirs:\n"
1406                               "\n"
1407                               "useProxy:\n"
1408                               "\n"
1409                               "proxyHost:\n"
1410                               "\n"
1411                               "proxyUsername:\n"
1412                               "\n"
1413                               "proxyPassword:\n"
1414                               "\n"
1415                               "clientAuthType:\n"
1416                               "\n"
1417                               "deviceId:\n"
1418                               "\n"
1419                               "enableWBXML:\n"
1420                               "\n"
1421                               "maxMsgSize:\n"
1422                               "maxObjSize:\n"
1423                               "\n"
1424                               "enableCompression:\n"
1425                               "\n"
1426                               "SSLServerCertificates:\n"
1427                               "\n"
1428                               "SSLVerifyServer:\n"
1429                               "\n"
1430                               "SSLVerifyHost:\n"
1431                               "\n"
1432                               "WebURL:\n"
1433                               "\n"
1434                               "IconURI:\n"
1435                               "\n"
1436                               "ConsumerReady:\n");
1437         string sourceProperties("sync:\n"
1438                                 "\n"
1439                                 "type:\n"
1440                                 "\n"
1441                                 "evolutionsource:\n"
1442                                 "\n"
1443                                 "uri:\n"
1444                                 "\n"
1445                                 "evolutionuser:\n"
1446                                 "evolutionpassword:\n");
1447
1448         {
1449             TestCmdline cmdline("--sync-property", "?",
1450                                 NULL);
1451             cmdline.doit();
1452             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1453             CPPUNIT_ASSERT_EQUAL_DIFF(syncProperties,
1454                                       filterIndented(cmdline.m_out.str()));
1455         }
1456
1457         {
1458             TestCmdline cmdline("--source-property", "?",
1459                                 NULL);
1460             cmdline.doit();
1461             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1462             CPPUNIT_ASSERT_EQUAL_DIFF(sourceProperties,
1463                                       filterIndented(cmdline.m_out.str()));
1464         }
1465
1466         {
1467             TestCmdline cmdline("--source-property", "?",
1468                                 "--sync-property", "?",
1469                                 NULL);
1470             cmdline.doit();
1471             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1472             CPPUNIT_ASSERT_EQUAL_DIFF(sourceProperties + syncProperties,
1473                                       filterIndented(cmdline.m_out.str()));
1474         }
1475
1476         {
1477             TestCmdline cmdline("--sync-property", "?",
1478                                 "--source-property", "?",
1479                                 NULL);
1480             cmdline.doit();
1481             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1482             CPPUNIT_ASSERT_EQUAL_DIFF(syncProperties + sourceProperties,
1483                                       filterIndented(cmdline.m_out.str()));
1484         }
1485     }
1486
1487     void testOldConfigure() {
1488         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1489         ScopedEnvChange home("HOME", m_testDir);
1490
1491         string oldConfig = OldScheduleWorldConfig();
1492         InitList<string> props = InitList<string>("serverNonce") +
1493             "clientNonce" +
1494             "devInfoHash" +
1495             "HashCode" +
1496             "ConfigDate" +
1497             "last";
1498         BOOST_FOREACH(string &prop, props) {
1499             boost::replace_all(oldConfig,
1500                                prop + " = ",
1501                                prop + " = internal value");
1502         }
1503
1504         rm_r(m_testDir);
1505         createFiles(m_testDir + "/.sync4j/evolution/scheduleworld", oldConfig);
1506         doConfigure(oldConfig, "spds/sources/addressbook/config.txt:");
1507     }
1508
1509     void doConfigure(const string &SWConfig, const string &addressbookPrefix) {
1510         string expected;
1511
1512         {
1513             TestCmdline cmdline("--configure",
1514                                 "--source-property", "sync = disabled",
1515                                 "scheduleworld",
1516                                 NULL);
1517             cmdline.doit();
1518             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1519             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
1520             expected = filterConfig(internalToIni(SWConfig));
1521             boost::replace_all(expected,
1522                                "sync = two-way",
1523                                "sync = disabled");
1524             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
1525                                       filterConfig(printConfig("scheduleworld")));
1526         }
1527
1528         {
1529             TestCmdline cmdline("--configure",
1530                                 "--source-property", "sync = one-way-from-server",
1531                                 "scheduleworld",
1532                                 "addressbook",
1533                                 NULL);
1534             cmdline.doit();
1535             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1536             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
1537             expected = SWConfig;
1538             boost::replace_all(expected,
1539                                "sync = two-way",
1540                                "sync = disabled");
1541             boost::replace_first(expected,
1542                                  addressbookPrefix + "sync = disabled",
1543                                  addressbookPrefix + "sync = one-way-from-server");
1544             expected = filterConfig(internalToIni(expected));
1545             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
1546                                       filterConfig(printConfig("scheduleworld")));
1547         }
1548
1549         {
1550             TestCmdline cmdline("--configure",
1551                                 "--sync", "two-way",
1552                                 "-z", "evolutionsource=source",
1553                                 "--sync-property", "maxlogdirs=20",
1554                                 "-y", "LOGDIR=logdir",
1555                                 "scheduleworld",
1556                                 NULL);
1557             cmdline.doit();
1558             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1559             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
1560             boost::replace_all(expected,
1561                                "sync = one-way-from-server",
1562                                "sync = two-way");
1563             boost::replace_all(expected,
1564                                "sync = disabled",
1565                                "sync = two-way");
1566             boost::replace_all(expected,
1567                                "# evolutionsource = ",
1568                                "evolutionsource = source");
1569             boost::replace_all(expected,
1570                                "# maxlogdirs = 10",
1571                                "maxlogdirs = 20");
1572             boost::replace_all(expected,
1573                                "# logdir = ",
1574                                "logdir = logdir");
1575             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
1576                                       filterConfig(printConfig("scheduleworld")));
1577         }
1578     }
1579
1580     void testListSources() {
1581         TestCmdline cmdline(NULL);
1582         cmdline.doit();
1583         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1584         // exact output varies, do not test
1585     }
1586
1587     void testMigrate() {
1588         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1589         ScopedEnvChange home("HOME", m_testDir);
1590
1591         rm_r(m_testDir);
1592         string oldRoot = m_testDir + "/.sync4j/evolution/scheduleworld";
1593         string newRoot = m_testDir + "/syncevolution/scheduleworld";
1594
1595         string oldConfig = OldScheduleWorldConfig();
1596
1597         {
1598             // migrate old config
1599             createFiles(oldRoot, oldConfig);
1600             string createdConfig = scanFiles(oldRoot);
1601             TestCmdline cmdline("--migrate",
1602                                 "scheduleworld",
1603                                 NULL);
1604             cmdline.doit();
1605             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1606             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
1607
1608             string migratedConfig = scanFiles(newRoot);
1609             CPPUNIT_ASSERT_EQUAL_DIFF(m_scheduleWorldConfig, migratedConfig);
1610             string renamedConfig = scanFiles(oldRoot + ".old");
1611             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
1612         }
1613
1614         {
1615             // rewrite existing config
1616             createFiles(newRoot,
1617                         "config.ini:# obsolete comment\n"
1618                         "config.ini:obsoleteprop = foo\n",
1619                         true);
1620             string createdConfig = scanFiles(newRoot);
1621
1622             TestCmdline cmdline("--migrate",
1623                                 "scheduleworld",
1624                                 NULL);
1625             cmdline.doit();
1626             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1627             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
1628
1629             string migratedConfig = scanFiles(newRoot);
1630             CPPUNIT_ASSERT_EQUAL_DIFF(m_scheduleWorldConfig, migratedConfig);
1631             string renamedConfig = scanFiles(newRoot + ".old");
1632             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
1633         }
1634
1635         {
1636             // migrate old config with changes, a second time
1637             createFiles(oldRoot, oldConfig);
1638             createFiles(oldRoot,
1639                         "spds/sources/addressbook/changes/config.txt:foo = bar\n"
1640                         "spds/sources/addressbook/changes/config.txt:foo2 = bar2\n",
1641                         true);
1642             string createdConfig = scanFiles(oldRoot);
1643             rm_r(newRoot);
1644             TestCmdline cmdline("--migrate",
1645                                 "scheduleworld",
1646                                 NULL);
1647             cmdline.doit();
1648             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1649             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
1650
1651             string migratedConfig = scanFiles(newRoot);
1652             string expected = m_scheduleWorldConfig;
1653             boost::replace_first(expected,
1654                                  "sources/addressbook/config.ini",
1655                                  "sources/addressbook/.other.ini:foo = bar\n"
1656                                  "sources/addressbook/.other.ini:foo2 = bar2\n"
1657                                  "sources/addressbook/config.ini");
1658             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
1659             string renamedConfig = scanFiles(oldRoot + ".old.1");
1660             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
1661         }
1662     }
1663
1664     const string m_testDir;
1665     string m_scheduleWorldConfig;
1666         
1667
1668 private:
1669
1670     /**
1671      * vararg constructor with NULL termination,
1672      * out and error stream into stringstream members
1673      */
1674     class TestCmdline {
1675     public:
1676         TestCmdline(const char *arg, ...) {
1677             va_list argList;
1678             va_start (argList, arg);
1679             for (const char *curr = arg;
1680                  curr;
1681                  curr = va_arg(argList, const char *)) {
1682                 m_argvstr.push_back(curr);
1683             }
1684             va_end(argList);
1685
1686             m_argv.reset(new const char *[m_argvstr.size() + 1]);
1687             m_argv[0] = "client-test";
1688             for (size_t index = 0;
1689                  index < m_argvstr.size();
1690                  ++index) {
1691                 m_argv[index + 1] = m_argvstr[index].c_str();
1692             }
1693
1694             m_cmdline.set(new SyncEvolutionCmdline(m_argvstr.size() + 1, m_argv.get(), m_out, m_err), "cmdline");
1695         }
1696
1697         void doit() {
1698             bool success;
1699             success = m_cmdline->parse() &&
1700                 m_cmdline->run();
1701             if (m_err.str().size()) {
1702                 cout << endl << m_err.str();
1703             }
1704             CPPUNIT_ASSERT(success);
1705         }
1706
1707         ostringstream m_out, m_err;
1708         cxxptr<SyncEvolutionCmdline> m_cmdline;
1709
1710     private:
1711         vector<string> m_argvstr;
1712         boost::scoped_array<const char *> m_argv;
1713     };
1714
1715     string ScheduleWorldConfig() {
1716         string config = m_scheduleWorldConfig;
1717
1718         if (isDir(string(TEMPLATE_DIR) + "/ScheduleWorld")) {
1719             boost::replace_all(config,
1720                                "# IconURI = ",
1721                                "IconURI = file://" TEMPLATE_DIR "/ScheduleWorld/icon.png");
1722         }
1723         return config;
1724     }
1725
1726     string OldScheduleWorldConfig() {
1727         string oldConfig = m_scheduleWorldConfig;
1728         boost::replace_all(oldConfig,
1729                            ".internal.ini",
1730                            "config.ini");
1731         InitList<string> sources = InitList<string>("addressbook") +
1732             "calendar" +
1733             "memo" +
1734             "todo";
1735         BOOST_FOREACH(string &source, sources) {
1736             boost::replace_all(oldConfig,
1737                                string("sources/") + source + "/config.ini",
1738                                string("spds/sources/") + source + "/config.txt");
1739         }
1740         boost::replace_all(oldConfig,
1741                            "config.ini",
1742                            "spds/syncml/config.txt");
1743         return oldConfig;
1744     }
1745
1746     string FunambolConfig() {
1747         string config = m_scheduleWorldConfig;
1748
1749         boost::replace_first(config,
1750                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
1751                              "syncURL = http://my.funambol.com/sync");
1752
1753         boost::replace_first(config,
1754                              "WebURL = http://sync.scheduleworld.com",
1755                              "WebURL = http://my.funambol.com");
1756
1757         boost::replace_first(config,
1758                              "# enableWBXML = 1",
1759                              "enableWBXML = 0");
1760
1761         boost::replace_first(config,
1762                              "addressbook/config.ini:uri = card3",
1763                              "addressbook/config.ini:uri = card");
1764         boost::replace_first(config,
1765                              "addressbook/config.ini:type = addressbook:text/vcard",
1766                              "addressbook/config.ini:type = addressbook");
1767
1768         boost::replace_first(config,
1769                              "calendar/config.ini:uri = cal2",
1770                              "calendar/config.ini:uri = event");
1771         boost::replace_first(config,
1772                              "calendar/config.ini:type = calendar",
1773                              "calendar/config.ini:type = calendar:text/calendar!");
1774
1775         boost::replace_first(config,
1776                              "todo/config.ini:uri = task2",
1777                              "todo/config.ini:uri = task");
1778         boost::replace_first(config,
1779                              "todo/config.ini:type = todo",
1780                              "todo/config.ini:type = todo:text/calendar!");
1781
1782         return config;
1783     }
1784
1785     string SynthesisConfig() {
1786         string config = m_scheduleWorldConfig;
1787         boost::replace_first(config,
1788                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
1789                              "syncURL = http://www.synthesis.ch/sync");
1790
1791         boost::replace_first(config,
1792                              "WebURL = http://sync.scheduleworld.com",
1793                              "WebURL = http://www.synthesis.ch");        
1794
1795         boost::replace_first(config,
1796                              "ConsumerReady = 1",
1797                              "# ConsumerReady = 0");
1798
1799         boost::replace_first(config,
1800                              "addressbook/config.ini:uri = card3",
1801                              "addressbook/config.ini:uri = contacts");
1802         boost::replace_first(config,
1803                              "addressbook/config.ini:type = addressbook:text/vcard",
1804                              "addressbook/config.ini:type = addressbook");
1805
1806         boost::replace_first(config,
1807                              "calendar/config.ini:uri = cal2",
1808                              "calendar/config.ini:uri = events");
1809         boost::replace_first(config,
1810                              "calendar/config.ini:sync = two-way",
1811                              "calendar/config.ini:sync = disabled");
1812
1813         boost::replace_first(config,
1814                              "memo/config.ini:uri = note",
1815                              "memo/config.ini:uri = notes");
1816
1817         boost::replace_first(config,
1818                              "todo/config.ini:uri = task2",
1819                              "todo/config.ini:uri = tasks");
1820         boost::replace_first(config,
1821                              "todo/config.ini:sync = two-way",
1822                              "todo/config.ini:sync = disabled");
1823
1824         return config;
1825     }
1826
1827     /** temporarily set env variable, restore old value on destruction */
1828     class ScopedEnvChange {
1829     public:
1830         ScopedEnvChange(const string &var, const string &value) :
1831             m_var(var)
1832         {
1833             const char *oldval = getenv(var.c_str());
1834             if (oldval) {
1835                 m_oldvalset = true;
1836                 m_oldval = oldval;
1837             } else {
1838                 m_oldvalset = false;
1839             }
1840             setenv(var.c_str(), value.c_str(), 1);
1841         }
1842         ~ScopedEnvChange()
1843         {
1844             if (m_oldvalset) {
1845                 setenv(m_var.c_str(), m_oldval.c_str(), 1);
1846             } else {
1847                 unsetenv(m_var.c_str());
1848             } 
1849         }
1850     private:
1851         string m_var, m_oldval;
1852         bool m_oldvalset;
1853     };
1854             
1855
1856     /** create directory hierarchy, overwriting previous content */
1857     void createFiles(const string &root, const string &content, bool append = false) {
1858         if (!append) {
1859             rm_r(root);
1860         }
1861
1862         size_t start = 0;
1863         ofstream out;
1864         string outname;
1865
1866         out.exceptions(ios_base::badbit|ios_base::failbit);
1867         while (start < content.size()) {
1868             size_t delim = content.find(':', start);
1869             size_t end = content.find('\n', start);
1870             if (delim == content.npos ||
1871                 end == content.npos) {
1872                 // invalid content ?!
1873                 break;
1874             }
1875             string newname = content.substr(start, delim - start);
1876             string line = content.substr(delim + 1, end - delim - 1);
1877             if (newname != outname) {
1878                 if (out.is_open()) {
1879                     out.close();
1880                 }
1881                 string fullpath = root + "/" + newname;
1882                 size_t fileoff = fullpath.rfind('/');
1883                 mkdir_p(fullpath.substr(0, fileoff));
1884                 out.open(fullpath.c_str(),
1885                          append ? ios_base::out : (ios_base::out|ios_base::trunc));
1886                 outname = newname;
1887             }
1888             out << line << endl;
1889             start = end + 1;
1890         }
1891     }
1892
1893     /** turn directory hierarchy into string */
1894     string scanFiles(const string &root, bool onlyProps = true) {
1895         ostringstream out;
1896
1897         scanFiles(root, "", out, onlyProps);
1898         return out.str();
1899     }
1900
1901     void scanFiles(const string &root, const string &dir, ostringstream &out, bool onlyProps) {
1902         string newroot = root;
1903         newroot += "/";
1904         newroot += dir;
1905         ReadDir readDir(newroot);
1906         sort(readDir.begin(), readDir.end());
1907
1908         BOOST_FOREACH(const string &entry, readDir) {
1909             if (isDir(newroot + "/" + entry)) {
1910                 scanFiles(root, dir + (dir.empty() ? "" : "/") + entry, out, onlyProps);
1911             } else {
1912                 ifstream in;
1913                 in.exceptions(ios_base::badbit /* failbit must not trigger exception because is set when reaching eof ?! */);
1914                 in.open((newroot + "/" + entry).c_str());
1915                 string line;
1916                 while (!in.eof()) {
1917                     getline(in, line);
1918                     if ((line.size() || !in.eof()) && 
1919                         (!onlyProps ||
1920                          (boost::starts_with(line, "# ") ?
1921                           isPropAssignment(line.substr(2)) :
1922                           !line.empty()))) {
1923                         if (dir.size()) {
1924                             out << dir << "/";
1925                         }
1926                         out << entry << ":";
1927                         out << line << '\n';
1928                     }
1929                 }
1930             }
1931         }
1932     }
1933
1934     string printConfig(const string &server) {
1935         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1936         ScopedEnvChange home("HOME", m_testDir);
1937
1938         TestCmdline cmdline("--print-config", server.c_str(), NULL);
1939         cmdline.doit();
1940         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1941         return cmdline.m_out.str();
1942     }
1943 };
1944
1945 SYNCEVOLUTION_TEST_SUITE_REGISTRATION(SyncEvolutionCmdlineTest);
1946
1947 #endif // ENABLE_UNIT_TESTS