trex_unit_test.py revision 23fabbe8
1#!/usr/bin/env python
2
3__copyright__ = "Copyright 2014"
4
5"""
6Name:
7     trex_unit_test.py
8
9
10Description:
11
12    This script creates the functionality to test the performance of the TRex traffic generator
13    The tested scenario is a TRex TG directly connected to a Cisco router.
14
15::
16
17    Topology:
18
19       -------                         --------
20      |       | Tx---1gig/10gig----Rx |        |
21      | TRex  |                       | router |
22      |       | Rx---1gig/10gig----Tx |        |
23       -------                         --------
24
25"""
26
27import os
28import sys
29import outer_packages
30import datetime
31import nose
32from nose.plugins import Plugin
33from nose.selector import Selector
34from nose.exc import SkipTest
35from nose.pyversion import force_unicode, format_exception
36import CustomLogger
37import misc_methods
38from rednose import RedNose
39import termstyle
40from trex import CTRexScenario
41from trex_stf_lib.trex_client import *
42from trex_stf_lib.trex_exceptions import *
43from trex_stl_lib.api import *
44from trex_stl_lib.utils.GAObjClass import GAmanager_Regression
45import trex_elk
46import trex
47import socket
48from pprint import pprint
49import time
50from distutils.dir_util import mkpath
51import re
52from io import StringIO
53
54
55
56TEST_ID = re.compile(r'^(.*?)(\(.*\))$')
57
58def id_split(idval):
59    m = TEST_ID.match(idval)
60    if m:
61        name, fargs = m.groups()
62        head, tail = name.rsplit(".", 1)
63        return [head, tail+fargs]
64    else:
65        return idval.rsplit(".", 1)
66
67# nose overrides
68
69# option to select wanted test by name without file, class etc.
70def new_Selector_wantMethod(self, method, orig_Selector_wantMethod = Selector.wantMethod):
71    result = orig_Selector_wantMethod(self, method)
72    return result and (not CTRexScenario.test or CTRexScenario.test in getattr(method, '__name__', ''))
73
74Selector.wantMethod = new_Selector_wantMethod
75
76def new_Selector_wantFunction(self, function, orig_Selector_wantFunction = Selector.wantFunction):
77    result = orig_Selector_wantFunction(self, function)
78    return result and (not CTRexScenario.test or CTRexScenario.test in getattr(function, '__name__', ''))
79
80Selector.wantFunction = new_Selector_wantFunction
81
82# override nose's strange representation of setUpClass errors
83def __suite_repr__(self):
84    if hasattr(self.context, '__module__'): # inside class, setUpClass etc.
85        class_repr = nose.suite._strclass(self.context)
86    else:                                   # outside of class, setUpModule etc.
87        class_repr = nose.suite._strclass(self.__class__)
88    return '%s.%s' % (class_repr, getattr(self.context, '__name__', self.context))
89
90nose.suite.ContextSuite.__repr__ = __suite_repr__
91nose.suite.ContextSuite.__str__  = __suite_repr__
92
93# /nose overrides
94
95def fatal(txt):
96    print(txt)
97    sys.exit(1)
98
99def check_trex_path(trex_path):
100    if os.path.isfile('%s/trex_daemon_server' % trex_path):
101        return os.path.abspath(trex_path)
102
103def check_setup_path(setup_path):
104    if os.path.isfile('%s/config.yaml' % setup_path):
105        return os.path.abspath(setup_path)
106
107
108def get_trex_path():
109    latest_build_path = check_trex_path(os.getenv('TREX_UNDER_TEST'))    # TREX_UNDER_TEST is env var pointing to <trex-core>/scripts
110    if not latest_build_path:
111        latest_build_path = check_trex_path(os.path.join(os.pardir, os.pardir))
112    if not latest_build_path:
113        fatal('Could not determine trex_under_test folder, try setting env.var. TREX_UNDER_TEST')
114    return latest_build_path
115
116
117def address_to_ip(address):
118    for i in range(5):
119        try:
120            return socket.gethostbyname(address)
121        except:
122            continue
123    return socket.gethostbyname(address)
124
125
126class TRexTee(object):
127    def __init__(self, encoding, *args):
128        self._encoding = encoding
129        self._streams = args
130
131    def write(self, data):
132        data = force_unicode(data, self._encoding)
133        for s in self._streams:
134            s.write(data)
135
136    def writelines(self, lines):
137        for line in lines:
138            self.write(line)
139
140    def flush(self):
141        for s in self._streams:
142            s.flush()
143
144    def isatty(self):
145        return False
146
147
148class CTRexTestConfiguringPlugin(Plugin):
149    encoding = 'UTF-8'
150
151    def __init__(self):
152        super(CTRexTestConfiguringPlugin, self).__init__()
153        self._capture_stack = []
154        self._currentStdout = None
155        self._currentStderr = None
156
157    def _timeTaken(self):
158        if hasattr(self, '_timer'):
159            taken = time.time() - self._timer
160        else:
161            # test died before it ran (probably error in setup())
162            # or success/failure added before test started probably
163            # due to custom TestResult munging
164            taken = 0.0
165        return taken
166
167    def _startCapture(self):
168        self._capture_stack.append((sys.stdout, sys.stderr))
169        self._currentStdout = StringIO()
170        self._currentStderr = StringIO()
171        sys.stdout = TRexTee(self.encoding, self._currentStdout, sys.stdout)
172        sys.stderr = TRexTee(self.encoding, self._currentStderr, sys.stderr)
173
174    def startContext(self, context):
175        self._startCapture()
176
177    def stopContext(self, context):
178        self._endCapture()
179
180    def beforeTest(self, test):
181        self._timer = time.time()
182        self._startCapture()
183
184    def _endCapture(self):
185        if self._capture_stack:
186            sys.stdout, sys.stderr = self._capture_stack.pop()
187
188    def afterTest(self, test):
189        self._endCapture()
190        self._currentStdout = None
191        self._currentStderr = None
192
193    def _getCapturedStdout(self):
194        if self._currentStdout:
195            value = self._currentStdout.getvalue()
196            if value:
197                return '<system-out><![CDATA[%s]]></system-out>' % escape_cdata(
198                        value)
199        return ''
200
201    def _getCapturedStderr(self):
202        if self._currentStderr:
203            value = self._currentStderr.getvalue()
204            if value:
205                return '<system-err><![CDATA[%s]]></system-err>' % escape_cdata(
206                        value)
207        return ''
208
209    def addError(self, test, err, capt=None):
210
211        taken = self._timeTaken()
212
213        if issubclass(err[0], SkipTest):
214            _type = 'SKIPPED'
215        else:
216            _type = 'ERROR'
217
218        tb = format_exception(err, self.encoding)
219        id = test.id()
220        err_msg=self._getCapturedStdout()+self._getCapturedStderr();
221        name=id_split(id)[-1]
222
223        elk = CTRexScenario.elk
224        if elk:
225            elk_obj = trex.copy_elk_info ()
226            elk_obj['test']={
227                       "name"   : name,
228                        "type"  : self.get_operation_mode (),
229                        "duration_sec"  : taken,
230                        "result" :  _type,
231                        "stdout" : err_msg,
232            };
233            #pprint(elk_obj['test']);
234            elk.reg.push_data(elk_obj)
235
236
237
238    def addFailure(self, test, err, capt=None, tb_info=None):
239        taken = self._timeTaken()
240        tb = format_exception(err, self.encoding)
241        id = test.id()
242        err_msg=self._getCapturedStdout()+self._getCapturedStderr();
243        name=id_split(id)[-1]
244
245        elk = CTRexScenario.elk
246        if elk:
247            elk_obj = trex.copy_elk_info ()
248            elk_obj['test']={
249                       "name"   : name,
250                        "type"  : self.get_operation_mode (),
251                        "duration_sec"  : taken,
252                        "result" :  "FAILURE",
253                        "stdout" : err_msg,
254            };
255            #pprint(elk_obj['test']);
256            elk.reg.push_data(elk_obj)
257
258
259
260    def addSuccess(self, test, capt=None):
261        taken = self._timeTaken()
262        id = test.id()
263        name=id_split(id)[-1]
264        elk = CTRexScenario.elk
265        if elk:
266            elk_obj = trex.copy_elk_info ()
267            elk_obj['test']={
268                       "name"   : name,
269                        "type"  : self.get_operation_mode (),
270                        "duration_sec"  : taken,
271                        "result" :  "PASS",
272                        "stdout" : "",
273            };
274            #pprint(elk_obj['test']);
275            elk.reg.push_data(elk_obj)
276
277
278
279    def get_operation_mode (self):
280        if self.stateful:
281            return('stateful');
282        return('stateless');
283
284
285
286
287##### option/configure
288
289    def options(self, parser, env = os.environ):
290        super(CTRexTestConfiguringPlugin, self).options(parser, env)
291        parser.add_option('--cfg', '--trex-scenario-config', action='store',
292                            dest='config_path',
293                            help='Specify path to folder with config.yaml and benchmark.yaml')
294        parser.add_option('--skip-clean', '--skip_clean', action='store_true',
295                            dest='skip_clean_config',
296                            help='Skip the clean configuration replace on the platform.')
297        parser.add_option('--load-image', '--load_image', action='store_true', default = False,
298                            dest='load_image',
299                            help='Install image specified in config file on router.')
300        parser.add_option('--log-path', '--log_path', action='store',
301                            dest='log_path',
302                            help='Specify path for the tests` log to be saved at. Once applied, logs capturing by nose will be disabled.') # Default is CURRENT/WORKING/PATH/trex_log/trex_log.log')
303        parser.add_option('--json-verbose', '--json_verbose', action="store_true", default = False,
304                            dest="json_verbose",
305                            help="Print JSON-RPC commands.")
306        parser.add_option('--telnet-verbose', '--telnet_verbose', action="store_true", default = False,
307                            dest="telnet_verbose",
308                            help="Print telnet commands and responces.")
309        parser.add_option('--server-logs', '--server_logs', action="store_true", default = False,
310                            dest="server_logs",
311                            help="Print server side (TRex and trex_daemon) logs per test.")
312        parser.add_option('--kill-running', '--kill_running', action="store_true", default = False,
313                            dest="kill_running",
314                            help="Kills running TRex process on remote server (useful for regression).")
315        parser.add_option('--func', '--functional', action="store_true", default = False,
316                            dest="functional",
317                            help="Run functional tests.")
318        parser.add_option('--stl', '--stateless', action="store_true", default = False,
319                            dest="stateless",
320                            help="Run stateless tests.")
321        parser.add_option('--stf', '--stateful', action="store_true", default = False,
322                            dest="stateful",
323                            help="Run stateful tests.")
324        parser.add_option('--pkg', type = str,
325                            help="Run with given TRex package. Make sure the path available at server machine. Implies --restart-daemon.")
326        parser.add_option('--restart-daemon', action="store_true", default = False,
327                            help="Flag that specifies to restart daemon. Implied by --pkg.")
328        parser.add_option('--collect', action="store_true", default = False,
329                            help="Alias to --collect-only.")
330        parser.add_option('--warmup', action="store_true", default = False,
331                            help="Warm up the system for stateful: run 30 seconds 9k imix test without check of results.")
332        parser.add_option('--test-client-package', '--test_client_package', action="store_true", default = False,
333                            help="Includes tests of client package.")
334        parser.add_option('--long', action="store_true", default = False,
335                            help="Flag of long tests (stability).")
336        parser.add_option('--ga', action="store_true", default = False,
337                            help="Flag to send benchmarks to GA.")
338        parser.add_option('--no-daemon', action="store_true", default = False,
339                            dest="no_daemon",
340                            help="Flag that specifies to use running stl server, no need daemons.")
341        parser.add_option('--debug-image', action="store_true", default = False,
342                            help="Flag that specifies to use t-rex-64-debug as TRex executable.")
343        parser.add_option('--trex-args', default = '',
344                            help="Additional TRex arguments (--no-watchdog etc.).")
345        parser.add_option('-t', '--test', type = str,
346                            help = 'Test name to run (without file, class etc.)')
347
348
349    def configure(self, options, conf):
350        self.collect_only   = options.collect_only
351        self.functional     = options.functional
352        self.stateless      = options.stateless
353        self.stateful       = options.stateful
354        self.pkg            = options.pkg
355        self.restart_daemon = options.restart_daemon
356        self.json_verbose   = options.json_verbose
357        self.telnet_verbose = options.telnet_verbose
358        self.no_daemon      = options.no_daemon
359        CTRexScenario.test  = options.test
360        if self.no_daemon and (self.pkg or self.restart_daemon):
361            fatal('You have specified both --no-daemon and either --pkg or --restart-daemon at same time.')
362        if self.no_daemon and self.stateful :
363            fatal("Can't run stateful without daemon.")
364        if self.collect_only or self.functional:
365            return
366        if CTRexScenario.setup_dir and options.config_path:
367            fatal('Please either define --cfg or use env. variable SETUP_DIR, not both.')
368        if not options.config_path and CTRexScenario.setup_dir:
369            options.config_path = CTRexScenario.setup_dir
370        if not options.config_path:
371            fatal('Please specify path to config.yaml using --cfg parameter or env. variable SETUP_DIR')
372        options.config_path = options.config_path.rstrip('/')
373        CTRexScenario.setup_name = os.path.basename(options.config_path)
374        self.configuration = misc_methods.load_complete_config_file(os.path.join(options.config_path, 'config.yaml'))
375        self.configuration.trex['trex_name'] = address_to_ip(self.configuration.trex['trex_name']) # translate hostname to ip
376        self.benchmark     = misc_methods.load_benchmark_config_file(os.path.join(options.config_path, 'benchmark.yaml'))
377        self.enabled       = True
378        self.modes         = self.configuration.trex.get('modes', [])
379        self.kill_running  = options.kill_running
380        self.load_image    = options.load_image
381        self.clean_config  = False if options.skip_clean_config else True
382        self.server_logs   = options.server_logs
383        if options.log_path:
384            self.loggerPath = options.log_path
385        # initialize CTRexScenario global testing class, to be used by all tests
386        CTRexScenario.configuration = self.configuration
387        CTRexScenario.no_daemon     = options.no_daemon
388        CTRexScenario.benchmark     = self.benchmark
389        CTRexScenario.modes         = set(self.modes)
390        CTRexScenario.server_logs   = self.server_logs
391        CTRexScenario.debug_image   = options.debug_image
392        CTRexScenario.json_verbose  = self.json_verbose
393        if not self.no_daemon:
394            CTRexScenario.trex      = CTRexClient(trex_host   = self.configuration.trex['trex_name'],
395                                                  verbose     = self.json_verbose,
396                                                  debug_image = options.debug_image,
397                                                  trex_args   = options.trex_args)
398
399        if self.pkg or self.restart_daemon:
400            if not CTRexScenario.trex.check_master_connectivity():
401                fatal('Could not connect to master daemon')
402        if options.ga and CTRexScenario.setup_name:
403            CTRexScenario.GAManager  = GAmanager_Regression(GoogleID         = 'UA-75220362-3',
404                                                            AnalyticsUserID  = CTRexScenario.setup_name,
405                                                            QueueSize        = 100,
406                                                            Timeout          = 3,  # seconds
407                                                            UserPermission   = 1,
408                                                            BlockingMode     = 0,
409                                                            appName          = 'TRex',
410                                                            appVer           = CTRexScenario.trex_version)
411
412            CTRexScenario.elk = trex_elk.TRexEs('sceasr-b20',9200);
413            self.set_cont_elk_info ()
414
415    def set_cont_elk_info (self):
416        elk_info={}
417        timestamp = datetime.datetime.now(); # need to update this
418        info = {};
419
420
421        img={}
422        img['sha'] = "v2.14"                #TBD
423        img['build_time'] = timestamp.strftime("%Y-%m-%d %H:%M:%S")
424        img['version'] = "v2.14"           #TBD need to fix
425        img['formal'] = False
426
427        setup={}
428
429        setup['distro']='None'            #TBD 'Ubunto14.03'
430        setup['kernel']='None'           #TBD '2.6.12'
431        setup['baremetal']=True          #TBD
432        setup['hypervisor']='None'       #TBD
433        setup['name']=CTRexScenario.setup_name
434
435        setup['cpu-sockets']=0           #TBD  2
436        setup['cores']=0                 #TBD 16
437        setup['cpu-speed']=-1            #TBD 3.5
438
439        setup['dut'] ='None'             #TBD 'loopback'
440        setup['drv-name']='None'         #TBD 'mlx5'
441        setup['nic-ports']=0             #TBD 2
442        setup['total-nic-ports']=0       #TBD 2
443        setup['nic-speed'] = "None"      #"40GbE" TBD
444
445
446
447        info['image'] = img
448        info['setup'] = setup
449
450        elk_info['info'] =info;
451
452        elk_info['timestamp']=timestamp.strftime("%Y-%m-%d %H:%M:%S")  # need to update it
453        elk_info['build_id']=os.environ.get('BUILD_ID')
454        elk_info['scenario']=os.environ.get('SCENARIO')
455
456        CTRexScenario.elk_info = elk_info
457
458
459    def begin (self):
460        client = CTRexScenario.trex
461        if self.pkg and not CTRexScenario.pkg_updated:
462            if client.master_daemon.is_trex_daemon_running() and client.get_trex_cmds() and not self.kill_running:
463                fatal("Can't update TRex, it's running. Consider adding --kill-running flag.")
464            print('Updating TRex to %s' % self.pkg)
465            if not client.master_daemon.update_trex(self.pkg):
466                fatal('Failed to update TRex.')
467            else:
468                print('Updated.')
469            CTRexScenario.pkg_updated = True
470        if self.functional or self.collect_only:
471            return
472        if self.pkg or self.restart_daemon:
473            print('Restarting TRex daemon server')
474            res = client.restart_trex_daemon()
475            if not res:
476                fatal('Could not restart TRex daemon server')
477            print('Restarted.')
478
479            if self.kill_running:
480                client.kill_all_trexes()
481            else:
482                if client.get_trex_cmds():
483                    fatal('TRex is already running')
484        if not self.no_daemon:
485            try:
486                client.check_server_connectivity()
487            except Exception as e:
488                fatal(e)
489
490
491        if 'loopback' not in self.modes:
492            CTRexScenario.router_cfg = dict(config_dict      = self.configuration.router,
493                                            forceImageReload = self.load_image,
494                                            silent_mode      = not self.telnet_verbose,
495                                            forceCleanConfig = self.clean_config,
496                                            tftp_config_dict = self.configuration.tftp)
497        try:
498            CustomLogger.setup_custom_logger('TRexLogger', self.loggerPath)
499        except AttributeError:
500            CustomLogger.setup_custom_logger('TRexLogger')
501
502    def finalize(self, result):
503        while self._capture_stack:
504            self._endCapture()
505
506        if self.functional or self.collect_only:
507            return
508        #CTRexScenario.is_init = False
509        if self.stateful:
510            CTRexScenario.trex = None
511        if self.stateless:
512            if self.no_daemon:
513                if CTRexScenario.stl_trex and CTRexScenario.stl_trex.is_connected():
514                    CTRexScenario.stl_trex.disconnect()
515            else:
516                CTRexScenario.trex.force_kill(False)
517            CTRexScenario.stl_trex = None
518
519
520def save_setup_info():
521    try:
522        if CTRexScenario.setup_name and CTRexScenario.trex_version:
523            setup_info = ''
524            for key, value in CTRexScenario.trex_version.items():
525                setup_info += '{0:8}: {1}\n'.format(key, value)
526            cfg = CTRexScenario.configuration
527            setup_info += 'Server: %s, Modes: %s' % (cfg.trex.get('trex_name'), cfg.trex.get('modes'))
528            if cfg.router:
529                setup_info += '\nRouter: Model: %s, Image: %s' % (cfg.router.get('model'), CTRexScenario.router_image)
530            if CTRexScenario.debug_image:
531                setup_info += '\nDebug image: %s' % CTRexScenario.debug_image
532
533            with open('%s/report_%s.info' % (CTRexScenario.report_dir, CTRexScenario.setup_name), 'w') as f:
534                f.write(setup_info)
535    except Exception as err:
536        print('Error saving setup info: %s ' % err)
537
538
539if __name__ == "__main__":
540
541    # setting defaults. By default we run all the test suite
542    specific_tests              = False
543    CTRexScenario.report_dir    = 'reports'
544    need_to_copy                = False
545    setup_dir                   = os.getenv('SETUP_DIR', '').rstrip('/')
546    CTRexScenario.setup_dir     = check_setup_path(setup_dir)
547    CTRexScenario.scripts_path  = get_trex_path()
548    if not CTRexScenario.setup_dir:
549        CTRexScenario.setup_dir = check_setup_path(os.path.join('setups', setup_dir))
550
551
552    nose_argv = ['', '-s', '-v', '--exe', '--rednose', '--detailed-errors']
553    test_client_package = False
554    if '--test-client-package' in sys.argv:
555        test_client_package = True
556
557    if '--collect' in sys.argv:
558        sys.argv.append('--collect-only')
559    if '--collect-only' in sys.argv: # this is a user trying simply to view the available tests. no need xunit.
560        CTRexScenario.is_test_list   = True
561        xml_arg                      = ''
562    else:
563        xml_name                     = 'unit_test.xml'
564        if CTRexScenario.setup_dir:
565            CTRexScenario.setup_name = os.path.basename(CTRexScenario.setup_dir)
566            xml_name = 'report_%s.xml' % CTRexScenario.setup_name
567        xml_arg= '--xunit-file=%s/%s' % (CTRexScenario.report_dir, xml_name)
568        mkpath(CTRexScenario.report_dir)
569
570    sys_args = sys.argv[:]
571    for i, arg in enumerate(sys.argv):
572        if 'log-path' in arg:
573            nose_argv += ['--nologcapture']
574        else:
575            for tests_type in CTRexScenario.test_types.keys():
576                if tests_type in arg:
577                    specific_tests = True
578                    CTRexScenario.test_types[tests_type].append(arg[arg.find(tests_type):])
579                    sys_args.remove(arg)
580
581    if not specific_tests:
582        for key in ('--func', '--functional'):
583            if key in sys_args:
584                CTRexScenario.test_types['functional_tests'].append('functional_tests')
585                sys_args.remove(key)
586        for key in ('--stf', '--stateful'):
587            if key in sys_args:
588                CTRexScenario.test_types['stateful_tests'].append('stateful_tests')
589                sys_args.remove(key)
590        for key in ('--stl', '--stateless'):
591            if key in sys_args:
592                CTRexScenario.test_types['stateless_tests'].append('stateless_tests')
593                sys_args.remove(key)
594        # Run all of the tests or just the selected ones
595        if not sum([len(x) for x in CTRexScenario.test_types.values()]):
596            for key in CTRexScenario.test_types.keys():
597                CTRexScenario.test_types[key].append(key)
598
599    nose_argv += sys_args
600
601    addplugins = [RedNose(), CTRexTestConfiguringPlugin()]
602    result = True
603    try:
604        if len(CTRexScenario.test_types['functional_tests']):
605            additional_args = ['--func'] + CTRexScenario.test_types['functional_tests']
606            if xml_arg:
607                additional_args += ['--with-xunit', xml_arg.replace('.xml', '_functional.xml')]
608            result = nose.run(argv = nose_argv + additional_args, addplugins = addplugins)
609        if len(CTRexScenario.test_types['stateful_tests']):
610            additional_args = ['--stf']
611            if '--warmup' in sys.argv:
612                additional_args.append('stateful_tests/trex_imix_test.py:CTRexIMIX_Test.test_warm_up')
613            additional_args += CTRexScenario.test_types['stateful_tests']
614            if not test_client_package:
615                additional_args.extend(['-a', '!client_package'])
616            if xml_arg:
617                additional_args += ['--with-xunit', xml_arg.replace('.xml', '_stateful.xml')]
618            result = nose.run(argv = nose_argv + additional_args, addplugins = addplugins) and result
619        if len(CTRexScenario.test_types['stateless_tests']):
620            additional_args = ['--stl', 'stateless_tests/stl_general_test.py:STLBasic_Test.test_connectivity'] + CTRexScenario.test_types['stateless_tests']
621            if not test_client_package:
622                additional_args.extend(['-a', '!client_package'])
623            if xml_arg:
624                additional_args += ['--with-xunit', xml_arg.replace('.xml', '_stateless.xml')]
625            result = nose.run(argv = nose_argv + additional_args, addplugins = addplugins) and result
626    #except Exception as e:
627    #    result = False
628    #    print(e)
629    finally:
630        save_setup_info()
631
632    if not CTRexScenario.is_test_list:
633        if result == True:
634            print(termstyle.green("""
635                 ..::''''::..
636               .;''        ``;.
637              ::    ::  ::    ::
638             ::     ::  ::     ::
639             ::     ::  ::     ::
640             :: .:' ::  :: `:. ::
641             ::  :          :  ::
642              :: `:.      .:' ::
643               `;..``::::''..;'
644                 ``::,,,,::''
645
646               ___  ___   __________
647              / _ \/ _ | / __/ __/ /
648             / ___/ __ |_\ \_\ \/_/
649            /_/  /_/ |_/___/___(_)
650
651        """))
652            sys.exit(0)
653        else:
654            print(termstyle.red("""
655           /\_/\
656          ( o.o )
657           > ^ <
658
659This cat is sad, test failed.
660        """))
661            sys.exit(-1)
662
663
664
665
666
667
668
669
670
671
672
673