class Syskit::GUI::Testing
GUI to interface with testing
Constants
- Stats
Attributes
@return [Roby::Application] the roby application we're working on
The count of slaves that are doing discovery
The item model that represents the subprocess state
@return [Autorespawn::Manager] the test manager
PID-to-slave mapping
@return [Hash<Integer,Autorespawn::Slave>]
The timer used to call {#manager}.poll periodically
Synchronization primitive between the DRb incoming thread and the Qt thread
Synchronization primitive between the DRb incoming thread and the Qt thread
The currently selected item
@return [Roby::App::TestServer] the test server that allow us to
communicate with the tests
Registered slaves
@return [Hash<Numeric,(Autorespawn::Slave,Qt::StandardItem)>]
The count of slaves that are doing discovery
Synchronization primitive between the DRb incoming thread and the Qt thread
Public Class Methods
# 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 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
# 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
# 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
# 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 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
# 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
# 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
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
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
# 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
# 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
# File lib/syskit/gui/testing.rb, line 592 def queue_work(&block) process_lock.synchronize do work_queue << block end end
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 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
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
# 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
# File lib/syskit/gui/testing.rb, line 263 def running? @running end
# File lib/syskit/gui/testing.rb, line 116 def save_to_settings(settings) end
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
# File lib/syskit/gui/testing.rb, line 267 def start return if running? @running = true emit statsChanged emit started end
# 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
# File lib/syskit/gui/testing.rb, line 274 def stop manager.kill process_pending_work @running = false emit statsChanged emit stopped end
# File lib/syskit/gui/testing.rb, line 257 def update_item_details if selected_item display_item_details(selected_item) end end
# 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
# 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