class Syskit::GUI::Testing

GUI to interface with testing

Constants

Stats

Attributes

app[R]

@return [Roby::Application] the roby application we're working on

discovery_count[R]

The count of slaves that are doing discovery

exception_rendering[R]
item_model[R]

The item model that represents the subprocess state

manager[R]

@return [Autorespawn::Manager] the test manager

pid_to_slave[R]

PID-to-slave mapping

@return [Hash<Integer,Autorespawn::Slave>]

poll_timer[R]

The timer used to call {#manager}.poll periodically

process_lock[R]

Synchronization primitive between the DRb incoming thread and the Qt thread

process_sync[R]

Synchronization primitive between the DRb incoming thread and the Qt thread

selected_item[R]

The currently selected item

server[R]

@return [Roby::App::TestServer] the test server that allow us to

communicate with the tests
slaves[R]

Registered slaves

@return [Hash<Numeric,(Autorespawn::Slave,Qt::StandardItem)>]

test_count[R]

The count of slaves that are doing discovery

test_list_ui[R]
test_result_page[R]
test_result_ui[R]
work_queue[R]

Synchronization primitive between the DRb incoming thread and the Qt thread

Public Class Methods

new(parent = nil, app: Roby.app, poll_period: 0.1) click to toggle source
Calls superclass method
# 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

Public Instance Methods

add_hooks() click to toggle source

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

# 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_slaves() click to toggle source
# 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_ui() click to toggle source
# 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_ui() click to toggle source
# 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) click to toggle source

Deregister a PID-to-slave mapping

@param [Integer] pid

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source

Resolves an item from the slave PID

@raise [ArgumentError] if there is no slave for the given PID

# 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) click to toggle source

Resolves a slave item from its object

@raise [ArgumentError] if no such slave has been registered with

{#register_slave}
# 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) click to toggle source
# 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_work() click to toggle source
# 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) click to toggle source
# File lib/syskit/gui/testing.rb, line 592
def queue_work(&block)
    process_lock.synchronize do
        work_queue << block
    end
end
register_slave(slave) click to toggle source

Register a new slave and add it to the item model

# 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) click to toggle source

Register a PID-to-slave mapping

@param [Autorespawn::Slave] slave the slave, whose {#pid}

attribute is expected to be set appropriately
# 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
reloaded() click to toggle source

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

# 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) click to toggle source
# 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?() click to toggle source
# File lib/syskit/gui/testing.rb, line 263
def running?
    @running
end
save_to_settings(settings) click to toggle source
# File lib/syskit/gui/testing.rb, line 116
def save_to_settings(settings)
end
slave_from_pid(pid) click to toggle source

Resolves a slave from its PID

@raise [ArgumentError] if there is no slave associated to this PID

# 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
start() click to toggle source
# File lib/syskit/gui/testing.rb, line 267
def start
    return if running?
    @running = true
    emit statsChanged
    emit started
end
stats() click to toggle source
# 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
stop() click to toggle source
# File lib/syskit/gui/testing.rb, line 274
def stop
    manager.kill
    process_pending_work
    @running = false
    emit statsChanged
    emit stopped
end
update_item_details() click to toggle source
# 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_state() click to toggle source
# 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) click to toggle source
# 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