##// END OF EJS Templates
Adde slimerjs support to JS tests...
Jonathan Frederic -
Show More
@@ -1,476 +1,505
1 1 //
2 2 // Utility functions for the HTML notebook's CasperJS tests.
3 3 //
4
5 4 casper.get_notebook_server = function () {
6 5 // Get the URL of a notebook server on which to run tests.
7 6 port = casper.cli.get("port");
8 7 port = (typeof port === 'undefined') ? '8888' : port;
9 8 return 'http://127.0.0.1:' + port;
10 9 };
11 10
12 11 casper.open_new_notebook = function () {
13 12 // Create and open a new notebook.
14 13 var baseUrl = this.get_notebook_server();
15 14 this.start(baseUrl);
16 15 this.thenClick('button#new_notebook');
17 16 this.waitForPopup('');
18 17
19 18 this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');});
20 19 this.then(function () {
21 20 this.open(this.popups[0].url);
22 21 });
23 22
24 23 // Make sure the kernel has started
25 24 this.waitFor( this.kernel_running );
26 25 // track the IPython busy/idle state
27 26 this.thenEvaluate(function () {
28 27 $([IPython.events]).on('status_idle.Kernel',function () {
29 28 IPython._status = 'idle';
30 29 });
31 30 $([IPython.events]).on('status_busy.Kernel',function () {
32 31 IPython._status = 'busy';
33 32 });
34 33 });
34
35 // Because of the asynchronous nature of SlimerJS (Gecko), we need to make
36 // sure the notebook has actually been loaded into the IPython namespace
37 // before running any tests.
38 this.waitFor(function() {
39 return this.evaluate(function () {
40 return IPython.notebook;
41 });
42 });
35 43 };
36 44
37 45 casper.kernel_running = function kernel_running() {
38 46 // Return whether or not the kernel is running.
39 47 return this.evaluate(function kernel_running() {
40 48 return IPython.notebook.kernel.running;
41 49 });
42 50 };
43 51
44 52 casper.shutdown_current_kernel = function () {
45 53 // Shut down the current notebook's kernel.
46 54 this.thenEvaluate(function() {
47 55 IPython.notebook.kernel.kill();
48 56 });
49 57 // We close the page right after this so we need to give it time to complete.
50 58 this.wait(1000);
51 59 };
52 60
53 61 casper.delete_current_notebook = function () {
54 62 // Delete created notebook.
55 63
56 64 // For some unknown reason, this doesn't work?!?
57 65 this.thenEvaluate(function() {
58 66 IPython.notebook.delete();
59 67 });
60 68 };
61 69
62 70 casper.wait_for_busy = function () {
63 71 // Waits for the notebook to enter a busy state.
64 72 this.waitFor(function () {
65 73 return this.evaluate(function () {
66 74 return IPython._status == 'busy';
67 75 });
68 76 });
69 77 };
70 78
71 79 casper.wait_for_idle = function () {
72 80 // Waits for the notebook to idle.
73 81 this.waitFor(function () {
74 82 return this.evaluate(function () {
75 83 return IPython._status == 'idle';
76 84 });
77 85 });
78 86 };
79 87
80 88 casper.wait_for_output = function (cell_num, out_num) {
81 89 // wait for the nth output in a given cell
82 90 this.wait_for_idle();
83 91 out_num = out_num || 0;
84 92 this.then(function() {
85 93 this.waitFor(function (c, o) {
86 94 return this.evaluate(function get_output(c, o) {
87 95 var cell = IPython.notebook.get_cell(c);
88 96 return cell.output_area.outputs.length > o;
89 97 },
90 98 // pass parameter from the test suite js to the browser code js
91 99 {c : cell_num, o : out_num});
92 100 });
93 101 },
94 102 function then() { },
95 103 function timeout() {
96 104 this.echo("wait_for_output timed out!");
97 105 });
98 106 };
99 107
100 108 casper.wait_for_widget = function (widget_info) {
101 109 // wait for a widget msg que to reach 0
102 110 //
103 111 // Parameters
104 112 // ----------
105 113 // widget_info : object
106 114 // Object which contains info related to the widget. The model_id property
107 115 // is used to identify the widget.
108 116 this.waitFor(function () {
109 117 var pending = this.evaluate(function (m) {
110 118 return IPython.notebook.kernel.widget_manager.get_model(m).pending_msgs;
111 119 }, {m: widget_info.model_id});
112 120
113 121 if (pending === 0) {
114 122 return true;
115 123 } else {
116 124 return false;
117 125 }
118 126 });
119 127 };
120 128
121 129 casper.get_output_cell = function (cell_num, out_num) {
122 130 // return an output of a given cell
123 131 out_num = out_num || 0;
124 132 var result = casper.evaluate(function (c, o) {
125 133 var cell = IPython.notebook.get_cell(c);
126 134 return cell.output_area.outputs[o];
127 135 },
128 136 {c : cell_num, o : out_num});
129 137 if (!result) {
130 138 var num_outputs = casper.evaluate(function (c) {
131 139 var cell = IPython.notebook.get_cell(c);
132 140 return cell.output_area.outputs.length;
133 141 },
134 142 {c : cell_num});
135 143 this.test.assertTrue(false,
136 144 "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
137 145 );
138 146 } else {
139 147 return result;
140 148 }
141 149 };
142 150
143 151 casper.get_cells_length = function () {
144 152 // return the number of cells in the notebook
145 153 var result = casper.evaluate(function () {
146 154 return IPython.notebook.get_cells().length;
147 155 });
148 156 return result;
149 157 };
150 158
151 159 casper.set_cell_text = function(index, text){
152 160 // Set the text content of a cell.
153 161 this.evaluate(function (index, text) {
154 162 var cell = IPython.notebook.get_cell(index);
155 163 cell.set_text(text);
156 164 }, index, text);
157 165 };
158 166
159 167 casper.get_cell_text = function(index){
160 168 // Get the text content of a cell.
161 169 return this.evaluate(function (index) {
162 170 var cell = IPython.notebook.get_cell(index);
163 171 return cell.get_text();
164 172 }, index);
165 173 };
166 174
167 175 casper.insert_cell_at_bottom = function(cell_type){
168 176 // Inserts a cell at the bottom of the notebook
169 177 // Returns the new cell's index.
170 178 return this.evaluate(function (cell_type) {
171 179 var cell = IPython.notebook.insert_cell_at_bottom(cell_type);
172 180 return IPython.notebook.find_cell_index(cell);
173 181 }, cell_type);
174 182 };
175 183
176 184 casper.append_cell = function(text, cell_type) {
177 185 // Insert a cell at the bottom of the notebook and set the cells text.
178 186 // Returns the new cell's index.
179 187 var index = this.insert_cell_at_bottom(cell_type);
180 188 if (text !== undefined) {
181 189 this.set_cell_text(index, text);
182 190 }
183 191 return index;
184 192 };
185 193
186 194 casper.execute_cell = function(index){
187 195 // Asynchronously executes a cell by index.
188 196 // Returns the cell's index.
189 197 var that = this;
190 198 this.then(function(){
191 199 that.evaluate(function (index) {
192 200 var cell = IPython.notebook.get_cell(index);
193 201 cell.execute();
194 202 }, index);
195 203 });
196 204 return index;
197 205 };
198 206
199 207 casper.execute_cell_then = function(index, then_callback) {
200 208 // Synchronously executes a cell by index.
201 209 // Optionally accepts a then_callback parameter. then_callback will get called
202 210 // when the cell has finished executing.
203 211 // Returns the cell's index.
204 212 var return_val = this.execute_cell(index);
205 213
206 214 this.wait_for_idle();
207 215
208 216 var that = this;
209 217 this.then(function(){
210 218 if (then_callback!==undefined) {
211 219 then_callback.apply(that, [index]);
212 220 }
213 221 });
214 222
215 223 return return_val;
216 224 };
217 225
218 226 casper.cell_element_exists = function(index, selector){
219 227 // Utility function that allows us to easily check if an element exists
220 228 // within a cell. Uses JQuery selector to look for the element.
221 229 return casper.evaluate(function (index, selector) {
222 230 var $cell = IPython.notebook.get_cell(index).element;
223 231 return $cell.find(selector).length > 0;
224 232 }, index, selector);
225 233 };
226 234
227 235 casper.cell_element_function = function(index, selector, function_name, function_args){
228 236 // Utility function that allows us to execute a jQuery function on an
229 237 // element within a cell.
230 238 return casper.evaluate(function (index, selector, function_name, function_args) {
231 239 var $cell = IPython.notebook.get_cell(index).element;
232 240 var $el = $cell.find(selector);
233 241 return $el[function_name].apply($el, function_args);
234 242 }, index, selector, function_name, function_args);
235 243 };
236 244
237 245 casper.validate_notebook_state = function(message, mode, cell_index) {
238 246 // Validate the entire dual mode state of the notebook. Make sure no more than
239 247 // one cell is selected, focused, in edit mode, etc...
240 248
241 249 // General tests.
242 250 this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
243 251 message + '; keyboard and notebook modes match');
244 252 // Is the selected cell the only cell that is selected?
245 253 if (cell_index!==undefined) {
246 254 this.test.assert(this.is_only_cell_selected(cell_index),
247 255 message + '; cell ' + cell_index + ' is the only cell selected');
248 256 }
249 257
250 258 // Mode specific tests.
251 259 if (mode==='command') {
252 260 // Are the notebook and keyboard manager in command mode?
253 261 this.test.assertEquals(this.get_keyboard_mode(), 'command',
254 262 message + '; in command mode');
255 263 // Make sure there isn't a single cell in edit mode.
256 264 this.test.assert(this.is_only_cell_edit(null),
257 265 message + '; all cells in command mode');
258 266 this.test.assert(this.is_cell_editor_focused(null),
259 267 message + '; no cell editors are focused while in command mode');
260 268
261 269 } else if (mode==='edit') {
262 270 // Are the notebook and keyboard manager in edit mode?
263 271 this.test.assertEquals(this.get_keyboard_mode(), 'edit',
264 272 message + '; in edit mode');
265 273 if (cell_index!==undefined) {
266 274 // Is the specified cell the only cell in edit mode?
267 275 this.test.assert(this.is_only_cell_edit(cell_index),
268 276 message + '; cell ' + cell_index + ' is the only cell in edit mode');
269 277 // Is the specified cell the only cell with a focused code mirror?
270 278 this.test.assert(this.is_cell_editor_focused(cell_index),
271 279 message + '; cell ' + cell_index + '\'s editor is appropriately focused');
272 280 }
273 281
274 282 } else {
275 283 this.test.assert(false, message + '; ' + mode + ' is an unknown mode');
276 284 }
277 285 };
278 286
279 287 casper.select_cell = function(index) {
280 288 // Select a cell in the notebook.
281 289 this.evaluate(function (i) {
282 290 IPython.notebook.select(i);
283 291 }, {i: index});
284 292 };
285 293
286 294 casper.click_cell_editor = function(index) {
287 295 // Emulate a click on a cell's editor.
288 296
289 297 // Code Mirror does not play nicely with emulated brower events.
290 298 // Instead of trying to emulate a click, here we run code similar to
291 299 // the code used in Code Mirror that handles the mousedown event on a
292 300 // region of codemirror that the user can focus.
293 301 this.evaluate(function (i) {
294 302 var cm = IPython.notebook.get_cell(i).code_mirror;
295 303 if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input))
296 304 cm.display.input.focus();
297 305 }, {i: index});
298 306 };
299 307
300 308 casper.set_cell_editor_cursor = function(index, line_index, char_index) {
301 309 // Set the Code Mirror instance cursor's location.
302 310 this.evaluate(function (i, l, c) {
303 311 IPython.notebook.get_cell(i).code_mirror.setCursor(l, c);
304 312 }, {i: index, l: line_index, c: char_index});
305 313 };
306 314
307 315 casper.focus_notebook = function() {
308 316 // Focus the notebook div.
309 317 this.evaluate(function (){
310 318 $('#notebook').focus();
311 319 }, {});
312 320 };
313 321
314 322 casper.trigger_keydown = function() {
315 323 // Emulate a keydown in the notebook.
316 324 for (var i = 0; i < arguments.length; i++) {
317 325 this.evaluate(function (k) {
318 326 var element = $(document);
319 327 var event = IPython.keyboard.shortcut_to_event(k, 'keydown');
320 328 element.trigger(event);
321 329 }, {k: arguments[i]});
322 330 }
323 331 };
324 332
325 333 casper.get_keyboard_mode = function() {
326 334 // Get the mode of the keyboard manager.
327 335 return this.evaluate(function() {
328 336 return IPython.keyboard_manager.mode;
329 337 }, {});
330 338 };
331 339
332 340 casper.get_notebook_mode = function() {
333 341 // Get the mode of the notebook.
334 342 return this.evaluate(function() {
335 343 return IPython.notebook.mode;
336 344 }, {});
337 345 };
338 346
339 347 casper.get_cell = function(index) {
340 348 // Get a single cell.
341 349 //
342 350 // Note: Handles to DOM elements stored in the cell will be useless once in
343 351 // CasperJS context.
344 352 return this.evaluate(function(i) {
345 353 var cell = IPython.notebook.get_cell(i);
346 354 if (cell) {
347 355 return cell;
348 356 }
349 357 return null;
350 358 }, {i : index});
351 359 };
352 360
353 361 casper.is_cell_editor_focused = function(index) {
354 362 // Make sure a cell's editor is the only editor focused on the page.
355 363 return this.evaluate(function(i) {
356 364 var focused_textarea = $('#notebook .CodeMirror-focused textarea');
357 365 if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; }
358 366 if (i === null) {
359 367 return focused_textarea.length === 0;
360 368 } else {
361 369 var cell = IPython.notebook.get_cell(i);
362 370 if (cell) {
363 371 return cell.code_mirror.getInputField() == focused_textarea[0];
364 372 }
365 373 }
366 374 return false;
367 375 }, {i : index});
368 376 };
369 377
370 378 casper.is_only_cell_selected = function(index) {
371 379 // Check if a cell is the only cell selected.
372 380 // Pass null as the index to check if no cells are selected.
373 381 return this.is_only_cell_on(index, 'selected', 'unselected');
374 382 };
375 383
376 384 casper.is_only_cell_edit = function(index) {
377 385 // Check if a cell is the only cell in edit mode.
378 386 // Pass null as the index to check if all of the cells are in command mode.
379 387 return this.is_only_cell_on(index, 'edit_mode', 'command_mode');
380 388 };
381 389
382 390 casper.is_only_cell_on = function(i, on_class, off_class) {
383 391 // Check if a cell is the only cell with the `on_class` DOM class applied to it.
384 392 // All of the other cells are checked for the `off_class` DOM class.
385 393 // Pass null as the index to check if all of the cells have the `off_class`.
386 394 var cells_length = this.get_cells_length();
387 395 for (var j = 0; j < cells_length; j++) {
388 396 if (j === i) {
389 397 if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) {
390 398 return false;
391 399 }
392 400 } else {
393 401 if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) {
394 402 return false;
395 403 }
396 404 }
397 405 }
398 406 return true;
399 407 };
400 408
401 409 casper.cell_has_class = function(index, classes) {
402 410 // Check if a cell has a class.
403 411 return this.evaluate(function(i, c) {
404 412 var cell = IPython.notebook.get_cell(i);
405 413 if (cell) {
406 414 return cell.element.hasClass(c);
407 415 }
408 416 return false;
409 417 }, {i : index, c: classes});
410 418 };
411 419
412 420 casper.notebook_test = function(test) {
413 421 // Wrap a notebook test to reduce boilerplate.
422 //
423 // If you want to read parameters from the commandline, use the following
424 // (i.e. value=):
425 // if (casper.cli.options.value) {
426 // casper.exit(1);
427 // }
414 428 this.open_new_notebook();
429
430 // Echo whether or not we are running this test using SlimerJS
431 if (this.evaluate(function(){
432 return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
433 })) { console.log('This test is running in SlimerJS.'); }
434
435 // Make sure to remove the onbeforeunload callback. This callback is
436 // responsable for the "Are you sure you want to quit?" type messages.
437 // PhantomJS ignores these prompts, SlimerJS does not which causes hangs.
438 this.then(function(){
439 this.evaluate(function(){
440 window.onbeforeunload = function(){};
441 });
442 });
443
415 444 this.then(test);
416 445
417 446 // Kill the kernel and delete the notebook.
418 447 this.shutdown_current_kernel();
419 448 // This is still broken but shouldn't be a problem for now.
420 449 // this.delete_current_notebook();
421 450
422 451 // This is required to clean up the page we just finished with. If we don't call this
423 452 // casperjs will leak file descriptors of all the open WebSockets in that page. We
424 453 // have to set this.page=null so that next time casper.start runs, it will create a
425 454 // new page from scratch.
426 455 this.then(function () {
427 456 this.page.close();
428 457 this.page = null;
429 458 });
430 459
431 460 // Run the browser automation.
432 461 this.run(function() {
433 462 this.test.done();
434 463 });
435 464 };
436 465
437 466 casper.wait_for_dashboard = function () {
438 467 // Wait for the dashboard list to load.
439 468 casper.waitForSelector('.list_item');
440 469 };
441 470
442 471 casper.open_dashboard = function () {
443 472 // Start casper by opening the dashboard page.
444 473 var baseUrl = this.get_notebook_server();
445 474 this.start(baseUrl);
446 475 this.wait_for_dashboard();
447 476 };
448 477
449 478 casper.dashboard_test = function (test) {
450 479 // Open the dashboard page and run a test.
451 480 this.open_dashboard();
452 481 this.then(test);
453 482
454 483 this.then(function () {
455 484 this.page.close();
456 485 this.page = null;
457 486 });
458 487
459 488 // Run the browser automation.
460 489 this.run(function() {
461 490 this.test.done();
462 491 });
463 492 };
464 493
465 494 casper.options.waitTimeout=10000;
466 495 casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
467 496 this.echo("Timeout for " + casper.get_notebook_server());
468 497 this.echo("Is the notebook server running?");
469 498 });
470 499
471 500 casper.print_log = function () {
472 501 // Pass `console.log` calls from page JS to casper.
473 502 this.on('remote.message', function(msg) {
474 503 this.echo('Remote message caught: ' + msg);
475 504 });
476 505 };
@@ -1,182 +1,188
1 1 var xor = function (a, b) {return !a ^ !b;};
2 var isArray = function (a) {return toString.call(a) === "[object Array]" || toString.call(a) === "[object RuntimeArray]";};
2 var isArray = function (a) {
3 try {
4 return Object.toString.call(a) === "[object Array]" || Object.toString.call(a) === "[object RuntimeArray]";
5 } catch (e) {
6 return Array.isArray(a);
7 }
8 };
3 9 var recursive_compare = function(a, b) {
4 10 // Recursively compare two objects.
5 11 var same = true;
6 same = same && !xor(a instanceof Object, b instanceof Object);
12 same = same && !xor(a instanceof Object || typeof a == 'object', b instanceof Object || typeof b == 'object');
7 13 same = same && !xor(isArray(a), isArray(b));
8 14
9 15 if (same) {
10 16 if (a instanceof Object) {
11 17 var key;
12 18 for (key in a) {
13 19 if (a.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
14 20 same = false;
15 21 break;
16 22 }
17 23 }
18 24 for (key in b) {
19 25 if (b.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
20 26 same = false;
21 27 break;
22 28 }
23 29 }
24 30 } else {
25 31 return a === b;
26 32 }
27 33 }
28 34
29 35 return same;
30 36 };
31 37
32 38 // Test the widget framework.
33 39 casper.notebook_test(function () {
34 40 var index;
35 41
36 42 this.then(function () {
37 43
38 44 // Check if the WidgetManager class is defined.
39 45 this.test.assert(this.evaluate(function() {
40 46 return IPython.WidgetManager !== undefined;
41 47 }), 'WidgetManager class is defined');
42 48 });
43 49
44 50 index = this.append_cell(
45 51 'from IPython.html import widgets\n' +
46 52 'from IPython.display import display, clear_output\n' +
47 53 'print("Success")');
48 54 this.execute_cell_then(index);
49 55
50 56 this.then(function () {
51 57 // Check if the widget manager has been instantiated.
52 58 this.test.assert(this.evaluate(function() {
53 59 return IPython.notebook.kernel.widget_manager !== undefined;
54 60 }), 'Notebook widget manager instantiated');
55 61
56 62 // Functions that can be used to test the packing and unpacking APIs
57 63 var that = this;
58 64 var test_pack = function (input) {
59 65 var output = that.evaluate(function(input) {
60 66 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
61 67 var results = model._pack_models(input);
62 68 return results;
63 69 }, {input: input});
64 70 that.test.assert(recursive_compare(input, output),
65 71 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
66 72 };
67 73 var test_unpack = function (input) {
68 74 var output = that.evaluate(function(input) {
69 75 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
70 76 var results = model._unpack_models(input);
71 77 return results;
72 78 }, {input: input});
73 79 that.test.assert(recursive_compare(input, output),
74 80 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
75 81 };
76 82 var test_packing = function(input) {
77 83 test_pack(input);
78 84 test_unpack(input);
79 85 };
80 86
81 87 test_packing({0: 'hi', 1: 'bye'});
82 88 test_packing(['hi', 'bye']);
83 89 test_packing(['hi', 5]);
84 90 test_packing(['hi', '5']);
85 91 test_packing([1.0, 0]);
86 92 test_packing([1.0, false]);
87 93 test_packing([1, false]);
88 94 test_packing([1, false, {a: 'hi'}]);
89 95 test_packing([1, false, ['hi']]);
90 96
91 97 // Test multi-set, single touch code. First create a custom widget.
92 98 this.evaluate(function() {
93 99 var MultiSetView = IPython.DOMWidgetView.extend({
94 100 render: function(){
95 101 this.model.set('a', 1);
96 102 this.model.set('b', 2);
97 103 this.model.set('c', 3);
98 104 this.touch();
99 105 },
100 106 });
101 107 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
102 108 }, {});
103 109 });
104 110
105 111 // Try creating the multiset widget, verify that sets the values correctly.
106 112 var multiset = {};
107 113 multiset.index = this.append_cell(
108 114 'from IPython.utils.traitlets import Unicode, CInt\n' +
109 115 'class MultiSetWidget(widgets.Widget):\n' +
110 116 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
111 117 ' a = CInt(0, sync=True)\n' +
112 118 ' b = CInt(0, sync=True)\n' +
113 119 ' c = CInt(0, sync=True)\n' +
114 120 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
115 121 ' def _handle_receive_state(self, sync_data):\n' +
116 122 ' widgets.Widget._handle_receive_state(self, sync_data)\n'+
117 123 ' self.d = len(sync_data)\n' +
118 124 'multiset = MultiSetWidget()\n' +
119 125 'display(multiset)\n' +
120 126 'print(multiset.model_id)');
121 127 this.execute_cell_then(multiset.index, function(index) {
122 128 multiset.model_id = this.get_output_cell(index).text.trim();
123 129 });
124 130
125 131 this.wait_for_widget(multiset);
126 132
127 133 index = this.append_cell(
128 134 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
129 135 this.execute_cell_then(index, function(index) {
130 136 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
131 137 'Multiple model.set calls and one view.touch update state in back-end.');
132 138 });
133 139
134 140 index = this.append_cell(
135 141 'print("%d" % (multiset.d))');
136 142 this.execute_cell_then(index, function(index) {
137 143 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
138 144 'Multiple model.set calls sent a partial state.');
139 145 });
140 146
141 147 var textbox = {};
142 148 throttle_index = this.append_cell(
143 149 'import time\n' +
144 150 'textbox = widgets.TextWidget()\n' +
145 151 'display(textbox)\n' +
146 152 'textbox.add_class("my-throttle-textbox")\n' +
147 153 'def handle_change(name, old, new):\n' +
148 154 ' print(len(new))\n' +
149 155 ' time.sleep(0.5)\n' +
150 156 'textbox.on_trait_change(handle_change, "value")\n' +
151 157 'print(textbox.model_id)');
152 158 this.execute_cell_then(throttle_index, function(index){
153 159 textbox.model_id = this.get_output_cell(index).text.trim();
154 160
155 161 this.test.assert(this.cell_element_exists(index,
156 162 '.widget-area .widget-subarea'),
157 163 'Widget subarea exists.');
158 164
159 165 this.test.assert(this.cell_element_exists(index,
160 166 '.my-throttle-textbox'), 'Textbox exists.');
161 167
162 168 // Send 20 characters
163 169 this.sendKeys('.my-throttle-textbox', '....................');
164 170 });
165 171
166 172 this.wait_for_widget(textbox);
167 173
168 174 this.then(function () {
169 175 var outputs = this.evaluate(function(i) {
170 176 return IPython.notebook.get_cell(i).output_area.outputs;
171 177 }, {i : throttle_index});
172 178
173 179 // Only 4 outputs should have printed, but because of timing, sometimes
174 180 // 5 outputs will print. All we need to do is verify num outputs <= 5
175 181 // because that is much less than 20.
176 182 this.test.assert(outputs.length <= 5, 'Messages throttled.');
177 183
178 184 // We also need to verify that the last state sent was correct.
179 185 var last_state = outputs[outputs.length-1].text;
180 186 this.test.assertEquals(last_state, "20\n", "Last state sent when throttling.");
181 187 });
182 188 });
@@ -1,520 +1,522
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Suite Runner.
3 3
4 4 This module provides a main entry point to a user script to test IPython
5 5 itself from the command line. There are two ways of running this script:
6 6
7 7 1. With the syntax `iptest all`. This runs our entire test suite by
8 8 calling this script (with different arguments) recursively. This
9 9 causes modules and package to be tested in different processes, using nose
10 10 or trial where appropriate.
11 11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 12 the script simply calls nose, but with special command line flags and
13 13 plugins loaded.
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2009-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27 from __future__ import print_function
28 28
29 29 # Stdlib
30 30 import glob
31 31 from io import BytesIO
32 32 import os
33 33 import os.path as path
34 34 import sys
35 35 from threading import Thread, Lock, Event
36 36 import warnings
37 37
38 38 # Now, proceed to import nose itself
39 39 import nose.plugins.builtin
40 40 from nose.plugins.xunit import Xunit
41 41 from nose import SkipTest
42 42 from nose.core import TestProgram
43 43 from nose.plugins import Plugin
44 44 from nose.util import safe_str
45 45
46 46 # Our own imports
47 47 from IPython.utils.process import is_cmd_found
48 48 from IPython.utils.importstring import import_item
49 49 from IPython.testing.plugin.ipdoctest import IPythonDoctest
50 50 from IPython.external.decorators import KnownFailure, knownfailureif
51 51
52 52 pjoin = path.join
53 53
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Globals
57 57 #-----------------------------------------------------------------------------
58 58
59 59
60 60 #-----------------------------------------------------------------------------
61 61 # Warnings control
62 62 #-----------------------------------------------------------------------------
63 63
64 64 # Twisted generates annoying warnings with Python 2.6, as will do other code
65 65 # that imports 'sets' as of today
66 66 warnings.filterwarnings('ignore', 'the sets module is deprecated',
67 67 DeprecationWarning )
68 68
69 69 # This one also comes from Twisted
70 70 warnings.filterwarnings('ignore', 'the sha module is deprecated',
71 71 DeprecationWarning)
72 72
73 73 # Wx on Fedora11 spits these out
74 74 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
75 75 UserWarning)
76 76
77 77 # ------------------------------------------------------------------------------
78 78 # Monkeypatch Xunit to count known failures as skipped.
79 79 # ------------------------------------------------------------------------------
80 80 def monkeypatch_xunit():
81 81 try:
82 82 knownfailureif(True)(lambda: None)()
83 83 except Exception as e:
84 84 KnownFailureTest = type(e)
85 85
86 86 def addError(self, test, err, capt=None):
87 87 if issubclass(err[0], KnownFailureTest):
88 88 err = (SkipTest,) + err[1:]
89 89 return self.orig_addError(test, err, capt)
90 90
91 91 Xunit.orig_addError = Xunit.addError
92 92 Xunit.addError = addError
93 93
94 94 #-----------------------------------------------------------------------------
95 95 # Check which dependencies are installed and greater than minimum version.
96 96 #-----------------------------------------------------------------------------
97 97 def extract_version(mod):
98 98 return mod.__version__
99 99
100 100 def test_for(item, min_version=None, callback=extract_version):
101 101 """Test to see if item is importable, and optionally check against a minimum
102 102 version.
103 103
104 104 If min_version is given, the default behavior is to check against the
105 105 `__version__` attribute of the item, but specifying `callback` allows you to
106 106 extract the value you are interested in. e.g::
107 107
108 108 In [1]: import sys
109 109
110 110 In [2]: from IPython.testing.iptest import test_for
111 111
112 112 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
113 113 Out[3]: True
114 114
115 115 """
116 116 try:
117 117 check = import_item(item)
118 118 except (ImportError, RuntimeError):
119 119 # GTK reports Runtime error if it can't be initialized even if it's
120 120 # importable.
121 121 return False
122 122 else:
123 123 if min_version:
124 124 if callback:
125 125 # extra processing step to get version to compare
126 126 check = callback(check)
127 127
128 128 return check >= min_version
129 129 else:
130 130 return True
131 131
132 132 # Global dict where we can store information on what we have and what we don't
133 133 # have available at test run time
134 134 have = {}
135 135
136 136 have['curses'] = test_for('_curses')
137 137 have['matplotlib'] = test_for('matplotlib')
138 138 have['numpy'] = test_for('numpy')
139 139 have['pexpect'] = test_for('IPython.external.pexpect')
140 140 have['pymongo'] = test_for('pymongo')
141 141 have['pygments'] = test_for('pygments')
142 142 have['qt'] = test_for('IPython.external.qt')
143 143 have['rpy2'] = test_for('rpy2')
144 144 have['sqlite3'] = test_for('sqlite3')
145 145 have['cython'] = test_for('Cython')
146 146 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
147 147 have['jinja2'] = test_for('jinja2')
148 148 have['requests'] = test_for('requests')
149 149 have['sphinx'] = test_for('sphinx')
150 150 have['casperjs'] = is_cmd_found('casperjs')
151 have['phantomjs'] = is_cmd_found('phantomjs')
152 have['slimerjs'] = is_cmd_found('slimerjs')
151 153
152 154 min_zmq = (2,1,11)
153 155
154 156 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
155 157
156 158 #-----------------------------------------------------------------------------
157 159 # Test suite definitions
158 160 #-----------------------------------------------------------------------------
159 161
160 162 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
161 163 'extensions', 'lib', 'terminal', 'testing', 'utils',
162 164 'nbformat', 'qt', 'html', 'nbconvert'
163 165 ]
164 166
165 167 class TestSection(object):
166 168 def __init__(self, name, includes):
167 169 self.name = name
168 170 self.includes = includes
169 171 self.excludes = []
170 172 self.dependencies = []
171 173 self.enabled = True
172 174
173 175 def exclude(self, module):
174 176 if not module.startswith('IPython'):
175 177 module = self.includes[0] + "." + module
176 178 self.excludes.append(module.replace('.', os.sep))
177 179
178 180 def requires(self, *packages):
179 181 self.dependencies.extend(packages)
180 182
181 183 @property
182 184 def will_run(self):
183 185 return self.enabled and all(have[p] for p in self.dependencies)
184 186
185 187 # Name -> (include, exclude, dependencies_met)
186 188 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
187 189
188 190 # Exclusions and dependencies
189 191 # ---------------------------
190 192
191 193 # core:
192 194 sec = test_sections['core']
193 195 if not have['sqlite3']:
194 196 sec.exclude('tests.test_history')
195 197 sec.exclude('history')
196 198 if not have['matplotlib']:
197 199 sec.exclude('pylabtools'),
198 200 sec.exclude('tests.test_pylabtools')
199 201
200 202 # lib:
201 203 sec = test_sections['lib']
202 204 if not have['zmq']:
203 205 sec.exclude('kernel')
204 206 # We do this unconditionally, so that the test suite doesn't import
205 207 # gtk, changing the default encoding and masking some unicode bugs.
206 208 sec.exclude('inputhookgtk')
207 209 # We also do this unconditionally, because wx can interfere with Unix signals.
208 210 # There are currently no tests for it anyway.
209 211 sec.exclude('inputhookwx')
210 212 # Testing inputhook will need a lot of thought, to figure out
211 213 # how to have tests that don't lock up with the gui event
212 214 # loops in the picture
213 215 sec.exclude('inputhook')
214 216
215 217 # testing:
216 218 sec = test_sections['testing']
217 219 # These have to be skipped on win32 because they use echo, rm, cd, etc.
218 220 # See ticket https://github.com/ipython/ipython/issues/87
219 221 if sys.platform == 'win32':
220 222 sec.exclude('plugin.test_exampleip')
221 223 sec.exclude('plugin.dtexample')
222 224
223 225 # terminal:
224 226 if (not have['pexpect']) or (not have['zmq']):
225 227 test_sections['terminal'].exclude('console')
226 228
227 229 # parallel
228 230 sec = test_sections['parallel']
229 231 sec.requires('zmq')
230 232 if not have['pymongo']:
231 233 sec.exclude('controller.mongodb')
232 234 sec.exclude('tests.test_mongodb')
233 235
234 236 # kernel:
235 237 sec = test_sections['kernel']
236 238 sec.requires('zmq')
237 239 # The in-process kernel tests are done in a separate section
238 240 sec.exclude('inprocess')
239 241 # importing gtk sets the default encoding, which we want to avoid
240 242 sec.exclude('zmq.gui.gtkembed')
241 243 if not have['matplotlib']:
242 244 sec.exclude('zmq.pylab')
243 245
244 246 # kernel.inprocess:
245 247 test_sections['kernel.inprocess'].requires('zmq')
246 248
247 249 # extensions:
248 250 sec = test_sections['extensions']
249 251 if not have['cython']:
250 252 sec.exclude('cythonmagic')
251 253 sec.exclude('tests.test_cythonmagic')
252 254 if not have['rpy2'] or not have['numpy']:
253 255 sec.exclude('rmagic')
254 256 sec.exclude('tests.test_rmagic')
255 257 # autoreload does some strange stuff, so move it to its own test section
256 258 sec.exclude('autoreload')
257 259 sec.exclude('tests.test_autoreload')
258 260 test_sections['autoreload'] = TestSection('autoreload',
259 261 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
260 262 test_group_names.append('autoreload')
261 263
262 264 # qt:
263 265 test_sections['qt'].requires('zmq', 'qt', 'pygments')
264 266
265 267 # html:
266 268 sec = test_sections['html']
267 269 sec.requires('zmq', 'tornado', 'requests', 'sqlite3')
268 270 # The notebook 'static' directory contains JS, css and other
269 271 # files for web serving. Occasionally projects may put a .py
270 272 # file in there (MathJax ships a conf.py), so we might as
271 273 # well play it safe and skip the whole thing.
272 274 sec.exclude('static')
273 275 sec.exclude('fabfile')
274 276 if not have['jinja2']:
275 277 sec.exclude('notebookapp')
276 278 if not have['pygments'] or not have['jinja2']:
277 279 sec.exclude('nbconvert')
278 280
279 281 # config:
280 282 # Config files aren't really importable stand-alone
281 283 test_sections['config'].exclude('profile')
282 284
283 285 # nbconvert:
284 286 sec = test_sections['nbconvert']
285 287 sec.requires('pygments', 'jinja2')
286 288 # Exclude nbconvert directories containing config files used to test.
287 289 # Executing the config files with iptest would cause an exception.
288 290 sec.exclude('tests.files')
289 291 sec.exclude('exporters.tests.files')
290 292 if not have['tornado']:
291 293 sec.exclude('nbconvert.post_processors.serve')
292 294 sec.exclude('nbconvert.post_processors.tests.test_serve')
293 295
294 296 #-----------------------------------------------------------------------------
295 297 # Functions and classes
296 298 #-----------------------------------------------------------------------------
297 299
298 300 def check_exclusions_exist():
299 301 from IPython.utils.path import get_ipython_package_dir
300 302 from IPython.utils.warn import warn
301 303 parent = os.path.dirname(get_ipython_package_dir())
302 304 for sec in test_sections:
303 305 for pattern in sec.exclusions:
304 306 fullpath = pjoin(parent, pattern)
305 307 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
306 308 warn("Excluding nonexistent file: %r" % pattern)
307 309
308 310
309 311 class ExclusionPlugin(Plugin):
310 312 """A nose plugin to effect our exclusions of files and directories.
311 313 """
312 314 name = 'exclusions'
313 315 score = 3000 # Should come before any other plugins
314 316
315 317 def __init__(self, exclude_patterns=None):
316 318 """
317 319 Parameters
318 320 ----------
319 321
320 322 exclude_patterns : sequence of strings, optional
321 323 Filenames containing these patterns (as raw strings, not as regular
322 324 expressions) are excluded from the tests.
323 325 """
324 326 self.exclude_patterns = exclude_patterns or []
325 327 super(ExclusionPlugin, self).__init__()
326 328
327 329 def options(self, parser, env=os.environ):
328 330 Plugin.options(self, parser, env)
329 331
330 332 def configure(self, options, config):
331 333 Plugin.configure(self, options, config)
332 334 # Override nose trying to disable plugin.
333 335 self.enabled = True
334 336
335 337 def wantFile(self, filename):
336 338 """Return whether the given filename should be scanned for tests.
337 339 """
338 340 if any(pat in filename for pat in self.exclude_patterns):
339 341 return False
340 342 return None
341 343
342 344 def wantDirectory(self, directory):
343 345 """Return whether the given directory should be scanned for tests.
344 346 """
345 347 if any(pat in directory for pat in self.exclude_patterns):
346 348 return False
347 349 return None
348 350
349 351
350 352 class StreamCapturer(Thread):
351 353 daemon = True # Don't hang if main thread crashes
352 354 started = False
353 355 def __init__(self):
354 356 super(StreamCapturer, self).__init__()
355 357 self.streams = []
356 358 self.buffer = BytesIO()
357 359 self.readfd, self.writefd = os.pipe()
358 360 self.buffer_lock = Lock()
359 361 self.stop = Event()
360 362
361 363 def run(self):
362 364 self.started = True
363 365
364 366 while not self.stop.is_set():
365 367 chunk = os.read(self.readfd, 1024)
366 368
367 369 with self.buffer_lock:
368 370 self.buffer.write(chunk)
369 371
370 372 os.close(self.readfd)
371 373 os.close(self.writefd)
372 374
373 375 def reset_buffer(self):
374 376 with self.buffer_lock:
375 377 self.buffer.truncate(0)
376 378 self.buffer.seek(0)
377 379
378 380 def get_buffer(self):
379 381 with self.buffer_lock:
380 382 return self.buffer.getvalue()
381 383
382 384 def ensure_started(self):
383 385 if not self.started:
384 386 self.start()
385 387
386 388 def halt(self):
387 389 """Safely stop the thread."""
388 390 if not self.started:
389 391 return
390 392
391 393 self.stop.set()
392 394 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
393 395 self.join()
394 396
395 397 class SubprocessStreamCapturePlugin(Plugin):
396 398 name='subprocstreams'
397 399 def __init__(self):
398 400 Plugin.__init__(self)
399 401 self.stream_capturer = StreamCapturer()
400 402 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
401 403 # This is ugly, but distant parts of the test machinery need to be able
402 404 # to redirect streams, so we make the object globally accessible.
403 405 nose.iptest_stdstreams_fileno = self.get_write_fileno
404 406
405 407 def get_write_fileno(self):
406 408 if self.destination == 'capture':
407 409 self.stream_capturer.ensure_started()
408 410 return self.stream_capturer.writefd
409 411 elif self.destination == 'discard':
410 412 return os.open(os.devnull, os.O_WRONLY)
411 413 else:
412 414 return sys.__stdout__.fileno()
413 415
414 416 def configure(self, options, config):
415 417 Plugin.configure(self, options, config)
416 418 # Override nose trying to disable plugin.
417 419 if self.destination == 'capture':
418 420 self.enabled = True
419 421
420 422 def startTest(self, test):
421 423 # Reset log capture
422 424 self.stream_capturer.reset_buffer()
423 425
424 426 def formatFailure(self, test, err):
425 427 # Show output
426 428 ec, ev, tb = err
427 429 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
428 430 if captured.strip():
429 431 ev = safe_str(ev)
430 432 out = [ev, '>> begin captured subprocess output <<',
431 433 captured,
432 434 '>> end captured subprocess output <<']
433 435 return ec, '\n'.join(out), tb
434 436
435 437 return err
436 438
437 439 formatError = formatFailure
438 440
439 441 def finalize(self, result):
440 442 self.stream_capturer.halt()
441 443
442 444
443 445 def run_iptest():
444 446 """Run the IPython test suite using nose.
445 447
446 448 This function is called when this script is **not** called with the form
447 449 `iptest all`. It simply calls nose with appropriate command line flags
448 450 and accepts all of the standard nose arguments.
449 451 """
450 452 # Apply our monkeypatch to Xunit
451 453 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
452 454 monkeypatch_xunit()
453 455
454 456 warnings.filterwarnings('ignore',
455 457 'This will be removed soon. Use IPython.testing.util instead')
456 458
457 459 arg1 = sys.argv[1]
458 460 if arg1 in test_sections:
459 461 section = test_sections[arg1]
460 462 sys.argv[1:2] = section.includes
461 463 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
462 464 section = test_sections[arg1[8:]]
463 465 sys.argv[1:2] = section.includes
464 466 else:
465 467 section = TestSection(arg1, includes=[arg1])
466 468
467 469
468 470 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
469 471
470 472 '--with-ipdoctest',
471 473 '--ipdoctest-tests','--ipdoctest-extension=txt',
472 474
473 475 # We add --exe because of setuptools' imbecility (it
474 476 # blindly does chmod +x on ALL files). Nose does the
475 477 # right thing and it tries to avoid executables,
476 478 # setuptools unfortunately forces our hand here. This
477 479 # has been discussed on the distutils list and the
478 480 # setuptools devs refuse to fix this problem!
479 481 '--exe',
480 482 ]
481 483 if '-a' not in argv and '-A' not in argv:
482 484 argv = argv + ['-a', '!crash']
483 485
484 486 if nose.__version__ >= '0.11':
485 487 # I don't fully understand why we need this one, but depending on what
486 488 # directory the test suite is run from, if we don't give it, 0 tests
487 489 # get run. Specifically, if the test suite is run from the source dir
488 490 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
489 491 # even if the same call done in this directory works fine). It appears
490 492 # that if the requested package is in the current dir, nose bails early
491 493 # by default. Since it's otherwise harmless, leave it in by default
492 494 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
493 495 argv.append('--traverse-namespace')
494 496
495 497 # use our plugin for doctesting. It will remove the standard doctest plugin
496 498 # if it finds it enabled
497 499 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
498 500 SubprocessStreamCapturePlugin() ]
499 501
500 502 # Use working directory set by parent process (see iptestcontroller)
501 503 if 'IPTEST_WORKING_DIR' in os.environ:
502 504 os.chdir(os.environ['IPTEST_WORKING_DIR'])
503 505
504 506 # We need a global ipython running in this process, but the special
505 507 # in-process group spawns its own IPython kernels, so for *that* group we
506 508 # must avoid also opening the global one (otherwise there's a conflict of
507 509 # singletons). Ultimately the solution to this problem is to refactor our
508 510 # assumptions about what needs to be a singleton and what doesn't (app
509 511 # objects should, individual shells shouldn't). But for now, this
510 512 # workaround allows the test suite for the inprocess module to complete.
511 513 if 'kernel.inprocess' not in section.name:
512 514 from IPython.testing import globalipapp
513 515 globalipapp.start_ipython()
514 516
515 517 # Now nose can run
516 518 TestProgram(argv=argv, addplugins=plugins)
517 519
518 520 if __name__ == '__main__':
519 521 run_iptest()
520 522
@@ -1,632 +1,660
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Process Controller
3 3
4 4 This module runs one or more subprocesses which will actually run the IPython
5 5 test suite.
6 6
7 7 """
8 8
9 9 # Copyright (c) IPython Development Team.
10 10 # Distributed under the terms of the Modified BSD License.
11 11
12 12 from __future__ import print_function
13 13
14 14 import argparse
15 15 import json
16 16 import multiprocessing.pool
17 17 import os
18 18 import shutil
19 19 import signal
20 20 import sys
21 21 import subprocess
22 22 import time
23 import re
23 24
24 25 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
25 26 from IPython.utils.path import compress_user
26 27 from IPython.utils.py3compat import bytes_to_str
27 28 from IPython.utils.sysinfo import get_sys_info
28 29 from IPython.utils.tempdir import TemporaryDirectory
30 from IPython.nbconvert.filters.ansi import strip_ansi
29 31
30 32 try:
31 33 # Python >= 3.3
32 34 from subprocess import TimeoutExpired
33 35 def popen_wait(p, timeout):
34 36 return p.wait(timeout)
35 37 except ImportError:
36 38 class TimeoutExpired(Exception):
37 39 pass
38 40 def popen_wait(p, timeout):
39 41 """backport of Popen.wait from Python 3"""
40 42 for i in range(int(10 * timeout)):
41 43 if p.poll() is not None:
42 44 return
43 45 time.sleep(0.1)
44 46 if p.poll() is None:
45 47 raise TimeoutExpired
46 48
47 49 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
48 50
49 51 class TestController(object):
50 52 """Run tests in a subprocess
51 53 """
52 54 #: str, IPython test suite to be executed.
53 55 section = None
54 56 #: list, command line arguments to be executed
55 57 cmd = None
56 58 #: dict, extra environment variables to set for the subprocess
57 59 env = None
58 60 #: list, TemporaryDirectory instances to clear up when the process finishes
59 61 dirs = None
60 62 #: subprocess.Popen instance
61 63 process = None
62 64 #: str, process stdout+stderr
63 65 stdout = None
64 66
65 67 def __init__(self):
66 68 self.cmd = []
67 69 self.env = {}
68 70 self.dirs = []
69 71
70 72 def setup(self):
71 73 """Create temporary directories etc.
72 74
73 75 This is only called when we know the test group will be run. Things
74 76 created here may be cleaned up by self.cleanup().
75 77 """
76 78 pass
77 79
78 80 def launch(self, buffer_output=False):
79 81 # print('*** ENV:', self.env) # dbg
80 82 # print('*** CMD:', self.cmd) # dbg
81 83 env = os.environ.copy()
82 84 env.update(self.env)
83 85 output = subprocess.PIPE if buffer_output else None
84 86 stdout = subprocess.STDOUT if buffer_output else None
85 87 self.process = subprocess.Popen(self.cmd, stdout=output,
86 88 stderr=stdout, env=env)
87 89
88 90 def wait(self):
89 91 self.stdout, _ = self.process.communicate()
90 92 return self.process.returncode
91 93
92 94 def print_extra_info(self):
93 95 """Print extra information about this test run.
94 96
95 97 If we're running in parallel and showing the concise view, this is only
96 98 called if the test group fails. Otherwise, it's called before the test
97 99 group is started.
98 100
99 101 The base implementation does nothing, but it can be overridden by
100 102 subclasses.
101 103 """
102 104 return
103 105
104 106 def cleanup_process(self):
105 107 """Cleanup on exit by killing any leftover processes."""
106 108 subp = self.process
107 109 if subp is None or (subp.poll() is not None):
108 110 return # Process doesn't exist, or is already dead.
109 111
110 112 try:
111 113 print('Cleaning up stale PID: %d' % subp.pid)
112 114 subp.kill()
113 115 except: # (OSError, WindowsError) ?
114 116 # This is just a best effort, if we fail or the process was
115 117 # really gone, ignore it.
116 118 pass
117 119 else:
118 120 for i in range(10):
119 121 if subp.poll() is None:
120 122 time.sleep(0.1)
121 123 else:
122 124 break
123 125
124 126 if subp.poll() is None:
125 127 # The process did not die...
126 128 print('... failed. Manual cleanup may be required.')
127 129
128 130 def cleanup(self):
129 131 "Kill process if it's still alive, and clean up temporary directories"
130 132 self.cleanup_process()
131 133 for td in self.dirs:
132 134 td.cleanup()
133 135
134 136 __del__ = cleanup
135 137
136 138 class PyTestController(TestController):
137 139 """Run Python tests using IPython.testing.iptest"""
138 140 #: str, Python command to execute in subprocess
139 141 pycmd = None
140 142
141 143 def __init__(self, section, options):
142 144 """Create new test runner."""
143 145 TestController.__init__(self)
144 146 self.section = section
145 147 # pycmd is put into cmd[2] in PyTestController.launch()
146 148 self.cmd = [sys.executable, '-c', None, section]
147 149 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
148 150 self.options = options
149 151
150 152 def setup(self):
151 153 ipydir = TemporaryDirectory()
152 154 self.dirs.append(ipydir)
153 155 self.env['IPYTHONDIR'] = ipydir.name
154 156 self.workingdir = workingdir = TemporaryDirectory()
155 157 self.dirs.append(workingdir)
156 158 self.env['IPTEST_WORKING_DIR'] = workingdir.name
157 159 # This means we won't get odd effects from our own matplotlib config
158 160 self.env['MPLCONFIGDIR'] = workingdir.name
159 161
160 162 # From options:
161 163 if self.options.xunit:
162 164 self.add_xunit()
163 165 if self.options.coverage:
164 166 self.add_coverage()
165 167 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
166 168 self.cmd.extend(self.options.extra_args)
167 169
168 170 @property
169 171 def will_run(self):
170 172 try:
171 173 return test_sections[self.section].will_run
172 174 except KeyError:
173 175 return True
174 176
175 177 def add_xunit(self):
176 178 xunit_file = os.path.abspath(self.section + '.xunit.xml')
177 179 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
178 180
179 181 def add_coverage(self):
180 182 try:
181 183 sources = test_sections[self.section].includes
182 184 except KeyError:
183 185 sources = ['IPython']
184 186
185 187 coverage_rc = ("[run]\n"
186 188 "data_file = {data_file}\n"
187 189 "source =\n"
188 190 " {source}\n"
189 191 ).format(data_file=os.path.abspath('.coverage.'+self.section),
190 192 source="\n ".join(sources))
191 193 config_file = os.path.join(self.workingdir.name, '.coveragerc')
192 194 with open(config_file, 'w') as f:
193 195 f.write(coverage_rc)
194 196
195 197 self.env['COVERAGE_PROCESS_START'] = config_file
196 198 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
197 199
198 200 def launch(self, buffer_output=False):
199 201 self.cmd[2] = self.pycmd
200 202 super(PyTestController, self).launch(buffer_output=buffer_output)
201 203
202 204 js_prefix = 'js/'
203 205
204 206 def get_js_test_dir():
205 207 import IPython.html.tests as t
206 208 return os.path.join(os.path.dirname(t.__file__), '')
207 209
208 210 def all_js_groups():
209 211 import glob
210 212 test_dir = get_js_test_dir()
211 213 all_subdirs = glob.glob(test_dir + '[!_]*/')
212 214 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
213 215
214 216 class JSController(TestController):
215 217 """Run CasperJS tests """
216 218 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3']
217 def __init__(self, section, enabled=True):
219 def __init__(self, section, enabled=True, engine='phantomjs'):
218 220 """Create new test runner."""
219 221 TestController.__init__(self)
222 self.engine = engine
220 223 self.section = section
221 224 self.enabled = enabled
225 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
222 226 js_test_dir = get_js_test_dir()
223 227 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
224 228 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
225 self.cmd = ['casperjs', 'test', includes, test_cases]
229 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
226 230
227 231 def setup(self):
228 232 self.ipydir = TemporaryDirectory()
229 233 self.nbdir = TemporaryDirectory()
230 234 self.dirs.append(self.ipydir)
231 235 self.dirs.append(self.nbdir)
232 236 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir1', u'sub ∂ir 1a')))
233 237 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir2', u'sub ∂ir 1b')))
234 238
235 239 # start the ipython notebook, so we get the port number
236 240 self.server_port = 0
237 241 self._init_server()
238 242 if self.server_port:
239 243 self.cmd.append("--port=%i" % self.server_port)
240 244 else:
241 245 # don't launch tests if the server didn't start
242 246 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
243 247
248 def launch(self, buffer_output):
249 # If the engine is SlimerJS, we need to buffer the output because
250 # SlimerJS does not support exit codes, therefor CasperJS always returns
251 # 0 which is a false positive.
252 buffer_output = (self.engine == 'slimerjs') or buffer_output
253 super(JSController, self).launch(buffer_output=buffer_output)
254
255 def wait(self, *pargs, **kwargs):
256 """Wait for the JSController to finish"""
257 ret = super(JSController, self).wait(*pargs, **kwargs)
258 # If this is a SlimerJS controller, echo the captured output.
259 if self.engine == 'slimerjs':
260 print(self.stdout)
261 # Return True if a failure occured.
262 return self.slimer_failure.search(strip_ansi(self.stdout))
263 else:
264 return ret
265
244 266 def print_extra_info(self):
245 267 print("Running tests with notebook directory %r" % self.nbdir.name)
246 268
247 269 @property
248 270 def will_run(self):
249 return self.enabled and all(have[a] for a in self.requirements)
271 return self.enabled and all(have[a] for a in self.requirements + [self.engine])
250 272
251 273 def _init_server(self):
252 274 "Start the notebook server in a separate process"
253 275 self.server_command = command = [sys.executable,
254 276 '-m', 'IPython.html',
255 277 '--no-browser',
256 278 '--ipython-dir', self.ipydir.name,
257 279 '--notebook-dir', self.nbdir.name,
258 280 ]
259 281 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
260 282 # which run afoul of ipc's maximum path length.
261 283 if sys.platform.startswith('linux'):
262 284 command.append('--KernelManager.transport=ipc')
263 285 self.stream_capturer = c = StreamCapturer()
264 286 c.start()
265 287 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
266 288 self.server_info_file = os.path.join(self.ipydir.name,
267 289 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
268 290 )
269 291 self._wait_for_server()
270 292
271 293 def _wait_for_server(self):
272 294 """Wait 30 seconds for the notebook server to start"""
273 295 for i in range(300):
274 296 if self.server.poll() is not None:
275 297 return self._failed_to_start()
276 298 if os.path.exists(self.server_info_file):
277 299 try:
278 300 self._load_server_info()
279 301 except ValueError:
280 302 # If the server is halfway through writing the file, we may
281 303 # get invalid JSON; it should be ready next iteration.
282 304 pass
283 305 else:
284 306 return
285 307 time.sleep(0.1)
286 308 print("Notebook server-info file never arrived: %s" % self.server_info_file,
287 309 file=sys.stderr
288 310 )
289 311
290 312 def _failed_to_start(self):
291 313 """Notebook server exited prematurely"""
292 314 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
293 315 print("Notebook failed to start: ", file=sys.stderr)
294 316 print(self.server_command)
295 317 print(captured, file=sys.stderr)
296 318
297 319 def _load_server_info(self):
298 320 """Notebook server started, load connection info from JSON"""
299 321 with open(self.server_info_file) as f:
300 322 info = json.load(f)
301 323 self.server_port = info['port']
302 324
303 325 def cleanup(self):
304 326 try:
305 327 self.server.terminate()
306 328 except OSError:
307 329 # already dead
308 330 pass
309 331 # wait 10s for the server to shutdown
310 332 try:
311 333 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
312 334 except TimeoutExpired:
313 335 # server didn't terminate, kill it
314 336 try:
315 337 print("Failed to terminate notebook server, killing it.",
316 338 file=sys.stderr
317 339 )
318 340 self.server.kill()
319 341 except OSError:
320 342 # already dead
321 343 pass
322 344 # wait another 10s
323 345 try:
324 346 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
325 347 except TimeoutExpired:
326 348 print("Notebook server still running (%s)" % self.server_info_file,
327 349 file=sys.stderr
328 350 )
329 351
330 352 self.stream_capturer.halt()
331 353 TestController.cleanup(self)
332 354
333 355
334 356 def prepare_controllers(options):
335 357 """Returns two lists of TestController instances, those to run, and those
336 358 not to run."""
337 359 testgroups = options.testgroups
338 360 if testgroups:
339 361 if 'js' in testgroups:
340 362 js_testgroups = all_js_groups()
341 363 else:
342 364 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
343 365
344 366 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
345 367 else:
346 368 py_testgroups = py_test_group_names
347 369 if not options.all:
348 370 js_testgroups = []
349 371 test_sections['parallel'].enabled = False
350 372 else:
351 373 js_testgroups = all_js_groups()
352 374
353 c_js = [JSController(name) for name in js_testgroups]
375 engine = 'phantomjs' if have['phantomjs'] and not options.slimerjs else 'slimerjs'
376 c_js = [JSController(name, engine=engine) for name in js_testgroups]
354 377 c_py = [PyTestController(name, options) for name in py_testgroups]
355 378
356 379 controllers = c_py + c_js
357 380 to_run = [c for c in controllers if c.will_run]
358 381 not_run = [c for c in controllers if not c.will_run]
359 382 return to_run, not_run
360 383
361 384 def do_run(controller, buffer_output=True):
362 385 """Setup and run a test controller.
363 386
364 387 If buffer_output is True, no output is displayed, to avoid it appearing
365 388 interleaved. In this case, the caller is responsible for displaying test
366 389 output on failure.
367 390
368 391 Returns
369 392 -------
370 393 controller : TestController
371 394 The same controller as passed in, as a convenience for using map() type
372 395 APIs.
373 396 exitcode : int
374 397 The exit code of the test subprocess. Non-zero indicates failure.
375 398 """
376 399 try:
377 400 try:
378 401 controller.setup()
379 402 if not buffer_output:
380 403 controller.print_extra_info()
381 404 controller.launch(buffer_output=buffer_output)
382 405 except Exception:
383 406 import traceback
384 407 traceback.print_exc()
385 408 return controller, 1 # signal failure
386 409
387 410 exitcode = controller.wait()
388 411 return controller, exitcode
389 412
390 413 except KeyboardInterrupt:
391 414 return controller, -signal.SIGINT
392 415 finally:
393 416 controller.cleanup()
394 417
395 418 def report():
396 419 """Return a string with a summary report of test-related variables."""
397 420 inf = get_sys_info()
398 421 out = []
399 422 def _add(name, value):
400 423 out.append((name, value))
401 424
402 425 _add('IPython version', inf['ipython_version'])
403 426 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
404 427 _add('IPython package', compress_user(inf['ipython_path']))
405 428 _add('Python version', inf['sys_version'].replace('\n',''))
406 429 _add('sys.executable', compress_user(inf['sys_executable']))
407 430 _add('Platform', inf['platform'])
408 431
409 432 width = max(len(n) for (n,v) in out)
410 433 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
411 434
412 435 avail = []
413 436 not_avail = []
414 437
415 438 for k, is_avail in have.items():
416 439 if is_avail:
417 440 avail.append(k)
418 441 else:
419 442 not_avail.append(k)
420 443
421 444 if avail:
422 445 out.append('\nTools and libraries available at test time:\n')
423 446 avail.sort()
424 447 out.append(' ' + ' '.join(avail)+'\n')
425 448
426 449 if not_avail:
427 450 out.append('\nTools and libraries NOT available at test time:\n')
428 451 not_avail.sort()
429 452 out.append(' ' + ' '.join(not_avail)+'\n')
430 453
431 454 return ''.join(out)
432 455
433 456 def run_iptestall(options):
434 457 """Run the entire IPython test suite by calling nose and trial.
435 458
436 459 This function constructs :class:`IPTester` instances for all IPython
437 460 modules and package and then runs each of them. This causes the modules
438 461 and packages of IPython to be tested each in their own subprocess using
439 462 nose.
440 463
441 464 Parameters
442 465 ----------
443 466
444 467 All parameters are passed as attributes of the options object.
445 468
446 469 testgroups : list of str
447 470 Run only these sections of the test suite. If empty, run all the available
448 471 sections.
449 472
450 473 fast : int or None
451 474 Run the test suite in parallel, using n simultaneous processes. If None
452 475 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
453 476
454 477 inc_slow : bool
455 478 Include slow tests, like IPython.parallel. By default, these tests aren't
456 479 run.
457 480
481 slimerjs : bool
482 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
483
458 484 xunit : bool
459 485 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
460 486
461 487 coverage : bool or str
462 488 Measure code coverage from tests. True will store the raw coverage data,
463 489 or pass 'html' or 'xml' to get reports.
464 490
465 491 extra_args : list
466 492 Extra arguments to pass to the test subprocesses, e.g. '-v'
467 493 """
468 494 to_run, not_run = prepare_controllers(options)
469 495
470 496 def justify(ltext, rtext, width=70, fill='-'):
471 497 ltext += ' '
472 498 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
473 499 return ltext + rtext
474 500
475 501 # Run all test runners, tracking execution time
476 502 failed = []
477 503 t_start = time.time()
478 504
479 505 print()
480 506 if options.fast == 1:
481 507 # This actually means sequential, i.e. with 1 job
482 508 for controller in to_run:
483 509 print('Test group:', controller.section)
484 510 sys.stdout.flush() # Show in correct order when output is piped
485 511 controller, res = do_run(controller, buffer_output=False)
486 512 if res:
487 513 failed.append(controller)
488 514 if res == -signal.SIGINT:
489 515 print("Interrupted")
490 516 break
491 517 print()
492 518
493 519 else:
494 520 # Run tests concurrently
495 521 try:
496 522 pool = multiprocessing.pool.ThreadPool(options.fast)
497 523 for (controller, res) in pool.imap_unordered(do_run, to_run):
498 524 res_string = 'OK' if res == 0 else 'FAILED'
499 525 print(justify('Test group: ' + controller.section, res_string))
500 526 if res:
501 527 controller.print_extra_info()
502 528 print(bytes_to_str(controller.stdout))
503 529 failed.append(controller)
504 530 if res == -signal.SIGINT:
505 531 print("Interrupted")
506 532 break
507 533 except KeyboardInterrupt:
508 534 return
509 535
510 536 for controller in not_run:
511 537 print(justify('Test group: ' + controller.section, 'NOT RUN'))
512 538
513 539 t_end = time.time()
514 540 t_tests = t_end - t_start
515 541 nrunners = len(to_run)
516 542 nfail = len(failed)
517 543 # summarize results
518 544 print('_'*70)
519 545 print('Test suite completed for system with the following information:')
520 546 print(report())
521 547 took = "Took %.3fs." % t_tests
522 548 print('Status: ', end='')
523 549 if not failed:
524 550 print('OK (%d test groups).' % nrunners, took)
525 551 else:
526 552 # If anything went wrong, point out what command to rerun manually to
527 553 # see the actual errors and individual summary
528 554 failed_sections = [c.section for c in failed]
529 555 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
530 556 nrunners, ', '.join(failed_sections)), took)
531 557 print()
532 558 print('You may wish to rerun these, with:')
533 559 print(' iptest', *failed_sections)
534 560 print()
535 561
536 562 if options.coverage:
537 563 from coverage import coverage
538 564 cov = coverage(data_file='.coverage')
539 565 cov.combine()
540 566 cov.save()
541 567
542 568 # Coverage HTML report
543 569 if options.coverage == 'html':
544 570 html_dir = 'ipy_htmlcov'
545 571 shutil.rmtree(html_dir, ignore_errors=True)
546 572 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
547 573 sys.stdout.flush()
548 574
549 575 # Custom HTML reporter to clean up module names.
550 576 from coverage.html import HtmlReporter
551 577 class CustomHtmlReporter(HtmlReporter):
552 578 def find_code_units(self, morfs):
553 579 super(CustomHtmlReporter, self).find_code_units(morfs)
554 580 for cu in self.code_units:
555 581 nameparts = cu.name.split(os.sep)
556 582 if 'IPython' not in nameparts:
557 583 continue
558 584 ix = nameparts.index('IPython')
559 585 cu.name = '.'.join(nameparts[ix:])
560 586
561 587 # Reimplement the html_report method with our custom reporter
562 588 cov._harvest_data()
563 589 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
564 590 html_title='IPython test coverage',
565 591 )
566 592 reporter = CustomHtmlReporter(cov, cov.config)
567 593 reporter.report(None)
568 594 print('done.')
569 595
570 596 # Coverage XML report
571 597 elif options.coverage == 'xml':
572 598 cov.xml_report(outfile='ipy_coverage.xml')
573 599
574 600 if failed:
575 601 # Ensure that our exit code indicates failure
576 602 sys.exit(1)
577 603
578 604 argparser = argparse.ArgumentParser(description='Run IPython test suite')
579 605 argparser.add_argument('testgroups', nargs='*',
580 606 help='Run specified groups of tests. If omitted, run '
581 607 'all tests.')
582 608 argparser.add_argument('--all', action='store_true',
583 609 help='Include slow tests not run by default.')
610 argparser.add_argument('--slimerjs', action='store_true',
611 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
584 612 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
585 613 help='Run test sections in parallel. This starts as many '
586 614 'processes as you have cores, or you can specify a number.')
587 615 argparser.add_argument('--xunit', action='store_true',
588 616 help='Produce Xunit XML results')
589 617 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
590 618 help="Measure test coverage. Specify 'html' or "
591 619 "'xml' to get reports.")
592 620 argparser.add_argument('--subproc-streams', default='capture',
593 621 help="What to do with stdout/stderr from subprocesses. "
594 622 "'capture' (default), 'show' and 'discard' are the options.")
595 623
596 624 def default_options():
597 625 """Get an argparse Namespace object with the default arguments, to pass to
598 626 :func:`run_iptestall`.
599 627 """
600 628 options = argparser.parse_args([])
601 629 options.extra_args = []
602 630 return options
603 631
604 632 def main():
605 633 # iptest doesn't work correctly if the working directory is the
606 634 # root of the IPython source tree. Tell the user to avoid
607 635 # frustration.
608 636 if os.path.exists(os.path.join(os.getcwd(),
609 637 'IPython', 'testing', '__main__.py')):
610 638 print("Don't run iptest from the IPython source directory",
611 639 file=sys.stderr)
612 640 sys.exit(1)
613 641 # Arguments after -- should be passed through to nose. Argparse treats
614 642 # everything after -- as regular positional arguments, so we separate them
615 643 # first.
616 644 try:
617 645 ix = sys.argv.index('--')
618 646 except ValueError:
619 647 to_parse = sys.argv[1:]
620 648 extra_args = []
621 649 else:
622 650 to_parse = sys.argv[1:ix]
623 651 extra_args = sys.argv[ix+1:]
624 652
625 653 options = argparser.parse_args(to_parse)
626 654 options.extra_args = extra_args
627 655
628 656 run_iptestall(options)
629 657
630 658
631 659 if __name__ == '__main__':
632 660 main()
General Comments 0
You need to be logged in to leave comments. Login now