blob: 8740d206ffc81d143406c785cc6a1f44aa9aeee7 [file] [log] [blame]
Chase Qi09edc7f2016-08-18 13:18:50 +08001#!/usr/bin/env python
2import argparse
3import csv
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +00004import cmd
Chase Qi09edc7f2016-08-18 13:18:50 +08005import json
6import logging
7import os
Chase Qi09edc7f2016-08-18 13:18:50 +08008import re
9import shutil
10import sys
11import time
Chase Qi09edc7f2016-08-18 13:18:50 +080012from uuid import uuid4
13
14
Chase Qifaf7d282016-08-29 19:34:01 +080015try:
16 import pexpect
17 import yaml
18except ImportError as e:
19 print(e)
20 print('Please run the below command to install modules required')
21 print('pip install -r ${REPO_PATH}/automated/utils/requirements.txt')
22 sys.exit(1)
23
24
Chase Qi09edc7f2016-08-18 13:18:50 +080025class TestPlan(object):
26 """
27 Analysis args specified, then generate test plan.
28 """
29
30 def __init__(self, args):
31 self.output = args.output
32 self.test_def = args.test_def
33 self.test_plan = args.test_plan
34 self.timeout = args.timeout
35 self.skip_install = args.skip_install
36 self.logger = logging.getLogger('RUNNER.TestPlan')
37
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +000038 def test_list(self, kind="automated"):
Chase Qi09edc7f2016-08-18 13:18:50 +080039 if self.test_def:
40 if not os.path.exists(self.test_def):
41 self.logger.error(' %s NOT found, exiting...' % self.test_def)
42 sys.exit(1)
43
44 test_list = [{'path': self.test_def}]
45 test_list[0]['uuid'] = str(uuid4())
46 test_list[0]['timeout'] = self.timeout
47 test_list[0]['skip_install'] = self.skip_install
48 elif self.test_plan:
49 if not os.path.exists(self.test_plan):
50 self.logger.error(' %s NOT found, exiting...' % self.test_plan)
51 sys.exit(1)
52
53 with open(self.test_plan, 'r') as f:
54 test_plan = yaml.safe_load(f)
55 try:
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +000056 test_list = []
57 for requirement in test_plan['requirements']:
58 if 'tests' in requirement.keys():
59 if requirement['tests'] and \
60 kind in requirement['tests'].keys() and \
61 requirement['tests'][kind]:
62 for test in requirement['tests'][kind]:
63 test_list.append(test)
Chase Qi09edc7f2016-08-18 13:18:50 +080064 for test in test_list:
65 test['uuid'] = str(uuid4())
66 except KeyError as e:
67 self.logger.error("%s is missing from test plan" % str(e))
68 sys.exit(1)
69 else:
70 self.logger.error('Plese specify a test or test plan.')
71 sys.exit(1)
72
73 return test_list
74
75
76class TestSetup(object):
77 """
78 Create directories required, then copy files needed to these directories.
79 """
80
81 def __init__(self, test, args):
82 self.output = os.path.realpath(args.output)
83 self.test_name = os.path.splitext(test['path'].split('/')[-1])[0]
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +000084 self.repo_test_path = test['path']
Chase Qi09edc7f2016-08-18 13:18:50 +080085 self.uuid = test['uuid']
86 self.test_uuid = self.test_name + '_' + self.uuid
87 self.test_path = os.path.join(self.output, self.test_uuid)
88 self.logger = logging.getLogger('RUNNER.TestSetup')
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +000089 self.test_kind = args.kind
Chase Qi09edc7f2016-08-18 13:18:50 +080090
91 def validate_env(self):
92 # Inspect if environment set properly.
93 try:
94 self.repo_path = os.environ['REPO_PATH']
95 except KeyError:
96 self.logger.error('KeyError: REPO_PATH')
97 self.logger.error("Please run '. ./bin/setenv.sh' to setup test environment")
98 sys.exit(1)
99
100 def create_dir(self):
101 if not os.path.exists(self.output):
102 os.makedirs(self.output)
103 self.logger.info('Output directory created: %s' % self.output)
104
105 def copy_test_repo(self):
106 self.validate_env()
107 shutil.rmtree(self.test_path, ignore_errors=True)
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000108 if self.test_kind == 'manual':
109 test_dir_path = os.path.join(self.repo_path, self.repo_test_path.rsplit("/", 1)[0])
110 shutil.copytree(test_dir_path, self.test_path, symlinks=True)
111 self.logger.info('Test copied to: %s' % self.test_path)
112 else:
113 if self.repo_path in self.test_path:
114 self.logger.error("Cannot copy repository into itself. Please choose output directory outside repository path")
115 sys.exit(1)
116 shutil.copytree(self.repo_path, self.test_path, symlinks=True)
117 self.logger.info('Test repo copied to: %s' % self.test_path)
Chase Qi09edc7f2016-08-18 13:18:50 +0800118
119 def create_uuid_file(self):
120 with open('%s/uuid' % self.test_path, 'w') as f:
121 f.write(self.uuid)
122
123
124class TestDefinition(object):
125 """
126 Convert test definition to testdef.yaml, testdef_metadata and run.sh.
127 """
128
129 def __init__(self, test, args):
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000130 self.test = test
131 self.args = args
Chase Qi09edc7f2016-08-18 13:18:50 +0800132 self.output = os.path.realpath(args.output)
133 self.test_def = test['path']
134 self.test_name = os.path.splitext(self.test_def.split('/')[-1])[0]
135 self.test_uuid = self.test_name + '_' + test['uuid']
136 self.test_path = os.path.join(self.output, self.test_uuid)
137 self.logger = logging.getLogger('RUNNER.TestDef')
138 self.skip_install = args.skip_install
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000139 self.is_manual = False
Chase Qi09edc7f2016-08-18 13:18:50 +0800140 if 'skip_install' in test:
141 self.skip_install = test['skip_install']
142 self.custom_params = None
143 if 'parameters' in test:
144 self.custom_params = test['parameters']
145 if 'params' in test:
146 self.custom_params = test['params']
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000147 self.exists = False
148 if os.path.isfile(self.test_def):
149 self.exists = True
150 with open(self.test_def, 'r') as f:
151 self.testdef = yaml.safe_load(f)
152 if self.testdef['metadata']['format'].startswith("Manual Test Definition"):
153 self.is_manual = True
Chase Qi09edc7f2016-08-18 13:18:50 +0800154
155 def definition(self):
156 with open('%s/testdef.yaml' % self.test_path, 'w') as f:
157 f.write(yaml.dump(self.testdef, encoding='utf-8', allow_unicode=True))
158
159 def metadata(self):
160 with open('%s/testdef_metadata' % self.test_path, 'w') as f:
161 f.write(yaml.dump(self.testdef['metadata'], encoding='utf-8', allow_unicode=True))
162
163 def run(self):
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000164 if not self.is_manual:
165 with open('%s/run.sh' % self.test_path, 'a') as f:
166 f.write('#!/bin/sh\n')
Chase Qi09edc7f2016-08-18 13:18:50 +0800167
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000168 self.parameters = self.handle_parameters()
169 if self.parameters:
170 for line in self.parameters:
171 f.write(line)
Chase Qi09edc7f2016-08-18 13:18:50 +0800172
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000173 f.write('set -e\n')
174 f.write('export TESTRUN_ID=%s\n' % self.testdef['metadata']['name'])
175 f.write('cd %s\n' % self.test_path)
176 f.write('UUID=`cat uuid`\n')
177 f.write('echo "<STARTRUN $TESTRUN_ID $UUID>"\n')
178 steps = self.testdef['run'].get('steps', [])
179 if steps:
180 for cmd in steps:
181 if '--cmd' in cmd or '--shell' in cmd:
182 cmd = re.sub(r'\$(\d+)\b', r'\\$\1', cmd)
183 f.write('%s\n' % cmd)
184 f.write('echo "<ENDRUN $TESTRUN_ID $UUID>"\n')
Chase Qi09edc7f2016-08-18 13:18:50 +0800185
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000186 os.chmod('%s/run.sh' % self.test_path, 0755)
187
188 def get_test_run(self):
189 if self.is_manual:
190 return ManualTestRun(self.test, self.args)
191 return AutomatedTestRun(self.test, self.args)
Chase Qi09edc7f2016-08-18 13:18:50 +0800192
193 def handle_parameters(self):
194 ret_val = ['###default parameters from test definition###\n']
195
196 if 'params' in self.testdef:
197 for def_param_name, def_param_value in list(self.testdef['params'].items()):
198 # ?'yaml_line'
199 if def_param_name is 'yaml_line':
200 continue
201 ret_val.append('%s=\'%s\'\n' % (def_param_name, def_param_value))
202 elif 'parameters' in self.testdef:
203 for def_param_name, def_param_value in list(self.testdef['parameters'].items()):
204 if def_param_name is 'yaml_line':
205 continue
206 ret_val.append('%s=\'%s\'\n' % (def_param_name, def_param_value))
207 else:
208 return None
209
210 ret_val.append('######\n')
211
212 ret_val.append('###custom parameters from test plan###\n')
213 if self.custom_params:
214 for param_name, param_value in list(self.custom_params.items()):
215 if param_name is 'yaml_line':
216 continue
217 ret_val.append('%s=\'%s\'\n' % (param_name, param_value))
218
219 if self.skip_install:
220 ret_val.append('SKIP_INSTALL="True"\n')
221 ret_val.append('######\n')
222
223 return ret_val
224
225
226class TestRun(object):
227 def __init__(self, test, args):
228 self.output = os.path.realpath(args.output)
229 self.test_name = os.path.splitext(test['path'].split('/')[-1])[0]
230 self.test_uuid = self.test_name + '_' + test['uuid']
231 self.test_path = os.path.join(self.output, self.test_uuid)
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000232 self.logger = logging.getLogger('RUNNER.TestRun')
Chase Qi09edc7f2016-08-18 13:18:50 +0800233 self.test_timeout = args.timeout
234 if 'timeout' in test:
235 self.test_timeout = test['timeout']
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000236
237 def run(self):
238 raise NotImplementedError
239
240 def check_result(self):
241 raise NotImplementedError
242
243
244class AutomatedTestRun(TestRun):
245 def run(self):
Chase Qi09edc7f2016-08-18 13:18:50 +0800246 self.logger.info('Executing %s/run.sh' % self.test_path)
247 shell_cmd = '%s/run.sh 2>&1 | tee %s/stdout.log' % (self.test_path, self.test_path)
248 self.child = pexpect.spawn('/bin/sh', ['-c', shell_cmd])
249
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000250 def check_result(self):
Chase Qi09edc7f2016-08-18 13:18:50 +0800251 if self.test_timeout:
252 self.logger.info('Test timeout: %s' % self.test_timeout)
253 test_end = time.time() + self.test_timeout
254
255 while self.child.isalive():
256 if self.test_timeout and time.time() > test_end:
257 self.logger.warning('%s test timed out, killing test process...' % self.test_uuid)
258 self.child.terminate(force=True)
259 break
260 try:
261 self.child.expect('\r\n')
262 print(self.child.before)
263 except pexpect.TIMEOUT:
264 continue
265 except pexpect.EOF:
266 self.logger.info('%s test finished.\n' % self.test_uuid)
267 break
268
269
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000270class ManualTestShell(cmd.Cmd):
271 def __init__(self, test_dict, result_path):
272 cmd.Cmd.__init__(self)
273 self.test_dict = test_dict
274 self.result_path = result_path
275 self.current_step_index = 0
276 self.steps = self.test_dict['run']['steps']
277 self.expected = self.test_dict['run']['expected']
278 self.prompt = "%s > " % self.test_dict['metadata']['name']
279 self.result = None
280 self.intro = """
281 Welcome to manual test executor. Type 'help' for available commands.
282 This shell is meant to be executed on your computer, not on the system
283 under test. Please execute the steps from the test case, compare to
284 expected result and record the test result as 'pass' or 'fail'. If there
285 is an issue that prevents from executing the step, please record the result
286 as 'skip'.
287 """
288
289 def do_quit(self, line):
290 """
291 Exit test execution
292 """
293 if self.result is not None:
294 return True
295 if line.find("-f") >= 0:
296 self._record_result("skip")
297 return True
298 print "Test result not recorded. Use -f to force. Forced quit records result as 'skip'"
299
300 do_EOF = do_quit
301
302 def do_description(self, line):
303 """
304 Prints current test overall description
305 """
306 print self.test_dict['metadata']['description']
307
308 def do_steps(self, line):
309 """
310 Prints all steps of the current test case
311 """
312 for index, step in enumerate(self.steps):
313 print "%s. %s" % (index, step)
314
315 def do_expected(self, line):
316 """
317 Prints all expected results of the current test case
318 """
319 for index, expected in enumerate(self.expected):
320 print "%s. %s" % (index, expected)
321
322 def do_current(self, line):
323 """
324 Prints current test step
325 """
326 self._print_step()
327
328 do_start = do_current
329
330 def do_next(self, line):
331 """
332 Prints next test step
333 """
334 if len(self.steps) > self.current_step_index + 1:
335 self.current_step_index += 1
336 self._print_step()
337
338 def _print_step(self):
339 print "%s. %s" % (self.current_step_index, self.steps[self.current_step_index])
340
341 def _record_result(self, result):
342 print "Recording %s in %s/stdout.log" % (result, self.result_path)
343 with open("%s/stdout.log" % self.result_path, "a") as f:
344 f.write("<LAVA_SIGNAL_TESTCASE TEST_CASE_ID=%s RESULT=%s>" %
345 (self.test_dict['metadata']['name'], result))
346
347 def do_pass(self, line):
348 """
349 Records PASS as test result
350 """
351 self.result = "pass"
352 self._record_result(self.result)
353 return True
354
355 def do_fail(self, line):
356 """
357 Records FAIL as test result
358 """
359 self.result = "fail"
360 self._record_result(self.result)
361 return True
362
363 def do_skip(self, line):
364 """
365 Records SKIP as test result
366 """
367 self.result = "skip"
368 self._record_result(self.result)
369 return True
370
371
372class ManualTestRun(TestRun, cmd.Cmd):
373 def run(self):
374 print self.test_name
375 with open('%s/testdef.yaml' % self.test_path, 'r') as f:
376 self.testdef = yaml.safe_load(f)
377
378 ManualTestShell(self.testdef, self.test_path).cmdloop()
379
380 def check_result(self):
381 pass
382
383
Chase Qi09edc7f2016-08-18 13:18:50 +0800384class ResultParser(object):
385 def __init__(self, test, args):
386 self.output = os.path.realpath(args.output)
387 self.test_name = os.path.splitext(test['path'].split('/')[-1])[0]
388 self.test_uuid = self.test_name + '_' + test['uuid']
389 self.result_path = os.path.join(self.output, self.test_uuid)
390 self.metrics = []
391 self.results = {}
392 self.results['test'] = self.test_name
393 self.results['id'] = self.test_uuid
394 self.logger = logging.getLogger('RUNNER.ResultParser')
Chase Qiae88be32016-11-23 20:32:21 +0800395 self.results['params'] = None
396 if 'parameters' in test.keys():
397 self.results['params'] = test['parameters']
398 if 'params' in test.keys():
399 self.results['params'] = test['params']
Chase Qi09edc7f2016-08-18 13:18:50 +0800400
401 def run(self):
402 self.parse_stdout()
403 self.dict_to_json()
404 self.dict_to_csv()
405 self.logger.info('Result files saved to: %s' % self.result_path)
406 print('--- Printing result.csv ---')
407 with open('%s/result.csv' % self.result_path) as f:
408 print(f.read())
409
410 def parse_stdout(self):
411 with open('%s/stdout.log' % self.result_path, 'r') as f:
412 for line in f:
413 if re.match(r'\<(|LAVA_SIGNAL_TESTCASE )TEST_CASE_ID=.*', line):
414 line = line.strip('\n').strip('<>').split(' ')
415 data = {'test_case_id': '',
416 'result': '',
417 'measurement': '',
418 'units': ''}
419
420 for string in line:
421 parts = string.split('=')
422 if len(parts) == 2:
423 key, value = parts
424 key = key.lower()
425 data[key] = value
426
427 self.metrics.append(data.copy())
428
429 self.results['metrics'] = self.metrics
430
431 def dict_to_json(self):
Chase Qi87f4f402016-11-07 15:32:01 +0800432 # Save test results to output/test_id/result.json
Chase Qi09edc7f2016-08-18 13:18:50 +0800433 with open('%s/result.json' % self.result_path, 'w') as f:
Chase Qi87f4f402016-11-07 15:32:01 +0800434 json.dump([self.results], f, indent=4)
435
436 # Collect test results of all tests in output/result.json
437 feeds = []
438 if os.path.isfile('%s/result.json' % self.output):
439 with open('%s/result.json' % self.output, 'r') as f:
440 feeds = json.load(f)
441
442 feeds.append(self.results)
443 with open('%s/result.json' % self.output, 'w') as f:
444 json.dump(feeds, f, indent=4)
Chase Qi09edc7f2016-08-18 13:18:50 +0800445
446 def dict_to_csv(self):
Chase Qica15cf52016-11-10 17:00:22 +0800447 # Convert dict self.results['params'] to a string.
448 test_params = ''
449 if self.results['params']:
450 params_dict = self.results['params']
451 test_params = ';'.join(['%s=%s' % (k, v) for k, v in params_dict.iteritems()])
Chase Qi09edc7f2016-08-18 13:18:50 +0800452
Chase Qica15cf52016-11-10 17:00:22 +0800453 for metric in self.results['metrics']:
454 metric['test'] = self.results['test']
455 metric['test_params'] = test_params
456
457 # Save test results to output/test_id/result.csv
458 fieldnames = ['test', 'test_case_id', 'result', 'measurement', 'units', 'test_params']
459 with open('%s/result.csv' % self.result_path, 'w') as f:
460 writer = csv.DictWriter(f, fieldnames=fieldnames)
Chase Qi09edc7f2016-08-18 13:18:50 +0800461 writer.writeheader()
462 for metric in self.results['metrics']:
463 writer.writerow(metric)
464
Chase Qi87f4f402016-11-07 15:32:01 +0800465 # Collect test results of all tests in output/result.csv
466 if not os.path.isfile('%s/result.csv' % self.output):
467 with open('%s/result.csv' % self.output, 'w') as f:
Chase Qi87f4f402016-11-07 15:32:01 +0800468 writer = csv.DictWriter(f, fieldnames=fieldnames)
469 writer.writeheader()
470
Chase Qi09edc7f2016-08-18 13:18:50 +0800471 with open('%s/result.csv' % self.output, 'a') as f:
472 writer = csv.DictWriter(f, fieldnames=fieldnames)
Chase Qi09edc7f2016-08-18 13:18:50 +0800473 for metric in self.results['metrics']:
Chase Qi09edc7f2016-08-18 13:18:50 +0800474 writer.writerow(metric)
475
476
477def get_args():
478 parser = argparse.ArgumentParser()
479 parser.add_argument('-o', '--output', default='/root/output', dest='output',
480 help='''
481 specify a directory to store test and result files.
482 Default: /root/output
483 ''')
484 parser.add_argument('-p', '--test_plan', default=None, dest='test_plan',
485 help='''
486 specify an test plan file which has tests and related
487 params listed in yaml format.
488 ''')
489 parser.add_argument('-d', '--test_def', default=None, dest='test_def',
490 help='''
491 base on test definition repo location, specify relative
492 path to the test definition to run.
493 Format example: "ubuntu/smoke-tests-basic.yaml"
494 ''')
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000495 parser.add_argument('-k', '--kind', default="automated", dest='kind',
496 choices=['automated', 'manual'],
497 help='''
498 Selects type of tests to be executed from the test plan.
499 Possible options: automated, manual
500 '''),
Chase Qi09edc7f2016-08-18 13:18:50 +0800501 parser.add_argument('-t', '--timeout', type=int, default=None,
502 dest='timeout', help='Specify test timeout')
503 parser.add_argument('-s', '--skip_install', dest='skip_install',
504 default=False, action='store_true',
505 help='skip install section defined in test definition.')
506 args = parser.parse_args()
507 return args
508
509
510def main():
511 # Setup logger.
512 logger = logging.getLogger('RUNNER')
513 logger.setLevel(logging.DEBUG)
514 ch = logging.StreamHandler()
515 ch.setLevel(logging.DEBUG)
516 formatter = logging.Formatter('%(asctime)s - %(name)s: %(levelname)s: %(message)s')
517 ch.setFormatter(formatter)
518 logger.addHandler(ch)
519
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000520 args = get_args()
521 if args.kind != "manual":
522 if os.geteuid() != 0:
523 logger.error("Sorry, you need to run this as root")
524 sys.exit(1)
Chase Qi09edc7f2016-08-18 13:18:50 +0800525
526 # Generate test plan.
Chase Qi09edc7f2016-08-18 13:18:50 +0800527 test_plan = TestPlan(args)
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000528 test_list = test_plan.test_list(args.kind)
Chase Qi09edc7f2016-08-18 13:18:50 +0800529 logger.info('Tests to run:')
530 for test in test_list:
531 print(test)
532
533 # Run tests.
534 for test in test_list:
535 # Create directories and copy files needed.
536 setup = TestSetup(test, args)
537 setup.create_dir()
538 setup.copy_test_repo()
539 setup.create_uuid_file()
540
541 # Convert test definition.
542 test_def = TestDefinition(test, args)
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000543 if test_def.exists:
544 test_def.definition()
545 test_def.metadata()
546 test_def.run()
Chase Qi09edc7f2016-08-18 13:18:50 +0800547
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000548 # Run test.
549 test_run = test_def.get_test_run()
550 test_run.run()
551 test_run.check_result()
Chase Qi09edc7f2016-08-18 13:18:50 +0800552
Milosz Wasilewski2fea70e2016-11-11 12:16:09 +0000553 # Parse test output, save results in json and csv format.
554 result_parser = ResultParser(test, args)
555 result_parser.run()
556 else:
557 logger.warning("Requested test definition %s doesn't exist" % test['path'])
Chase Qi09edc7f2016-08-18 13:18:50 +0800558
559if __name__ == "__main__":
560 main()