blob: 3860d1919836c77f30988888dbccce23e19f9b02 [file] [log] [blame]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02001# osmo_gsm_tester: test suite
2#
3# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
4#
5# Author: Neels Hofmeyr <neels@hofmeyr.de>
6#
7# This program is free software: you can redistribute it and/or modify
Harald Welte27205342017-06-03 09:51:45 +02008# it under the terms of the GNU General Public License as
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02009# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Harald Welte27205342017-06-03 09:51:45 +020015# GNU General Public License for more details.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020016#
Harald Welte27205342017-06-03 09:51:45 +020017# You should have received a copy of the GNU General Public License
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020018# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20import os
Neels Hofmeyr3531a192017-03-28 14:30:28 +020021import sys
22import time
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +020023import pprint
Pau Espin Pedrolf574a462020-05-05 12:18:35 +020024from . import config
25from . import log
26from . import util
27from . import schema
28from . import resource
Pau Espin Pedrol4e6b5072020-05-11 15:12:07 +020029from . import scenario
Pau Espin Pedrolf574a462020-05-05 12:18:35 +020030from . import test
Neels Hofmeyr1ffc3fe2017-05-07 02:15:21 +020031
Neels Hofmeyr3531a192017-03-28 14:30:28 +020032class SuiteDefinition(log.Origin):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020033 '''A test suite reserves resources for a number of tests.
34 Each test requires a specific number of modems, BTSs etc., which are
35 reserved beforehand by a test suite. This way several test suites can be
36 scheduled dynamically without resource conflicts arising halfway through
37 the tests.'''
38
39 CONF_FILENAME = 'suite.conf'
40
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041 def __init__(self, suite_dir):
Pau Espin Pedrol30637302020-05-06 21:11:02 +020042 self._suite_name = os.path.basename(suite_dir)
43 super().__init__(log.C_CNF, self._suite_name)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020044 self.suite_dir = suite_dir
Pau Espin Pedrol30637302020-05-06 21:11:02 +020045 self.conf = None
46 self._schema = None
Pau Espin Pedrolc3cf6822020-06-12 17:54:55 +020047 self.test_basenames = []
48 self.load_test_basenames()
Neels Hofmeyr3531a192017-03-28 14:30:28 +020049 self.read_conf()
50
51 def read_conf(self):
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020052 self.dbg('reading %s' % SuiteDefinition.CONF_FILENAME)
53 if not os.path.isdir(self.suite_dir):
54 raise RuntimeError('No such directory: %r' % self.suite_dir)
55 self.conf = config.read(os.path.join(self.suite_dir,
Pau Espin Pedrol30637302020-05-06 21:11:02 +020056 SuiteDefinition.CONF_FILENAME))
57 # Drop schema part since it's dynamically defining content, makes no sense to validate it.
58 self._schema = self.conf.pop('schema', {})
Pau Espin Pedrolc3cf6822020-06-12 17:54:55 +020059 # Add per-test 'timeout' attribute:
60 d = {t.rstrip('.py'):{'timeout': schema.DURATION} for t in self.test_basenames}
61 schema.combine(self._schema, d)
62 # Convert config file format to proper schema format and register it:
Pau Espin Pedrol30637302020-05-06 21:11:02 +020063 sdef = schema.config_to_schema_def(self._schema, "%s." % self._suite_name)
64 schema.register_config_schema('suite', sdef)
Pau Espin Pedrolc3cf6822020-06-12 17:54:55 +020065 # Finally validate the file:
Pau Espin Pedrol30637302020-05-06 21:11:02 +020066 schema.validate(self.conf, schema.get_all_schema())
Neels Hofmeyr3531a192017-03-28 14:30:28 +020067
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020068 def load_test_basenames(self):
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020069 for basename in sorted(os.listdir(self.suite_dir)):
70 if not basename.endswith('.py'):
71 continue
72 self.test_basenames.append(basename)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020073
Pau Espin Pedrol30637302020-05-06 21:11:02 +020074
Neels Hofmeyr3531a192017-03-28 14:30:28 +020075class SuiteRun(log.Origin):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +020076 UNKNOWN = 'UNKNOWN'
77 PASS = 'PASS'
78 FAIL = 'FAIL'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020080 def __init__(self, trial, suite_scenario_str, suite_definition, scenarios=[]):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020081 super().__init__(log.C_TST, suite_scenario_str)
Pau Espin Pedrol58603672018-08-09 13:45:55 +020082 self.start_timestamp = None
Andre Puschmannd81b1e42020-07-21 11:43:00 +020083 self.duration = 0
Pau Espin Pedrol58603672018-08-09 13:45:55 +020084 self.reserved_resources = None
Pau Espin Pedrol58603672018-08-09 13:45:55 +020085 self._resource_requirements = None
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020086 self._resource_modifiers = None
Pau Espin Pedrol58603672018-08-09 13:45:55 +020087 self._config = None
Pau Espin Pedrol58603672018-08-09 13:45:55 +020088 self._run_dir = None
Pau Espin Pedrola442cb82020-05-05 12:54:37 +020089 self._trial = trial
Neels Hofmeyr3531a192017-03-28 14:30:28 +020090 self.definition = suite_definition
91 self.scenarios = scenarios
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092 self.resources_pool = resource.ResourcesPool()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +020093 self.status = SuiteRun.UNKNOWN
94 self.load_tests()
95
Pau Espin Pedrol30637302020-05-06 21:11:02 +020096 def suite_name(self):
97 'Return name of suite without scenarios'
98 return self.definition.name()
99
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200100 def trial(self):
101 return self._trial
102
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200103 def load_tests(self):
104 self.tests = []
105 for test_basename in self.definition.test_basenames:
Pau Espin Pedrola75f85a2020-06-12 17:13:26 +0200106 test_specific_config = self.config_suite_specific().get(test_basename.rstrip('.py'), {})
107 self.tests.append(test.Test(self, test_basename, test_specific_config))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200108
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200109 def mark_start(self):
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200110 self.start_timestamp = time.time()
111 self.duration = 0
Pau Espin Pedrol0ffb4142017-05-15 18:24:35 +0200112 self.status = SuiteRun.UNKNOWN
113
Pau Espin Pedrolc264d3d2018-08-27 12:49:35 +0200114 def combined(self, conf_name, replicate_times=True):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200115 log.dbg(combining=conf_name)
116 log.ctx(combining_scenarios=conf_name)
Pau Espin Pedrolc264d3d2018-08-27 12:49:35 +0200117 combination = self.definition.conf.get(conf_name, {})
118 if replicate_times:
119 combination = config.replicate_times(combination)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200120 log.dbg(definition_conf=combination)
Pau Espin Pedrol4e6b5072020-05-11 15:12:07 +0200121 for sc in self.scenarios:
122 log.ctx(combining_scenarios=conf_name, scenario=sc.name())
123 c = sc.get(conf_name, {})
Pau Espin Pedrolc264d3d2018-08-27 12:49:35 +0200124 if replicate_times:
125 c = config.replicate_times(c)
Pau Espin Pedrol4e6b5072020-05-11 15:12:07 +0200126 log.dbg(scenario=sc.name(), conf=c)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200127 if c is None:
128 continue
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +0200129 schema.combine(combination, c)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200130 return combination
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131
Pau Espin Pedrold0912332017-06-14 13:27:08 +0200132 def get_run_dir(self):
133 if self._run_dir is None:
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200134 self._run_dir = util.Dir(self._trial.get_run_dir().new_dir(self.name()))
Pau Espin Pedrold0912332017-06-14 13:27:08 +0200135 return self._run_dir
136
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137 def resource_requirements(self):
138 if self._resource_requirements is None:
139 self._resource_requirements = self.combined('resources')
140 return self._resource_requirements
141
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200142 def resource_modifiers(self):
143 if self._resource_modifiers is None:
144 self._resource_modifiers = self.combined('modifiers')
145 return self._resource_modifiers
146
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200147 def config(self):
148 if self._config is None:
Pau Espin Pedrolc264d3d2018-08-27 12:49:35 +0200149 self._config = self.combined('config', False)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200150 return self._config
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200151
Pau Espin Pedrol30637302020-05-06 21:11:02 +0200152 def config_suite_specific(self):
153 return self.config().get('suite', {}).get(self.suite_name(), {})
154
Pau Espin Pedrolaa1cbdc2020-05-04 20:21:31 +0200155 def resource_pool(self):
156 return self.resources_pool
157
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 def reserve_resources(self):
159 if self.reserved_resources:
160 raise RuntimeError('Attempt to reserve resources twice for a SuiteRun')
Neels Hofmeyr7e2e8f12017-05-14 03:37:13 +0200161 self.log('reserving resources in', self.resources_pool.state_dir, '...')
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200162 self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements(), self.resource_modifiers())
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200163
Pau Espin Pedrolaa1cbdc2020-05-04 20:21:31 +0200164 def get_reserved_resource(self, resource_class_str, specifics):
165 return self.reserved_resources.get(resource_class_str, specifics=specifics)
166
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167 def run_tests(self, names=None):
Pau Espin Pedrol7e02d202018-05-08 15:28:48 +0200168 suite_libdir = os.path.join(self.definition.suite_dir, 'lib')
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200169 try:
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200170 log.large_separator(self._trial.name(), self.name(), sublevel=2)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200171 self.mark_start()
Pau Espin Pedrol7e02d202018-05-08 15:28:48 +0200172 util.import_path_prepend(suite_libdir)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200173 if not self.reserved_resources:
174 self.reserve_resources()
Pau Espin Pedrolfd5de3d2017-11-09 14:26:35 +0100175 for t in self.tests:
176 if names and not t.name() in names:
177 t.set_skip()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200178 continue
Pau Espin Pedrolfd5de3d2017-11-09 14:26:35 +0100179 self.current_test = t
180 t.run()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200181 except Exception:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200182 log.log_exn()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200183 except BaseException as e:
184 # when the program is aborted by a signal (like Ctrl-C), escalate to abort all.
185 self.err('SUITE RUN ABORTED: %s' % type(e).__name__)
186 raise
Pau Espin Pedrol469316f2017-05-17 14:51:31 +0200187 finally:
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200188 self.free_resources()
Pau Espin Pedrol7e02d202018-05-08 15:28:48 +0200189 util.import_path_remove(suite_libdir)
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200190 self.duration = time.time() - self.start_timestamp
191
Pau Espin Pedrold4dc2ad2020-06-15 13:27:07 +0200192 self.determine_status()
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200193
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200194 log.large_separator(self._trial.name(), self.name(), self.status, sublevel=2, space_above=False)
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200195
196 def passed(self):
197 return self.status == SuiteRun.PASS
198
Pau Espin Pedrold4dc2ad2020-06-15 13:27:07 +0200199 def determine_status(self):
200 passed, skipped, failed, errors = self.count_test_results()
201 # if no tests ran, count it as failure
202 if passed and not failed and not errors:
203 self.status = SuiteRun.PASS
204 else:
205 self.status = SuiteRun.FAIL
206
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200207 def count_test_results(self):
208 passed = 0
209 skipped = 0
210 failed = 0
Pau Espin Pedrol02e8a8d2020-03-05 17:22:40 +0100211 errors = 0
Pau Espin Pedrolfd5de3d2017-11-09 14:26:35 +0100212 for t in self.tests:
Pau Espin Pedrol02e8a8d2020-03-05 17:22:40 +0100213 if t.status == test.Test.SKIP:
214 skipped += 1
215 elif t.status == test.Test.PASS:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200216 passed += 1
Pau Espin Pedrolfd5de3d2017-11-09 14:26:35 +0100217 elif t.status == test.Test.FAIL:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200218 failed += 1
Pau Espin Pedrol02e8a8d2020-03-05 17:22:40 +0100219 else: # error, could not run
220 errors += 1
221 return (passed, skipped, failed, errors)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200222
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200223 def free_resources(self):
224 if self.reserved_resources is None:
225 return
226 self.reserved_resources.free()
227
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200228 def resource_status_str(self):
229 return '\n'.join(('',
230 'SUITE RUN: %s' % self.origin_id(),
231 'ASKED FOR:', pprint.pformat(self._resource_requirements),
232 'RESERVED COUNT:', pprint.pformat(self.reserved_resources.counts()),
233 'RESOURCES STATE:', repr(self.reserved_resources)))
234
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200235loaded_suite_definitions = {}
236
237def load(suite_name):
238 global loaded_suite_definitions
239
240 suite = loaded_suite_definitions.get(suite_name)
241 if suite is not None:
242 return suite
243
Pau Espin Pedrol66ef9452020-05-25 13:26:41 +0200244 suites_dirs = config.get_suites_dirs()
245 suite_dir = None
246 found = False
247 for d in suites_dirs:
248 suite_dir = d.child(suite_name)
249 if d.exists(suite_name) and d.isdir(suite_name):
250 found = True
251 break
252 if not found:
253 raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dirs))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200254
255 suite_def = SuiteDefinition(suite_dir)
256 loaded_suite_definitions[suite_name] = suite_def
257 return suite_def
258
259def parse_suite_scenario_str(suite_scenario_str):
260 tokens = suite_scenario_str.split(':')
261 if len(tokens) > 2:
262 raise RuntimeError('invalid combination string: %r' % suite_scenario_str)
263
264 suite_name = tokens[0]
265 if len(tokens) <= 1:
266 scenario_names = []
267 else:
268 scenario_names = tokens[1].split('+')
269
270 return suite_name, scenario_names
271
272def load_suite_scenario_str(suite_scenario_str):
273 suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
274 suite = load(suite_name)
Pau Espin Pedrol4e6b5072020-05-11 15:12:07 +0200275 scenarios = [scenario.get_scenario(scenario_name, schema.get_all_schema()) for scenario_name in scenario_names]
Your Name44af3412017-04-13 03:11:59 +0200276 return (suite_scenario_str, suite, scenarios)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200277
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200278# vim: expandtab tabstop=4 shiftwidth=4