1#!/usr/bin/env python
2
3"""
4Command line utility for executing MongoDB tests of all kinds.
5"""
6
7from __future__ import absolute_import
8
9import json
10import os.path
11import random
12import signal
13import sys
14import time
15import traceback
16
17# Get relative imports to work when the package is not installed on the PYTHONPATH.
18if __name__ == "__main__" and __package__ is None:
19    sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20    from buildscripts import resmokelib
21
22
23def _execute_suite(suite, logging_config):
24    """
25    Executes each test group of 'suite', failing fast if requested.
26
27    Returns true if the execution of the suite was interrupted by the
28    user, and false otherwise.
29    """
30
31    logger = resmokelib.logging.loggers.EXECUTOR
32
33    for group in suite.test_groups:
34        if resmokelib.config.SHUFFLE:
35            logger.info("Shuffling order of tests for %ss in suite %s. The seed is %d.",
36                        group.test_kind, suite.get_name(), resmokelib.config.RANDOM_SEED)
37            random.seed(resmokelib.config.RANDOM_SEED)
38            random.shuffle(group.tests)
39
40        if resmokelib.config.DRY_RUN == "tests":
41            sb = []
42            sb.append("Tests that would be run for %ss in suite %s:"
43                      % (group.test_kind, suite.get_name()))
44            if len(group.tests) > 0:
45                for test in group.tests:
46                    sb.append(test)
47            else:
48                sb.append("(no tests)")
49            logger.info("\n".join(sb))
50
51            # Set a successful return code on the test group because we want to output the tests
52            # that would get run by any other suites the user specified.
53            group.return_code = 0
54            continue
55
56        if len(group.tests) == 0:
57            logger.info("Skipping %ss, no tests to run", group.test_kind)
58            continue
59
60        group_config = suite.get_executor_config().get(group.test_kind, {})
61        executor = resmokelib.testing.executor.TestGroupExecutor(logger,
62                                                                 group,
63                                                                 logging_config,
64                                                                 **group_config)
65
66        try:
67            executor.run()
68            if resmokelib.config.FAIL_FAST and group.return_code != 0:
69                suite.return_code = group.return_code
70                return False
71        except resmokelib.errors.UserInterrupt:
72            suite.return_code = 130  # Simulate SIGINT as exit code.
73            return True
74        except:
75            logger.exception("Encountered an error when running %ss of suite %s.",
76                             group.test_kind, suite.get_name())
77            suite.return_code = 2
78            return False
79
80
81def _log_summary(logger, suites, time_taken):
82    if len(suites) > 1:
83        sb = []
84        sb.append("Summary of all suites: %d suites ran in %0.2f seconds"
85                  % (len(suites), time_taken))
86        for suite in suites:
87            suite_sb = []
88            suite.summarize(suite_sb)
89            sb.append("    %s: %s" % (suite.get_name(), "\n    ".join(suite_sb)))
90
91        logger.info("=" * 80)
92        logger.info("\n".join(sb))
93
94
95def _summarize_suite(suite):
96    sb = []
97    suite.summarize(sb)
98    return "\n".join(sb)
99
100
101def _dump_suite_config(suite, logging_config):
102    """
103    Returns a string that represents the YAML configuration of a suite.
104
105    TODO: include the "options" key in the result
106    """
107
108    sb = []
109    sb.append("YAML configuration of suite %s" % (suite.get_name()))
110    sb.append(resmokelib.utils.dump_yaml({"selector": suite.get_selector_config()}))
111    sb.append("")
112    sb.append(resmokelib.utils.dump_yaml({"executor": suite.get_executor_config()}))
113    sb.append("")
114    sb.append(resmokelib.utils.dump_yaml({"logging": logging_config}))
115    return "\n".join(sb)
116
117
118def _write_report_file(suites, pathname):
119    """
120    Writes the report.json file if requested.
121    """
122
123    reports = []
124    for suite in suites:
125        for group in suite.test_groups:
126            reports.extend(group.get_reports())
127
128    combined_report_dict = resmokelib.testing.report.TestReport.combine(*reports).as_dict()
129    with open(pathname, "w") as fp:
130        json.dump(combined_report_dict, fp)
131
132
133def main():
134    start_time = time.time()
135
136    values, args = resmokelib.parser.parse_command_line()
137
138    logging_config = resmokelib.parser.get_logging_config(values)
139    resmokelib.logging.config.apply_config(logging_config)
140    resmokelib.logging.flush.start_thread()
141
142    resmokelib.parser.update_config_vars(values)
143
144    exec_logger = resmokelib.logging.loggers.EXECUTOR
145    resmoke_logger = resmokelib.logging.loggers.new_logger("resmoke", parent=exec_logger)
146
147    if values.list_suites:
148        suite_names = resmokelib.parser.get_named_suites()
149        resmoke_logger.info("Suites available to execute:\n%s", "\n".join(suite_names))
150        sys.exit(0)
151
152    interrupted = False
153    suites = resmokelib.parser.get_suites(values, args)
154    try:
155        for suite in suites:
156            resmoke_logger.info(_dump_suite_config(suite, logging_config))
157
158            suite.record_start()
159            interrupted = _execute_suite(suite, logging_config)
160            suite.record_end()
161
162            resmoke_logger.info("=" * 80)
163            resmoke_logger.info("Summary of %s suite: %s",
164                                suite.get_name(), _summarize_suite(suite))
165
166            if interrupted or (resmokelib.config.FAIL_FAST and suite.return_code != 0):
167                time_taken = time.time() - start_time
168                _log_summary(resmoke_logger, suites, time_taken)
169                sys.exit(suite.return_code)
170
171        time_taken = time.time() - start_time
172        _log_summary(resmoke_logger, suites, time_taken)
173
174        # Exit with a nonzero code if any of the suites failed.
175        exit_code = max(suite.return_code for suite in suites)
176        sys.exit(exit_code)
177    finally:
178        if not interrupted:
179            resmokelib.logging.flush.stop_thread()
180
181        if resmokelib.config.REPORT_FILE is not None:
182            _write_report_file(suites, resmokelib.config.REPORT_FILE)
183
184
185if __name__ == "__main__":
186
187    def _dump_stacks(signum, frame):
188        """
189        Signal handler that will dump the stacks of all threads.
190        """
191
192        header_msg = "Dumping stacks due to SIGUSR1 signal"
193
194        sb = []
195        sb.append("=" * len(header_msg))
196        sb.append(header_msg)
197        sb.append("=" * len(header_msg))
198
199        frames = sys._current_frames()
200        sb.append("Total threads: %d" % (len(frames)))
201        sb.append("")
202
203        for thread_id in frames:
204            stack = frames[thread_id]
205            sb.append("Thread %d:" % (thread_id))
206            sb.append("".join(traceback.format_stack(stack)))
207
208        sb.append("=" * len(header_msg))
209        print "\n".join(sb)
210
211    try:
212        signal.signal(signal.SIGUSR1, _dump_stacks)
213    except AttributeError:
214        print "Cannot catch signals on Windows"
215
216    main()
217