interop_cli.py 34.8 KB
Newer Older
1 2 3
import os
import sys
import pika
4
import base64
5 6 7
import logging
import threading
import traceback
8

9
import click
10 11 12
from click_repl import ExitReplException
from click_repl import repl as repl_base
from prompt_toolkit.history import FileHistory
13

14 15 16
# for using it as library and as a __main__
try:
    from messages import *
17
    from event_bus_utils import AmqpListener
18 19 20
    from tabulate import tabulate
except:
    from .messages import *
21
    from .event_bus_utils import AmqpListener
22 23
    from .tabulate import tabulate

24 25
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING)

Federico Sismondi's avatar
Federico Sismondi committed
26 27
COMPONENT_ID = 'cli'

28 29 30 31 32 33 34 35 36 37
# click colors:: black (might gray) , red, green, yellow (might be an orange), blue, magenta, cyan, white (might gray)
COLOR_DEFAULT = 'white'
COLOR_SESSION_LOG = 'white'
COLOR_COORDINATION_MESSAGE = 'cyan'
COLOR_SESSION_ASSISTANCE = 'cyan'
COLOR_CHAT_MESSAGE = 'green'
COLOR_CHAT_MESSAGE_ECHO = 'green'
COLOR_ERROR_MESSAGE = 'red'
COLOR_TEST_SESSION_HELPER_MESSAGE = 'yellow'

38 39 40
# DIR used for network dumps and other type of tmp files
TEMP_DIR = 'tmp'

41 42 43 44 45 46 47 48 49 50 51
DEFAULT_TOPIC_SUBSCRIPTIONS = [
    # 'control.testcoordination',
    # 'control.dissection',
    # 'control.session',
    '#'
]

MESSAGE_TYPES_NOT_ECHOED = [
    MsgPacketInjectRaw,
]

Federico Sismondi's avatar
Federico Sismondi committed
52 53
CONNECTION_SETUP_RETRIES = 3

54 55 56 57 58 59 60 61 62 63
session_profile = OrderedDict(
    {
        'user_name': "Walter White",
        'protocol': "coap",
        'node': "both",
        'amqp_url': "amqp://{0}:{1}@{2}/{3}".format("guest", "guest", "localhost", "/"),
        'amqp_exchange': "amq.topic",
    }
)

64 65 66 67 68 69 70 71 72 73
# TODO handle the lock automatically with an state object
# def some_dummuy_release():
#     _echo_log_message('releasing!')
# def some_dummuy_acquire():
#     _echo_log_message('acquiring!')
# state_lock = Message()
# state_lock.__setattr__('release', some_dummuy_release)
# state_lock.__setattr__('acquire', some_dummuy_acquire)

state_lock = threading.RLock()
74 75 76 77 78 79 80 81 82
state = {
    'testcase_id': None,
    'step_id': None,
    'last_message': None,
    'suggested_cmd': None,
    'connection': None,
    'channel': None,
}
profile_choices = {
Federico Sismondi's avatar
Federico Sismondi committed
83 84
    'protocol': ['coap', '6lowpan', 'onem2m'],
    'node': ['coap_client', 'coap_server']
85 86
}

Federico Sismondi's avatar
Federico Sismondi committed
87
# e.g. MsgTestingToolConfigured is normally followed by a test suite start (ts_start)
88 89 90 91 92 93 94 95
UI_suggested_actions = {
    MsgTestingToolConfigured: 'ts_start',
    MsgTestCaseReady: 'tc_start',
    MsgStepStimuliExecute: 'stimuli',
    MsgStepVerifyExecute: 'verify',
}


Federico Sismondi's avatar
Federico Sismondi committed
96 97
def _init_action_suggested():
    state['suggested_cmd'] = 'ts_start'
98 99


100
def amqp_request(request_message, component_id=COMPONENT_ID):
Federico Sismondi's avatar
Federico Sismondi committed
101 102 103
    """
    NOTE: channel must be a pika channel
    """
104 105
    state_lock.acquire()
    channel = state['channel']
Federico Sismondi's avatar
Federico Sismondi committed
106 107 108 109 110 111 112 113 114 115 116 117 118
    amqp_exchange = session_profile['amqp_exchange']

    # check first that sender didnt forget about reply to and corr id
    assert request_message.reply_to
    assert request_message.correlation_id

    if amqp_exchange is None:
        amqp_exchange = 'amq.topic'

    response = None

    reply_queue_name = 'amqp_rpc_%s@%s' % (str(uuid.uuid4())[:8], component_id)

119
    try:
Federico Sismondi's avatar
Federico Sismondi committed
120

121
        result = channel.queue_declare(queue=reply_queue_name, auto_delete=True)
Federico Sismondi's avatar
Federico Sismondi committed
122

123
        callback_queue = result.method.queue
Federico Sismondi's avatar
Federico Sismondi committed
124

125 126 127 128 129 130
        # bind and listen to reply_to topic
        channel.queue_bind(
            exchange=amqp_exchange,
            queue=callback_queue,
            routing_key=request_message.reply_to
        )
Federico Sismondi's avatar
Federico Sismondi committed
131

132 133 134 135 136 137
        channel.basic_publish(
            exchange=amqp_exchange,
            routing_key=request_message.routing_key,
            properties=pika.BasicProperties(**request_message.get_properties()),
            body=request_message.to_json(),
        )
Federico Sismondi's avatar
Federico Sismondi committed
138

139 140
        time.sleep(0.2)
        retries_left = 5
Federico Sismondi's avatar
Federico Sismondi committed
141

142 143 144 145 146 147 148 149
        while retries_left > 0:
            time.sleep(0.5)
            method, props, body = channel.basic_get(reply_queue_name)
            if method:
                channel.basic_ack(method.delivery_tag)
                if hasattr(props, 'correlation_id') and props.correlation_id == request_message.correlation_id:
                    break
            retries_left -= 1
Federico Sismondi's avatar
Federico Sismondi committed
150

151
        if retries_left > 0:
Federico Sismondi's avatar
Federico Sismondi committed
152

153 154 155 156 157 158 159 160 161
            body_dict = json.loads(body.decode('utf-8'), object_pairs_hook=OrderedDict)
            response = MsgReply(request_message, **body_dict)

        else:
            raise Exception(
                "Response timeout! rkey: %s , request type: %s" % (
                    request_message.routing_key,
                    request_message._type
                )
Federico Sismondi's avatar
Federico Sismondi committed
162 163
            )

164 165 166
    finally:
        # clean up
        channel.queue_delete(reply_queue_name)
167
        state_lock.release()
Federico Sismondi's avatar
Federico Sismondi committed
168 169 170 171

    return response


172 173 174 175 176 177 178 179 180
def publish_message(message):
    if not _connection_ok():
        _echo_dispatcher('No connection established yet')
        return

    _echo_dispatcher('Sending message..')

    for i in range(1, 4):
        try:
181
            state_lock.acquire()
182 183 184 185 186 187 188 189 190 191 192 193 194
            state['channel'].basic_publish(
                exchange=session_profile['amqp_exchange'],
                routing_key=message.routing_key,
                properties=pika.BasicProperties(**message.get_properties()),
                body=message.to_json(),
            )
            break

        except pika.exceptions.ConnectionClosed as err:
            _echo_error(err)
            _echo_error('Unexpected connection closed, retrying %s/%s' % (i, 4))
            _set_up_connection()

195 196 197
        finally:
            state_lock.acquire()

198 199 200 201 202 203

@click.group()
def cli():
    pass


204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
@cli.command()
def repl():
    """
    Interactive shell
    """
    prompt_kwargs = {
        'history': FileHistory('tmp/myrepl-history'),
    }

    _echo_welcome_message()
    _pre_configuration()
    _echo_session_helper("\nYou can press TAB for the available commands at any time \n")

    _echo_session_helper("\nThe command <action [param]> needs to be used for executing the test actions\n")
    _echo_session_helper(
        "\nNote that <action suggested> will help you navigate through the session by executing the "
        "actions the backend normally expects for a standard session flow :)\n")
    _init_action_suggested()

    _set_up_connection()

    repl_base(click.get_current_context(), prompt_kwargs=prompt_kwargs)


228
@cli.command()
229 230 231 232 233
@click.option('-ll', '--lazy-listener',
              is_flag=True,
              default=False,
              help="lazy-listener doest perform convertion to Messages class")
def connect(lazy_listener):
234 235 236
    """
    Connect to an AMQP session and start consuming messages
    """
237
    _set_up_connection(lazy_listener=lazy_listener)
238 239 240 241 242 243 244 245 246 247


@cli.command()
def exit():
    """
    Exit test CLI

    """
    _exit()

248

249 250 251 252 253 254 255 256 257 258 259 260 261 262
@cli.command()
def download_network_traces():
    """
    Downloads all networks traces generated during the session
    """
    global state

    _handle_get_testcase_list()
    ls = state['tc_list'].copy()

    for tc_item in ls:
        try:
            tc_id = tc_item['testcase_id']
            msg = MsgSniffingGetCapture(capture_id=tc_id)
263
            response = amqp_request(msg, COMPONENT_ID)
264 265 266 267 268 269 270 271 272 273 274

            if response.ok:
                save_pcap_from_base64(response.filename, response.value, TEMP_DIR)
                _echo_input("downloaded network trace %s , into dir: %s" % (response.filename, TEMP_DIR))
            else:
                raise Exception(response.error_message)

        except Exception as e:
            _echo_error('Error trying to download network traces for testcase : %s' % tc_id)
            _echo_error(e)

275 276 277 278 279 280 281 282 283 284

@cli.command()
def clear():
    """
    Clear screen
    """

    click.clear()


Federico Sismondi's avatar
Federico Sismondi committed
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
def _handle_testcase_select():
    #  requires testing tool to implement GetTestCases feature see MsgTestSuiteGetTestCases
    _handle_get_testcase_list()
    ls = state['tc_list'].copy()

    i = 1
    for tc_item in ls:
        _echo_dispatcher("%s -> %s" % (i, tc_item['testcase_id']))
        i += 1

    resp = click.prompt('Select number of test case to execute from list', type=int)

    try:
        _echo_input("entered %s, corresponding to %s" % (resp, ls[resp - 1]['testcase_id']))
    except Exception as e:
        _echo_error("wrong input \n %s" % e)
        return

    msg = MsgTestCaseSelect(
        testcase_id=ls[resp - 1]['testcase_id']
    )

    publish_message(msg)


def _handle_get_testcase_list():
Federico Sismondi's avatar
Federico Sismondi committed
311
    #  requires testing tool to implement GetTestCases feature, see MsgTestSuiteGetTestCases
Federico Sismondi's avatar
Federico Sismondi committed
312
    if _connection_ok():
313

Federico Sismondi's avatar
Federico Sismondi committed
314 315
        request_message = MsgTestSuiteGetTestCases()

Federico Sismondi's avatar
Federico Sismondi committed
316
        try:
317
            testcases_list_reponse = amqp_request(request_message, COMPONENT_ID)
Federico Sismondi's avatar
Federico Sismondi committed
318 319 320 321 322
        except Exception as e:
            _echo_error('Is testing tool up?')
            _echo_error(e)
            return

Federico Sismondi's avatar
Federico Sismondi committed
323 324 325 326
        try:
            state['tc_list'] = testcases_list_reponse.tc_list
        except Exception as e:
            _echo_error(e)
Federico Sismondi's avatar
Federico Sismondi committed
327
            return
Federico Sismondi's avatar
Federico Sismondi committed
328 329 330 331 332

        _echo_list_of_dicts_as_table(state['tc_list'])
    else:
        _echo_error('No connection established')

333

334 335 336 337 338 339 340 341
def _handle_action_testsuite_start():
    if click.confirm('Do you want START test suite?'):
        msg = MsgTestSuiteStart()
        publish_message(msg)


def _handle_action_testcase_start():
    if click.confirm('Do you want START test case?'):
342
        msg = MsgTestCaseStart()  # TODO no testcase id input?
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
        publish_message(msg)


def _handle_action_testsuite_abort():
    if click.confirm('Do you want ABORT test suite?'):
        msg = MsgTestSuiteAbort()
        publish_message(msg)


def _handle_action_testcase_skip():
    if click.confirm('Do you want SKIP current test case?'):
        msg = MsgTestCaseSkip()
        publish_message(msg)


def _handle_action_testcase_restart():
    if click.confirm('Do you want RESTART current test case?'):
        msg = MsgTestCaseRestart()
        publish_message(msg)


def _handle_action_stimuli():
    if isinstance(state['last_message'], MsgStepStimuliExecute):
        _echo_session_helper(list_to_str(state['last_message'].description))

    resp = click.confirm('Did you execute last STIMULI step (if any received)?')

    if resp:
        msg = MsgStepStimuliExecuted(
            node=session_profile['node'],
            node_execution_mode="user_assisted"
        )
        publish_message(msg)

    else:
        _echo_error('Please execute all pending STIMULI steps')


def _handle_action_verify():
    if isinstance(state['last_message'], MsgStepVerifyExecute):
        _echo_session_helper(list_to_str(state['last_message'].description))

    resp = click.prompt("Last verify step was <ok> or not <nok>", type=click.Choice(['ok', 'nok']))

    msg = MsgStepVerifyExecuted(
        response_type="bool",
        verify_response=True if resp == 'ok' else False,
        node=session_profile['node'],
        node_execution_mode="user_assisted"
    )

    publish_message(msg)


message_handles_options = {'ts_start': _handle_action_testsuite_start,
                           'ts_abort': _handle_action_testsuite_abort,
                           'tc_start': _handle_action_testcase_start,
                           'tc_restart': _handle_action_testcase_restart,
                           'tc_skip': _handle_action_testcase_skip,
Federico Sismondi's avatar
Federico Sismondi committed
402 403
                           'tc_list': _handle_get_testcase_list,
                           'tc_select': _handle_testcase_select,
404 405 406 407 408 409 410 411 412 413
                           'verify': _handle_action_verify,
                           'stimuli': _handle_action_stimuli,
                           'suggested': None,
                           }


@cli.command()
@click.argument('api_call', type=click.Choice(message_handles_options.keys()))
def action(api_call):
    """
414
    Execute interop test action
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
    """

    _echo_input(api_call)

    if not _connection_ok():
        _echo_dispatcher('No connection established yet')
        return

    if api_call == 'suggested':
        if state['suggested_cmd']:
            _echo_dispatcher("Executing : %s" % state['suggested_cmd'])
            message_handles_options[state['suggested_cmd']]()
            state['suggested_cmd'] = None
            return
        else:
            _echo_error('No suggested message yet.')
            return

    elif api_call in message_handles_options:
        func = message_handles_options[api_call]
        func()

    else:
        _echo_dispatcher('Command <action %s> not accepted' % api_call)


@cli.command()
@click.argument('message_type', type=click.Choice(['dissections']))
def ignore(message_type):
    """
    Do not notify any more on message type
    """
    message_types = {
        'dissections': [MsgDissectionAutoDissect],
Federico Sismondi's avatar
Federico Sismondi committed
449
        'packets': [MsgPacketSniffedRaw, MsgPacketInjectRaw]
450 451 452 453 454 455 456 457
    }

    if message_type in message_types:
        for item in message_types[message_type]:
            _add_to_ignore_message_list(item)
            _echo_dispatcher('Ignore message category %s: (%s)' % (message_type, str(item)))


458 459 460 461 462 463 464 465
@cli.command()
def enter_debug_context():
    """
    Provides user with some extra debugging commands

    """
    global message_handles_options

466 467
    # TODO group cmds
    @cli.command()
468
    def _sniffer_start():
469 470 471 472 473 474
        """
        Sniffer start
        """
        _snif_start()

    @cli.command()
475
    def _sniffer_stop():
476 477 478 479 480 481
        """
        Sniffer stop
        """
        _snif_start()

    @cli.command()
482
    def _sniffer_get_last_capture():
483 484 485 486 487
        """
        Sniffer get last capture
        """
        _snif_get_last_capture()

488 489 490 491 492 493
    @cli.command()
    def _configure_perf_tt():
        """
        Send example configuration message for perf TT
        """
        _send_configuration_default_message_for_performance_testsuite()
494

495 496 497 498 499 500 501 502 503 504 505 506 507 508
    @cli.command()
    def _configure_comi_tt():
        """
        Send example configuration message for CoMI TT
        """
        _send_configuration_default_message_for_comi_testsuite()

    @cli.command()
    def _configure_coap_tt():
        """
        Send example configuration message for CoAP TT
        """
        _send_configuration_default_message_for_coap_testsuite()

509 510 511 512 513 514 515
    @cli.command()
    def _get_session_configuration_from_ui():
        """
        Get session config from UI
        """
        _get_session_configuration()

516 517
    @cli.command()
    @click.argument('testcase_id')
518
    def _testcase_skip(testcase_id):
519
        """
520
        Skip a particular testcase
521 522 523 524 525
        """
        _tescase_skip(testcase_id)

    @cli.command()
    @click.argument('text')
526
    def _ui_display_markdown_text(text):
527
        """
528
        Send message to GUI
529 530 531
        """
        _ui_send_markdown_display(text)

532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
    @cli.command()
    @click.argument('text')
    def _ui_send_confirmation_button(text):
        """
        Send button to GUI
        """
        _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)

        msg = MsgUiRequestConfirmationButton()

        _echo_input(text)

        msg.fields = [{
            "name": text,
            "type": "button",
            "value": True
        }]

        publish_message(msg)

552
    _echo_session_helper("Entering debugger context, added extra CMDs, please type --help for more info")
553 554


555 556 557 558
@cli.command()
@click.argument('message', nargs=-1)
def chat(message):
    """
559
    Send chat message, useful for user-to-user test sessions
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
    """

    if not _connection_ok():
        _echo_dispatcher('No connection established yet')
        return

    m = ''

    for word in message:
        m += " %s" % word

    c = MsgSessionChat(description=m,
                       user_name=session_profile['user_name'],
                       iut_node=session_profile['node'])
    publish_message(c)


@cli.command()
def check_connection():
    """
    Check if AMQP connection is active
    """
    conn_ok = _connection_ok()
    _echo_dispatcher('connection is %s' % 'OK' if conn_ok else 'not OK')
    return conn_ok

586

587 588 589 590 591 592 593 594 595 596 597
@cli.command()
def get_session_status():
    """
    Retrieves status information from testing tool
    """

    #  requires testing tool to implement GetStatus feature, see MsgTestSuiteGetStatus
    if _connection_ok():
        request_message = MsgTestSuiteGetStatus()

        try:
598
            status_resp = amqp_request(request_message, COMPONENT_ID)
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
        except Exception as e:
            _echo_error('Is testing tool up?')
            _echo_error(e)
            return

        resp = status_resp.to_dict()
        tc_states = resp['tc_list']
        del resp['tc_list']

        # print general states
        _echo_dict_as_table(resp)

        list = []
        list.append(('testcase id', 'testcase ref', 'testcase status'))
        for tc in tc_states:
            if tc:
                val1, val2, val3, _, _, _ = tc.values()
                list.append((val1, val2, val3))
        # print tc states
        _echo_list_as_table(list, first_row_is_header=True)

    else:
        _echo_error('No connection established')

623 624

@cli.command()
625
def get_session_parameters():
626 627 628 629 630 631 632 633 634 635
    """
    Print session state and parameters
    """

    _echo_context()


def _connection_ok():
    conn_ok = False
    try:
636
        conn_ok = state['connection'] is not None and state['connection'].is_open
637 638 639 640 641 642 643 644 645 646
    except AttributeError as ae:
        pass
    except TypeError as ae:
        pass

    return conn_ok


def _echo_context():
    table = []
647 648 649 650 651
    d = {}
    d.update(session_profile)
    d.update(state)
    for key, val in d.items():
        table.append((key, list_to_str(str(val))))
652 653 654
    _echo_list_as_table(table)


655
def _set_up_connection(lazy_listener=False):
656 657
    # conn for repl publisher
    try:
Federico Sismondi's avatar
Federico Sismondi committed
658
        retries_left = CONNECTION_SETUP_RETRIES
659
        state_lock.acquire()
Federico Sismondi's avatar
Federico Sismondi committed
660 661
        while retries_left > 0:
            try:
662

Federico Sismondi's avatar
Federico Sismondi committed
663 664 665 666 667
                state['connection'] = pika.BlockingConnection(pika.URLParameters(session_profile['amqp_url']))
                state['channel'] = state['connection'].channel()
                break
            except pika.exceptions.ConnectionClosed:
                retries_left -= 1
668 669
                _echo_session_helper("Couldnt establish connection, retrying .. %s/%s " % (
                    CONNECTION_SETUP_RETRIES - retries_left, CONNECTION_SETUP_RETRIES))
Federico Sismondi's avatar
Federico Sismondi committed
670

671 672 673 674 675 676
    except pika.exceptions.ProbableAccessDeniedError:
        _echo_error('Probable access denied error. Is provided AMQP_URL correct?')
        state['connection'] = None
        state['channel'] = None
        return

677 678 679
    finally:
        state_lock.release()

680 681 682 683 684 685 686 687 688 689
    # note we have a separate conn for amqp listener (each pika threads needs a different connection)
    if 'amqp_listener_thread' in state and state['amqp_listener_thread'] is not None:
        _echo_log_message('stopping amqp listener thread')
        th = state['amqp_listener_thread']
        th.stop()
        th.join(2)
        if th.isAlive():
            _echo_log_message('amqp listener thread doesnt want to stop, lets terminate it..')
            th.terminate()

690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705
    if lazy_listener:
        amqp_listener_thread = AmqpListener(
            amqp_url=session_profile['amqp_url'],
            amqp_exchange=session_profile['amqp_exchange'],
            callback=None,
            topics=DEFAULT_TOPIC_SUBSCRIPTIONS,
            use_message_typing=False,
        )
    else:
        amqp_listener_thread = AmqpListener(
            amqp_url=session_profile['amqp_url'],
            amqp_exchange=session_profile['amqp_exchange'],
            callback=_message_handler,
            topics=DEFAULT_TOPIC_SUBSCRIPTIONS,
            use_message_typing=True,
        )
Federico Sismondi's avatar
Federico Sismondi committed
706

707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783
    amqp_listener_thread.start()
    state['amqp_listener_thread'] = amqp_listener_thread


def _pre_configuration():
    global session_profile

    for key, _ in session_profile.items():
        if key in profile_choices.keys():
            selection_type = click.Choice(profile_choices[key])
        else:
            selection_type = str

        value = click.prompt('Please type %s ' % key,
                             type=selection_type,
                             default=session_profile[key])
        _echo_input(value)
        session_profile.update({key: value})


def _add_to_ignore_message_list(msg_type):
    global MESSAGE_TYPES_NOT_ECHOED
    if msg_type.__name__ in globals():
        MESSAGE_TYPES_NOT_ECHOED.append(msg_type)


def _message_handler(msg):
    global state
    """
    This method first prints message into user interface then evaluates if there's any associated action to message.
    :param msg:
    :return:
    """

    if type(msg) in MESSAGE_TYPES_NOT_ECHOED:
        pass  # do not echo
    else:
        # echo
        _echo_dispatcher(msg)

    # process message
    if isinstance(msg, Message):
        state['last_message'] = msg
        if type(msg) in UI_suggested_actions:
            state['suggested_cmd'] = UI_suggested_actions[type(msg)]
            _echo_session_helper(
                'Suggested following action to execute: <action %s> or or <action suggested>' % state['suggested_cmd'])

    elif isinstance(msg, MsgTestCaseVerdict):
        #  Save verdict
        json_file = os.path.join(
            TEMP_DIR,
            msg.testcase_id + '_verdict.json'
        )
        with open(json_file, 'w') as f:
            f.write(msg.to_json())

    elif isinstance(msg, (MsgStepStimuliExecute, MsgStepVerifyExecute)):
        state['step_id'] = msg.step_id

    elif isinstance(msg, MsgTestCaseReady):
        state['testcase_id'] = msg.testcase_id


def _exit():
    _quit_callback()

    if 'amqp_listener_thread' in state and state['amqp_listener_thread'] is not None:
        state['amqp_listener_thread'].stop()
        state['amqp_listener_thread'].join()

    if 'connection' in state and state['connection'] is not None:
        state['connection'].close()

    raise ExitReplException()


Federico Sismondi's avatar
Federico Sismondi committed
784 785 786
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# UI echo functions

787 788 789
def _echo_welcome_message():
    m = """
    Welcome to F-Interop platform!
Federico Sismondi's avatar
Federico Sismondi committed
790 791 792 793 794 795 796 797 798 799
    The Test assistant will help you go through the interoperability session (messages in cyan).

    """
    _echo_session_helper(m)

    m = """
    *********************************************************************************
    *   If you experience any problems, or you have any suggestions or feedback     *
    *   don't hesitate to drop me an email at:  federico.sismondi@inria.fr          *
    *********************************************************************************
800 801 802 803 804 805 806 807 808 809 810 811 812 813
    """
    _echo_session_helper(m)


def _echo_dispatcher(msg):
    """
    :param msg: String, dict or Message object
    :return: echoes using click API
    """

    if type(msg) is str:
        click.echo(click.style(msg, fg=COLOR_DEFAULT))
    elif isinstance(msg, MsgSessionLog):
        _echo_log_message(msg)
Federico Sismondi's avatar
Federico Sismondi committed
814 815
    elif isinstance(msg, MsgPacketSniffedRaw):
        _echo_data_message(msg)
816 817
    elif isinstance(msg, MsgSessionChat):
        _echo_chat_message(msg)
818 819 820
    elif isinstance(msg, MsgSessionConfiguration):
        # fixme hanlde extremly long fields in a more generic way
        msg.configuration = ['...ommited fields...']  # this fields is normally monstrously big
821 822 823
        _echo_backend_message(msg)
    elif isinstance(msg, dict):
        click.echo(click.style(repr(msg), fg=COLOR_DEFAULT))
824 825 826 827 828 829
    elif isinstance(msg, (MsgUiDisplay, MsgUiDisplayMarkdownText, MsgUiRequestConfirmationButton)):
        _echo_gui_message(msg)

    # default echo for objects of Message type
    elif isinstance(msg, Message):
        _echo_backend_message(msg)
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
    else:
        click.echo(click.style(msg, fg=COLOR_DEFAULT))


def _quit_callback():
    click.echo(click.style('Quitting!', fg=COLOR_ERROR_MESSAGE))


def _echo_backend_message(msg):
    assert isinstance(msg, Message)

    try:
        m = "\n[Session message] [%s] " % msg._type
        if hasattr(m, 'description'):
            m += m.description

        click.echo(click.style(m, fg=COLOR_TEST_SESSION_HELPER_MESSAGE))

    except AttributeError as err:
        _echo_error(err)

    if isinstance(msg, MsgTestCaseReady):
        pass

    elif isinstance(msg, MsgDissectionAutoDissect):
        _echo_frames_as_table(msg.frames)
        return

    elif isinstance(msg, MsgTestCaseVerdict):
859 860 861 862 863 864 865
        verdict = msg.to_dict()
        partial_verdict = verdict.pop('partial_verdicts')

        _echo_dict_as_table(verdict)
        click.echo()

        if partial_verdict:
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901
            click.echo(click.style("Partial verdicts:", fg=COLOR_TEST_SESSION_HELPER_MESSAGE))
            _echo_testcase_partial_verdicts_as_table(msg.partial_verdicts)
        return

    elif isinstance(msg, MsgTestSuiteReport):
        _echo_report_as_table(msg.to_dict())
        return

    elif isinstance(msg, MsgTestingToolComponentReady):
        pass

    elif isinstance(msg, MsgTestingToolComponentShutdown):
        pass

    _echo_dict_as_table(msg.to_dict())


def _echo_testcase_partial_verdicts_as_table(pvs):
    assert type(pvs) is list

    table = []
    table.append(('Step ID', 'Partial verdict', 'Description'))
    for item in pvs:
        try:
            assert type(item) is list
            cell_1 = item.pop(0)
            cell_2 = item.pop(0)
            cell_3 = list_to_str(item)
            table.append((cell_1, cell_2, cell_3))
        except Exception as e:

            _echo_error(e)
            _echo_error(traceback.format_exc())

    click.echo(click.style(tabulate(table, headers="firstrow"), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))

Federico Sismondi's avatar
Federico Sismondi committed
902

Federico Sismondi's avatar
Federico Sismondi committed
903 904 905 906
def _echo_list_of_dicts_as_table(l):
    try:

        assert type(l) is list
907

Federico Sismondi's avatar
Federico Sismondi committed
908 909 910 911 912 913 914 915 916 917
        table = []
        first = True

        for d in l:  # for each dict obj in the list
            if d:
                if first:  # adds table header , we assume all dicts have same keys
                    first = False
                    table.append(tuple(d.keys()))
                table.append(tuple(d.values()))

918
        _echo_list_as_table(table, first_row_is_header=True)
919

Federico Sismondi's avatar
Federico Sismondi committed
920 921
    except Exception as e:
        _echo_error('wrong frame format passed?')
Federico Sismondi's avatar
Federico Sismondi committed
922 923
        if l:
            _echo_error(l)
Federico Sismondi's avatar
Federico Sismondi committed
924 925
        _echo_error(e)
        _echo_error(traceback.format_exc())
926

Federico Sismondi's avatar
Federico Sismondi committed
927

Federico Sismondi's avatar
Federico Sismondi committed
928
def _echo_report_as_table(report_dict):
929 930
    try:

Federico Sismondi's avatar
Federico Sismondi committed
931
        assert type(report_dict) is dict
932 933 934 935 936

        testcases = [(k, v) for k, v in report_dict.items() if k.lower().startswith('td')]

        for tc_name, tc_report in testcases:
            table = []
Federico Sismondi's avatar
Federico Sismondi committed
937 938 939 940 941 942 943 944 945 946 947 948
            if tc_report:
                table.append(("Testcase ID", 'Final verdict', 'Description'))
                table.append((tc_name, tc_report['verdict'], tc_report['description']))

                # testcase report
                click.echo()
                click.echo(click.style(tabulate(table, headers="firstrow"), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))
                click.echo()
                _echo_testcase_partial_verdicts_as_table(tc_report['partial_verdicts'])
                click.echo()
            else:
                _echo_error('No report for testcase %s ' % tc_name)
949 950 951 952 953

    except Exception as e:
        _echo_error('wrong frame format passed?')
        _echo_error(e)
        _echo_error(traceback.format_exc())
Federico Sismondi's avatar
Federico Sismondi committed
954
        _echo_error(json.dumps(report_dict))
955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988


def _echo_frames_as_table(frames: list):
    assert type(frames) is list

    try:
        for frame in frames:
            table = []
            assert type(frame) is dict
            table.append(('frame id', frame['id']))
            table.append(('frame timestamp', frame['timestamp']))
            table.append(('frame error', frame['error']))

            # frame header print
            click.echo()  # new line
            click.echo(click.style(tabulate(table), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))

            # print one table per layer
            for layer_as_dict in frame['protocol_stack']:
                assert type(layer_as_dict) is dict
                table = []
                for key, value in layer_as_dict.items():
                    temp = [key, value]
                    table.append(temp)
                click.echo(click.style(tabulate(table), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))

            click.echo()  # new line

    except Exception as e:
        _echo_error('wrong frame format passed?')
        _echo_error(e)
        _echo_error(traceback.format_exc())


Federico Sismondi's avatar
Federico Sismondi committed
989 990 991 992 993 994 995 996 997 998 999 1000 1001
def _echo_list_as_table(ls: list, first_row_is_header=False):
    list_flat_items = []
    assert type(ls) is list

    for row in ls:
        assert type(row) is not str
        list_flat_items.append(tuple(list_to_str(item) for item in row))

    if first_row_is_header:
        click.echo(click.style(tabulate(list_flat_items, headers="firstrow"), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))
    else:
        click.echo(click.style(tabulate(list_flat_items), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))

Federico Sismondi's avatar
Federico Sismondi committed
1002
    click.echo()  # new line
1003 1004 1005 1006 1007


def _echo_dict_as_table(d: dict):
    table = []
    for key, value in d.items():
Federico Sismondi's avatar
Federico Sismondi committed
1008 1009 1010 1011
        if type(value) is list:
            temp = [key, list_to_str(value)]
        else:
            temp = [key, value]
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
        table.append(temp)

    click.echo()  # new line
    click.echo(click.style(tabulate(table), fg=COLOR_TEST_SESSION_HELPER_MESSAGE))


def _echo_session_helper(msg: str):
    click.echo(click.style('[Test Assistant] %s' % msg, fg=COLOR_SESSION_ASSISTANCE))


def _echo_input(msg):
    click.echo(click.style('[User input] %s' % msg, fg=COLOR_DEFAULT))


def _echo_error(msg):
    click.echo(click.style('[Error] %s' % msg, fg=COLOR_ERROR_MESSAGE))


def _echo_chat_message(msg: MsgSessionChat):
    if msg.iut_node == session_profile['node']:  # it's echo message
        click.echo(click.style('[Chat message sent] %s' % list_to_str(msg.description), fg=COLOR_CHAT_MESSAGE_ECHO))
    else:
        click.echo(click.style('[Chat message from %s] %s' % (msg.user_name, list_to_str(msg.description)),
                               fg=COLOR_CHAT_MESSAGE))


Federico Sismondi's avatar
Federico Sismondi committed
1038 1039 1040 1041 1042 1043 1044 1045
def _echo_data_message(msg):
    assert isinstance(msg, (MsgPacketInjectRaw, MsgPacketSniffedRaw))
    click.echo(click.style(
        '[agent] Packet captured on %s. Routing key: %s' % (msg.interface_name, msg.routing_key),
        fg=COLOR_SESSION_LOG)
    )


1046 1047
def _echo_gui_message(msg):
    click.echo(
1048 1049 1050 1051 1052 1053
        click.style("[UI message]\n\tMessage: %s \n\ttags: %s\n\tFields: %s \n\tR_key: %s \n\tcorr_id: %s" %
                    (
                        repr(msg)[:70],
                        str(msg.tags),
                        str(msg.fields)[:70],
                        msg.routing_key,
1054
                        msg.correlation_id if hasattr(msg, 'correlation_id') else ''
1055 1056
                    ),
                    fg=COLOR_SESSION_LOG))
1057 1058


1059 1060
def _echo_log_message(msg):
    if isinstance(msg, MsgSessionLog):
Federico Sismondi's avatar
Federico Sismondi committed
1061
        click.echo(click.style("[log][%s] %s" % (msg.component, list_to_str(msg.message)), fg=COLOR_SESSION_LOG))
1062
    else:
Federico Sismondi's avatar
Federico Sismondi committed
1063
        click.echo(click.style("[%s] %s" % ('log', list_to_str(msg)), fg=COLOR_SESSION_LOG))
1064 1065


1066
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1067
# debugging actions
1068

1069 1070
def _tescase_skip(testcase_id):
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
1071 1072

    msg = MsgTestCaseSkip(
1073
        testcase_id=testcase_id
1074 1075 1076 1077
    )
    publish_message(msg)


1078
def _snif_start(testcase_id=None):
1079 1080 1081 1082 1083
    if testcase_id is None:
        testcase_id = 'PCAP_TEST'

    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
    msg = MsgSniffingStart(capture_id=testcase_id,
1084 1085 1086 1087 1088
                           filter_if='tun0',
                           filter_proto='udp')
    publish_message(msg)


1089 1090
def _snif_stop():
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
1091 1092 1093 1094
    msg = MsgSniffingStop()
    publish_message(msg)


1095 1096
def _snif_get_last_capture():
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
1097 1098 1099 1100
    msg = MsgSniffingGetCaptureLast()
    publish_message(msg)


1101 1102 1103
def _send_configuration_default_message_for_performance_testsuite():
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
    from message_examples import PERF_TT_CONFIGURATION
1104
    message = MsgSessionConfiguration(**PERF_TT_CONFIGURATION)  # builds a config for the perf TT
1105 1106 1107
    publish_message(message)


1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
def _send_configuration_default_message_for_comi_testsuite():
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
    from message_examples import COMI_TT_CONFIGURATION
    message = MsgSessionConfiguration(**COMI_TT_CONFIGURATION)  # builds a config message
    publish_message(message)


def _send_configuration_default_message_for_coap_testsuite():
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
    from message_examples import COAP_TT_CONFIGURATION
    message = MsgSessionConfiguration(**COAP_TT_CONFIGURATION)  # builds a config message
    publish_message(message)


1122 1123 1124 1125 1126 1127
def _get_session_configuration():
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)
    req = MsgUiRequestSessionConfiguration()
    publish_message(req)


1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
def _ui_send_confirmation_button(text=None):
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)

    msg = MsgUiRequestConfirmationButton()

    _echo_input(text)

    msg.fields = [{
        "name": text,
        "type": "button",
        "value": True
    }]

    publish_message(msg)


1144 1145 1146 1147 1148
def _ui_send_markdown_display(text=None):
    _echo_input("Executing debug message %s" % sys._getframe().f_code.co_name)

    msg = MsgUiDisplayMarkdownText()

Federico Sismondi's avatar
Federico Sismondi committed
1149
    _echo_input(text)
1150

1151
    if text:
1152 1153 1154 1155 1156 1157 1158
        fields = [
            {
                'type': 'p',
                'value': text
            }
        ]
        msg.fields = fields
1159 1160 1161

    publish_message(msg)

1162

1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# some auxiliary functions


def list_to_str(ls):
    """
    flattens a nested list up to two levels of depth

    :param ls: the list, supports str also
    :return: single string with all the items inside the list
    """

    ret = ''

Federico Sismondi's avatar
Federico Sismondi committed
1177 1178 1179
    if ls is None:
        return 'None'

1180 1181 1182
    if type(ls) is str:
        return ls

1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198
    try:
        for l in ls:
            if l and isinstance(l, list):
                for sub_l in l:
                    if sub_l and not isinstance(sub_l, list):
                        ret += str(sub_l) + ' \n '
                    else:
                        # I truncate in the second level
                        pass
            else:
                ret += str(l) + ' \n '

    except TypeError as e:
        _echo_error(e)
        return str(ls)

1199 1200 1201
    return ret


1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220
def save_pcap_from_base64(filename, pcap_file_base64, dir=None):
    """
    Returns number of bytes saved.

    :param filename:
    :param pcap_file_base64:
    :return:
    """

    if dir:
        file_path = os.path.join(dir, filename)
    else:
        file_path = os.path.join(os.getcwd(), filename)

    with open(file_path, "wb") as pcap_file:
        nb = pcap_file.write(base64.b64decode(pcap_file_base64))
        return nb


1221 1222 1223 1224 1225 1226 1227 1228
if __name__ == "__main__":

    try:
        session_profile.update({'amqp_exchange': str(os.environ['AMQP_EXCHANGE'])})
    except KeyError as e:
        pass  # use default

    try:
1229
        # url = str(os.environ['AMQP_URL'])
1230 1231

        url = '%s?%s&%s&%s&%s&%s' % (
Federico Sismondi's avatar
Federico Sismondi committed
1232 1233
            str(os.environ['AMQP_URL']),
            "heartbeat_interval=600",
1234 1235 1236 1237
            "blocked_connection_timeout=300",
            "retry_delay=1",
            "socket_timeout=1",
            "connection_attempts=3"
Federico Sismondi's avatar
Federico Sismondi committed
1238 1239
        )
        session_profile.update({'amqp_url': url})
1240 1241 1242 1243 1244 1245 1246 1247
    except KeyError as e:
        pass  # use default

    try:
        cli()
    except ExitReplException:
        sys.exit(0)
        print('Bye!')