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