Merge branch 'master' into 3859-api-job-lock-method
[arvados.git] / services / api / test / unit / job_test.rb
1 require 'test_helper'
2 require 'helpers/git_test_helper'
3
4 class JobTest < ActiveSupport::TestCase
5   include GitTestHelper
6
7   BAD_COLLECTION = "#{'f' * 32}+0"
8
9   setup do
10     set_user_from_auth :active
11   end
12
13   def job_attrs merge_me={}
14     # Default (valid) set of attributes, with given overrides
15     {
16       script: "hash",
17       script_version: "master",
18       repository: "foo",
19     }.merge(merge_me)
20   end
21
22   test "Job without Docker image doesn't get locator" do
23     job = Job.new job_attrs
24     assert job.valid?, job.errors.full_messages.to_s
25     assert_nil job.docker_image_locator
26   end
27
28   { 'name' => [:links, :docker_image_collection_tag, :name],
29     'hash' => [:links, :docker_image_collection_hash, :name],
30     'locator' => [:collections, :docker_image, :portable_data_hash],
31   }.each_pair do |spec_type, (fixture_type, fixture_name, fixture_attr)|
32     test "Job initialized with Docker image #{spec_type} gets locator" do
33       image_spec = send(fixture_type, fixture_name).send(fixture_attr)
34       job = Job.new job_attrs(runtime_constraints:
35                               {'docker_image' => image_spec})
36       assert job.valid?, job.errors.full_messages.to_s
37       assert_equal(collections(:docker_image).portable_data_hash, job.docker_image_locator)
38     end
39
40     test "Job modified with Docker image #{spec_type} gets locator" do
41       job = Job.new job_attrs
42       assert job.valid?, job.errors.full_messages.to_s
43       assert_nil job.docker_image_locator
44       image_spec = send(fixture_type, fixture_name).send(fixture_attr)
45       job.runtime_constraints['docker_image'] = image_spec
46       assert job.valid?, job.errors.full_messages.to_s
47       assert_equal(collections(:docker_image).portable_data_hash, job.docker_image_locator)
48     end
49   end
50
51   test "removing a Docker runtime constraint removes the locator" do
52     image_locator = collections(:docker_image).portable_data_hash
53     job = Job.new job_attrs(runtime_constraints:
54                             {'docker_image' => image_locator})
55     assert job.valid?, job.errors.full_messages.to_s
56     assert_equal(image_locator, job.docker_image_locator)
57     job.runtime_constraints = {}
58     assert job.valid?, job.errors.full_messages.to_s + "after clearing runtime constraints"
59     assert_nil job.docker_image_locator
60   end
61
62   test "locate a Docker image with a repository + tag" do
63     image_repo, image_tag =
64       links(:docker_image_collection_tag2).name.split(':', 2)
65     job = Job.new job_attrs(runtime_constraints:
66                             {'docker_image' => image_repo,
67                               'docker_image_tag' => image_tag})
68     assert job.valid?, job.errors.full_messages.to_s
69     assert_equal(collections(:docker_image).portable_data_hash, job.docker_image_locator)
70   end
71
72   test "can't locate a Docker image with a nonexistent tag" do
73     image_repo = links(:docker_image_collection_tag).name
74     image_tag = '__nonexistent tag__'
75     job = Job.new job_attrs(runtime_constraints:
76                             {'docker_image' => image_repo,
77                               'docker_image_tag' => image_tag})
78     assert(job.invalid?, "Job with bad Docker tag valid")
79   end
80
81   test "locate a Docker image with a partial hash" do
82     image_hash = links(:docker_image_collection_hash).name[0..24]
83     job = Job.new job_attrs(runtime_constraints:
84                             {'docker_image' => image_hash})
85     assert job.valid?, job.errors.full_messages.to_s + " with partial hash #{image_hash}"
86     assert_equal(collections(:docker_image).portable_data_hash, job.docker_image_locator)
87   end
88
89   { 'name' => 'arvados_test_nonexistent',
90     'hash' => 'f' * 64,
91     'locator' => BAD_COLLECTION,
92   }.each_pair do |spec_type, image_spec|
93     test "Job validation fails with nonexistent Docker image #{spec_type}" do
94       job = Job.new job_attrs(runtime_constraints:
95                               {'docker_image' => image_spec})
96       assert(job.invalid?, "nonexistent Docker image #{spec_type} was valid")
97     end
98   end
99
100   test "Job validation fails with non-Docker Collection constraint" do
101     job = Job.new job_attrs(runtime_constraints:
102                             {'docker_image' => collections(:foo_file).uuid})
103     assert(job.invalid?, "non-Docker Collection constraint was valid")
104   end
105
106   test "can create Job with Docker image Collection without Docker links" do
107     image_uuid = collections(:unlinked_docker_image).portable_data_hash
108     job = Job.new job_attrs(runtime_constraints: {"docker_image" => image_uuid})
109     assert(job.valid?, "Job created with unlinked Docker image was invalid")
110     assert_equal(image_uuid, job.docker_image_locator)
111   end
112
113   test "can't create Job with Docker image locator" do
114     begin
115       job = Job.new job_attrs(docker_image_locator: BAD_COLLECTION)
116     rescue ActiveModel::MassAssignmentSecurity::Error
117       # Test passes - expected attribute protection
118     else
119       assert_nil job.docker_image_locator
120     end
121   end
122
123   test "can't assign Docker image locator to Job" do
124     job = Job.new job_attrs
125     begin
126       Job.docker_image_locator = BAD_COLLECTION
127     rescue NoMethodError
128       # Test passes - expected attribute protection
129     end
130     assert_nil job.docker_image_locator
131   end
132
133   [
134    {script_parameters: ""},
135    {script_parameters: []},
136    {script_parameters: {symbols: :are_not_allowed_here}},
137    {runtime_constraints: ""},
138    {runtime_constraints: []},
139    {tasks_summary: ""},
140    {tasks_summary: []},
141    {script_version: "no/branch/could/ever/possibly/have/this/name"},
142   ].each do |invalid_attrs|
143     test "validation failures set error messages: #{invalid_attrs.to_json}" do
144       # Ensure valid_attrs doesn't produce errors -- otherwise we will
145       # not know whether errors reported below are actually caused by
146       # invalid_attrs.
147       dummy = Job.create! job_attrs
148
149       job = Job.create job_attrs(invalid_attrs)
150       assert_raises(ActiveRecord::RecordInvalid, ArgumentError,
151                     "save! did not raise the expected exception") do
152         job.save!
153       end
154       assert_not_empty job.errors, "validation failure did not provide errors"
155     end
156   end
157
158   [
159     # Each test case is of the following format
160     # Array of parameters where each parameter is of the format:
161     #  attr name to be changed, attr value, and array of expectations (where each expectation is an array)
162     [['running', false, [['state', 'Queued']]]],
163     [['state', 'Running', [['started_at', 'not_nil']]]],
164     [['is_locked_by_uuid', 'use_current_user_uuid', [['state', 'Queued']]], ['state', 'Running', [['running', true], ['started_at', 'not_nil'], ['success', 'nil']]]],
165     [['running', false, [['state', 'Queued']]], ['state', 'Complete', [['success', true]]]],
166     [['running', true, [['state', 'Running']]], ['cancelled_at', Time.now, [['state', 'Cancelled']]]],
167     [['running', true, [['state', 'Running']]], ['state', 'Cancelled', [['cancelled_at', 'not_nil']]]],
168     [['running', true, [['state', 'Running']]], ['success', true, [['state', 'Complete']]]],
169     [['running', true, [['state', 'Running']]], ['success', false, [['state', 'Failed']]]],
170     [['running', true, [['state', 'Running']]], ['state', 'Complete', [['success', true],['finished_at', 'not_nil']]]],
171     [['running', true, [['state', 'Running']]], ['state', 'Failed', [['success', false],['finished_at', 'not_nil']]]],
172     [['cancelled_at', Time.now, [['state', 'Cancelled']]], ['success', false, [['state', 'Cancelled'],['finished_at', 'nil'], ['cancelled_at', 'not_nil']]]],
173     [['cancelled_at', Time.now, [['state', 'Cancelled'],['running', false]]], ['success', true, [['state', 'Cancelled'],['running', false],['finished_at', 'nil'],['cancelled_at', 'not_nil']]]],
174     # potential migration cases
175     [['state', nil, [['state', 'Queued']]]],
176     [['state', nil, [['state', 'Queued']]], ['cancelled_at', Time.now, [['state', 'Cancelled']]]],
177     [['running', true, [['state', 'Running']]], ['state', nil, [['state', 'Running']]]],
178   ].each do |parameters|
179     test "verify job status #{parameters}" do
180       job = Job.create! job_attrs
181       assert job.valid?, job.errors.full_messages.to_s
182       assert_equal 'Queued', job.state, "job.state"
183
184       parameters.each do |parameter|
185         expectations = parameter[2]
186         if parameter[1] == 'use_current_user_uuid'
187           parameter[1] = Thread.current[:user].uuid
188         end
189
190         if expectations.instance_of? Array
191           job[parameter[0]] = parameter[1]
192           assert_equal true, job.save, job.errors.full_messages.to_s
193           expectations.each do |expectation|
194             if expectation[1] == 'not_nil'
195               assert_not_nil job[expectation[0]], expectation[0]
196             elsif expectation[1] == 'nil'
197               assert_nil job[expectation[0]], expectation[0]
198             else
199               assert_equal expectation[1], job[expectation[0]], expectation[0]
200             end
201           end
202         else
203           raise 'I do not know how to handle this expectation'
204         end
205       end
206     end
207   end
208
209   test "Test job locking" do
210     set_user_from_auth :active_trustedclient
211     job = Job.create! job_attrs
212
213     assert_equal "Queued", job.state
214
215     # Should be able to lock successfully
216     job.lock current_user.uuid
217     assert_equal "Running", job.state
218
219     assert_raises ArvadosModel::ConflictError do
220       # Can't lock it again
221       job.lock current_user.uuid
222     end
223     job.reload
224     assert_equal "Running", job.state
225
226     set_user_from_auth :project_viewer
227     assert_raises ArvadosModel::ConflictError do
228       # Can't lock it as a different user either
229       job.lock current_user.uuid
230     end
231     job.reload
232     assert_equal "Running", job.state
233
234     assert_raises ArvadosModel::PermissionDeniedError do
235       # Can't update fields as a different user
236       job.update_attributes(state: "Failed")
237     end
238     job.reload
239     assert_equal "Running", job.state
240
241
242     set_user_from_auth :active_trustedclient
243
244     # Can update fields as the locked_by user
245     job.update_attributes(state: "Failed")
246     assert_equal "Failed", job.state
247   end
248
249 end