64
65
def write(self, a):
68
class SubunitWindow(object):
69
"""Per-window application logic.
71
Contains references to many gtk controls, in particular
73
:ivar subunit_window: the top level gtk window
74
:ivar model_filter: TreeModelFilter controlling what's actually visible
76
:ivar _outcome_filter: dict from outcome strings (eg 'passed') to bool
77
saying whether they should be shown
80
def __init__(self, glade_xml):
81
self.glade = glade_xml
91
w = glade_xml.get_widget(i)
92
assert w is not None, \
93
"couldn't load %r" % i
95
# acts like a TestSuite and feeds into a gtk tree; also sets some
96
# parameters on the view
97
self.test_result = tree.TableTestResults(self)
99
glade_xml.signal_autoconnect(self)
100
self._outcome_filter = dict(
105
self._glade_xml = glade_xml
106
self._get_widget = glade_xml.get_widget
107
# XXX: send misunderstood subunit data to stderr for the sake of
108
# debugging; eventually we'd like to show it in a window -- mbp
110
self._subunit_passthrough = sys.stderr # NullOutputStream()
111
self._running_test_statusbar_context = \
112
self.statusbar.get_context_id("current test")
115
def _connect_tree(self):
116
"""Connect the tree model to the view in table view."""
117
view = self.test_treeview
118
self.model_filter = self.test_result.store.filter_new()
119
self.model_filter.set_visible_func(self._row_visible_func)
120
view.set_model(self.model_filter)
122
view.set_headers_visible(True)
123
view.set_headers_clickable(True)
124
view.set_show_expanders(False)
126
outcome_col = gtk.TreeViewColumn("Outcome",
127
gtk.CellRendererText(),
128
text=self.test_result.COL_OUTCOME)
129
view.append_column(outcome_col)
131
test_name_renderer = gtk.CellRendererText()
132
test_name_renderer.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
134
test_name_col = gtk.TreeViewColumn("Test",
136
text=self.test_result.COL_TEST_ID)
137
view.append_column(test_name_col)
139
def load_from_subunit_stream(self, input_file):
140
# feed things from the subunit stream into self.test_result
142
# this currently works by running the subunit stream as if it were an
143
# actually running test, through the python unittest protocol. This
144
# squashes some of the interesting data, such as folding the
145
# attachments into a _StringException. We probably actually need to
146
# load it through a Subunit-specific protocol.
147
context = self.statusbar.get_context_id("Loading")
148
self.statusbar.push(context, "Loading stream...")
149
self.test_result._clear()
150
# probably a good guess that the user now wants to look through the
152
self.test_treeview.grab_focus()
154
self.statusbar.pop(context)
156
# dummy_result = testtools.TestResult()
158
protocol_server = TestProtocolServer(
160
self._subunit_passthrough,
162
# if it's a real file, we should do nonblocking callback io
163
if getattr(input_file, 'fileno', None):
164
driver_class = FileInputDriver
166
driver_class = InProcessInputDriver
167
self.input_driver = driver_class(input_file,
169
protocol_server=protocol_server)
170
self.input_driver.start_reading()
172
def on_about_menuitem_activate(self, menuitem):
173
aboutdialog = gtk.AboutDialog()
174
aboutdialog.set_authors((
179
aboutdialog.set_copyright(u"Copyright \u00a9 2008-2010 Contributors")
180
aboutdialog.set_comments(
181
"A graphical inspector for Subunit test results")
182
aboutdialog.set_license("""
183
Licensed under the Apache License, Version 2.0 (the "License");
184
you may not use this program except in compliance with the License.
185
You may obtain a copy of the License at
187
http://www.apache.org/licenses/LICENSE-2.0
189
Unless required by applicable law or agreed to in writing, software
190
distributed under the License is distributed on an "AS IS" BASIS,
191
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
192
See the License for the specific language governing permissions and
193
limitations under the License.""")
194
aboutdialog.set_website("http://launchpad.net/tribunal")
196
aboutdialog.destroy()
198
def on_filter_failed_menuitem_toggled(self, menuitem):
199
self.set_outcome_filter('failed', menuitem.get_active())
201
def on_filter_skipped_menuitem_toggled(self, menuitem):
202
self.set_outcome_filter('skipped', menuitem.get_active())
204
def on_filter_passed_menuitem_toggled(self, menuitem):
205
self.set_outcome_filter('passed', menuitem.get_active())
207
def on_filter_xfail_menuitem_toggled(self, menuitem):
208
self.set_outcome_filter('xfail', menuitem.get_active())
210
def on_pause_input_tb_toggled(self, toggle):
211
self.input_driver.set_paused(toggle.get_active())
213
def on_reload_button_clicked(self, reload_button):
215
self.reload_from_source()
216
except UnreloadableStream:
217
dialog = gtk.Dialog(title="Stream not reloadable",
218
parent=self.subunit_window,
219
buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
220
label = gtk.Label("This stream is not reloadable. Perhaps it is a pipe?")
221
dialog.vbox.pack_start(label, True, True, 0)
226
def on_show_failed_tb_toggled(self, tb_toggle):
227
self.set_outcome_filter('failed', tb_toggle.get_active())
229
def on_show_skipped_tb_toggled(self, tb_toggle):
230
self.set_outcome_filter('skipped', tb_toggle.get_active())
232
def on_show_passed_tb_toggled(self, tb_toggle):
233
self.set_outcome_filter('passed', tb_toggle.get_active())
235
def on_show_xfail_tb_toggled(self, tb_toggle):
236
self.set_outcome_filter('xfail', tb_toggle.get_active())
238
def on_test_treeview_row_activated(self, treeview, path, view_column):
239
self._show_results_for_tree_path(path)
241
def on_test_concluded(self, test_id, outcome):
242
# called back by TribunalTestResult when a test finishes
243
# self.statusbar.pop(self._running_test_statusbar_context)
244
self._update_outcome_counts()
246
def on_test_started(self, test_id):
247
# self.statusbar.push(self._running_test_statusbar_context,
248
# "running %s" % test_id)
251
def on_test_treeview_cursor_changed(self, treeview):
252
path, column = treeview.get_cursor()
253
# print 'cursor to path %r, column %r' % (path, column)
254
self._show_results_for_tree_path(path)
256
def _row_visible_func(self, model, iter, user_data=None):
257
"""Called back from TreeModelFilter to check if a row is visible."""
258
row_outcome = model.get_value(iter, tree.TableTestResults.COL_OUTCOME)
259
return self._outcome_filter.get(row_outcome, True)
261
def set_outcome_filter(self, outcome, show):
262
if show != self._outcome_filter[outcome]:
263
self._outcome_filter[outcome] = show
264
self._get_widget('show_%s_tb' % outcome).set_active(show)
265
self._get_widget('filter_%s_menuitem' % outcome).set_active(show)
266
self.refresh_filter()
268
def set_source(self, test_source):
269
self.source = test_source
272
# show the top level window and the rest will follow
273
self.subunit_window.show_all()
275
def _show_results_for_tree_path(self, path):
276
# apparent path is given in the filtered view; the actual path
277
# we need is the one in the underlying model
278
real_path = self.model_filter.convert_path_to_child_path(path)
279
test_id = self.test_result.get_test_id(real_path)
281
details = self.test_result.get_detail_text_by_test_id(
283
outcome = self.test_result.get_outcome_by_test_id(test_id)
284
error = self.test_result.get_error_by_test_id(test_id)
286
error = details = outcome = None
288
self.result_frame_label.set_label('<b>%s %s:</b>'
289
% tuple(map(xml_escape, [test_id, outcome])))
290
self.result_frame.set_sensitive(True)
292
self.result_frame_label.set_label('Details:')
293
self.result_frame.set_sensitive(False)
295
self.result_textview.set_sensitive(True)
296
t = (error or '') + '\n' + details_dict_to_text(details)
297
self.result_textview.get_buffer().set_text(t)
299
self.result_textview.set_sensitive(False)
300
self.result_textview.get_buffer().set_text('')
302
def refresh_filter(self):
303
self.model_filter.refilter()
305
def reload_from_source(self):
306
self.load_from_subunit_stream(self.source.reload())
308
def _update_outcome_counts(self):
309
for status in ['failed', 'skipped', 'passed', 'xfail']:
310
tb_button = self._glade_xml.get_widget('show_%s_tb' % status)
311
tb_button.set_label("%d %s" %
312
(self.test_result._counts_by_status[status],
313
status.capitalize()))
316
68
class ViewSubunitApp(object):
317
69
"""Application object.