Fix typo in example config file
[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: "active/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   [
82     false,
83     true
84   ].each do |use_config|
85     test "Job with no Docker image uses default docker image when configuration is set #{use_config}" do
86       default_docker_image = collections(:docker_image)[:portable_data_hash]
87       Rails.configuration.default_docker_image_for_jobs = default_docker_image if use_config
88
89       job = Job.new job_attrs
90       assert job.valid?, job.errors.full_messages.to_s
91
92       if use_config
93         refute_nil job.docker_image_locator
94         assert_equal default_docker_image, job.docker_image_locator
95       else
96         assert_nil job.docker_image_locator
97       end
98     end
99   end
100
101   test "create a job with a disambiguated script_version branch name" do
102     job = Job.
103       new(script: "testscript",
104           script_version: "heads/7387838c69a21827834586cc42b467ff6c63293b",
105           repository: "active/shabranchnames",
106           script_parameters: {})
107     assert(job.save)
108     assert_equal("abec49829bf1758413509b7ffcab32a771b71e81", job.script_version)
109   end
110
111   test "locate a Docker image with a partial hash" do
112     image_hash = links(:docker_image_collection_hash).name[0..24]
113     job = Job.new job_attrs(runtime_constraints:
114                             {'docker_image' => image_hash})
115     assert job.valid?, job.errors.full_messages.to_s + " with partial hash #{image_hash}"
116     assert_equal(collections(:docker_image).portable_data_hash, job.docker_image_locator)
117   end
118
119   { 'name' => 'arvados_test_nonexistent',
120     'hash' => 'f' * 64,
121     'locator' => BAD_COLLECTION,
122   }.each_pair do |spec_type, image_spec|
123     test "Job validation fails with nonexistent Docker image #{spec_type}" do
124       job = Job.new job_attrs(runtime_constraints:
125                               {'docker_image' => image_spec})
126       assert(job.invalid?, "nonexistent Docker image #{spec_type} was valid")
127     end
128   end
129
130   test "Job validation fails with non-Docker Collection constraint" do
131     job = Job.new job_attrs(runtime_constraints:
132                             {'docker_image' => collections(:foo_file).uuid})
133     assert(job.invalid?, "non-Docker Collection constraint was valid")
134   end
135
136   test "can create Job with Docker image Collection without Docker links" do
137     image_uuid = collections(:unlinked_docker_image).portable_data_hash
138     job = Job.new job_attrs(runtime_constraints: {"docker_image" => image_uuid})
139     assert(job.valid?, "Job created with unlinked Docker image was invalid")
140     assert_equal(image_uuid, job.docker_image_locator)
141   end
142
143   def check_attrs_unset(job, attrs)
144     assert_empty(attrs.each_key.map { |key| job.send(key) }.compact,
145                  "job has values for #{attrs.keys}")
146   end
147
148   def check_creation_prohibited(attrs)
149     begin
150       job = Job.new(job_attrs(attrs))
151     rescue ActiveModel::MassAssignmentSecurity::Error
152       # Test passes - expected attribute protection
153     else
154       check_attrs_unset(job, attrs)
155     end
156   end
157
158   def check_modification_prohibited(attrs)
159     job = Job.new(job_attrs)
160     attrs.each_pair do |key, value|
161       assert_raises(NoMethodError) { job.send("{key}=".to_sym, value) }
162     end
163     check_attrs_unset(job, attrs)
164   end
165
166   test "can't create Job with Docker image locator" do
167     check_creation_prohibited(docker_image_locator: BAD_COLLECTION)
168   end
169
170   test "can't assign Docker image locator to Job" do
171     check_modification_prohibited(docker_image_locator: BAD_COLLECTION)
172   end
173
174   [
175    {script_parameters: ""},
176    {script_parameters: []},
177    {script_parameters: {symbols: :are_not_allowed_here}},
178    {runtime_constraints: ""},
179    {runtime_constraints: []},
180    {tasks_summary: ""},
181    {tasks_summary: []},
182    {script_version: "no/branch/could/ever/possibly/have/this/name"},
183   ].each do |invalid_attrs|
184     test "validation failures set error messages: #{invalid_attrs.to_json}" do
185       # Ensure valid_attrs doesn't produce errors -- otherwise we will
186       # not know whether errors reported below are actually caused by
187       # invalid_attrs.
188       dummy = Job.create! job_attrs
189
190       job = Job.create job_attrs(invalid_attrs)
191       assert_raises(ActiveRecord::RecordInvalid, ArgumentError,
192                     "save! did not raise the expected exception") do
193         job.save!
194       end
195       assert_not_empty job.errors, "validation failure did not provide errors"
196     end
197   end
198
199   [
200     # Each test case is of the following format
201     # Array of parameters where each parameter is of the format:
202     #  attr name to be changed, attr value, and array of expectations (where each expectation is an array)
203     [['running', false, [['state', 'Queued']]]],
204     [['state', 'Running', [['started_at', 'not_nil']]]],
205     [['is_locked_by_uuid', 'use_current_user_uuid', [['state', 'Queued']]], ['state', 'Running', [['running', true], ['started_at', 'not_nil'], ['success', 'nil']]]],
206     [['running', false, [['state', 'Queued']]], ['state', 'Complete', [['success', true]]]],
207     [['running', true, [['state', 'Running']]], ['cancelled_at', Time.now, [['state', 'Cancelled']]]],
208     [['running', true, [['state', 'Running']]], ['state', 'Cancelled', [['cancelled_at', 'not_nil']]]],
209     [['running', true, [['state', 'Running']]], ['success', true, [['state', 'Complete']]]],
210     [['running', true, [['state', 'Running']]], ['success', false, [['state', 'Failed']]]],
211     [['running', true, [['state', 'Running']]], ['state', 'Complete', [['success', true],['finished_at', 'not_nil']]]],
212     [['running', true, [['state', 'Running']]], ['state', 'Failed', [['success', false],['finished_at', 'not_nil']]]],
213     [['cancelled_at', Time.now, [['state', 'Cancelled']]], ['success', false, [['state', 'Cancelled'],['finished_at', 'nil'], ['cancelled_at', 'not_nil']]]],
214     [['cancelled_at', Time.now, [['state', 'Cancelled'],['running', false]]], ['success', true, [['state', 'Cancelled'],['running', false],['finished_at', 'nil'],['cancelled_at', 'not_nil']]]],
215     # potential migration cases
216     [['state', nil, [['state', 'Queued']]]],
217     [['state', nil, [['state', 'Queued']]], ['cancelled_at', Time.now, [['state', 'Cancelled']]]],
218     [['running', true, [['state', 'Running']]], ['state', nil, [['state', 'Running']]]],
219   ].each do |parameters|
220     test "verify job status #{parameters}" do
221       job = Job.create! job_attrs
222       assert_equal 'Queued', job.state, "job.state"
223
224       parameters.each do |parameter|
225         expectations = parameter[2]
226         if parameter[1] == 'use_current_user_uuid'
227           parameter[1] = Thread.current[:user].uuid
228         end
229
230         if expectations.instance_of? Array
231           job[parameter[0]] = parameter[1]
232           assert_equal true, job.save, job.errors.full_messages.to_s
233           expectations.each do |expectation|
234             if expectation[1] == 'not_nil'
235               assert_not_nil job[expectation[0]], expectation[0]
236             elsif expectation[1] == 'nil'
237               assert_nil job[expectation[0]], expectation[0]
238             else
239               assert_equal expectation[1], job[expectation[0]], expectation[0]
240             end
241           end
242         else
243           raise 'I do not know how to handle this expectation'
244         end
245       end
246     end
247   end
248
249   test "Test job state changes" do
250     all = ["Queued", "Running", "Complete", "Failed", "Cancelled"]
251     valid = {"Queued" => all, "Running" => ["Complete", "Failed", "Cancelled"]}
252     all.each do |start|
253       all.each do |finish|
254         if start != finish
255           job = Job.create! job_attrs(state: start)
256           assert_equal start, job.state
257           job.state = finish
258           job.save
259           job.reload
260           if valid[start] and valid[start].include? finish
261             assert_equal finish, job.state
262           else
263             assert_equal start, job.state
264           end
265         end
266       end
267     end
268   end
269
270   test "Test job locking" do
271     set_user_from_auth :active_trustedclient
272     job = Job.create! job_attrs
273
274     assert_equal "Queued", job.state
275
276     # Should be able to lock successfully
277     job.lock current_user.uuid
278     assert_equal "Running", job.state
279
280     assert_raises ArvadosModel::AlreadyLockedError do
281       # Can't lock it again
282       job.lock current_user.uuid
283     end
284     job.reload
285     assert_equal "Running", job.state
286
287     set_user_from_auth :project_viewer
288     assert_raises ArvadosModel::AlreadyLockedError do
289       # Can't lock it as a different user either
290       job.lock current_user.uuid
291     end
292     job.reload
293     assert_equal "Running", job.state
294
295     assert_raises ArvadosModel::PermissionDeniedError do
296       # Can't update fields as a different user
297       job.update_attributes(state: "Failed")
298     end
299     job.reload
300     assert_equal "Running", job.state
301
302
303     set_user_from_auth :active_trustedclient
304
305     # Can update fields as the locked_by user
306     job.update_attributes(state: "Failed")
307     assert_equal "Failed", job.state
308   end
309
310   test "verify job queue position" do
311     job1 = Job.create! job_attrs
312     assert_equal 'Queued', job1.state, "Incorrect job state for newly created job1"
313
314     job2 = Job.create! job_attrs
315     assert_equal 'Queued', job2.state, "Incorrect job state for newly created job2"
316
317     assert_not_nil job1.queue_position, "Expected non-nil queue position for job1"
318     assert_not_nil job2.queue_position, "Expected non-nil queue position for job2"
319     assert_not_equal job1.queue_position, job2.queue_position
320   end
321
322   SDK_MASTER = "ca68b24e51992e790f29df5cc4bc54ce1da4a1c2"
323   SDK_TAGGED = "00634b2b8a492d6f121e3cf1d6587b821136a9a7"
324
325   def sdk_constraint(version)
326     {runtime_constraints: {
327         "arvados_sdk_version" => version,
328         "docker_image" => links(:docker_image_collection_tag).name,
329       }}
330   end
331
332   def check_job_sdk_version(expected)
333     job = yield
334     if expected.nil?
335       refute(job.valid?, "job valid with bad Arvados SDK version")
336     else
337       assert(job.valid?, "job not valid with good Arvados SDK version")
338       assert_equal(expected, job.arvados_sdk_version)
339     end
340   end
341
342   { "master" => SDK_MASTER,
343     "commit2" => SDK_TAGGED,
344     SDK_TAGGED[0, 8] => SDK_TAGGED,
345     "__nonexistent__" => nil,
346   }.each_pair do |search, commit_hash|
347     test "creating job with SDK version '#{search}'" do
348       check_job_sdk_version(commit_hash) do
349         Job.new(job_attrs(sdk_constraint(search)))
350       end
351     end
352
353     test "updating job from no SDK to version '#{search}'" do
354       job = Job.create!(job_attrs)
355       assert_nil job.arvados_sdk_version
356       check_job_sdk_version(commit_hash) do
357         job.runtime_constraints = sdk_constraint(search)[:runtime_constraints]
358         job
359       end
360     end
361
362     test "updating job from SDK version 'master' to '#{search}'" do
363       job = Job.create!(job_attrs(sdk_constraint("master")))
364       assert_equal(SDK_MASTER, job.arvados_sdk_version)
365       check_job_sdk_version(commit_hash) do
366         job.runtime_constraints = sdk_constraint(search)[:runtime_constraints]
367         job
368       end
369     end
370   end
371
372   test "clear the SDK version" do
373     job = Job.create!(job_attrs(sdk_constraint("master")))
374     assert_equal(SDK_MASTER, job.arvados_sdk_version)
375     job.runtime_constraints = {}
376     assert(job.valid?, "job invalid after clearing SDK version")
377     assert_nil(job.arvados_sdk_version)
378   end
379
380   test "job with SDK constraint, without Docker image is invalid" do
381     sdk_attrs = sdk_constraint("master")
382     sdk_attrs[:runtime_constraints].delete("docker_image")
383     job = Job.create(job_attrs(sdk_attrs))
384     refute(job.valid?, "Job valid with SDK version, without Docker image")
385     sdk_errors = job.errors.messages[:arvados_sdk_version] || []
386     refute_empty(sdk_errors.grep(/\bDocker\b/),
387                  "no Job SDK errors mention that Docker is required")
388   end
389
390   test "invalid to clear Docker image constraint when SDK constraint exists" do
391     job = Job.create!(job_attrs(sdk_constraint("master")))
392     job.runtime_constraints.delete("docker_image")
393     refute(job.valid?,
394            "Job with SDK constraint valid after clearing Docker image")
395   end
396
397   test "can't create job with SDK version assigned directly" do
398     check_creation_prohibited(arvados_sdk_version: SDK_MASTER)
399   end
400
401   test "can't modify job to assign SDK version directly" do
402     check_modification_prohibited(arvados_sdk_version: SDK_MASTER)
403   end
404
405   test "job validation fails when collection uuid found in script_parameters" do
406     bad_params = {
407       script_parameters: {
408         'input' => {
409           'param1' => 'the collection uuid zzzzz-4zz18-012345678901234'
410         }
411       }
412     }
413     assert_raises(ActiveRecord::RecordInvalid,
414                   "created job with a collection uuid in script_parameters") do
415       job = Job.create!(job_attrs(bad_params))
416     end
417   end
418
419   test "job validation succeeds when no collection uuid in script_parameters" do
420     good_params = {
421       script_parameters: {
422         'arg1' => 'foo',
423         'arg2' => [ 'bar', 'baz' ],
424         'arg3' => {
425           'a' => 1,
426           'b' => [2, 3, 4],
427         }
428       }
429     }
430     job = Job.create!(job_attrs(good_params))
431     assert job.valid?
432   end
433
434   test 'update job uuid tag in internal.git when version changes' do
435     authorize_with :active
436     j = jobs :queued
437     j.update_attributes repository: 'active/foo', script_version: 'b1'
438     assert_equal('1de84a854e2b440dc53bf42f8548afa4c17da332',
439                  internal_tag(j.uuid))
440     j.update_attributes repository: 'active/foo', script_version: 'master'
441     assert_equal('077ba2ad3ea24a929091a9e6ce545c93199b8e57',
442                  internal_tag(j.uuid))
443   end
444 end