4046: update assert error message
[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 state changes" do
210     all = ["Queued", "Running", "Complete", "Failed", "Cancelled"]
211     valid = {"Queued" => all, "Running" => ["Complete", "Failed", "Cancelled"]}
212     all.each do |start|
213       all.each do |finish|
214         if start != finish
215           job = Job.create! job_attrs(state: start)
216           assert_equal start, job.state
217           job.state = finish
218           job.save
219           job.reload
220           if valid[start] and valid[start].include? finish
221             assert_equal finish, job.state
222           else
223             assert_equal start, job.state
224           end
225         end
226       end
227     end
228   end
229
230   test "Test job locking" do
231     set_user_from_auth :active_trustedclient
232     job = Job.create! job_attrs
233
234     assert_equal "Queued", job.state
235
236     # Should be able to lock successfully
237     job.lock current_user.uuid
238     assert_equal "Running", job.state
239
240     assert_raises ArvadosModel::AlreadyLockedError do
241       # Can't lock it again
242       job.lock current_user.uuid
243     end
244     job.reload
245     assert_equal "Running", job.state
246
247     set_user_from_auth :project_viewer
248     assert_raises ArvadosModel::AlreadyLockedError do
249       # Can't lock it as a different user either
250       job.lock current_user.uuid
251     end
252     job.reload
253     assert_equal "Running", job.state
254
255     assert_raises ArvadosModel::PermissionDeniedError do
256       # Can't update fields as a different user
257       job.update_attributes(state: "Failed")
258     end
259     job.reload
260     assert_equal "Running", job.state
261
262
263     set_user_from_auth :active_trustedclient
264
265     # Can update fields as the locked_by user
266     job.update_attributes(state: "Failed")
267     assert_equal "Failed", job.state
268   end
269
270   test "verify job queue position" do
271     job1 = Job.create! job_attrs
272     assert job1.valid?, job1.errors.full_messages.to_s
273     assert_equal 'Queued', job1.state, "Incorrect job state for newly created job1"
274
275     job2 = Job.create! job_attrs
276     assert job2.valid?, job2.errors.full_messages.to_s
277     assert_equal 'Queued', job2.state, "Incorrect job state for newly created job2"
278
279     assert_not_nil job1.queue_position, "Expected non-nil queue position for job1"
280     assert_not_nil job2.queue_position, "Expected non-nil queue position for job2"
281     assert_not_equal job1.queue_position, job2.queue_position
282   end
283
284 end