Class: Syskit::GUI::Testing

Inherits:
Qt::Widget
  • Object
show all
Defined in:
lib/syskit/gui/testing.rb

Overview

GUI to interface with testing

Defined Under Namespace

Classes: Stats, SubprocessItem

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent = nil, app: Roby.app, poll_period: 0.1) ⇒ Testing

Returns a new instance of Testing



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/syskit/gui/testing.rb', line 61

def initialize(parent = nil, app: Roby.app, poll_period: 0.1)
    super(parent)
    @app = app
    @slaves = Hash.new
    @pid_to_slave = Hash.new

    @work_queue = Array.new
    @process_lock = Mutex.new
    @process_sync = ConditionVariable.new
    @discovery_count = 0
    @test_count = 0
    @running = false

    @manager = Autorespawn::Manager.new(name: Hash[models: ['syskit-ide']])
    @server  = Roby::App::TestServer.start(Process.pid)
    @item_model = Qt::StandardItemModel.new(self)
    create_ui
    @test_result_page = MetaRuby::GUI::HTML::Page.new(test_result_ui.page)
    @exception_rendering = Roby::GUI::ExceptionRendering.new(test_result_page)
    test_result_page.enable_exception_rendering(exception_rendering)
    exception_rendering.add_excluded_pattern(/\/lib\/minitest(?:\.rb:|\/)/)
    exception_rendering.add_excluded_pattern(/\/lib\/autorespawn(?:\.rb:|\/)/)

    connect test_result_page, SIGNAL('fileOpenClicked(const QUrl&)'), self, SIGNAL('fileOpenClicked(const QUrl&)')

    test_list_ui.connect(SIGNAL('clicked(const QModelIndex&)')) do |index|
        item = item_model.item_from_index(index)
        display_item_details(item)
    end
    test_list_ui.connect(SIGNAL('doubleClicked(const QModelIndex&)')) do |index|
        item = item_model.item_from_index(index)
        manager.queue(item.slave)
    end
    add_hooks

    @poll_timer = Qt::Timer.new
    poll_timer.connect(SIGNAL('timeout()')) do
        manager.poll(autospawn: running?)
        process_pending_work
        if item = @selected_item
            runtime = @selected_item.runtime
            if runtime != @selected_item_runtime
                update_selected_item_state
                @selected_item_runtime = runtime
            end
        end
    end
    poll_timer.start(Integer(poll_period * 1000))

    add_test_slaves
    emit statsChanged
end

Instance Attribute Details

#appRoby::Application (readonly)

Returns the roby application we're working on

Returns:

  • (Roby::Application)

    the roby application we're working on



11
12
13
# File 'lib/syskit/gui/testing.rb', line 11

def app
  @app
end

#discovery_countObject (readonly)

The count of slaves that are doing discovery



53
54
55
# File 'lib/syskit/gui/testing.rb', line 53

def discovery_count
  @discovery_count
end

#exception_renderingObject (readonly)

Returns the value of attribute exception_rendering



35
36
37
# File 'lib/syskit/gui/testing.rb', line 35

def exception_rendering
  @exception_rendering
end

#item_modelObject (readonly)

The item model that represents the subprocess state



31
32
33
# File 'lib/syskit/gui/testing.rb', line 31

def item_model
  @item_model
end

#managerAutorespawn::Manager (readonly)

Returns the test manager

Returns:

  • (Autorespawn::Manager)

    the test manager



14
15
16
# File 'lib/syskit/gui/testing.rb', line 14

def manager
  @manager
end

#pid_to_slaveHash<Integer,Autorespawn::Slave> (readonly)

PID-to-slave mapping

Returns:

  • (Hash<Integer,Autorespawn::Slave>)


28
29
30
# File 'lib/syskit/gui/testing.rb', line 28

def pid_to_slave
  @pid_to_slave
end

#poll_timerObject (readonly)

The timer used to call #manager.poll periodically



38
39
40
# File 'lib/syskit/gui/testing.rb', line 38

def poll_timer
  @poll_timer
end

#process_lockObject (readonly)

Synchronization primitive between the DRb incoming thread and the Qt thread



46
47
48
# File 'lib/syskit/gui/testing.rb', line 46

def process_lock
  @process_lock
end

#process_syncObject (readonly)

Synchronization primitive between the DRb incoming thread and the Qt thread



50
51
52
# File 'lib/syskit/gui/testing.rb', line 50

def process_sync
  @process_sync
end

#selected_itemObject (readonly)

The currently selected item



59
60
61
# File 'lib/syskit/gui/testing.rb', line 59

def selected_item
  @selected_item
end

#serverRoby::App::TestServer (readonly)

Returns the test server that allow us to communicate with the tests

Returns:

  • (Roby::App::TestServer)

    the test server that allow us to communicate with the tests



18
19
20
# File 'lib/syskit/gui/testing.rb', line 18

def server
  @server
end

#slavesHash<Numeric,(Autorespawn::Slave,Qt::StandardItem)> (readonly)

Registered slaves

Returns:

  • (Hash<Numeric,(Autorespawn::Slave,Qt::StandardItem)>)


23
24
25
# File 'lib/syskit/gui/testing.rb', line 23

def slaves
  @slaves
end

#test_countObject (readonly)

The count of slaves that are doing discovery



56
57
58
# File 'lib/syskit/gui/testing.rb', line 56

def test_count
  @test_count
end

#test_list_uiObject (readonly)

Returns the value of attribute test_list_ui



32
33
34
# File 'lib/syskit/gui/testing.rb', line 32

def test_list_ui
  @test_list_ui
end

#test_result_pageObject (readonly)

Returns the value of attribute test_result_page



34
35
36
# File 'lib/syskit/gui/testing.rb', line 34

def test_result_page
  @test_result_page
end

#test_result_uiObject (readonly)

Returns the value of attribute test_result_ui



33
34
35
# File 'lib/syskit/gui/testing.rb', line 33

def test_result_ui
  @test_result_ui
end

#work_queueObject (readonly)

Synchronization primitive between the DRb incoming thread and the Qt thread



42
43
44
# File 'lib/syskit/gui/testing.rb', line 42

def work_queue
  @work_queue
end

Instance Method Details

#add_hooksObject

Add hooks on #manager and #server that will allow us to track the test progress



607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/syskit/gui/testing.rb', line 607

def add_hooks
    manager.on_slave_new do |slave|
        queue_work do
            register_slave(slave)
            emit statsChanged
        end
    end
    manager.on_slave_start do |slave|
        queue_work do
            register_slave_pid(slave)
            item = item_from_slave(slave)
            item.start
            if selected_item == item
                update_item_details
            end
            emit statsChanged
        end
    end
    manager.on_slave_finished do |slave|
        queue_work do
            deregister_slave_pid(slave.pid)
            item = item_from_slave(slave)
            item.finished(slave.status)
            if selected_item == item
                update_item_details
            end
        end
    end
    server.on_exception do |pid, exception|
        queue_work do
            item = item_from_pid(pid)
            item.add_exception(exception)
            if selected_item == item
                update_item_details
            end
        end
    end
    server.on_discovery_start do |pid|
        queue_work do
            @discovery_count += 1
            item_from_pid(pid).discovery_start
        end
    end
    server.on_discovery_finished do |pid|
        queue_work do
            @discovery_count -= 1
            item_from_pid(pid).discovery_finished
        end
    end
    server.on_test_start do |pid|
        queue_work do
            @test_count += 1
            item_from_pid(pid).test_start
        end
    end
    server.on_test_result do |pid, file, test_case_name, test_name, failures, assertions, time|
        queue_work do
            item = item_from_pid(pid)
            item.add_test_result(file, test_case_name, test_name, failures, assertions, time)
            if !selected_item || (selected_item == item)
                update_item_details
            end
            emit statsChanged
        end
    end
    server.on_test_finished do |pid|
        queue_work do
            @test_count -= 1
            item_from_pid(pid).test_finished
        end
    end
end

#add_test_slavesObject



680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
# File 'lib/syskit/gui/testing.rb', line 680

def add_test_slaves
    tests = app.discover_test_files.map do |path, models|
        process_id = Hash[path: path]
        if !models.empty?
            process_id[:models] = models.map(&:name).sort
        end
        [path, process_id]
    end
    argv_set = app.argv_set.flat_map do |string|
        ['--set', string]
    end

    tests.sort_by(&:first).each do |path, process_id|
        slave = manager.add_slave(
            Gem.ruby, '-S', 'roby', 'autotest', '--server', server.server_id.to_s, path,
            '-r', app.robot_name,
            *argv_set,
            name: process_id)
        slave.register_files([Pathname.new(path)])
    end
end

#create_status_bar_uiObject



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
# File 'lib/syskit/gui/testing.rb', line 126

def create_status_bar_ui
    status_bar = Qt::HBoxLayout.new
    status_bar.add_widget(start_stop_button =
        Qt::PushButton.new("Start Tests", self))
    connect SIGNAL('started()') do
        start_stop_button.text = "Stop"
    end
    connect SIGNAL('stopped()') do
        start_stop_button.text = "Start"
    end

    start_stop_button.connect(SIGNAL('clicked()')) do
        if running?
            stop
        else start
        end
    end

    status_bar.add_widget(status_label = StateLabel.new(parent: self), 1)
    status_label.declare_state("STOPPED", :blue)
    status_label.declare_state("RUNNING", :green)
    connect SIGNAL('statsChanged()') do
        update_status_label(status_label)
    end

    return status_bar
end

#create_uiObject



154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/syskit/gui/testing.rb', line 154

def create_ui
    layout = Qt::VBoxLayout.new(self)

    status_bar = create_status_bar_ui
    layout.add_layout(status_bar)

    splitter = Qt::Splitter.new(self)
    layout.add_widget(splitter, 1)
    splitter.add_widget(@test_list_ui = Qt::ListView.new(self))
    test_list_ui.model = item_model
    test_list_ui.edit_triggers = Qt::AbstractItemView::NoEditTriggers
    splitter.add_widget(@test_result_ui = Qt::WebView.new(self))
end

#deregister_slave_pid(pid) ⇒ Object

Deregister a PID-to-slave mapping

Parameters:

  • pid (Integer)


576
577
578
579
580
# File 'lib/syskit/gui/testing.rb', line 576

def deregister_slave_pid(pid)
    if !(slave = pid_to_slave.delete(pid))
        Roby.warn "no slave registered for PID #{pid}"
    end
end

#discover_exceptions_from_failure(failure) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
# File 'lib/syskit/gui/testing.rb', line 245

def discover_exceptions_from_failure(failure)
    if failure.kind_of?(Minitest::UnexpectedError)
        return discover_exceptions_from_failure(failure.exception)
    end

    result = [failure]
    if failure.respond_to?(:original_exceptions)
        result.concat failure.original_exceptions.flat_map { |e| discover_exceptions_from_failure(e) }
    end
    result.uniq
end

#display_item_details(item) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/syskit/gui/testing.rb', line 182

def display_item_details(item)
    @selected_item = item
    @selected_item_runtime = nil
    test_result_page.clear
    if test_file_path = item.slave.name[:path]
        items = []
        items << ['title', 'Test file']
        items << ['', test_result_page.link_to(Pathname.new(test_file_path), test_file_path)]

        models = (item.slave.name[:models] || Array.new)
        if !models.empty?
            items << ['title', 'Models']
            models.sort.each do |model_name|
                begin
                    model_object = constant(model_name)
                rescue NameError
                    items << ['', model_name]
                    next
                end

                if definition_file = Roby.app.definition_file_for(model_object)
                    link = test_result_page.link_to(Pathname.new(definition_file), "Open File")
                    items << ['', "#{model_name} [#{link}]"]
                else
                    items << ['', model_name]
                end
            end
        end

        items = items.map do |li_class, text|
            if li_class.empty?
                "<li>#{text}</li>"
            else
                "<li class=\"#{li_class}\">#{text}</li>"
            end
        end
        test_result_page.push nil, "<ul class='body-header-list'>#{items.join("")}</ul>"
    end
    update_selected_item_state

    item.exceptions.each do |e|
        test_result_page.push_exception(nil, e)
    end
    item.each_test_result do |r|
        name = "#{r.test_case_name}::#{r.test_name}"
        info = "#{r.skip_count} skips, #{r.failure_count} failures and #{r.assertions} assertions executed in %.3fs" % [r.time]

        color = if r.failure_count > 0 then :red
                elsif r.skip_count > 0 then :orange
                else :green
                end
        color = SubprocessItem.html_color(color)
        style = "padding: .1em; background-color: #{color}"
        test_result_page.push(nil, "<div class=\"test_result\" style=\"#{style}\">#{MetaRuby::GUI::HTML.escape_html(name)}: #{MetaRuby::GUI::HTML.escape_html(info)}</div>")
        all_exceptions = r.failures.flat_map do |e|
            discover_exceptions_from_failure(e)
        end.uniq
        all_exceptions.each do |e|
            test_result_page.push_exception(nil, e)
        end
    end
end

#item_from_pid(pid) ⇒ Object

Resolves an item from the slave PID

Raises:

  • (ArgumentError)

    if there is no slave for the given PID



552
553
554
# File 'lib/syskit/gui/testing.rb', line 552

def item_from_pid(pid)
    item_from_slave(slave_from_pid(pid))
end

#item_from_slave(slave) ⇒ Object

Resolves a slave item from its object

Raises:

  • (ArgumentError)

    if no such slave has been registered with #register_slave



530
531
532
533
534
535
536
# File 'lib/syskit/gui/testing.rb', line 530

def item_from_slave(slave)
    if info = slaves[slave.object_id]
        return info[1]
    else
        Kernel.raise ArgumentError, "#{slave} is not registered"
    end
end

#process(&block) ⇒ Object



598
599
600
601
602
603
# File 'lib/syskit/gui/testing.rb', line 598

def process(&block)
    process_lock.synchronize do
        work_queue << block
        process_sync.wait(process_lock)
    end
end

#process_pending_workObject



582
583
584
585
586
587
588
589
# File 'lib/syskit/gui/testing.rb', line 582

def process_pending_work
    process_lock.synchronize do
        while !work_queue.empty?
            work_queue.shift.call
        end
        process_sync.signal
    end
end

#queue_work(&block) ⇒ Object



592
593
594
595
596
# File 'lib/syskit/gui/testing.rb', line 592

def queue_work(&block)
    process_lock.synchronize do
        work_queue << block
    end
end

#register_slave(slave) ⇒ Object

Register a new slave and add it to the item model



557
558
559
560
561
# File 'lib/syskit/gui/testing.rb', line 557

def register_slave(slave)
    item = SubprocessItem.new(app, slave)
    slaves[slave.object_id] = [slave, item]
    item_model.append_row(item)
end

#register_slave_pid(slave) ⇒ Object

Register a PID-to-slave mapping

Parameters:

  • slave (Autorespawn::Slave)

    the slave, whose #pid attribute is expected to be set appropriately



567
568
569
570
571
# File 'lib/syskit/gui/testing.rb', line 567

def register_slave_pid(slave)
    item = item_from_slave(slave)
    pid_to_slave[slave.pid] = slave
    item.slave_pid = slave.pid
end

#reloadedObject

Call this after reloading the app so that the list of tests gets refreshed as well



312
313
314
315
316
317
318
# File 'lib/syskit/gui/testing.rb', line 312

def reloaded
    slaves.clear
    pid_to_slave.clear
    item_model.clear
    manager.clear
    add_test_slaves
end

#restore_from_settings(settings) ⇒ Object



119
120
121
122
123
124
# File 'lib/syskit/gui/testing.rb', line 119

def restore_from_settings(settings)
    parallel = settings.value('parallel_level')
    if !parallel.null?
        manager.parallel_level = parallel.to_int
    end
end

#running?Boolean

Returns:

  • (Boolean)


263
264
265
# File 'lib/syskit/gui/testing.rb', line 263

def running?
    @running
end

#save_to_settings(settings) ⇒ Object



116
117
# File 'lib/syskit/gui/testing.rb', line 116

def save_to_settings(settings)
end

#slave_from_pid(pid) ⇒ Object

Resolves a slave from its PID

Raises:

  • (ArgumentError)

    if there is no slave associated to this PID



541
542
543
544
545
546
547
# File 'lib/syskit/gui/testing.rb', line 541

def slave_from_pid(pid)
    if slave = pid_to_slave[pid]
        return slave
    else
        Kernel.raise ArgumentError, "no slave registered for PID #{pid}"
    end
end

#startObject



267
268
269
270
271
272
# File 'lib/syskit/gui/testing.rb', line 267

def start
    return if running?
    @running = true
    emit statsChanged
    emit started
end

#statsObject



286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/syskit/gui/testing.rb', line 286

def stats
    stats = Stats.new(manager.slave_count, 0, 0, 0, 0, 0, 0)
    slaves.each_value do |_, slave|
        stats.executed_test_count += 1 if slave.has_tested?
        stats.executed_count += 1 if slave.executed?
        stats.run_count += slave.run_count
        stats.failure_count += slave.failure_count
        stats.assertions_count += slave.assertions_count
        stats.skip_count += slave.skip_count
    end
    # Remove the "self" slave
    stats.executed_count -= 1
    stats
end

#stopObject



274
275
276
277
278
279
280
# File 'lib/syskit/gui/testing.rb', line 274

def stop
    manager.kill
    process_pending_work
    @running = false
    emit statsChanged
    emit stopped
end

#update_item_detailsObject



257
258
259
260
261
# File 'lib/syskit/gui/testing.rb', line 257

def update_item_details
    if selected_item
        display_item_details(selected_item)
    end
end

#update_selected_item_stateObject



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/syskit/gui/testing.rb', line 168

def update_selected_item_state
    item = @selected_item
    if runtime = item.runtime
        status_text = item.status_text.join("<br/>")
        if item.finished?
            test_result_page.push nil, "Run %i ran for %.01fs: %s" % [item.total_run_count, runtime, status_text], id: 'status'
        elsif runtime = item.runtime
            test_result_page.push nil, "Run %i currently running %.01fs: %s" % [item.total_run_count, runtime, status_text], id: 'status'
        end
    else
        test_result_page.push nil, "Never ran", id: 'status'
    end
end

#update_status_label(status_label) ⇒ Object



301
302
303
304
305
306
307
308
# File 'lib/syskit/gui/testing.rb', line 301

def update_status_label(status_label)
    stats = self.stats
    state_name = if running? then 'RUNNING'
                 else 'STOPPED'
                 end
    status_label.update_state(
        state_name, text: "#{stats.executed_count} of #{stats.test_count} test files executed, #{stats.run_count} runs, #{stats.skip_count} skips, #{stats.failure_count} failures and #{stats.assertions_count} assertions")
end