10111: Merge branch 'master' into 10111-collection-labels
[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   def parse_browser_timestamp t
9     # Timestamps are displayed in the browser's time zone (which can
10     # differ from ours) and they come from toLocaleTimeString (which
11     # means they don't necessarily tell us which time zone they're
12     # using). In order to make sense of them, we need to ask the
13     # browser to parse them and generate a timestamp that can be
14     # parsed reliably.
15     #
16     # Note: Even with all this help, phantomjs seem to behave badly
17     # when parsing timestamps on the other side of a DST transition.
18     # See skipped tests below.
19
20     # In some locales (e.g., en_CA.UTF-8) Firefox can't parse what its
21     # own toLocaleString() puts out.
22     t.sub!(/(\d\d\d\d)-(\d\d)-(\d\d)/, '\2/\3/\1')
23
24     if /(\d+:\d+ [AP]M) (\d+\/\d+\/\d+)/ =~ t
25       # Currently dates.js renders timestamps as
26       # '{t.toLocaleTimeString()} {t.toLocaleDateString()}' which even
27       # en_US browsers can't make sense of. First we need to flip it
28       # around so it looks like what toLocaleString() would have made.
29       t = $~[2] + ', ' + $~[1]
30     end
31
32     utc = page.evaluate_script("new Date('#{t}').toUTCString()")
33     DateTime.parse(utc).to_time
34   end
35
36   if false
37     # No need to test (or mention) these all the time. If they start
38     # working (without need_selenium) then some real tests might not
39     # need_selenium any more.
40
41     test 'phantomjs DST' do
42       skip '^^'
43       t0s = '3/8/2015, 01:59 AM'
44       t1s = '3/8/2015, 03:01 AM'
45       t0 = parse_browser_timestamp t0s
46       t1 = parse_browser_timestamp t1s
47       assert_equal 120, t1-t0, "'#{t0s}' to '#{t1s}' was reported as #{t1-t0} seconds, should be 120"
48     end
49
50     test 'phantomjs DST 2' do
51       skip '^^'
52       t0s = '2015-03-08T10:43:00Z'
53       t1s = '2015-03-09T03:43:00Z'
54       t0 = parse_browser_timestamp page.evaluate_script("new Date('#{t0s}').toLocaleString()")
55       t1 = parse_browser_timestamp page.evaluate_script("new Date('#{t1s}').toLocaleString()")
56       assert_equal 17*3600, t1-t0, "'#{t0s}' to '#{t1s}' was reported as #{t1-t0} seconds, should be #{17*3600} (17 hours)"
57     end
58   end
59
60   test 'Create and run a pipeline' do
61     visit page_with_token('active_trustedclient', '/pipeline_templates')
62     within('tr', text: 'Two Part Pipeline Template') do
63       find('a,button', text: 'Run').click
64     end
65
66     # project chooser
67     within('.modal-dialog') do #FIXME: source of 1 test error
68       find('.selectable', text: 'A Project').click
69       find('button', text: 'Choose').click
70     end
71
72     # This pipeline needs input. So, Run should be disabled
73     page.assert_selector 'a.disabled,button.disabled', text: 'Run'
74
75     instance_page = current_path
76
77     # Add this collection to the project
78     visit '/projects'
79     find("#projects-menu").click
80     find('.dropdown-menu a,button', text: 'A Project').click
81     find('.btn', text: 'Add data').click
82     find('.dropdown-menu a,button', text: 'Copy data from another project').click
83     within('.modal-dialog') do
84       wait_for_ajax
85       first('span', text: 'foo_tag').click
86       find('.btn', text: 'Copy').click
87     end
88     using_wait_time(Capybara.default_max_wait_time * 3) do
89       wait_for_ajax
90     end
91
92     click_link 'Pipelines and processes'
93     find('tr[data-kind="arvados#pipelineInstance"]', text: '(none)').
94       find('a', text: 'Show').
95       click
96
97     assert find('p', text: 'Provide a value')
98
99     find('div.form-group', text: 'Foo/bar pair').
100       find('.btn', text: 'Choose').
101       click
102
103     within('.modal-dialog') do
104       assert(has_text?("Foo/bar pair"),
105              "pipeline input picker missing name of input")
106       wait_for_ajax
107       first('span', text: 'foo_tag').click
108       find('button', text: 'OK').click
109     end
110     wait_for_ajax
111
112     # The input, after being specified, should still be displayed (#3382)
113     assert find('div.form-group', text: 'Foo/bar pair')
114
115     # The input, after being specified, should still be editable (#3382)
116     find('div.form-group', text: 'Foo/bar pair').
117       find('.btn', text: 'Choose').click
118
119     within('.modal-dialog') do
120       assert(has_text?("Foo/bar pair"),
121              "pipeline input picker missing name of input")
122       wait_for_ajax
123       first('span', text: 'foo_tag').click
124       find('button', text: 'OK').click
125     end
126
127     # For good measure, check one last time that the input, after being specified twice, is still be displayed (#3382)
128     assert find('div.form-group', text: 'Foo/bar pair')
129
130     # Ensure that the collection's portable_data_hash, uuid and name
131     # are saved in the desired places. (#4015)
132
133     # foo_collection_in_aproject is the collection tagged with foo_tag.
134     collection = api_fixture('collections', 'foo_collection_in_aproject')
135     click_link 'Advanced'
136     click_link 'API response'
137     api_response = JSON.parse(find('div#advanced_api_response pre').text)
138     input_params = api_response['components']['part-one']['script_parameters']['input']
139     assert_equal input_params['value'], collection['portable_data_hash']
140     assert_equal input_params['selection_name'], collection['name']
141     assert_equal input_params['selection_uuid'], collection['uuid']
142
143     # "Run" button is now enabled
144     page.assert_no_selector 'a.disabled,button.disabled', text: 'Run'
145
146     first('a,button', text: 'Run').click
147
148     # Pipeline is running. We have a "Pause" button instead now.
149     page.assert_selector 'a,button', text: 'Pause'
150     find('a,button', text: 'Pause').click
151
152     # Pipeline is stopped. It should now be in paused state and Runnable again.
153     assert page.has_text? 'Paused'
154     page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
155     page.assert_selector 'a,button', text: 'Re-run with latest'
156     page.assert_selector 'a,button', text: 'Re-run options'
157
158     # Since it is test env, no jobs are created to run. So, graph not visible
159     assert page.has_no_text? 'Graph'
160   end
161
162   # Create a pipeline instance from within a project and run
163   test 'Create pipeline inside a project and run' do
164     visit page_with_token('active_trustedclient', '/projects')
165
166     # Add collection to the project using Add data button
167     find("#projects-menu").click
168     find('.dropdown-menu a,button', text: 'A Project').click
169     find('.btn', text: 'Add data').click
170     find('.dropdown-menu a,button', text: 'Copy data from another project').click
171     within('.modal-dialog') do
172       wait_for_ajax
173       first('span', text: 'foo_tag').click
174       find('.btn', text: 'Copy').click
175     end
176     using_wait_time(Capybara.default_max_wait_time * 3) do
177       wait_for_ajax
178     end
179
180     create_and_run_pipeline_in_aproject true, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false
181   end
182
183   # Create a pipeline instance from outside of a project
184   test 'Run a pipeline from dashboard' do
185     visit page_with_token('active_trustedclient')
186     create_and_run_pipeline_in_aproject false, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false
187   end
188
189   test 'view pipeline with job and see graph' do
190     visit page_with_token('active_trustedclient', '/pipeline_instances')
191     assert page.has_text? 'pipeline_with_job'
192
193     find('a', text: 'pipeline_with_job').click
194
195     # since the pipeline component has a job, expect to see the graph
196     assert page.has_text? 'Graph'
197     click_link 'Graph'
198     page.assert_selector "#provenance_graph"
199   end
200
201   test 'pipeline description' do
202     visit page_with_token('active_trustedclient', '/pipeline_instances')
203     assert page.has_text? 'pipeline_with_job'
204
205     find('a', text: 'pipeline_with_job').click
206
207     within('.arv-description-as-subtitle') do
208       find('.fa-pencil').click
209       find('.editable-input textarea').set('*Textile description for pipeline instance*')
210       find('.editable-submit').click
211     end
212     wait_for_ajax
213
214     # verify description
215     assert page.has_no_text? '*Textile description for pipeline instance*'
216     assert page.has_text? 'Textile description for pipeline instance'
217   end
218
219   test "JSON popup available for strange components" do
220     uuid = api_fixture("pipeline_instances")["components_is_jobspec"]["uuid"]
221     visit page_with_token("active", "/pipeline_instances/#{uuid}")
222     click_on "Components"
223     assert(page.has_no_text?("script_parameters"),
224            "components JSON visible without popup")
225     click_on "Show components JSON"
226     assert(page.has_text?("script_parameters"),
227            "components JSON not found")
228   end
229
230   def create_pipeline_from(template_name, project_name="Home")
231     # Visit the named pipeline template and create a pipeline instance from it.
232     # The instance will be created under the named project.
233     template_uuid = api_fixture("pipeline_templates", template_name, "uuid")
234     visit page_with_token("active", "/pipeline_templates/#{template_uuid}")
235     click_on "Run this pipeline"
236     within(".modal-dialog") do # FIXME: source of 3 test errors
237       # Set project for the new pipeline instance
238       find(".selectable", text: project_name).click
239       click_on "Choose"
240     end
241     assert(has_text?("This pipeline was created from the template"),
242            "did not land on pipeline instance page")
243   end
244
245   PROJECT_WITH_SEARCH_COLLECTION = "A Subproject"
246   def check_parameter_search(proj_name)
247     create_pipeline_from("parameter_with_search", proj_name)
248     search_text = api_fixture("pipeline_templates", "parameter_with_search",
249                               "components", "with-search",
250                               "script_parameters", "input", "search_for")
251     first("a.btn,button", text: "Choose").click
252     within(".modal-body") do
253       if (proj_name != PROJECT_WITH_SEARCH_COLLECTION)
254         # Switch finder modal to Subproject to find the Collection.
255         click_on proj_name
256         click_on PROJECT_WITH_SEARCH_COLLECTION
257       end
258       assert_equal(search_text, first("input").value,
259                    "parameter search not preseeded")
260       assert(has_text?(api_fixture("collections")["baz_collection_name_in_asubproject"]["name"]),
261              "baz Collection not in preseeded search results")
262     end
263   end
264
265   test "Workbench respects search_for parameter in templates" do
266     check_parameter_search(PROJECT_WITH_SEARCH_COLLECTION)
267   end
268
269   test "Workbench preserves search_for parameter after project switch" do
270     check_parameter_search("A Project")
271   end
272
273   test "enter a float for a number pipeline input" do
274     # Poltergeist either does not support the HTML 5 <input
275     # type="number">, or interferes with the associated X-Editable
276     # validation code.  If the input field has type=number (forcing an
277     # integer), this test will yield a false positive under
278     # Poltergeist.  --Brett, 2015-02-05
279     need_selenium "for strict X-Editable input validation"
280     create_pipeline_from("template_with_dataclass_number")
281     INPUT_SELECTOR =
282       ".editable[data-name='[components][work][script_parameters][input][value]']"
283     find(INPUT_SELECTOR).click
284     find(".editable-input input").set("12.34")
285     find("#editable-submit").click
286     assert_no_selector(".editable-popup")
287     assert_selector(INPUT_SELECTOR, text: "12.34")
288   end
289
290   [
291     [true, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false],
292     [false, 'Two Part Pipeline Template', 'foo_collection_in_aproject', false],
293     [true, 'Two Part Template with dataclass File', 'foo_collection_in_aproject', true],
294     [false, 'Two Part Template with dataclass File', 'foo_collection_in_aproject', true],
295     [true, 'Two Part Pipeline Template', 'collection_with_no_name_in_aproject', false],
296   ].each do |in_aproject, template_name, collection, choose_file|
297     test "Run pipeline instance in #{in_aproject} with #{template_name} with #{collection} file #{choose_file}" do
298       if in_aproject
299         visit page_with_token 'active', \
300         '/projects/'+api_fixture('groups')['aproject']['uuid']
301       else
302         visit page_with_token 'active', '/'
303       end
304
305       # need bigger modal size when choosing a file from collection
306       if Capybara.current_driver == :selenium
307         Capybara.current_session.driver.browser.manage.window.resize_to(1200, 800)
308       end
309
310       create_and_run_pipeline_in_aproject in_aproject, template_name, collection, choose_file
311       instance_path = current_path
312
313       # Pause the pipeline
314       find('a,button', text: 'Pause').click
315       assert page.has_text? 'Paused'
316       page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
317       page.assert_selector 'a,button', text: 'Re-run with latest'
318       page.assert_selector 'a,button', text: 'Re-run options'
319
320       # Verify that the newly created instance is created in the right project.
321       assert page.has_text? 'Home'
322       if in_aproject
323         assert page.has_text? 'A Project'
324       else
325         assert page.has_no_text? 'A Project'
326       end
327     end
328   end
329
330   [
331     ['active', false, false, false],
332     ['active', false, false, true],
333     ['active', true, false, false],
334     ['active', true, true, false],
335     ['active', true, false, true],
336     ['active', true, true, true],
337     ['project_viewer', false, false, true],
338     ['project_viewer', true, true, true],
339   ].each do |user, with_options, choose_options, in_aproject|
340     test "Rerun pipeline instance as #{user} using options #{with_options} #{choose_options} in #{in_aproject}" do
341       if in_aproject
342         path = '/pipeline_instances/'+api_fixture('pipeline_instances')['pipeline_owned_by_active_in_aproject']['uuid']
343       else
344         path = '/pipeline_instances/'+api_fixture('pipeline_instances')['pipeline_owned_by_active_in_home']['uuid']
345       end
346
347       visit page_with_token(user, path)
348
349       page.assert_selector 'a,button', text: 'Re-run with latest'
350       page.assert_selector 'a,button', text: 'Re-run options'
351
352       if user == 'project_viewer' && in_aproject
353         assert page.has_text? 'A Project'
354       end
355
356       # Now re-run the pipeline
357       if with_options
358         assert_triggers_dom_event 'shown.bs.modal' do
359           find('a,button', text: 'Re-run options').click
360         end
361         within('.modal-dialog') do
362           page.assert_selector 'a,button', text: 'Copy and edit inputs'
363           page.assert_selector 'a,button', text: 'Run now'
364           if choose_options
365             find('button', text: 'Copy and edit inputs').click
366           else
367             find('button', text: 'Run now').click
368           end
369         end
370       else
371         find('a,button', text: 'Re-run with latest').click
372       end
373
374       # Verify that the newly created instance is created in the right
375       # project. In case of project_viewer user, since the user cannot
376       # write to the project, the pipeline should have been created in
377       # the user's Home project.
378       assert_not_equal path, current_path, 'Rerun instance path expected to be different'
379       assert_text 'Home'
380       if in_aproject && (user != 'project_viewer')
381         assert_text 'A Project'
382       else
383         assert_no_text 'A Project'
384       end
385     end
386   end
387
388   # Create and run a pipeline for 'Two Part Pipeline Template' in 'A Project'
389   def create_and_run_pipeline_in_aproject in_aproject, template_name, collection_fixture, choose_file=false
390     # collection in aproject to be used as input
391     collection = api_fixture('collections', collection_fixture)
392
393     # create a pipeline instance
394     find('.btn', text: 'Run a process').click
395     within('.modal-dialog') do
396       find('.selectable', text: template_name).click
397       find('.btn', text: 'Next: choose inputs').click
398     end
399
400     assert find('p', text: 'Provide a value')
401
402     find('div.form-group', text: 'Foo/bar pair').
403       find('.btn', text: 'Choose').
404       click
405
406     within('.modal-dialog') do
407       if in_aproject
408         assert_selector 'button.dropdown-toggle', text: 'A Project'
409         wait_for_ajax
410       else
411         assert_selector 'button.dropdown-toggle', text: 'Home'
412         wait_for_ajax
413         click_button "Home"
414         click_link "A Project"
415         wait_for_ajax
416       end
417
418       if collection_fixture == 'foo_collection_in_aproject'
419         first('span', text: 'foo_tag').click
420       elsif collection['name']
421         first('span', text: "#{collection['name']}").click
422       else
423         collection_uuid = collection['uuid']
424         find("div[data-object-uuid=#{collection_uuid}]").click
425       end
426
427       if choose_file
428         wait_for_ajax
429         find('.preview-selectable', text: 'foo').click
430       end
431       find('button', text: 'OK').click
432     end
433
434     # The input, after being specified, should still be displayed (#3382)
435     assert find('div.form-group', text: 'Foo/bar pair')
436
437     # Ensure that the collection's portable_data_hash, uuid and name
438     # are saved in the desired places. (#4015)
439     click_link 'Advanced'
440     click_link 'API response'
441
442     api_response = JSON.parse(find('div#advanced_api_response pre').text)
443     input_params = api_response['components']['part-one']['script_parameters']['input']
444     assert_equal(input_params['selection_uuid'], collection['uuid'], "Not found expected input param uuid")
445     if choose_file
446       assert_equal(input_params['value'], collection['portable_data_hash']+'/foo', "Not found expected input file param value")
447       assert_equal(input_params['selection_name'], collection['name']+'/foo', "Not found expected input file param name")
448     else
449       assert_equal(input_params['value'], collection['portable_data_hash'], "Not found expected input param value")
450       assert_equal(input_params['selection_name'], collection['name'], "Not found expected input selection name")
451     end
452
453     # "Run" button present and enabled
454     page.assert_no_selector 'a.disabled,button.disabled', text: 'Run'
455     first('a,button', text: 'Run').click
456
457     # Pipeline is running. We have a "Pause" button instead now.
458     page.assert_no_selector 'a,button', text: 'Run'
459     page.assert_no_selector 'a.disabled,button.disabled', text: 'Resume'
460     page.assert_selector 'a,button', text: 'Pause'
461
462     # Since it is test env, no jobs are created to run. So, graph not visible
463     assert page.has_no_text? 'Graph'
464   end
465
466   [
467     ['user1_with_load', 'zzzzz-d1hrv-10pipelines0001', 0], # run time 0 minutes
468     ['user1_with_load', 'zzzzz-d1hrv-10pipelines0010', 17*60*60 + 51*60], # run time 17 hours and 51 minutes
469     ['active', 'zzzzz-d1hrv-runningpipeline', nil], # state = running
470   ].each do |user, uuid, run_time|
471     test "pipeline start and finish time display for #{uuid}" do
472       need_selenium 'to parse timestamps correctly across DST boundaries'
473       visit page_with_token(user, "/pipeline_instances/#{uuid}")
474
475       assert page.has_text? 'This pipeline started at'
476       page_text = page.text
477
478       if run_time
479         match = /This pipeline started at (.*)\. It failed after (.*) at (.*)\. Check the Log/.match page_text
480       else
481         match = /This pipeline started at (.*). It has been active for(.*)/.match page_text
482       end
483       assert_not_nil(match, 'Did not find text - This pipeline started at . . . ')
484
485       start_at = match[1]
486       assert_not_nil(start_at, 'Did not find start_at time')
487
488       start_time = parse_browser_timestamp start_at
489       if run_time
490         finished_at = match[3]
491         assert_not_nil(finished_at, 'Did not find finished_at time')
492         finished_time = parse_browser_timestamp finished_at
493         assert_equal(run_time, finished_time-start_time,
494           "Time difference did not match for start_at #{start_at}, finished_at #{finished_at}, ran_for #{match[2]}")
495       else
496         match = /\d(.*)/.match match[2]
497         assert_not_nil match, 'Did not find expected match for running component'
498       end
499     end
500   end
501
502   [
503     ['fuse', nil, 2, 20],                           # has 2 as of 11-07-2014
504     ['user1_with_load', '000025pipelines', 25, 25], # owned_by the project zzzzz-j7d0g-000025pipelines, two pages
505     ['admin', 'pipeline_20', 1, 1],
506     ['active', 'no such match', 0, 0],
507   ].each do |user, search_filter, expected_min, expected_max|
508     test "scroll pipeline instances page for #{user} with search filter #{search_filter}
509           and expect #{expected_min} <= found_items <= #{expected_max}" do
510       visit page_with_token(user, "/pipeline_instances")
511
512       if search_filter
513         find('.recent-pipeline-instances-filterable-control').set(search_filter)
514         # Wait for 250ms debounce timer (see filterable.js)
515         sleep 0.350
516         wait_for_ajax
517       end
518
519       page_scrolls = expected_max/20 + 2    # scroll num_pages+2 times to test scrolling is disabled when it should be
520       within('.arv-recent-pipeline-instances') do
521         (0..page_scrolls).each do |i|
522           page.driver.scroll_to 0, 999000
523           begin
524             wait_for_ajax
525           rescue
526           end
527         end
528       end
529
530       # Verify that expected number of pipeline instances are found
531       found_items = page.all('tr[data-kind="arvados#pipelineInstance"]')
532       found_count = found_items.count
533       if expected_min == expected_max
534         assert_equal(true, found_count == expected_min,
535           "Not found expected number of items. Expected #{expected_min} and found #{found_count}")
536         assert page.has_no_text? 'request failed'
537       else
538         assert_equal(true, found_count>=expected_min,
539           "Found too few items. Expected at least #{expected_min} and found #{found_count}")
540         assert_equal(true, found_count<=expected_max,
541           "Found too many items. Expected at most #{expected_max} and found #{found_count}")
542       end
543     end
544   end
545
546   test 'render job run time when job record is inaccessible' do
547     pi = api_fixture('pipeline_instances', 'has_component_with_completed_jobs')
548     visit page_with_token 'active', '/pipeline_instances/' + pi['uuid']
549     assert_text 'Queued for '
550   end
551
552   test "job logs linked for running pipeline" do
553     pi = api_fixture("pipeline_instances", "running_pipeline_with_complete_job")
554     visit(page_with_token("active", "/pipeline_instances/#{pi['uuid']}"))
555     find(:xpath, "//a[@href='#Log']").click
556     within "#Log" do
557       assert_text "Log for previous"
558       log_link = find("a", text: "Log for previous")
559       assert_includes(log_link[:href],
560                       "/jobs/#{pi["components"]["previous"]["job"]["uuid"]}#Log")
561       assert_selector "#event_log_div"
562     end
563   end
564
565   test "job logs linked for complete pipeline" do
566     pi = api_fixture("pipeline_instances", "complete_pipeline_with_two_jobs")
567     visit(page_with_token("active", "/pipeline_instances/#{pi['uuid']}"))
568     find(:xpath, "//a[@href='#Log']").click
569     within "#Log" do
570       assert_text "Log for previous"
571       pi["components"].each do |cname, cspec|
572         log_link = find("a", text: "Log for #{cname}")
573         assert_includes(log_link[:href], "/jobs/#{cspec["job"]["uuid"]}#Log")
574       end
575       assert_no_selector "#event_log_div"
576     end
577   end
578
579   test "job logs linked for failed pipeline" do
580     pi = api_fixture("pipeline_instances", "failed_pipeline_with_two_jobs")
581     visit(page_with_token("active", "/pipeline_instances/#{pi['uuid']}"))
582     find(:xpath, "//a[@href='#Log']").click
583     within "#Log" do
584       assert_text "Log for previous"
585       pi["components"].each do |cname, cspec|
586         log_link = find("a", text: "Log for #{cname}")
587         assert_includes(log_link[:href], "/jobs/#{cspec["job"]["uuid"]}#Log")
588       end
589       assert_no_selector "#event_log_div"
590     end
591   end
592 end