mscHandler.py 16.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
    TASTE ASN.1 Value Editor

    Interface to the TASTE MSC Viewer

    Copyright (c) 2012-2015 European Space Agency

    Designed and implemented by Maxime Perrotin

    Contact: maxime.perrotin@esa.int

    License is LGPLv3 - Check the LICENSE file
"""

Maxime Perrotin's avatar
Maxime Perrotin committed
17
18
19
20
21
22
23
import os
import sys
import subprocess
from time import strftime
from multiprocessing import Process, Pipe

from PySide.QtGui import(QFileDialog, QGraphicsView, QDockWidget, QMessageBox,
24
                         QErrorMessage, QTableWidget, QTableWidgetItem,
25
                         QUndoCommand, QUndoStack)
Maxime Perrotin's avatar
Maxime Perrotin committed
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from PySide.QtCore import Slot, Qt, QFile, QTimer
from PySide.QtUiTools import QUiLoader

from mscStreamingScene import MscStreamingScene
import resources

CUR_DIR = os.path.dirname(os.path.realpath(__file__))
SCENARIO_DIALOG_FILE = ':/logging.ui'

# Convenient type for handling MSC messages
# holds direction (in/out) and label (message+parameter)
MSG = type('MSG', (), {'direction': '', 'label': ''})


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class AddToMsc(QUndoCommand):
    ''' Undo command to add something to the MSC (message, timer, box) '''
    def __init__(self, handler, direction, message):
        ''' Init: prepare the message '''
        super(AddToMsc, self).__init__()
        self.handler = handler
        self.message = message
        self.increase_factor = 30
        self.txt = MSG()
        self.txt.direction = direction
        # Add a counter (unique Id) incremented for each message
        # (eg. out MsgName,1(Param);) - to respect MSC syntax
        split = message.strip().split('(', 1)
        if len(split) > 1:
            self.txt.label = (',%d(' % handler.cnt_id).join(split)
        else:
            self.txt.label = split[0] + ',%d' % handler.cnt_id
        # Create the graphical item
        if direction in ('in', 'out'):
            if direction == 'out':
                start_item = handler.gui_instance
                end_item = handler.taste_instance
            elif direction == 'in':
                start_item = handler.taste_instance
                end_item = handler.gui_instance
            self.item = handler.msc_scene.addMessage(
                    start_item, end_item, handler.next_y, label=message)
        elif direction == 'set':
            self.item = handler.msc_scene.addSetTimer(handler.taste_instance,
                                                      handler.next_y,
                                                      message)
        elif direction == 'reset':
            self.item = handler.msc_scene.addResetTimer(handler.taste_instance,
                                                        handler.next_y,
                                                        message)
        elif direction == 'timeout':
            self.item = handler.msc_scene.addTimeout(handler.taste_instance,
                                                     handler.next_y,
                                                     message)
        elif direction == 'condition':
            # Condition: single word with no space
            self.item = handler.msc_scene.addCondition(handler.taste_instance,
                                                       y=handler.next_y,
Maxime Perrotin's avatar
Maxime Perrotin committed
83
                                                       label=message)
84
            self.increase_factor = 60
85
86
87
88
89
        elif direction == 'procedure_call':
            # Local procedure call: an arrow from and to the same instance
            self.item = handler.msc_scene.addProcedure(handler.taste_instance,
                                                       y=handler.next_y,
                                                       label=message)
90
            self.increase_factor = 60
Maxime Perrotin's avatar
Maxime Perrotin committed
91
        #self.item.hide()
92
93
94
95
96
97
98
99
100
        # This is how to add a comment on the graph:
        # comment = handler.msc_scene.addComment(msg)
        #comment.setCommentText("Hello!")

    def undo(self):
        ''' Undo: delete the item from the MSC '''
        self.handler.cnt_id -= 1
        self.handler.msg.pop()
        self.handler.next_y -= self.increase_factor
101
        self.handler.msc_scene.visible_items.pop()
102
103
104
105
106
107
108
        self.item.hide()

    def redo(self):
        ''' Redo: add the item to the MSC '''
        self.handler.cnt_id += 1
        self.handler.msg.append(self.txt)
        self.handler.next_y += self.increase_factor
109
110
        if self.handler.msc_scene.visible_items[-1] != self.item:
            self.handler.msc_scene.visible_items.append(self.item)
111
112
113
        self.item.show()


Maxime Perrotin's avatar
Maxime Perrotin committed
114
115
116
117
118
class mscHandler(object):
    '''
        Class managing MSC operations
        (streaming, loading, recording, compiling to Python, executing)
    '''
119
120
    def __init__(self, parent, msc_list, fv_name, msgs, udpController=None,
                 instance_name='TASTE_System'):
Maxime Perrotin's avatar
Maxime Perrotin committed
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
        '''
            Startup: check if there are some MSC files in
            the directory and update the list
        '''
        all_files = os.listdir('.')
        self.parent = parent
        self.error_msg = QErrorMessage(self.parent)
        # cnt_id is a counter incremented at each message to make
        # the correspondance between gate in/out and instance in/out messages
        self.cnt_id = 1
        self.log = None
        self.ivpath = ''
        self.msc_list = msc_list
        self.fv_name = fv_name
        self.udp = udpController
        self.msc_started = False
        for msc_file in all_files:
            if msc_file.endswith('.msc'):
                self.msc_list.addItem(msc_file)
        # Build the MSC header
        self.msc = ['''mscdocument automade;
language ASN.1;
data dataview-uniq.asn;
inst {fv};'''.format(fv=fv_name)]
        self.py_script = None
        # Add the list of messages and their parameters
        for msg in msgs:
            self.msc.append('msg ' + msg['name'] + ' : (' + msg['type'] + ');')
        self.msc.append('msc recorded;')
        # Save the list of messages to be added to the MSC
        self.msg = []
        # Create an MSC viewer
        self.msc_scene = MscStreamingScene()
        self.msc_view = QGraphicsView(self.msc_scene)
        self.dock = QDockWidget('MSC Recorder', parent)
        self.dock.setFloating(True)
        self.dock.setObjectName('MSCRecorder')
        parent.addDockWidget(Qt.RightDockWidgetArea, self.dock)
        self.dock.setAllowedAreas(Qt.NoDockWidgetArea)
        # Create two MSC instances on the diagram
        self.env_x = 80.0
        self.taste_x = 600.0
        self.gui_instance = self.msc_scene.addInstance(self.env_x, 0,
                                                     label=fv_name)
        self.taste_instance = self.msc_scene.addInstance(self.taste_x, 0,
166
                                                       label=instance_name)
Maxime Perrotin's avatar
Maxime Perrotin committed
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
        self.next_y = 130.0
        self.msc_view.show()
        self.dock.setWidget(self.msc_view)
        self.running = False
        self.dock.hide()
        # Preload the sceneario log dialog box
        loader = QUiLoader()
        ui_file = QFile(SCENARIO_DIALOG_FILE)
        ui_file.open(QFile.ReadOnly)
        self.dialog = loader.load(ui_file)
        ui_file.close()
        self.dialog.setParent(parent, Qt.Dialog)
        self.dialog.rejected.connect(self.stop_scenario)
        self.log_window = self.dialog.findChild(QTableWidget, 'log_table')
        self.log_window.setShowGrid(False)
        self.log_window.setAlternatingRowColors(True)
        self.dialog.hide()
        # Pipe to communicate with a running scenario
        self.pipe_in, self.pipe_out = None, None
        # Process to handle the execution of a scenario
        self.proc = None
        # Timer to periodically monitor a scenario
        self.timer = QTimer()
        self.timer.timeout.connect(self.monitor_scenario)
191
        self.undo_stack = QUndoStack()
Maxime Perrotin's avatar
Maxime Perrotin committed
192
193
194
195
196
197
198
199
200
201

    @Slot()
    def startStop(self):
        ''' Trigger or stop the MSC recording/streaming '''
        self.running = not self.running
        if self.running:
            self.dock.show()
        else:
            self.dock.hide()

202
    @Slot()
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
    def undo(self):
        ''' Remove the last items from the MSC diagram using the undo stack '''
        self.undo_stack.undo()

    @Slot()
    def redo(self):
        ''' Add back the last items on the MSC diagram from the undo stack '''
        self.undo_stack.redo()

    @Slot()
    def start_undo_macro(self):
        ''' Allow a remote control of undo macros '''
        self.undo_stack.beginMacro('Add Items')

    @Slot()
    def stop_undo_macro(self):
        ''' Allow a remote control of undo macros '''
        self.undo_stack.endMacro()

    @Slot()
    def addToMsc(self, direction, message):
Maxime Perrotin's avatar
Maxime Perrotin committed
224
225
226
227
        ''' Add a message to the MSC '''
        if not self.running:
            return
        self.msc_started = True
228
        undo_cmd = AddToMsc(self, direction, message)
229
        self.undo_stack.push(undo_cmd)
Maxime Perrotin's avatar
Maxime Perrotin committed
230

Maxime Perrotin's avatar
Maxime Perrotin committed
231
232
233
234
235
236
237
238
239
    @Slot()
    def stopMscRecording(self):
        ''' Add footer and close MSC file '''
        if not self.msc_started or not self.running:
            return
        for msg in self.msg:
            if msg.direction == 'out':
                self.msc.append('    gate in ' + msg.label
                                + ' from {fv};'.format(fv=self.fv_name))
Maxime Perrotin's avatar
Maxime Perrotin committed
240
            elif msg.direction == 'in':
Maxime Perrotin's avatar
Maxime Perrotin committed
241
242
243
244
245
246
                self.msc.append('    gate out ' + msg.label
                                + ' to {fv};'.format(fv=self.fv_name))
        self.msc.append('    {fv}: instance;'.format(fv=self.fv_name))
        for msg in self.msg:
            if msg.direction == 'out':
                self.msc.append('        out ' + msg.label + ' to env;')
Maxime Perrotin's avatar
Maxime Perrotin committed
247
            elif msg.direction == 'in':
Maxime Perrotin's avatar
Maxime Perrotin committed
248
249
250
251
252
253
254
                self.msc.append('        in ' + msg.label + ' from env;')
        self.msc.append('''    endinstance;
endmsc;
endmscdocument;
''')
        msc_name = self.fv_name + '_trace_' + strftime("%Y%m%d%H%S") + '.msc'
        msc_file = open(msc_name, 'w')
Maxime Perrotin's avatar
Maxime Perrotin committed
255
        msc_file.write('\n'.join(self.msc).encode('latin1'))
Maxime Perrotin's avatar
Maxime Perrotin committed
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
        msc_file.close()

    def run(self, script=None):
        '''
           Compile the selected MSC using msc2py.exe and execute it,
           or directly run a Python script
        '''
        if self.py_script:
            self.log.error('A scenario is already running, please wait...')
            return
        selected_msc = None
        if not script:
            selection = self.msc_list.selectedItems()
            if len(selection) == 0:
                return
            else:
                selected_msc = selection[0].text()
        else:
            if script.endswith('.msc'):
                selected_msc = script
            elif script.endswith('.py'):
                sys.path.append(os.path.dirname(script))
                py_script_name = os.path.basename(script)[:-3]

        if selected_msc is not None:
            py_script_name = os.path.basename(selected_msc[:-4])
            self.log.info('Converting MSC script '
                           + selected_msc + ' to Python')
            commandline = ['msc2py.exe',
                           '-i', os.path.abspath(self.ivpath),
                           '-a', 'dataview-uniq.asn',
                           '-p', py_script_name + '.py',
                           '-m', selected_msc]
            self.log.debug(' '.join(commandline))
            if 0 != subprocess.call(commandline):
                self.log.error('Impossible to convert the MSC script')
                self.error_msg.showMessage(
                        'Impossible to convert the MSC script to Python!',
                        'MSC Scenario')
                return
        try:
            self.log.debug('Importing Python module ' + py_script_name)
            try:
                self.py_script = reload(py_script_name)
            except (ImportError, TypeError):
                try:
                    self.py_script = __import__(py_script_name)
                except ImportError:
                    self.log.error('Impossible to import the Python script')
                    self.error_msg.showMessage(
                            'Impossible to import the Python script!',
                            'MSC Scenario')
            if self.py_script:
                self.log.debug('Executing script')
                # Run in a different process, to avoid blocking the GUI
                log_stdout = sys.stdout
                sys.stdout = sys.__stdout__
                # Create a new pipe for interprocess communication
                self.pipe_in, self.pipe_out = Pipe()
                self.log_window.setRowCount(0)
                #for row in xrange(self.log_window.rowCount()):
                #    self.log_window.removeRow(0)
                self.proc = Process(target=self.py_script.runScenario,
                                    args=(self.pipe_in, self.pipe_out))
                self.proc.start()
                sys.stdout = log_stdout
                # Start monitoring the process
                self.timer.start(500)
                self.dialog.show()

        except ImportError:
            self.log.error('Impossible to run the Python script')

    def load(self):
        ''' Load an MSC or Python script and run it (compile it if .msc) '''
        filename = QFileDialog.getOpenFileName(None,
            "Load MSC or Python script", ".", "Script (*.py *.msc)")[0]
        if len(filename) == 0:
            return
        self.run(filename)

    def edit(self):
        ''' Open a text editor to edit the selected MSC '''
        selected_msc = None
        selection = self.msc_list.selectedItems()
        if len(selection) == 0:
            return
        else:
            selected_msc = selection[0].text()
345
346
347
348
349
350
351
352
353
        ret = 1
        try:
            commandline = ["msce.py", "--open", selected_msc]
            ret = subprocess.call(commandline)
        except:
            commandline = ["taste-msc-editor", "--open", selected_msc]
            ret = subprocess.call(commandline)

        if 0 != ret:
Maxime Perrotin's avatar
Maxime Perrotin committed
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
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
            self.log.error('MSC Editor returned an error')

    def monitor_scenario(self):
        ''' Periodic monitoring/logging of the scenario '''
        # Check if a message was received, and update the GUI table
        if self.pipe_in.poll():
            try:
                (sc_id, severity, msg) = self.pipe_in.recv()
                id_item = QTableWidgetItem(str(sc_id))
                severity_item = QTableWidgetItem(str(severity))
                msg_item = QTableWidgetItem(str(msg))
                row = self.log_window.rowCount()
                self.log_window.insertRow(row)
                self.log_window.setItem(row, 0, id_item)
                self.log_window.setItem(row, 1, severity_item)
                self.log_window.setItem(row, 2, msg_item)
                self.log_window.scrollToItem(id_item)
                if severity == 'ERROR':
                    self.stop_scenario(status='Error')
                    return
                elif severity == 'COMMAND' and msg == 'END':
                    self.stop_scenario(status='Success')
                    return
            except EOFError:
                # Remote side closed the connection
                return
        if not self.proc.is_alive():
            self.stop_scenario(status='Error')
            return

    def stop_scenario(self, status='Error'):
        ''' When user pressed the Cancel button on the log window '''
        try:
            # Gently ask the process to stop
            self.pipe_in.send('STOP')
            # In case scenario is still running after 2 seconds, kill process
            QTimer.singleShot(2000, self.proc.terminate)
        except AttributeError:
            pass
        self.timer.stop()
        self.pipe_in, self.pipe_out = None, None
        if status == 'Success':
            msg = 'Test case successfully completed!'
            self.error_msg.showMessage(msg, 'MSC Scenario ' + status)
        else:
            msg = 'Test case failed!'
            self.log.info('End of scenario')
            msg_box = QMessageBox(self.parent)
            msg_box.setText(msg)
            msg_box.setInformativeText(
                    'Click on "Show details" for information')
            msg_box.setStandardButtons(QMessageBox.Discard)
            msg_box.setWindowTitle('MSC Scenario')
            detail = []
            for row in xrange(self.log_window.rowCount()):
                detail.append('{sc_id} - {sev} - {msg}'
                        .format(sc_id=self.log_window.item(row, 0).text(),
                            sev=self.log_window.item(row, 1).text(),
                            msg=self.log_window.item(row, 2).text()))
            msg_box.setDetailedText('\n'.join(detail))
            msg_box.exec_()
        self.dialog.hide()
        self.py_script = None