Merge branch '4312-crunch-report-sdk-version' closes #4312
authorPeter Amstutz <peter.amstutz@curoverse.com>
Thu, 8 Jan 2015 18:49:05 +0000 (13:49 -0500)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Thu, 8 Jan 2015 18:49:05 +0000 (13:49 -0500)
28 files changed:
apps/workbench/app/assets/javascripts/angular_shim.js
apps/workbench/app/assets/javascripts/filterable.js
apps/workbench/app/assets/javascripts/infinite_scroll.js
apps/workbench/app/assets/javascripts/select_modal.js
apps/workbench/app/views/layouts/body.html.erb
apps/workbench/test/diagnostics/pipeline_test.rb
apps/workbench/test/integration/application_layout_test.rb
apps/workbench/test/integration/collection_upload_test.rb
apps/workbench/test/integration/collections_test.rb
apps/workbench/test/integration/errors_test.rb
apps/workbench/test/integration/filterable_infinite_scroll_test.rb
apps/workbench/test/integration/jobs_test.rb
apps/workbench/test/integration/logins_test.rb
apps/workbench/test/integration/pipeline_instances_test.rb
apps/workbench/test/integration/pipeline_templates_test.rb
apps/workbench/test/integration/projects_test.rb
apps/workbench/test/integration/report_issue_test.rb
apps/workbench/test/integration/search_box_test.rb
apps/workbench/test/integration/smoke_test.rb
apps/workbench/test/integration/user_agreements_test.rb
apps/workbench/test/integration/user_manage_account_test.rb
apps/workbench/test/integration/user_profile_test.rb
apps/workbench/test/integration/users_test.rb
apps/workbench/test/integration/virtual_machines_test.rb
apps/workbench/test/integration/websockets_test.rb
apps/workbench/test/integration_helper.rb
apps/workbench/test/performance/browsing_test.rb
services/api/script/crunch_failure_report.py [new file with mode: 0755]

index a5366e3ccba517abf23aee732f074878b1cc5743..72729cdc0abd9bed822d362b0fd5944688520a00 100644 (file)
@@ -1,9 +1,9 @@
 // Compile any new HTML content that was loaded via jQuery.ajax().
-// Currently this only works for tabs because they emit an
+// Currently this only works for tabs, and only because they emit an
 // arv:pane:loaded event after updating the DOM.
 
 $(document).on('arv:pane:loaded', function(event, $updatedElement) {
-    if ($updatedElement) {
+    if (angular && $updatedElement) {
         angular.element($updatedElement).injector().invoke(function($compile) {
             var scope = angular.element($updatedElement).scope();
             $compile($updatedElement)(scope);
index cd01f64a74f539945d5fb2f0f7cef7399a574be3..34075ca56c3c0f684a353f72b1bbbd9a480ced66 100644 (file)
@@ -83,6 +83,7 @@ $(document).
         });
     }).
     on('paste keyup input', 'input[type=text].filterable-control', function(e) {
+        var regexp;
         if (this != e.target) return;
         var $target = $($(this).attr('data-filterable-target'));
         var currentquery = $target.data('filterable-query');
@@ -113,9 +114,20 @@ $(document).
         } else {
             // Target does not have infinite-scroll capability. Just
             // filter the rows in the browser using a RegExp.
+            regexp = undefined;
+            try {
+                regexp = new RegExp($(this).val(), 'i');
+            } catch(e) {
+                if (e instanceof SyntaxError) {
+                    // Invalid/partial regexp. See 'has-error' below.
+                } else {
+                    throw e;
+                }
+            }
             $target.
+                toggleClass('has-error', regexp === undefined).
                 addClass('filterable-container').
-                data('q', new RegExp($(this).val(), 'i')).
+                data('q', regexp).
                 trigger('refresh');
         }
     }).on('refresh', '.filterable-container', function() {
index d7ad41abdd1779d727a6d6aae0435cc4f816d27d..81a3a4639b8c7f63a2b42a416252664d746a6b78 100644 (file)
@@ -245,8 +245,8 @@ $(document).
         // put it in the browser history state if browser allows it
         if( hasHTML5History() ) {
             var tabId = $(this).closest('div.tab-pane').attr('id');
-            var state =  history.state;
-            if( state.order === undefined) {
+            var state =  history.state || {};
+            if( state.order === undefined ) {
                 state.order = {};
             }
             state.order[tabId] = order;
index bd68bc8a2facf92e0769b3ccec9dc9cd384e8aba..3b51faad6a8b67c5dde1b174c1f02410e17677b4 100644 (file)
@@ -49,6 +49,7 @@ $(document).on('click', '.selectable', function() {
     var selection = [];
     var data = [];
     var $modal = $(this).closest('.modal');
+    var http_method = $(this).attr('data-method').toUpperCase();
     var action_data = $(this).data('action-data');
     var action_data_from_params = $(this).data('action-data-from-params');
     var selection_param = action_data.selection_param;
@@ -75,9 +76,17 @@ $(document).on('click', '.selectable', function() {
                    data.push({name: key, value: value});
                }
            });
+    if (http_method === 'PATCH') {
+        // Some user agents do not support HTTP PATCH (notably,
+        // phantomjs silently ignores our "data" and sends an empty
+        // request body) so we use POST instead, and supply a
+        // _method=PATCH param to tell Rails what we really want.
+        data.push({name: '_method', value: http_method});
+        http_method = 'POST';
+    }
     $.ajax($(this).attr('data-action-href'),
            {dataType: 'json',
-            type: $(this).attr('data-method'),
+            type: http_method,
             data: data,
             traditional: false,
             context: {modal: $modal, action_data: action_data}}).
index 824e370c582d6ad2ea80f12ac8b342bb099a2464..5cfa2ca373c79f7708f63f77b65dc547adab5f42 100644 (file)
               <li><%= link_to raw('<i class="fa fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
               <li role="presentation" class="divider"></li>
               <li> <%= link_to report_issue_popup_path(popup_type: 'version', current_location: request.url, current_path: request.fullpath, action_method: 'post'),
-                      {class: 'report-issue-modal-window',  :remote => true, return_to: request.url} do %>
+                      {class: 'report-issue-modal-window', remote: true, return_to: request.url} do %>
                        <i class="fa fa-fw fa-support"></i> Show version / debugging info ...
                       <% end %>
               </li>
               <li> <%= link_to report_issue_popup_path(popup_type: 'report', current_location: request.url, current_path: request.fullpath, action_method: 'post'),
-                      {class: 'report-issue-modal-window', :remote => true, return_to: request.url} do %>
+                      {class: 'report-issue-modal-window', remote: true, return_to: request.url} do %>
                        <i class="fa fa-fw fa-support"></i> Report a problem ...
                       <% end %>
               </li>
index fbc144a996c8993d966179f790e646f7cbdbd8b9..3a4ad64bbf763df2b7d507f7f792613c1770e4c2 100644 (file)
@@ -1,14 +1,10 @@
 require 'diagnostics_test_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class PipelineTest < DiagnosticsTest
   pipelines_to_test = Rails.configuration.pipelines_to_test.andand.keys
 
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   pipelines_to_test.andand.each do |pipeline_to_test|
index 093915905498f9cd529690614e36f54751119813..8a2906a43ac218c5a7a8e34fb54bbea48c388a2d 100644 (file)
@@ -1,6 +1,4 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class ApplicationLayoutTest < ActionDispatch::IntegrationTest
   # These tests don't do state-changing API calls. Save some time by
@@ -9,9 +7,7 @@ class ApplicationLayoutTest < ActionDispatch::IntegrationTest
   reset_api_fixtures :after_suite, true
 
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   def verify_homepage user, invited, has_profile
index 9a2637365cedc66ce3d20715d4c301c1cd2850a4..a2405765b0815fba1d631ec8dbe68e74309c656a 100644 (file)
@@ -1,10 +1,6 @@
 require 'integration_helper'
 
 class CollectionUploadTest < ActionDispatch::IntegrationTest
-  setup do
-    Headless.new.start
-  end
-
   setup do
     testfiles.each do |filename, content|
       open(testfile_path(filename), 'w') do |io|
@@ -20,7 +16,7 @@ class CollectionUploadTest < ActionDispatch::IntegrationTest
   end
 
   test "Create new collection using upload button" do
-    Capybara.current_driver = :poltergeist
+    need_javascript
     visit page_with_token 'active', aproject_path
     find('.btn', text: 'Add data').click
     click_link 'Upload files from my computer'
@@ -32,14 +28,14 @@ class CollectionUploadTest < ActionDispatch::IntegrationTest
   end
 
   test "No Upload tab on non-writable collection" do
-    Capybara.current_driver = :poltergeist
+    need_javascript
     visit(page_with_token 'active',
           '/collections/'+api_fixture('collections')['user_agreement']['uuid'])
     assert_no_selector '.nav-tabs Upload'
   end
 
   test "Upload two empty files with the same name" do
-    Capybara.current_driver = :selenium
+    need_selenium "to make file uploads work"
     visit page_with_token 'active', sandbox_path
     find('.nav-tabs a', text: 'Upload').click
     attach_file 'file_selector', testfile_path('empty.txt')
@@ -53,14 +49,14 @@ class CollectionUploadTest < ActionDispatch::IntegrationTest
   end
 
   test "Upload non-empty files, report errors" do
-    Capybara.current_driver = :selenium
+    need_selenium "to make file uploads work"
     visit page_with_token 'active', sandbox_path
     find('.nav-tabs a', text: 'Upload').click
     attach_file 'file_selector', testfile_path('a')
     attach_file 'file_selector', testfile_path('foo.txt')
     assert_selector 'button:not([disabled])', text: 'Start'
     click_button 'Start'
-    if "test environment does not have a keepproxy yet, see #4534"
+    if "test environment does not have a keepproxy yet, see #4534" != "fixed"
       using_wait_time 20 do
         assert_text :visible, 'error'
       end
index 201be6d77696671b022bca6cd44f0aa03e8dd9d3..4338d19ea1fa5a61f6244dadb674e5feb6e35c97 100644 (file)
@@ -1,10 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class CollectionsTest < ActionDispatch::IntegrationTest
   setup do
-    Capybara.current_driver = :rack_test
+    need_javascript
   end
 
   # check_checkboxes_state asserts that the page holds at least one
@@ -18,8 +16,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "Can copy a collection to a project" do
-    Capybara.current_driver = Capybara.javascript_driver
-
     collection_uuid = api_fixture('collections')['foo_file']['uuid']
     collection_name = api_fixture('collections')['foo_file']['name']
     project_uuid = api_fixture('groups')['aproject']['uuid']
@@ -35,6 +31,7 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "Collection page renders name" do
+    Capybara.current_driver = :rack_test
     uuid = api_fixture('collections')['foo_file']['uuid']
     coll_name = api_fixture('collections')['foo_file']['name']
     visit page_with_token('active', "/collections/#{uuid}")
@@ -62,7 +59,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "creating and uncreating a sharing link" do
-    Capybara.current_driver = Capybara.javascript_driver
     coll_uuid = api_fixture("collections", "collection_owned_by_active", "uuid")
     download_link_re =
       Regexp.new(Regexp.escape("/collections/download/#{coll_uuid}/"))
@@ -74,6 +70,7 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "can download an entire collection with a reader token" do
+    Capybara.current_driver = :rack_test
     CollectionsController.any_instance.
       stubs(:file_enumerator).returns(["foo\n", "file\n"])
     uuid = api_fixture('collections')['foo_file']['uuid']
@@ -105,16 +102,13 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "can view empty collection" do
+    Capybara.current_driver = :rack_test
     uuid = 'd41d8cd98f00b204e9800998ecf8427e+0'
     visit page_with_token('active', "/collections/#{uuid}")
     assert page.has_text?(/This collection is empty|The following collections have this content/)
   end
 
   test "combine selected collections into new collection" do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-
     foo_collection = api_fixture('collections')['foo_file']
     bar_collection = api_fixture('collections')['bar_file']
 
@@ -144,7 +138,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     assert(page.has_text?('bar'), "Collection page did not include bar file")
     assert(page.has_text?('Created new collection in your Home project'),
                           'Not found flash message that new collection is created in Home project')
-    headless.stop
   end
 
   [
@@ -154,10 +147,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     ['project_viewer', 'foo_collection_in_aproject', false], #aproject not writable
   ].each do |user, collection, expect_collection_in_aproject|
     test "combine selected collection files into new collection #{user} #{collection} #{expect_collection_in_aproject}" do
-      headless = Headless.new
-      headless.start
-      Capybara.current_driver = :selenium
-
       my_collection = api_fixture('collections')[collection]
 
       visit page_with_token(user, "/collections")
@@ -187,16 +176,10 @@ class CollectionsTest < ActionDispatch::IntegrationTest
         assert page.has_text?("Created new collection in your Home project"),
                               'Not found flash message that new collection is created in Home project'
       end
-
-      headless.stop
     end
   end
 
   test "combine selected collection files from collection subdirectory" do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-
     visit page_with_token('user1_with_load', "/collections/zzzzz-4zz18-filesinsubdir00")
 
     # now in collection page
@@ -216,8 +199,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     assert(page.has_text?('file2_in_subdir3.txt'), 'file not found - file2_in_subdir3.txt')
     assert(page.has_text?('file1_in_subdir4.txt'), 'file not found - file1_in_subdir4.txt')
     assert(page.has_text?('file2_in_subdir4.txt'), 'file not found - file1_in_subdir4.txt')
-
-    headless.stop
   end
 
   test "Collection portable data hash redirect" do
@@ -246,9 +227,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "Filtering collection files by regexp" do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
     col = api_fixture('collections', 'multilevel_collection_1')
     visit page_with_token('active', "/collections/#{col['uuid']}")
 
@@ -327,10 +305,6 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "Creating collection from list of filtered files" do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-
     col = api_fixture('collections', 'collection_with_files_in_subdir')
     visit page_with_token('user1_with_load', "/collections/#{col['uuid']}")
     assert page.has_text?('file_in_subdir1'), 'expected file_in_subdir1 not found'
index ce90068b9676c2a840d8f7c7853cd5e2f97d4cb2..03c359e089590f20f8ea12334b7d518923923479 100644 (file)
@@ -1,12 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class ErrorsTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   BAD_UUID = "ffffffffffffffffffffffffffffffff+0"
index 4434f9a5772b56dcddb8b873858ad53641807f17..b4dadcd13f853f2086cb62797d1e12a67da3105d 100644 (file)
@@ -2,9 +2,7 @@ require 'integration_helper'
 
 class FilterableInfiniteScrollTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   # Chrome remembers what you had in the text field when you hit
index d1f5e780932fcaab959bf07ad486c07aefc68edc..716e7319874d56189b145acae6fd4c137a590f23 100644 (file)
@@ -14,7 +14,7 @@ class JobsTest < ActionDispatch::IntegrationTest
   end
 
   test "add job description" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     visit page_with_token("active", "/jobs")
 
     # go to job running the script "doesnotexist"
@@ -39,7 +39,7 @@ class JobsTest < ActionDispatch::IntegrationTest
   end
 
   test "view job log" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     job = api_fixture('jobs')['job_with_real_log']
 
     IO.expects(:popen).returns(fakepipe_with_log_data)
@@ -58,7 +58,7 @@ class JobsTest < ActionDispatch::IntegrationTest
   end
 
   test 'view partial job log' do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     # This config will be restored during teardown by ../test_helper.rb:
     Rails.configuration.log_viewer_max_bytes = 100
 
index 9d4e04bfee5f4cee67638c607f2cc1ddcd7d902a..2e2de70318e7aa84c2c2e92d0526d2a99424a38b 100644 (file)
@@ -2,7 +2,7 @@ require 'integration_helper'
 
 class LoginsTest < ActionDispatch::IntegrationTest
   setup do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
   end
 
   test "login with api_token works after redirect" do
index e9c84c158b945bf0d589a0da11a8614ccdae1c3c..bab40cc7dee463f5e16b6bf6b462d1a4ef871682 100644 (file)
@@ -1,13 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class PipelineInstancesTest < ActionDispatch::IntegrationTest
   setup do
-    # Selecting collections requiresLocalStorage
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   test 'Create and run a pipeline' do
@@ -69,8 +64,7 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
 
     # The input, after being specified, should still be editable (#3382)
     find('div.form-group', text: 'Foo/bar pair').
-      find('.btn', text: 'Choose').
-      click
+      find('.btn', text: 'Choose').click
 
     within('.modal-dialog') do
       assert(has_text?("Foo/bar pair"),
@@ -79,7 +73,6 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
       first('span', text: 'foo_tag').click
       find('button', text: 'OK').click
     end
-    wait_for_ajax
 
     # For good measure, check one last time that the input, after being specified twice, is still be displayed (#3382)
     assert find('div.form-group', text: 'Foo/bar pair')
@@ -230,14 +223,16 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
     [true, 'Two Part Pipeline Template', 'collection_with_no_name_in_aproject', false],
   ].each do |in_aproject, template_name, collection, choose_file|
     test "Run pipeline instance in #{in_aproject} with #{template_name} with #{collection} file #{choose_file}" do
-      visit page_with_token('active')
+      if in_aproject
+        visit page_with_token 'active', \
+        '/projects/'+api_fixture('groups')['aproject']['uuid']
+      else
+        visit page_with_token 'active', '/'
+      end
 
       # need bigger modal size when choosing a file from collection
-      Capybara.current_session.driver.browser.manage.window.resize_to(1024, 768)
-
-      if in_aproject
-        find("#projects-menu").click
-        find('.dropdown-menu a,button', text: 'A Project').click
+      if Capybara.current_driver == :selenium
+        Capybara.current_session.driver.browser.manage.window.resize_to(1200, 800)
       end
 
       create_and_run_pipeline_in_aproject in_aproject, template_name, collection, choose_file
@@ -271,14 +266,16 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
     ['project_viewer', true, true, true],
   ].each do |user, with_options, choose_options, in_aproject|
     test "Rerun pipeline instance as #{user} using options #{with_options} #{choose_options} in #{in_aproject}" do
-      visit page_with_token('active')
+      if in_aproject
+        visit page_with_token 'active', \
+        '/projects/'+api_fixture('groups')['aproject']['uuid']
+      else
+        visit page_with_token 'active', '/'
+      end
 
       # need bigger modal size when choosing a file from collection
-      Capybara.current_session.driver.browser.manage.window.resize_to(1024, 768)
-
-      if in_aproject
-        find("#projects-menu").click
-        find('.dropdown-menu a,button', text: 'A Project').click
+      if Capybara.current_driver == :selenium
+        Capybara.current_session.driver.browser.manage.window.resize_to(1200, 800)
       end
 
       create_and_run_pipeline_in_aproject in_aproject, 'Two Part Pipeline Template', 'foo_collection_in_aproject'
@@ -302,7 +299,9 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
 
       # Now re-run the pipeline
       if with_options
-        find('a,button', text: 'Re-run options').click
+        assert_triggers_dom_event 'shown.bs.modal' do
+          find('a,button', text: 'Re-run options').click
+        end
         within('.modal-dialog') do
           page.assert_selector 'a,button', text: 'Copy and edit inputs'
           page.assert_selector 'a,button', text: 'Run now'
@@ -316,15 +315,16 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
         find('a,button', text: 'Re-run with latest').click
       end
 
-      # Verify that the newly created instance is created in the right project.
-      # In case of project_viewer user, since the use cannot write to the project,
-      # the pipeline should have been created in the user's Home project.
+      # Verify that the newly created instance is created in the right
+      # project. In case of project_viewer user, since the user cannot
+      # write to the project, the pipeline should have been created in
+      # the user's Home project.
       assert_not_equal instance_path, current_path, 'Rerun instance path expected to be different'
-      assert page.has_text? 'Home'
+      assert_text 'Home'
       if in_aproject && (user != 'project_viewer')
-        assert page.has_text? 'A Project'
+        assert_text 'A Project'
       else
-        assert page.has_no_text? 'A Project'
+        assert_no_text 'A Project'
       end
     end
   end
@@ -374,7 +374,6 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
       end
       find('button', text: 'OK').click
     end
-    wait_for_ajax
 
     # The input, after being specified, should still be displayed (#3382)
     assert find('div.form-group', text: 'Foo/bar pair')
@@ -383,6 +382,7 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
     # are saved in the desired places. (#4015)
     click_link 'Advanced'
     click_link 'API response'
+
     api_response = JSON.parse(find('div#advanced_api_response pre').text)
     input_params = api_response['components']['part-one']['script_parameters']['input']
     assert_equal(input_params['selection_uuid'], collection['uuid'], "Not found expected input param uuid")
@@ -462,7 +462,7 @@ class PipelineInstancesTest < ActionDispatch::IntegrationTest
       page_scrolls = expected_max/20 + 2    # scroll num_pages+2 times to test scrolling is disabled when it should be
       within('.arv-recent-pipeline-instances') do
         (0..page_scrolls).each do |i|
-          page.execute_script "window.scrollBy(0,999000)"
+          page.driver.scroll_to 0, 999000
           begin
             wait_for_ajax
           rescue
index b909ac0378da9497beab2fda4783cd3e138c73f8..19a51095de3c50569c12c111e5ff5e1dda7cf480 100644 (file)
@@ -2,7 +2,7 @@ require 'integration_helper'
 
 class PipelineTemplatesTest < ActionDispatch::IntegrationTest
   test "JSON popup available for strange components" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     uuid = api_fixture("pipeline_templates")["components_is_jobspec"]["uuid"]
     visit page_with_token("active", "/pipeline_templates/#{uuid}")
     click_on "Components"
@@ -14,7 +14,7 @@ class PipelineTemplatesTest < ActionDispatch::IntegrationTest
   end
 
   test "pipeline template description" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     visit page_with_token("active", "/pipeline_templates")
 
     # go to Two Part pipeline template
index e5aa791de7c9494023d7c3d93f9d3f8f3b662fb0..ce5b47e5d9bf5fef050972379c1124c6389f473d 100644 (file)
@@ -1,15 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class ProjectsTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-
-    # project tests need bigger page size to be able to see all the buttons
-    Capybara.current_session.driver.browser.manage.window.resize_to(1152, 768)
+    need_javascript
   end
 
   test 'Check collection count for A Project in the tab pane titles' do
@@ -226,7 +219,11 @@ class ProjectsTest < ActionDispatch::IntegrationTest
       assert(has_link?("Write"),
              "failed to change access level on new share")
       click_on "Revoke"
-      page.driver.browser.switch_to.alert.accept
+      if Capybara.current_driver == :selenium
+        page.driver.browser.switch_to.alert.accept
+      else
+        # poltergeist returns true for confirm(), so we don't need to accept.
+      end
     end
     wait_for_ajax
     using_wait_time(Capybara.default_wait_time * 3) do
@@ -485,8 +482,11 @@ class ProjectsTest < ActionDispatch::IntegrationTest
       assert_selector 'li', text: 'Remove selected'
     end
 
+    # Close the dropdown by clicking outside it.
+    find('.dropdown-toggle', text: 'Selection').find(:xpath, '..').click
+
     # Go back to Data collections tab
-    click_link 'Data collections'
+    find('.nav-tabs a', text: 'Data collections').click
     click_button 'Selection'
     within('.selection-action-container') do
       assert_no_selector 'li.disabled', text: 'Create new collection with selected collections'
@@ -748,6 +748,7 @@ class ProjectsTest < ActionDispatch::IntegrationTest
   test "first tab loads data when visiting other tab directly" do
     # As of 2014-12-19, the first tab of project#show uses infinite scrolling.
     # Make sure that it loads data even if we visit another tab directly.
+    need_selenium 'to land on specified tab using {url}#Advanced'
     project = api_fixture("groups", "aproject")
     visit(page_with_token("active_trustedclient",
                           "/projects/#{project['uuid']}#Advanced"))
index ac9e596f8cb65053b6a217646757c5db2288ad80..7d4058db4a974126625af8d1648680924b8f9a4f 100644 (file)
@@ -1,13 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class ReportIssueTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-
+    need_javascript
     @user_profile_form_fields = Rails.configuration.user_profile_form_fields
   end
 
@@ -17,9 +12,9 @@ class ReportIssueTest < ActionDispatch::IntegrationTest
 
   # test version info and report issue from help menu
   def check_version_info_and_report_issue_from_help_menu
-    within('.navbar-fixed-top') do
-      page.find("#arv-help").click
-      within('.dropdown-menu') do
+    within '.navbar-fixed-top' do
+      find('.help-menu > a').click
+      within '.help-menu .dropdown-menu' do
         assert page.has_link?('Tutorials and User guide'), 'No link - Tutorials and User guide'
         assert page.has_link?('API Reference'), 'No link - API Reference'
         assert page.has_link?('SDK Reference'), 'No link - SDK Reference'
@@ -47,11 +42,9 @@ class ReportIssueTest < ActionDispatch::IntegrationTest
     end
 
     # check report issue link
-    within('.navbar-fixed-top') do
-      page.find("#arv-help").click
-      within('.dropdown-menu') do
-        click_link 'Report a problem ...'
-      end
+    within '.navbar-fixed-top' do
+      find('.help-menu > a').click
+      find('.help-menu .dropdown-menu a', text: 'Report a problem ...').click
     end
 
     within '.modal-content' do
index 9a259e2703b510a39599d98f9e59ea697f838993..05c7f25185f92a4bba3aafc9d820f49bd0f9d26b 100644 (file)
@@ -1,12 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class SearchBoxTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   # test the search box
index 1e3337044738cf3bca873d1545c96c850f636df0..a626e242843262643768afdfbbd3ca813c698d8c 100644 (file)
@@ -3,7 +3,7 @@ require 'uri'
 
 class SmokeTest < ActionDispatch::IntegrationTest
   setup do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
   end
 
   def assert_visit_success(allowed=[200])
index dd263a25845a7f91240d0e034d1e7c75dc8f1f08..16b320885e61bac7ee1e5d21bf1a75a22fc7345c 100644 (file)
@@ -1,11 +1,9 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class UserAgreementsTest < ActionDispatch::IntegrationTest
 
   setup do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
   end
 
   def continuebutton_selector
index a4defda806fcc9357683ce8ceb760d77fc0a24ff..fae7e62e728d12212dd4f2c3cb69dcbe420f83d2 100644 (file)
@@ -1,12 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class UserManageAccountTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_javascript
   end
 
   # test manage_account page
index fd190a2042f48ad7e3de340ce6016e63733db473..cbd591a38dcc69ff60f0340cc2d736ea3f01dd00 100644 (file)
@@ -1,13 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class UserProfileTest < ActionDispatch::IntegrationTest
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-
+    need_javascript
     @user_profile_form_fields = Rails.configuration.user_profile_form_fields
   end
 
index 58432f7d5e13108115802afce78ad2ee884dd04a..4a45a6a87c143ebfc3914864581390f3ee4a0061 100644 (file)
@@ -1,18 +1,16 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class UsersTest < ActionDispatch::IntegrationTest
 
   test "login as active user but not admin" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     visit page_with_token('active_trustedclient')
 
     assert page.has_no_link? 'Users' 'Found Users link for non-admin user'
   end
 
   test "login as admin user and verify active user data" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     visit page_with_token('admin_trustedclient')
 
     # go to Users list page
@@ -44,10 +42,7 @@ class UsersTest < ActionDispatch::IntegrationTest
   end
 
   test "create a new user" do
-    headless = Headless.new
-    headless.start
-
-    Capybara.current_driver = :selenium
+    need_javascript
 
     visit page_with_token('admin_trustedclient')
 
@@ -88,15 +83,10 @@ class UsersTest < ActionDispatch::IntegrationTest
     click_link 'Metadata'
     assert page.has_text? 'Repository: test_repo'
     assert !(page.has_text? 'VirtualMachine:')
-
-    headless.stop
   end
 
   test "setup the active user" do
-    headless = Headless.new
-    headless.start
-
-    Capybara.current_driver = :selenium
+    need_javascript
     visit page_with_token('admin_trustedclient')
 
     find('#system-menu').click
@@ -145,15 +135,10 @@ class UsersTest < ActionDispatch::IntegrationTest
     click_link 'Metadata'
     assert page.has_text? 'Repository: second_test_repo'
     assert page.has_text? 'VirtualMachine: testvm.shell'
-
-    headless.stop
   end
 
   test "unsetup active user" do
-    headless = Headless.new
-    headless.start
-
-    Capybara.current_driver = :selenium
+    need_javascript
 
     visit page_with_token('admin_trustedclient')
 
@@ -180,11 +165,15 @@ class UsersTest < ActionDispatch::IntegrationTest
     # unsetup user and verify all the above links are deleted
     click_link 'Admin'
     click_button 'Deactivate Active User'
-    sleep(0.1)
 
-    # Should now be back in the Attributes tab for the user
-    page.driver.browser.switch_to.alert.accept
+    if Capybara.current_driver == :selenium
+      sleep(0.1)
+      page.driver.browser.switch_to.alert.accept
+    else
+      # poltergeist returns true for confirm(), so we don't need to accept.
+    end
 
+    # Should now be back in the Attributes tab for the user
     assert page.has_text? 'modified_by_user_uuid'
     page.within(:xpath, '//span[@data-name="is_active"]') do
       assert_equal "false", text, "Expected user's is_active to be false after unsetup"
@@ -213,8 +202,6 @@ class UsersTest < ActionDispatch::IntegrationTest
     click_link 'Metadata'
     assert page.has_text? 'Repository: second_test_repo'
     assert page.has_text? 'VirtualMachine: testvm.shell'
-
-    headless.stop
   end
 
 end
index 28763da0876b14d8698f5e4478ac2f98508d7258..1d398a595ce854335df8e3c1d5a434632ed89728 100644 (file)
@@ -2,7 +2,7 @@ require 'integration_helper'
 
 class VirtualMachinesTest < ActionDispatch::IntegrationTest
   test "make and name a new virtual machine" do
-    Capybara.current_driver = Capybara.javascript_driver
+    need_javascript
     visit page_with_token('admin_trustedclient')
     find('#system-menu').click
     click_link 'Virtual machines'
index c22b3ff58fc79bd941809f603bb113232d5b7445..efc2539e7070d14efbd1c25c79a8838398bdd518 100644 (file)
@@ -1,13 +1,8 @@
 require 'integration_helper'
-require 'selenium-webdriver'
-require 'headless'
 
 class WebsocketTest < ActionDispatch::IntegrationTest
-
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
+    need_selenium "to make websockets work"
   end
 
   test "test page" do
index 2cf6bca2ad37b9064945957933b737158d44421b..1a6a4f054ac68782d4a8314f215678edd2e3efa1 100644 (file)
@@ -4,6 +4,14 @@ require 'capybara/poltergeist'
 require 'uri'
 require 'yaml'
 
+Capybara.register_driver :poltergeist do |app|
+  Capybara::Poltergeist::Driver.new app, {
+    window_size: [1200, 800],
+    phantomjs_options: ['--ignore-ssl-errors=true'],
+    inspector: true,
+  }
+end
+
 module WaitForAjax
   Capybara.default_wait_time = 5
   def wait_for_ajax
@@ -17,11 +25,69 @@ module WaitForAjax
   end
 end
 
+module AssertDomEvent
+  # Yield the supplied block, then wait for an event to arrive at a
+  # DOM element.
+  def assert_triggers_dom_event events, target='body'
+    magic = 'RXC0lObcVwEXwSvA-' + rand(2**20).to_s(36)
+    page.evaluate_script <<eos
+      $('#{target}').one('#{events}', function() {
+        $('body').append('<div id="#{magic}"></div>');
+      });
+eos
+    yield
+    assert_selector "##{magic}"
+    page.evaluate_script "$('##{magic}').remove();";
+  end
+end
+
+module HeadlessHelper
+  class HeadlessSingleton
+    def self.get
+      @headless ||= Headless.new reuse: false
+    end
+  end
+
+  Capybara.default_driver = :rack_test
+
+  def self.included base
+    base.class_eval do
+      setup do
+        Capybara.use_default_driver
+        @headless = false
+      end
+
+      teardown do
+        if @headless
+          @headless.stop
+          @headless = false
+        end
+      end
+    end
+  end
+
+  def need_selenium reason=nil
+    Capybara.current_driver = :selenium
+    unless ENV['ARVADOS_TEST_HEADFUL'] or @headless
+      @headless = HeadlessSingleton.get
+      @headless.start
+    end
+  end
+
+  def need_javascript reason=nil
+    unless Capybara.current_driver == :selenium
+      Capybara.current_driver = :poltergeist
+    end
+  end
+end
+
 class ActionDispatch::IntegrationTest
   # Make the Capybara DSL available in all integration tests
   include Capybara::DSL
   include ApiFixtureLoader
   include WaitForAjax
+  include AssertDomEvent
+  include HeadlessHelper
 
   @@API_AUTHS = self.api_fixture('api_client_authorizations')
 
index ec299e295528b6c35632fa39d6119435dc9059a6..f15e3ea5fd0762ccaf650eb0302d00ed1764783d 100644 (file)
@@ -13,10 +13,7 @@ class BrowsingTest < WorkbenchPerformanceTest
                            :formats => [:flat] }
 
   setup do
-    headless = Headless.new
-    headless.start
-    Capybara.current_driver = :selenium
-    Capybara.current_session.driver.browser.manage.window.resize_to(1024, 768)
+    need_javascript
   end
 
   test "home page" do
diff --git a/services/api/script/crunch_failure_report.py b/services/api/script/crunch_failure_report.py
new file mode 100755 (executable)
index 0000000..31ad0fe
--- /dev/null
@@ -0,0 +1,219 @@
+#! /usr/bin/env python
+
+import argparse
+import datetime
+import json
+import re
+import sys
+
+import arvados
+
+# Useful configuration variables:
+
+# Number of log lines to use as context in diagnosing failure.
+LOG_CONTEXT_LINES = 10
+
+# Regex that signifies a failed task.
+FAILED_TASK_REGEX = re.compile(' \d+ failure (.*permanent)')
+
+# Regular expressions used to classify failure types.
+JOB_FAILURE_TYPES = {
+    'sys/docker': 'Cannot destroy container',
+    'crunch/node': 'User not found on host',
+    'slurm/comm':  'Communication connection failure'
+}
+
+def parse_arguments(arguments):
+    arg_parser = argparse.ArgumentParser(
+        description='Produce a report of Crunch failures within a specified time range')
+
+    arg_parser.add_argument(
+        '--start',
+        help='Start date and time')
+    arg_parser.add_argument(
+        '--end',
+        help='End date and time')
+
+    args = arg_parser.parse_args(arguments)
+
+    if args.start and not is_valid_timestamp(args.start):
+        raise ValueError(args.start)
+    if args.end and not is_valid_timestamp(args.end):
+        raise ValueError(args.end)
+
+    return args
+
+
+def api_timestamp(when=None):
+    """Returns a string representing the timestamp 'when' in a format
+    suitable for delivering to the API server.  Defaults to the
+    current time.
+    """
+    if when is None:
+        when = datetime.datetime.utcnow()
+    return when.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+
+def is_valid_timestamp(ts):
+    return re.match(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z', ts)
+
+
+def jobs_created_between_dates(api, start, end):
+    return arvados.util.list_all(
+        api.jobs().list,
+        filters=json.dumps([ ['created_at', '>=', start],
+                             ['created_at', '<=', end] ]))
+
+
+def job_logs(api, job):
+    # Returns the contents of the log for this job (as an array of lines).
+    if job['log']:
+        log_collection = arvados.CollectionReader(job['log'], api)
+        log_filename = "{}.log.txt".format(job['uuid'])
+        return log_collection.open(log_filename).readlines()
+    return []
+
+
+user_names = {}
+def job_user_name(api, user_uuid):
+    def _lookup_user_name(api, user_uuid):
+        try:
+            return api.users().get(uuid=user_uuid).execute()['full_name']
+        except arvados.errors.ApiError:
+            return user_uuid
+
+    if user_uuid not in user_names:
+        user_names[user_uuid] = _lookup_user_name(api, user_uuid)
+    return user_names[user_uuid]
+
+
+job_pipeline_names = {}
+def job_pipeline_name(api, job_uuid):
+    def _lookup_pipeline_name(api, job_uuid):
+        try:
+            pipelines = api.pipeline_instances().list(
+                filters='[["components", "like", "%{}%"]]'.format(job_uuid)).execute()
+            pi = pipelines['items'][0]
+            if pi['name']:
+                return pi['name']
+            else:
+                # Use the pipeline template name
+                pt = api.pipeline_templates().get(uuid=pi['pipeline_template_uuid']).execute()
+                return pt['name']
+        except (TypeError, ValueError, IndexError):
+            return ""
+
+    if job_uuid not in job_pipeline_names:
+        job_pipeline_names[job_uuid] = _lookup_pipeline_name(api, job_uuid)
+    return job_pipeline_names[job_uuid]
+
+
+def is_failed_task(logline):
+    return FAILED_TASK_REGEX.search(logline) != None
+
+
+def main(arguments=None, stdout=sys.stdout, stderr=sys.stderr):
+    args = parse_arguments(arguments)
+
+    api = arvados.api('v1')
+
+    now = datetime.datetime.utcnow()
+    start_time = args.start or api_timestamp(now - datetime.timedelta(days=1))
+    end_time = args.end or api_timestamp(now)
+
+    # Find all jobs created within the specified window,
+    # and their corresponding job logs.
+    jobs_created = jobs_created_between_dates(api, start_time, end_time)
+    jobs_by_state = {}
+    for job in jobs_created:
+        jobs_by_state.setdefault(job['state'], [])
+        jobs_by_state[job['state']].append(job)
+
+    # Find failed jobs and record the job failure text.
+
+    # failure_stats maps failure types (e.g. "sys/docker") to
+    # a set of job UUIDs that failed for that reason.
+    failure_stats = {}
+    for job in jobs_by_state['Failed']:
+        job_uuid = job['uuid']
+        logs = job_logs(api, job)
+        # Find the first permanent task failure, and collect the
+        # preceding log lines.
+        failure_type = None
+        for i, lg in enumerate(logs):
+            if is_failed_task(lg):
+                # Get preceding log record to provide context.
+                log_start = i - LOG_CONTEXT_LINES if i >= LOG_CONTEXT_LINES else 0
+                log_end = i + 1
+                lastlogs = ''.join(logs[log_start:log_end])
+                # try to identify the type of failure.
+                for key, rgx in JOB_FAILURE_TYPES.iteritems():
+                    if re.search(rgx, lastlogs):
+                        failure_type = key
+                        break
+            if failure_type is not None:
+                break
+        if failure_type is None:
+            failure_type = 'unknown'
+        failure_stats.setdefault(failure_type, set())
+        failure_stats[failure_type].add(job_uuid)
+
+    # Report percentages of successful, failed and unfinished jobs.
+    print "Start: {:20s}".format(start_time)
+    print "End:   {:20s}".format(end_time)
+    print ""
+
+    print "Overview"
+    print ""
+
+    job_start_count = len(jobs_created)
+    print "  {: <25s} {:4d}".format('Started', job_start_count)
+    for state in ['Complete', 'Failed', 'Queued', 'Cancelled', 'Running']:
+        if state in jobs_by_state:
+            job_count = len(jobs_by_state[state])
+            job_percentage = job_count / float(job_start_count)
+            print "  {: <25s} {:4d} ({: >4.0%})".format(state,
+                                                        job_count,
+                                                        job_percentage)
+    print ""
+
+    # Report failure types.
+    failure_summary = ""
+    failure_detail = ""
+
+    # Generate a mapping from failed job uuids to job records, to assist
+    # in generating detailed statistics for job failures.
+    jobs_failed_map = { job['uuid']: job for job in jobs_by_state.get('Failed', []) }
+
+    # sort the failure stats in descending order by occurrence.
+    sorted_failures = sorted(failure_stats,
+                             reverse=True,
+                             key=lambda failure_type: len(failure_stats[failure_type]))
+    for failtype in sorted_failures:
+        job_uuids = failure_stats[failtype]
+        failstat = "  {: <25s} {:4d} ({: >4.0%})\n".format(
+            failtype,
+            len(job_uuids),
+            len(job_uuids) / float(len(jobs_by_state['Failed'])))
+        failure_summary = failure_summary + failstat
+        failure_detail = failure_detail + failstat
+        for j in job_uuids:
+            job_info = jobs_failed_map[j]
+            job_owner = job_user_name(api, job_info['modified_by_user_uuid'])
+            job_name = job_pipeline_name(api, job_info['uuid'])
+            failure_detail = failure_detail + "    {}  {: <15.15s}  {:29.29s}\n".format(j, job_owner, job_name)
+        failure_detail = failure_detail + "\n"
+
+    print "Failures by class"
+    print ""
+    print failure_summary
+
+    print "Failures by class (detail)"
+    print ""
+    print failure_detail
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())