// 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);
});
}).
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');
} 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() {
// 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;
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;
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}}).
<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>
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|
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
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
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|
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'
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')
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
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
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']
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}")
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}/"))
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']
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']
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
[
['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")
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
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
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']}")
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'
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"
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
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"
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)
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
class LoginsTest < ActionDispatch::IntegrationTest
setup do
- Capybara.current_driver = Capybara.javascript_driver
+ need_javascript
end
test "login with api_token works after redirect" do
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
# 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"),
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')
[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
['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'
# 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'
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
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')
# 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")
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
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"
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
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
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
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'
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"))
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
# 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'
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
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
class SmokeTest < ActionDispatch::IntegrationTest
setup do
- Capybara.current_driver = Capybara.javascript_driver
+ need_javascript
end
def assert_visit_success(allowed=[200])
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
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
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
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
end
test "create a new user" do
- headless = Headless.new
- headless.start
-
- Capybara.current_driver = :selenium
+ need_javascript
visit page_with_token('admin_trustedclient')
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
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')
# 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"
click_link 'Metadata'
assert page.has_text? 'Repository: second_test_repo'
assert page.has_text? 'VirtualMachine: testvm.shell'
-
- headless.stop
end
end
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'
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
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
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')
: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
--- /dev/null
+#! /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())