Merge branch 'master' into 5349-timestamp-error-for-running-pipeline
[arvados.git] / apps / workbench / test / integration / pipeline_instances_test.rb
1 require 'integration_helper'
2
3 class PipelineInstancesTest < ActionDispatch::IntegrationTest
4   setup do
5     need_javascript
6   end
7
8   test 'Create and run a pipeline' do
9     visit page_with_token('active_trustedclient', '/pipeline_templates')
10     within('tr', text: 'Two Part Pipeline Template') do
11       find('a,button', text: 'Run').click
12     end
13
14     # project chooser
15     within('.modal-dialog') do
16       find('.selectable', text: 'A Project').click
17       find('button', text: 'Choose').click
18     end
19
20     # This pipeline needs input. So, Run should be disabled
21     page.assert_selector 'a.disabled,button.disabled', text: 'Run'
22
23     instance_page = current_path
24
25     # Add this collection to the project
26     visit '/projects'
27     find("#projects-menu").click
28     find('.dropdown-menu a,button', text: 'A Project').click
29     find('.btn', text: 'Add data').click
30     find('.dropdown-menu a,button', text: 'Copy data from another project').click
31     within('.modal-dialog') do
32       wait_for_ajax
33       first('span', text: 'foo_tag').click
34       find('.btn', text: 'Copy').click
35     end
36     using_wait_time(Capybara.default_wait_time * 3) do
37       wait_for_ajax
38     end
39
40     click_link 'Jobs and pipelines'
41     find('tr[data-kind="arvados#pipelineInstance"]', text: '(none)').
42       find('a', text: 'Show').
43       click
44
45     assert find('p', text: 'Provide a value')
46
47     find('div.form-group', text: 'Foo/bar pair').
48       find('.btn', text: 'Choose').
49       click
50
51     within('.modal-dialog') do
52       assert(has_text?("Foo/bar pair"),
53              "pipeline input picker missing name of input")
54       wait_for_ajax
55       first('span', text: 'foo_tag').click
56       find('button', text: 'OK').click
57     end
58     wait_for_ajax
59
60     # The input, after being specified, should still be displayed (#3382)
61     assert find('div.form-group', text: 'Foo/bar pair')
62
63     # The input, after being specified, should still be editable (#3382)
64     find('div.form-group', text: 'Foo/bar pair').
65       find('.btn', text: 'Choose').click
66
67     within('.modal-dialog') do
68       assert(has_text?("Foo/bar pair"),
69              "pipeline input picker missing name of input")
70       wait_for_ajax
71       first('span', text: 'foo_tag').click
72       find('button', text: 'OK').click
73     end
74
75     # For good measure, check one last time that the input, after being specified twice, is still be displayed (#3382)
76     assert find('div.form-group', text: 'Foo/bar pair')
77
78     # Ensure that the collection's portable_data_hash, uuid and name
79     # are saved in the desired places. (#4015)
80
81     # foo_collection_in_aproject is the collection tagged with foo_tag.
82     collection = api_fixture('collections', 'foo_collection_in_aproject')
83     click_link 'Advanced'
84     click_link 'API response'
85     api_response = JSON.parse(find('div#advanced_api_response pre').text)
86     input_params = api_response['components']['part-one']['script_parameters']['input']
87     assert_equal input_params['value'], collection['portable_data_hash']
88     assert_equal input_params['selection_name'], collection['name']
89     assert_equal input_params['selection_uuid'], collection['uuid']
90
91     # "Run" button is now enabled
92     page.assert_no_selector 'a.disabled,button.disabled', text: 'Run'
93
94     first('a,button', text: 'Run').click
95
96     # Pipeline is running. We have a "Pause" button instead now.
97     page.assert_selector 'a,button', text: 'Pause'
98     find('a,button', text: 'Pause').click
99
100     # Pipeline is stopped. It should now be in paused state and Runnable again.
101     assert page.has_text? 'Paused'
102     page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
103     page.assert_selector 'a,button', text: 'Re-run with latest'
104     page.assert_selector 'a,button', text: 'Re-run options'
105
106     # Since it is test env, no jobs are created to run. So, graph not visible
107     assert_not page.has_text? 'Graph'
108   end
109
110   # Create a pipeline instance from within a project and run
111   test 'Create pipeline inside a project and run' do
112     visit page_with_token('active_trustedclient', '/projects')
113
114     # Add collection to the project using Add data button
115     find("#projects-menu").click
116     find('.dropdown-menu a,button', text: 'A Project').click
117     find('.btn', text: 'Add data').click
118     find('.dropdown-menu a,button', text: 'Copy data from another project').click
119     within('.modal-dialog') do
120       wait_for_ajax
121       first('span', text: 'foo_tag').click
122       find('.btn', text: 'Copy').click
123     end
124     using_wait_time(Capybara.default_wait_time * 3) do
125       wait_for_ajax
126     end
127
128     create_and_run_pipeline_in_aproject true, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false
129   end
130
131   # Create a pipeline instance from outside of a project
132   test 'Run a pipeline from dashboard' do
133     visit page_with_token('active_trustedclient')
134     create_and_run_pipeline_in_aproject false, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false
135   end
136
137   test 'view pipeline with job and see graph' do
138     visit page_with_token('active_trustedclient', '/pipeline_instances')
139     assert page.has_text? 'pipeline_with_job'
140
141     find('a', text: 'pipeline_with_job').click
142
143     # since the pipeline component has a job, expect to see the graph
144     assert page.has_text? 'Graph'
145     click_link 'Graph'
146     page.assert_selector "#provenance_graph"
147   end
148
149   test 'pipeline description' do
150     visit page_with_token('active_trustedclient', '/pipeline_instances')
151     assert page.has_text? 'pipeline_with_job'
152
153     find('a', text: 'pipeline_with_job').click
154
155     within('.arv-description-as-subtitle') do
156       find('.fa-pencil').click
157       find('.editable-input textarea').set('*Textile description for pipeline instance*')
158       find('.editable-submit').click
159     end
160     wait_for_ajax
161
162     # verify description
163     assert page.has_no_text? '*Textile description for pipeline instance*'
164     assert page.has_text? 'Textile description for pipeline instance'
165   end
166
167   test "JSON popup available for strange components" do
168     uuid = api_fixture("pipeline_instances")["components_is_jobspec"]["uuid"]
169     visit page_with_token("active", "/pipeline_instances/#{uuid}")
170     click_on "Components"
171     assert(page.has_no_text?("script_parameters"),
172            "components JSON visible without popup")
173     click_on "Show components JSON"
174     assert(page.has_text?("script_parameters"),
175            "components JSON not found")
176   end
177
178   def create_pipeline_from(template_name, project_name="Home")
179     # Visit the named pipeline template and create a pipeline instance from it.
180     # The instance will be created under the named project.
181     template_uuid = api_fixture("pipeline_templates", template_name, "uuid")
182     visit page_with_token("active", "/pipeline_templates/#{template_uuid}")
183     click_on "Run this pipeline"
184     within(".modal-dialog") do
185       # Set project for the new pipeline instance
186       find(".selectable", text: project_name).click
187       click_on "Choose"
188     end
189     assert(has_text?("This pipeline was created from the template"),
190            "did not land on pipeline instance page")
191   end
192
193   PROJECT_WITH_SEARCH_COLLECTION = "A Subproject"
194   def check_parameter_search(proj_name)
195     create_pipeline_from("parameter_with_search", proj_name)
196     search_text = api_fixture("pipeline_templates", "parameter_with_search",
197                               "components", "with-search",
198                               "script_parameters", "input", "search_for")
199     first("a.btn,button", text: "Choose").click
200     within(".modal-body") do
201       if (proj_name != PROJECT_WITH_SEARCH_COLLECTION)
202         # Switch finder modal to Subproject to find the Collection.
203         click_on proj_name
204         click_on PROJECT_WITH_SEARCH_COLLECTION
205       end
206       assert_equal(search_text, first("input").value,
207                    "parameter search not preseeded")
208       assert(has_text?(api_fixture("collections")["baz_collection_name_in_asubproject"]["name"]),
209              "baz Collection not in preseeded search results")
210     end
211   end
212
213   test "Workbench respects search_for parameter in templates" do
214     check_parameter_search(PROJECT_WITH_SEARCH_COLLECTION)
215   end
216
217   test "Workbench preserves search_for parameter after project switch" do
218     check_parameter_search("A Project")
219   end
220
221   test "enter a float for a number pipeline input" do
222     # Poltergeist either does not support the HTML 5 <input
223     # type="number">, or interferes with the associated X-Editable
224     # validation code.  If the input field has type=number (forcing an
225     # integer), this test will yield a false positive under
226     # Poltergeist.  --Brett, 2015-02-05
227     need_selenium "for strict X-Editable input validation"
228     create_pipeline_from("template_with_dataclass_number")
229     INPUT_SELECTOR =
230       ".editable[data-name='[components][work][script_parameters][input][value]']"
231     find(INPUT_SELECTOR).click
232     find(".editable-input input").set("12.34")
233     find("#editable-submit").click
234     assert_no_selector(".editable-popup")
235     assert_selector(INPUT_SELECTOR, text: "12.34")
236   end
237
238   [
239     [true, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false],
240     [false, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false],
241     [true, 'Two Part Template with dataclass File', 'foo_collection_in_aproject', true],
242     [false, 'Two Part Template with dataclass File', 'foo_collection_in_aproject', true],
243     [true, 'Two Part Pipeline Template', 'collection_with_no_name_in_aproject', false],
244   ].each do |in_aproject, template_name, collection, choose_file|
245     test "Run pipeline instance in #{in_aproject} with #{template_name} with #{collection} file #{choose_file}" do
246       if in_aproject
247         visit page_with_token 'active', \
248         '/projects/'+api_fixture('groups')['aproject']['uuid']
249       else
250         visit page_with_token 'active', '/'
251       end
252
253       # need bigger modal size when choosing a file from collection
254       if Capybara.current_driver == :selenium
255         Capybara.current_session.driver.browser.manage.window.resize_to(1200, 800)
256       end
257
258       create_and_run_pipeline_in_aproject in_aproject, template_name, collection, choose_file
259       instance_path = current_path
260
261       # Pause the pipeline
262       find('a,button', text: 'Pause').click
263       assert page.has_text? 'Paused'
264       page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
265       page.assert_selector 'a,button', text: 'Re-run with latest'
266       page.assert_selector 'a,button', text: 'Re-run options'
267
268       # Verify that the newly created instance is created in the right project.
269       assert page.has_text? 'Home'
270       if in_aproject
271         assert page.has_text? 'A Project'
272       else
273         assert page.has_no_text? 'A Project'
274       end
275     end
276   end
277
278   [
279     ['active', false, false, false],
280     ['active', false, false, true],
281     ['active', true, false, false],
282     ['active', true, true, false],
283     ['active', true, false, true],
284     ['active', true, true, true],
285     ['project_viewer', false, false, true],
286     ['project_viewer', true, true, true],
287   ].each do |user, with_options, choose_options, in_aproject|
288     test "Rerun pipeline instance as #{user} using options #{with_options} #{choose_options} in #{in_aproject}" do
289       if in_aproject
290         path = '/pipeline_instances/'+api_fixture('pipeline_instances')['pipeline_owned_by_active_in_aproject']['uuid']
291       else
292         path = '/pipeline_instances/'+api_fixture('pipeline_instances')['pipeline_owned_by_active_in_home']['uuid']
293       end
294
295       visit page_with_token(user, path)
296
297       page.assert_selector 'a,button', text: 'Re-run with latest'
298       page.assert_selector 'a,button', text: 'Re-run options'
299
300       if user == 'project_viewer' && in_aproject
301         assert page.has_text? 'A Project'
302       end
303
304       # Now re-run the pipeline
305       if with_options
306         assert_triggers_dom_event 'shown.bs.modal' do
307           find('a,button', text: 'Re-run options').click
308         end
309         within('.modal-dialog') do
310           page.assert_selector 'a,button', text: 'Copy and edit inputs'
311           page.assert_selector 'a,button', text: 'Run now'
312           if choose_options
313             find('button', text: 'Copy and edit inputs').click
314           else
315             find('button', text: 'Run now').click
316           end
317         end
318       else
319         find('a,button', text: 'Re-run with latest').click
320       end
321
322       # Verify that the newly created instance is created in the right
323       # project. In case of project_viewer user, since the user cannot
324       # write to the project, the pipeline should have been created in
325       # the user's Home project.
326       assert_not_equal path, current_path, 'Rerun instance path expected to be different'
327       assert_text 'Home'
328       if in_aproject && (user != 'project_viewer')
329         assert_text 'A Project'
330       else
331         assert_no_text 'A Project'
332       end
333     end
334   end
335
336   # Create and run a pipeline for 'Two Part Pipeline Template' in 'A Project'
337   def create_and_run_pipeline_in_aproject in_aproject, template_name, collection_fixture, choose_file=false
338     # collection in aproject to be used as input
339     collection = api_fixture('collections', collection_fixture)
340
341     # create a pipeline instance
342     find('.btn', text: 'Run a pipeline').click
343     within('.modal-dialog') do
344       find('.selectable', text: template_name).click
345       find('.btn', text: 'Next: choose inputs').click
346     end
347
348     assert find('p', text: 'Provide a value')
349
350     find('div.form-group', text: 'Foo/bar pair').
351       find('.btn', text: 'Choose').
352       click
353
354     within('.modal-dialog') do
355       if in_aproject
356         assert_selector 'button.dropdown-toggle', text: 'A Project'
357         wait_for_ajax
358       else
359         assert_selector 'button.dropdown-toggle', text: 'Home'
360         wait_for_ajax
361         click_button "Home"
362         click_link "A Project"
363         wait_for_ajax
364       end
365
366       if collection_fixture == 'foo_collection_in_aproject'
367         first('span', text: 'foo_tag').click
368       elsif collection['name']
369         first('span', text: "#{collection['name']}").click
370       else
371         collection_uuid = collection['uuid']
372         find("div[data-object-uuid=#{collection_uuid}]").click
373       end
374
375       if choose_file
376         wait_for_ajax
377         find('.preview-selectable', text: 'foo').click
378       end
379       find('button', text: 'OK').click
380     end
381
382     # The input, after being specified, should still be displayed (#3382)
383     assert find('div.form-group', text: 'Foo/bar pair')
384
385     # Ensure that the collection's portable_data_hash, uuid and name
386     # are saved in the desired places. (#4015)
387     click_link 'Advanced'
388     click_link 'API response'
389
390     api_response = JSON.parse(find('div#advanced_api_response pre').text)
391     input_params = api_response['components']['part-one']['script_parameters']['input']
392     assert_equal(input_params['selection_uuid'], collection['uuid'], "Not found expected input param uuid")
393     if choose_file
394       assert_equal(input_params['value'], collection['portable_data_hash']+'/foo', "Not found expected input file param value")
395       assert_equal(input_params['selection_name'], collection['name']+'/foo', "Not found expected input file param name")
396     else
397       assert_equal(input_params['value'], collection['portable_data_hash'], "Not found expected input param value")
398       assert_equal(input_params['selection_name'], collection['name'], "Not found expected input selection name")
399     end
400
401     # "Run" button present and enabled
402     page.assert_no_selector 'a.disabled,button.disabled', text: 'Run'
403     first('a,button', text: 'Run').click
404
405     # Pipeline is running. We have a "Pause" button instead now.
406     page.assert_no_selector 'a,button', text: 'Run'
407     page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
408     page.assert_selector 'a,button', text: 'Pause'
409
410     # Since it is test env, no jobs are created to run. So, graph not visible
411     assert_not page.has_text? 'Graph'
412   end
413
414   [
415     ['zzzzz-d1hrv-10pipelines0001', 0], # run time 0 minutes
416     ['zzzzz-d1hrv-10pipelines0010', 17*60*60 + 51*60], # run time 17 hours and 51 minutes
417     ['zzzzz-d1hrv-10pipelines0002', nil], # state = running
418   ].each do |uuid, run_time|
419     test "pipeline start and finish time display for #{uuid}" do
420       visit page_with_token("user1_with_load", "/pipeline_instances/#{uuid}")
421
422       assert page.has_text? 'This pipeline started at'
423       page_text = page.text
424
425       if run_time
426         match = /This pipeline started at (.*)\. It failed after (.*) seconds at (.*)\. Check the Log/.match page_text
427       else
428         match = /This pipeline started at (.*). It has been active for(.*)/.match page_text
429       end
430       assert_not_nil(match, 'Did not find text - This pipeline started at . . . ')
431
432       start_at = match[1]
433       assert_not_nil(start_at, 'Did not find start_at time')
434
435       # start and finished time display is of the format '2:20 PM 10/20/2014'
436       start_time = DateTime.strptime(start_at, '%H:%M %p %m/%d/%Y').to_time
437       if run_time
438         finished_at = match[3]
439         assert_not_nil(finished_at, 'Did not find finished_at time')
440         finished_time = DateTime.strptime(finished_at, '%H:%M %p %m/%d/%Y').to_time
441         assert_equal(run_time, finished_time-start_time,
442           "Time difference did not match for start_at #{start_at}, finished_at #{finished_at}, ran_for #{match[2]}")
443       else
444         match = /\d(.*)/.match match[2]
445         assert_not_nil match, 'Did not find expected match for running component'
446       end
447     end
448   end
449
450   [
451     ['fuse', nil, 2, 20],                           # has 2 as of 11-07-2014
452     ['user1_with_load', '000025pipelines', 25, 25], # owned_by the project zzzzz-j7d0g-000025pipelines, two pages
453     ['admin', 'pipeline_20', 1, 1],
454     ['active', 'no such match', 0, 0],
455   ].each do |user, search_filter, expected_min, expected_max|
456     test "scroll pipeline instances page for #{user} with search filter #{search_filter}
457           and expect #{expected_min} <= found_items <= #{expected_max}" do
458       visit page_with_token(user, "/pipeline_instances")
459
460       if search_filter
461         find('.recent-pipeline-instances-filterable-control').set(search_filter)
462         # Wait for 250ms debounce timer (see filterable.js)
463         sleep 0.350
464         wait_for_ajax
465       end
466
467       page_scrolls = expected_max/20 + 2    # scroll num_pages+2 times to test scrolling is disabled when it should be
468       within('.arv-recent-pipeline-instances') do
469         (0..page_scrolls).each do |i|
470           page.driver.scroll_to 0, 999000
471           begin
472             wait_for_ajax
473           rescue
474           end
475         end
476       end
477
478       # Verify that expected number of pipeline instances are found
479       found_items = page.all('tr[data-kind="arvados#pipelineInstance"]')
480       found_count = found_items.count
481       if expected_min == expected_max
482         assert_equal(true, found_count == expected_min,
483           "Not found expected number of items. Expected #{expected_min} and found #{found_count}")
484         assert page.has_no_text? 'request failed'
485       else
486         assert_equal(true, found_count>=expected_min,
487           "Found too few items. Expected at least #{expected_min} and found #{found_count}")
488         assert_equal(true, found_count<=expected_max,
489           "Found too many items. Expected at most #{expected_max} and found #{found_count}")
490       end
491     end
492   end
493 end