source: tags/ms_r16q2/UNIT_TESTER/TestEnvironment.cxx

Last change on this file was 13077, checked in by westram, 10 years ago
File size: 22.4 KB
Line 
1// ================================================================= //
2//                                                                   //
3//   File      : TestEnvironment.cxx                                 //
4//   Purpose   : Global! setup/cleanup for tests                     //
5//                                                                   //
6//   Coded by Ralf Westram (coder@reallysoft.de) in September 2010   //
7//   Institute of Microbiology (Technical University Munich)         //
8//   http://www.arb-home.de/                                         //
9//                                                                   //
10// ================================================================= //
11
12#include <arb_simple_assert.h>
13
14#include <arb_defs.h>
15#include <arb_str.h>
16#include <arb_file.h>
17#include <arb_diff.h>
18
19#include <arbdb.h>
20#include <PT_com.h>
21#include <client.h>
22#include <servercntrl.h>
23
24#include "test_unit.h"
25#include "UnitTester.hxx"
26
27#include <string>
28#include <set>
29#include <unistd.h>
30
31#include <sys/stat.h>
32#include <sys/time.h>
33
34#include "../SOURCE_TOOLS/arb_main.h"
35
36#define env_assert(cond) arb_assert(cond)
37
38// #define SIMULATE_ENVSETUP_TIMEOUT // makes tests fail
39// #define SIMULATE_ENVSETUP_SLOW_OVERTIME // should not make tests fail
40
41using namespace arb_test;
42using namespace std;
43
44enum Mode { UNKNOWN, SETUP, CLEAN };
45static const char *mode_command[] = { NULL, "setup", "clean" }; // same order as in enum Mode
46static Mode other_mode[] = { UNKNOWN, CLEAN, SETUP }; 
47
48typedef SmartCharPtr Error;
49
50static const char *upcase(const char *str) {
51    char *upstr = strdup(str);
52    ARB_strupper(upstr);
53    RETURN_LOCAL_ALLOC(upstr);
54}
55
56#define WARN_NOT_IMPLEMENTED(fun,mode) TEST_WARNING2("%s(%s) does nothing", #fun, upcase(mode_command[mode]));
57
58static const char *unitTesterDir() {
59    RETURN_ONETIME_ALLOC(strdup(GB_concat_full_path(GB_getenvARBHOME(), "UNIT_TESTER")));
60}
61static const char *flagDir() {
62    RETURN_ONETIME_ALLOC(strdup(GB_concat_full_path(unitTesterDir(), FLAGS_DIR)));
63}
64static const char *runDir() {
65    RETURN_ONETIME_ALLOC(strdup(GB_concat_full_path(unitTesterDir(), "run")));
66}
67static const char *mutexDir(const char *name) {
68    return GB_concat_full_path(flagDir(), GBS_global_string("mutex_%s", name));
69}
70
71#if defined(DEBUG)
72// #define DUMP_MUTEX_ACCESS
73#endif
74
75
76class Mutex : virtual Noncopyable {
77    string name;
78    string mutexdir;
79
80    bool mutexdir_exists() const { return GB_is_directory(mutexdir.c_str()); }
81
82    static const char *now() {
83        static char buf[100];
84        timeval t;
85        gettimeofday(&t, NULL);
86        sprintf(buf, "%li.%li", t.tv_sec, t.tv_usec);
87        return buf;
88    }
89
90    static set<string> known_mutexes;
91
92public:
93    Mutex(const char *mutex_name)
94        : name(mutex_name)
95    {
96        mutexdir = mutexDir(mutex_name);
97
98        bool gotMutex = false;
99        int  maxwait  = MAX_EXEC_MS_SLOW-5; // seconds
100        int  wait     = 0;
101
102        arb_assert(maxwait>0);
103
104        test_data().entered_mutex_loop = true; // avoid race-condition
105        while (!gotMutex) {
106            if (mutexdir_exists()) {
107                if (wait>maxwait && mutexdir_exists()) {
108                    GBK_terminatef("[%s] Failed to get mutex for more than %i seconds", now(), maxwait);
109                }
110                printf("[%s] mutex '%s' exists.. sleeping\n", now(), name.c_str());
111                sleepms(1000);
112                wait++;
113            }
114            else {
115                int res = mkdir(mutexdir.c_str(), S_IRWXU);
116                if (res == 0) {
117#if defined(DUMP_MUTEX_ACCESS)
118                    printf("[%s] allocated mutex '%s'\n", now(), name.c_str());
119#endif
120                    gotMutex = true;
121                }
122                else {
123                    wait = 0; // reset timeout
124                    printf("[%s] lost race for mutex '%s'\n", now(), name.c_str());
125                }
126            }
127        }
128        known_mutexes.insert(name);
129    }
130    ~Mutex() {
131        if (!mutexdir_exists()) {
132            printf("[%s] Strange - mutex '%s' has vanished\n", now(), name.c_str());
133        }
134        else {
135            if (rmdir(mutexdir.c_str()) != 0) {
136                const char *error = GB_IO_error("remove", mutexdir.c_str());
137                GBK_terminatef("[%s] Failed to release mutex dir (%s)", now(), error);
138            }
139            else {
140#if defined(DUMP_MUTEX_ACCESS)
141                printf("[%s] released mutex '%s'\n", now(), mutexdir.c_str());
142#endif
143            }
144        }
145        known_mutexes.erase(name);
146    }
147
148    static bool owned(const char *mutex_name) {
149        return known_mutexes.find(mutex_name) != known_mutexes.end();
150    }
151
152    static void own_or_terminate(const char *mutex_name) {
153        if (!Mutex::owned(mutex_name)) {
154            GBK_terminatef("Expected to own mutex '%s'", mutex_name);
155        }
156    }
157};
158
159set<string> Mutex::known_mutexes;
160
161// -----------------------
162//      PersistantFlag
163
164static const char *FLAG_MUTEX = "flag_access";
165
166class PersistantFlag {
167    // persistant flags keep their value even if the program is restarted(!)
168
169    string       name;
170    mutable bool value; // has to be mutable to support volatile behavior
171    bool         is_volatile;
172
173    const char *flagFileName() const {
174        return GB_concat_full_path(flagDir(), GBS_global_string("%s." FLAGS_EXT, name.c_str()));
175    }
176
177    bool flagFileExists() const {
178        Mutex::own_or_terminate(FLAG_MUTEX);
179        return GB_is_regularfile(flagFileName());
180    }
181
182    void createFlagFile() const {
183        Mutex::own_or_terminate(FLAG_MUTEX);
184        const char *flagfile = flagFileName();
185        FILE       *fp       = fopen(flagfile, "w");
186        if (!fp) {
187            GB_ERROR error = GB_IO_error("creating flag", flagfile);
188            HERE.errorf(true, "%s\n", error);
189        }
190        else {
191            fclose(fp);
192        }
193        env_assert(flagFileExists());
194    }
195    void removeFlagFile() const {
196        const char *flagfile = flagFileName();
197        StaticCode::printf("flagfile='%s'\n", flagfile);
198        if (!flagFileExists()) {
199            GBK_dump_backtrace(stderr, "tried to remove non-existing flagfile");
200        }
201        int res = unlink(flagfile);
202        if (res != 0) {
203            GB_ERROR error = GB_IO_error("unlinking", flagfile);
204            HERE.errorf(true, "%s\n", error);
205        }
206        env_assert(!flagFileExists());
207    }
208    void updateFlagFile() const {
209        if (value) createFlagFile();
210        else removeFlagFile();
211    }
212
213public:
214    PersistantFlag(const string& name_)
215        : name(name_),
216          value(flagFileExists()), 
217          is_volatile(false)
218    {}
219    PersistantFlag(const string& name_, bool is_volatile_)
220        : name(name_),
221          value(flagFileExists()),
222          is_volatile(is_volatile_)
223    {}
224    ~PersistantFlag() {
225        if (flagFileExists() != bool(*this)) {
226            StaticCode::printf("Mismatch between internal value(=%i) and flagfile='%s'\n", int(value), name.c_str());
227        }
228        env_assert(flagFileExists() == value);
229    }
230    operator bool() const {
231        if (is_volatile) value = flagFileExists();
232        return value;
233    }
234
235    PersistantFlag& operator = (bool b) {
236        if (b != bool(*this)) {
237            StaticCode::printf("Changing flag '%s' to %i\n", name.c_str(), int(b));
238            value = b;
239            updateFlagFile();
240        }
241        return *this;
242    }
243};
244
245class LazyPersistantFlag {
246    // wrapper for PersistantFlag - instanciates on r/w access
247
248    SmartPtr<PersistantFlag> flag;
249
250    string name;
251    bool   will_be_volatile;
252
253    void instanciate() {
254        flag = new PersistantFlag(name, will_be_volatile);
255    }
256    void instanciate_on_demand() const {
257        if (flag.isNull()) {
258            const_cast<LazyPersistantFlag*>(this)->instanciate();
259        }
260    }
261
262public:
263    LazyPersistantFlag(const char *name_) : name(name_), will_be_volatile(false) {}
264    LazyPersistantFlag(const char *name_, bool is_volatile_) : name(name_), will_be_volatile(is_volatile_) {}
265
266    operator bool() const {
267        instanciate_on_demand();
268        return *flag;
269    }
270
271    LazyPersistantFlag& operator = (bool b) {
272        instanciate_on_demand();
273        *flag = b;
274        return *this;
275    }
276
277    void get_lazy_again() { flag.SetNull(); }
278};
279
280
281// --------------------------------------------------------------------------------
282// add environment setup/cleanup functions here (and add name to environments[] below)
283//
284// each environment
285// - is setup on client-request (using TEST_SETUP_GLOBAL_ENVIRONMENT)
286// - is cleaned up after ALL unit tests were executed (only if it has been setup)
287//
288// Functions start in same directory where tests starts.
289//
290// Note: you cannot simply share data between different modes since they are
291//       executed in different instances of the executable 'test_environment'!
292//
293//       Use 'PersistantFlag' to share bools between instances.
294// --------------------------------------------------------------------------------
295
296// -----------------
297//      ptserver
298
299static void test_ptserver_activate(bool start, int serverid) {
300    const char *server_tag = GBS_ptserver_tag(serverid);
301    if (start) {
302        TEST_EXPECT_NO_ERROR(arb_look_and_start_server(AISC_MAGIC_NUMBER, server_tag));
303    }
304    else { // stop
305        GB_ERROR kill_error = arb_look_and_kill_server(AISC_MAGIC_NUMBER, server_tag);
306        if (kill_error) TEST_EXPECT_EQUAL(kill_error, "Server is not running");
307    }
308}
309
310// to activate INDEX_HEXDUMP uncomment TEST_AUTO_UPDATE and INDEX_HEXDUMP once (before modifying index),
311// then comment out TEST_AUTO_UPDATE again
312//
313// #define TEST_AUTO_UPDATE
314// #define INDEX_HEXDUMP
315
316static Error ptserver(Mode mode) {
317    // test-ptserver is restarted and rebuild.
318    // This is done only once in the complete test suite.
319    //
320    // every unit-test using the test-ptserver should simply call
321    // TEST_SETUP_GLOBAL_ENVIRONMENT("ptserver");
322
323    Error error;
324    switch (mode) {
325        case SETUP: {
326            test_ptserver_activate(false, TEST_SERVER_ID);                      // first kill pt-server (otherwise we may test an outdated pt-server)
327            TEST_EXPECT_NO_ERROR(GBK_system("cp TEST_pt_src.arb TEST_pt.arb")); // force rebuild
328            test_ptserver_activate(true, TEST_SERVER_ID);
329#if defined(INDEX_HEXDUMP)
330            TEST_DUMP_FILE("TEST_pt.arb.pt", "TEST_pt.arb.pt.dump");
331#endif
332
333#if defined(TEST_AUTO_UPDATE)
334            TEST_COPY_FILE("TEST_pt.arb.pt", "TEST_pt.arb.pt.expected");
335# if defined(INDEX_HEXDUMP)
336            TEST_COPY_FILE("TEST_pt.arb.pt.dump", "TEST_pt.arb.pt.dump.expected");
337# endif
338#else // !defined(TEST_AUTO_UPDATE)
339# if defined(INDEX_HEXDUMP)
340            TEST_EXPECT_TEXTFILES_EQUAL("TEST_pt.arb.pt.dump.expected", "TEST_pt.arb.pt.dump");
341# endif
342            TEST_EXPECT_FILES_EQUAL("TEST_pt.arb.pt.expected", "TEST_pt.arb.pt");
343#endif
344            TEST_EXPECT_LESS_EQUAL(GB_time_of_file("TEST_pt.arb"), GB_time_of_file("TEST_pt.arb.pt"));
345            break;
346        }
347        case CLEAN: {
348            test_ptserver_activate(false, TEST_SERVER_ID);
349            TEST_EXPECT_ZERO_OR_SHOW_ERRNO(unlink("TEST_pt.arb.pt"));
350#if defined(INDEX_HEXDUMP)
351            TEST_EXPECT_ZERO_OR_SHOW_ERRNO(unlink("TEST_pt.arb.pt.dump"));
352#endif
353            break;
354        }
355        case UNKNOWN:
356            env_assert(0);
357            break;
358    }
359
360    return error;
361}
362
363static Error ptserver_gene(Mode mode) {
364    // test-gene-ptserver is restarted and rebuild.
365    // This is done only once in the complete test suite.
366    //
367    // every unit-test using the test-gene-ptserver should simply call
368    // TEST_SETUP_GLOBAL_ENVIRONMENT("ptserver_gene");
369
370    Error error;
371    switch (mode) {
372        case SETUP: {
373            test_ptserver_activate(false, TEST_GENESERVER_ID);                     // first kill pt-server (otherwise we may test an outdated pt-server)
374            TEST_EXPECT_NO_ERROR(GBK_system("arb_gene_probe TEST_gpt_src.arb TEST_gpt.arb")); // prepare gene-ptserver-db (forcing rebuild)
375
376            // GBK_terminatef("test-crash of test_environment");
377
378#if defined(TEST_AUTO_UPDATE)
379            TEST_COPY_FILE("TEST_gpt.arb", "TEST_gpt.arb.expected");
380#else // !defined(TEST_AUTO_UPDATE)
381            TEST_EXPECT_FILES_EQUAL("TEST_gpt.arb.expected", "TEST_gpt.arb");
382#endif
383
384            test_ptserver_activate(true, TEST_GENESERVER_ID);
385#if defined(INDEX_HEXDUMP)
386            TEST_DUMP_FILE("TEST_gpt.arb.pt", "TEST_gpt.arb.pt.dump");
387#endif
388
389#if defined(TEST_AUTO_UPDATE)
390            TEST_COPY_FILE("TEST_gpt.arb.pt", "TEST_gpt.arb.pt.expected");
391# if defined(INDEX_HEXDUMP)
392            TEST_COPY_FILE("TEST_gpt.arb.pt.dump", "TEST_gpt.arb.pt.dump.expected");
393# endif
394#else // !defined(TEST_AUTO_UPDATE)
395# if defined(INDEX_HEXDUMP)
396            TEST_EXPECT_TEXTFILES_EQUAL("TEST_gpt.arb.pt.dump.expected", "TEST_gpt.arb.pt.dump");
397# endif
398            TEST_EXPECT_FILES_EQUAL("TEST_gpt.arb.pt.expected", "TEST_gpt.arb.pt");
399#endif
400
401            TEST_EXPECT_LESS_EQUAL(GB_time_of_file("TEST_gpt.arb"), GB_time_of_file("TEST_gpt.arb.pt"));
402            break;
403        }
404        case CLEAN: {
405            test_ptserver_activate(false, TEST_GENESERVER_ID);
406            TEST_EXPECT_ZERO_OR_SHOW_ERRNO(unlink("TEST_gpt.arb.pt"));
407#if defined(INDEX_HEXDUMP)
408            TEST_EXPECT_ZERO_OR_SHOW_ERRNO(unlink("TEST_gpt.arb.pt.dump"));
409#endif
410            break;
411        }
412        case UNKNOWN:
413            env_assert(0);
414            break;
415    }
416
417    return error;
418}
419
420#undef TEST_AUTO_UPDATE
421
422// --------------------------------------------------------------------------------
423
424typedef Error (*Environment_cb)(Mode mode);
425
426static Environment_cb wrapped_cb   = NULL;
427static Mode           wrapped_mode = UNKNOWN;
428static SmartCharPtr   wrapped_error;
429
430static void wrapped() { wrapped_error = wrapped_cb(wrapped_mode); }
431
432class FunInfo {
433    Environment_cb     cb;
434    string             name;
435    LazyPersistantFlag is_setup;
436    LazyPersistantFlag changing; // some process is currently setting up/cleaning the environment
437    LazyPersistantFlag failed;   // some process failed to setup the environment
438
439    void all_get_lazy_again() {
440        changing.get_lazy_again();
441        is_setup.get_lazy_again();
442        failed.get_lazy_again();
443    }
444
445    Error set_to(Mode mode) {
446        StaticCode::printf("[%s environment '%s' START]\n", mode_command[mode], get_name());
447
448        wrapped_cb   = cb;
449        wrapped_mode = mode;
450        wrapped_error.SetNull();
451
452        Error error;
453        if (chdir(runDir()) != 0) {
454            error = strdup(GB_IO_error("changing dir to", runDir()));
455        }
456        else {
457            long           duration;
458            UnitTestResult guard_says = execute_guarded(wrapped, &duration, MAX_EXEC_MS_ENV, false);
459
460            switch (guard_says) {
461                case TEST_OK:
462                    if (wrapped_error.isSet()) {
463                        error = GBS_global_string_copy("returns error: %s", &*wrapped_error);
464                    }
465                    break;
466
467                case TEST_TRAPPED:
468                case TEST_INTERRUPTED:  {
469                    const char *what_happened = guard_says == TEST_TRAPPED
470                        ? "trapped"
471                        : "has been interrupted (might be a deaklock)";
472
473                    error = GBS_global_string_copy("%s%s",
474                                                   what_happened,
475                                                   wrapped_error.isSet() ? GBS_global_string(" (wrapped_error='%s')", &*wrapped_error) : "");
476                    break;
477                }
478                case TEST_THREW:
479                    error = strdup("has thrown an exception");
480                    break;
481
482                case TEST_INVALID:
483                    error = strdup("is invalid");
484                    break;
485
486                case TEST_FAILED_POSTCONDITION:
487                case TEST_UNKNOWN_RESULT:
488                    env_assert(0); // should not happen here
489                    break;
490            }
491            if (error.isSet()) {
492                error = GBS_global_string_copy("%s(%s) %s", name.c_str(), upcase(mode_command[mode]), &*error);
493            }
494            else {
495                Mutex m(FLAG_MUTEX);
496
497                env_assert(changing);
498                is_setup = (mode == SETUP);
499#if defined(SIMULATE_ENVSETUP_TIMEOUT)
500                if (mode == SETUP) {
501                    StaticCode::printf("[simulating a timeout during SETUP]\n");
502                    sleepms(MAX_EXEC_MS_ENV+MAX_EXEC_MS_SLOW+100); // simulate a timeout
503                }
504#endif
505#if defined(SIMULATE_ENVSETUP_SLOW_OVERTIME)
506                if (mode == SETUP) {
507                    StaticCode::printf("[simulating overtime during SETUP]\n");
508                    sleepms(MAX_EXEC_MS_SLOW+100); // simulate overtime
509                }
510#endif
511                changing = false;
512                all_get_lazy_again();
513            }
514        }
515
516        StaticCode::printf("[%s environment '%s' END]\n", mode_command[mode], get_name());
517        return error;
518    }
519
520public:
521
522    FunInfo(Environment_cb cb_, const char *name_)
523        : cb(cb_),
524          name(name_),
525          is_setup(name_),
526          changing(GBS_global_string("changing_%s", name_)),
527          failed(GBS_global_string("failed_%s", name_))
528    {}
529
530    Error switch_to(Mode mode) { // @@@ need to return allocated msg (it gets overwritten)
531        Error error;
532        bool  want_setup = (mode == SETUP);
533
534        bool perform_change  = false;
535        bool wait_for_change = false;
536        {
537            Mutex m(FLAG_MUTEX);
538
539            if (changing) { // somebody is changing the environment state
540                if (is_setup == want_setup) { // wanted state was reached, but somebody is altering it
541                    error = GBS_global_string_copy("[environment '%s' was %s, but somebody is changing it to %s]",
542                                                   get_name(), mode_command[mode], mode_command[other_mode[mode]]);
543                }
544                else { // the somebody is changing to my wanted state
545                    wait_for_change = true;
546                }
547            }
548            else {
549                if (is_setup == want_setup) {
550                    StaticCode::printf("[environment '%s' already was %s]\n", get_name(), mode_command[mode]);
551                }
552                else {
553                    changing = perform_change = true;
554                }
555            }
556
557            all_get_lazy_again();
558        }
559
560        env_assert(!(perform_change && wait_for_change));
561
562        if (perform_change) {
563            error = set_to(mode);
564            if (error.isSet()) {
565                {
566                    Mutex m(FLAG_MUTEX);
567                    failed = true;
568                    all_get_lazy_again();
569                }
570                Error clean_error = set_to(CLEAN);
571                if (clean_error.isSet()) {
572                    StaticCode::printf("[environment '%s' failed to reach '%s' after failure (Reason: %s)]\n",
573                                       get_name(), mode_command[CLEAN], &*clean_error);
574                }
575            }
576        }
577
578        if (wait_for_change) {
579            bool reached = false;
580            while (!reached && !error.isSet()) {
581                StaticCode::printf("[waiting until environment '%s' reaches '%s']\n", get_name(), mode_command[mode]);
582                sleepms(1000);
583                {
584                    Mutex m(FLAG_MUTEX);
585                    if (!changing && is_setup == want_setup) reached = true;
586                    if (failed) {
587                        error = GBS_global_string_copy("[environment '%s' failed to reach '%s' (in another process)]", get_name(), mode_command[mode]);
588                    }
589                    all_get_lazy_again();
590                }
591            }
592            if (reached) StaticCode::printf("[environment '%s' has reached '%s']\n", get_name(), mode_command[mode]);
593        }
594
595        return error;
596    }
597
598    const char *get_name() const { return name.c_str(); }
599    bool has_name(const char *oname) const { return name == oname; }
600};
601
602#define FUNINFO(fun) FunInfo(fun,#fun)
603
604static FunInfo modules[] = { // ExistingEnvironments
605    FUNINFO(ptserver),
606    FUNINFO(ptserver_gene),
607};
608
609const size_t MODULES = ARRAY_ELEMS(modules);
610
611static FunInfo *find_module(const char *moduleName) {
612    for (size_t e = 0; e<MODULES; ++e) {
613        if (modules[e].has_name(moduleName)) {
614            return &modules[e];
615        }
616    }
617    return NULL;
618}
619
620static Error set_all_modules_to(Mode mode) {
621    Error error;
622    for (size_t e = 0; error.isNull() && e<MODULES; ++e) {
623        error = modules[e].switch_to(mode);
624    }
625    return error;
626}
627
628static const char *known_modules(char separator) {
629    const char *mods = NULL;
630    for (size_t i = 0; i<MODULES; ++i) {
631        mods = mods
632            ? GBS_global_string("%s%c%s", mods, separator, modules[i].get_name())
633            : modules[i].get_name();
634    }
635    return mods;
636}
637static const char *known_modes(char separator) {
638    const char *modes = NULL;
639    for (size_t i = 0; i<ARRAY_ELEMS(mode_command); ++i) {
640        if (mode_command[i]) {
641            modes = modes
642                ? GBS_global_string("%s%c%s", modes, separator, mode_command[i])
643                : mode_command[i];
644        }
645    }
646    return modes;
647}
648
649
650int main(int argc, char* argv[]) {
651    start_of_main();
652
653    Error error;
654    bool  showUsage = true;
655
656    if (argc<2 || argc>3) error = strdup("Wrong number of arguments");
657    else {
658        const char *modearg = argv[1];
659        Mode        mode    = UNKNOWN;
660
661        for (size_t i = 0; i<ARRAY_ELEMS(mode_command); ++i) {
662            if (mode_command[i] && strcmp(modearg, mode_command[i]) == 0) mode = (Mode)i;
663        }
664
665        if (mode == UNKNOWN) {
666            error = GBS_global_string_copy("unknown argument '%s' (known modes are: %s)", modearg, known_modes(' '));
667        }
668        else {
669            if (argc == 2) {
670                error     = set_all_modules_to(mode);
671                showUsage = false;
672            }
673            else {
674                const char *modulearg = argv[2];
675                FunInfo    *module    = find_module(modulearg);
676
677                if (!module) {
678                    error = GBS_global_string_copy("unknown argument '%s' (known modules are: %s)", modulearg, known_modules(' '));
679                }
680                else {
681                    error     = module->switch_to(mode);
682                    showUsage = false;
683                }
684            }
685        }
686    }
687
688    if (error.isSet()) {
689        const char *exename = argv[0];
690        StaticCode::printf("Error in %s: %s\n", exename, &*error);
691        if (showUsage) StaticCode::printf("Usage: %s [%s] [%s]\n", exename, known_modes('|'), known_modules('|'));
692        return EXIT_FAILURE;
693    }
694
695    return EXIT_SUCCESS;
696}
697
Note: See TracBrowser for help on using the repository browser.