3195: Add job reuse test.
[arvados.git] / services / api / test / functional / arvados / v1 / job_reuse_controller_test.rb
1 require 'test_helper'
2 require 'helpers/git_test_helper'
3
4 class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
5   fixtures :repositories, :users, :jobs, :links, :collections
6
7   # See git_setup.rb for the commit log for test.git.tar
8   include GitTestHelper
9
10   setup do
11     @controller = Arvados::V1::JobsController.new
12     authorize_with :active
13   end
14
15   test "reuse job with no_reuse=false" do
16     post :create, job: {
17       no_reuse: false,
18       script: "hash",
19       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
20       repository: "foo",
21       script_parameters: {
22         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
23         an_integer: '1'
24       }
25     }
26     assert_response :success
27     assert_not_nil assigns(:object)
28     new_job = JSON.parse(@response.body)
29     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
30     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
31   end
32
33   test "reuse job with find_or_create=true" do
34     post :create, {
35       job: {
36         script: "hash",
37         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
38         repository: "foo",
39         script_parameters: {
40           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
41           an_integer: '1'
42         }
43       },
44       find_or_create: true
45     }
46     assert_response :success
47     assert_not_nil assigns(:object)
48     new_job = JSON.parse(@response.body)
49     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
50     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
51   end
52
53   test "reuse job with symbolic script_version" do
54     post :create, {
55       job: {
56         script: "hash",
57         script_version: "tag1",
58         repository: "foo",
59         script_parameters: {
60           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
61           an_integer: '1'
62         }
63       },
64       find_or_create: true
65     }
66     assert_response :success
67     assert_not_nil assigns(:object)
68     new_job = JSON.parse(@response.body)
69     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
70     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
71   end
72
73   test "do not reuse job because no_reuse=true" do
74     post :create, {
75       job: {
76         no_reuse: true,
77         script: "hash",
78         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
79         repository: "foo",
80         script_parameters: {
81           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
82           an_integer: '1'
83         }
84       }
85     }
86     assert_response :success
87     assert_not_nil assigns(:object)
88     new_job = JSON.parse(@response.body)
89     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
90     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
91   end
92
93   test "do not reuse job because find_or_create=false" do
94     post :create, {
95       job: {
96         script: "hash",
97         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
98         repository: "foo",
99         script_parameters: {
100           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
101           an_integer: '1'
102         }
103       },
104       find_or_create: false
105     }
106     assert_response :success
107     assert_not_nil assigns(:object)
108     new_job = JSON.parse(@response.body)
109     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
110     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
111   end
112
113   test "test_cannot_reuse_job_no_output" do
114     post :create, job: {
115       no_reuse: false,
116       script: "hash",
117       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
118       repository: "foo",
119       script_parameters: {
120         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
121         an_integer: '2'
122       }
123     }
124     assert_response :success
125     assert_not_nil assigns(:object)
126     new_job = JSON.parse(@response.body)
127     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykppp', new_job['uuid']
128   end
129
130   test "test_reuse_job_range" do
131     post :create, job: {
132       no_reuse: false,
133       script: "hash",
134       minimum_script_version: "tag1",
135       script_version: "master",
136       repository: "foo",
137       script_parameters: {
138         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
139         an_integer: '1'
140       }
141     }
142     assert_response :success
143     assert_not_nil assigns(:object)
144     new_job = JSON.parse(@response.body)
145     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
146     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
147   end
148
149   test "cannot_reuse_job_no_minimum_given_so_must_use_specified_commit" do
150     post :create, job: {
151       no_reuse: false,
152       script: "hash",
153       script_version: "master",
154       repository: "foo",
155       script_parameters: {
156         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
157         an_integer: '1'
158       }
159     }
160     assert_response :success
161     assert_not_nil assigns(:object)
162     new_job = JSON.parse(@response.body)
163     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
164     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
165   end
166
167   test "test_cannot_reuse_job_different_input" do
168     post :create, job: {
169       no_reuse: false,
170       script: "hash",
171       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
172       repository: "foo",
173       script_parameters: {
174         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
175         an_integer: '2'
176       }
177     }
178     assert_response :success
179     assert_not_nil assigns(:object)
180     new_job = JSON.parse(@response.body)
181     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
182     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
183   end
184
185   test "test_cannot_reuse_job_different_version" do
186     post :create, job: {
187       no_reuse: false,
188       script: "hash",
189       script_version: "master",
190       repository: "foo",
191       script_parameters: {
192         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
193         an_integer: '2'
194       }
195     }
196     assert_response :success
197     assert_not_nil assigns(:object)
198     new_job = JSON.parse(@response.body)
199     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
200     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
201   end
202
203   test "test_can_reuse_job_submitted_nondeterministic" do
204     post :create, job: {
205       no_reuse: false,
206       script: "hash",
207       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
208       repository: "foo",
209       script_parameters: {
210         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
211         an_integer: '1'
212       },
213       nondeterministic: true
214     }
215     assert_response :success
216     assert_not_nil assigns(:object)
217     new_job = JSON.parse(@response.body)
218     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
219     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
220   end
221
222   test "test_cannot_reuse_job_past_nondeterministic" do
223     post :create, job: {
224       no_reuse: false,
225       script: "hash2",
226       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
227       repository: "foo",
228       script_parameters: {
229         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
230         an_integer: '1'
231       }
232     }
233     assert_response :success
234     assert_not_nil assigns(:object)
235     new_job = JSON.parse(@response.body)
236     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykyyy', new_job['uuid']
237     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
238   end
239
240   test "test_cannot_reuse_job_no_permission" do
241     authorize_with :spectator
242     post :create, job: {
243       no_reuse: false,
244       script: "hash",
245       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
246       repository: "foo",
247       script_parameters: {
248         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
249         an_integer: '1'
250       }
251     }
252     assert_response :success
253     assert_not_nil assigns(:object)
254     new_job = JSON.parse(@response.body)
255     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
256     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
257   end
258
259   test "test_cannot_reuse_job_excluded" do
260     post :create, job: {
261       no_reuse: false,
262       script: "hash",
263       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
264       script_version: "master",
265       repository: "foo",
266       exclude_script_versions: ["tag1"],
267       script_parameters: {
268         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
269         an_integer: '1'
270       }
271     }
272     assert_response :success
273     assert_not_nil assigns(:object)
274     new_job = JSON.parse(@response.body)
275     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
276     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
277   end
278
279   test "cannot reuse job with find_or_create but excluded version" do
280     post :create, {
281       job: {
282         script: "hash",
283         script_version: "master",
284         repository: "foo",
285         script_parameters: {
286           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
287           an_integer: '1'
288         }
289       },
290       find_or_create: true,
291       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
292       exclude_script_versions: ["tag1"],
293     }
294     assert_response :success
295     assert_not_nil assigns(:object)
296     new_job = JSON.parse(@response.body)
297     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
298     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
299   end
300
301   BASE_FILTERS = {
302     'repository' => ['=', 'foo'],
303     'script' => ['=', 'hash'],
304     'script_version' => ['in git', 'master'],
305     'docker_image_locator' => ['=', nil],
306   }
307
308   def filters_from_hash(hash)
309     hash.each_pair.map { |name, filter| [name] + filter }
310   end
311
312   test "can reuse a Job based on filters" do
313     filter_h = BASE_FILTERS.
314       merge('script_version' => ['in git', 'tag1'])
315     post(:create, {
316            job: {
317              script: "hash",
318              script_version: "master",
319              repository: "foo",
320              script_parameters: {
321                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
322                an_integer: '1'
323              }
324            },
325            filters: filters_from_hash(filter_h),
326            find_or_create: true,
327          })
328     assert_response :success
329     assert_not_nil assigns(:object)
330     new_job = JSON.parse(@response.body)
331     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
332     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
333   end
334
335   test "can not reuse a Job based on filters" do
336     filter_a = filters_from_hash(BASE_FILTERS.reject { |k| k == 'script_version' })
337     filter_a += [["script_version", "in git",
338                   "31ce37fe365b3dc204300a3e4c396ad333ed0556"],
339                  ["script_version", "not in git", ["tag1"]]]
340     post(:create, {
341            job: {
342              script: "hash",
343              script_version: "master",
344              repository: "foo",
345              script_parameters: {
346                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
347                an_integer: '1'
348              }
349            },
350            filters: filter_a,
351            find_or_create: true,
352          })
353     assert_response :success
354     assert_not_nil assigns(:object)
355     new_job = JSON.parse(@response.body)
356     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
357     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
358   end
359
360   test "can not reuse a Job based on arbitrary filters" do
361     filter_h = BASE_FILTERS.
362       merge("created_at" => ["<", "2010-01-01T00:00:00Z"])
363     post(:create, {
364            job: {
365              script: "hash",
366              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
367              repository: "foo",
368              script_parameters: {
369                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
370                an_integer: '1'
371              }
372            },
373            filters: filters_from_hash(filter_h),
374            find_or_create: true,
375          })
376     assert_response :success
377     assert_not_nil assigns(:object)
378     new_job = JSON.parse(@response.body)
379     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
380     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
381   end
382
383   test "can reuse a Job with a Docker image" do
384     post(:create, {
385            job: {
386              script: "hash",
387              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
388              repository: "foo",
389              script_parameters: {
390                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
391                an_integer: '1'
392              },
393              runtime_constraints: {
394                docker_image: 'arvados/apitestfixture',
395              }
396            },
397            find_or_create: true,
398          })
399     assert_response :success
400     new_job = assigns(:object)
401     assert_not_nil new_job
402     target_job = jobs(:previous_docker_job_run)
403     [:uuid, :script_version, :docker_image_locator].each do |attr|
404       assert_equal(target_job.send(attr), new_job.send(attr))
405     end
406   end
407
408   test "can reuse a Job with a Docker image hash filter" do
409     filter_h = BASE_FILTERS.
410       merge("script_version" =>
411               ["=", "4fe459abe02d9b365932b8f5dc419439ab4e2577"],
412             "docker_image_locator" =>
413               ["in docker", links(:docker_image_collection_hash).name])
414     post(:create, {
415            job: {
416              script: "hash",
417              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
418              repository: "foo",
419              script_parameters: {
420                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
421                an_integer: '1'
422              },
423            },
424            filters: filters_from_hash(filter_h),
425            find_or_create: true,
426          })
427     assert_response :success
428     new_job = assigns(:object)
429     assert_not_nil new_job
430     target_job = jobs(:previous_docker_job_run)
431     [:uuid, :script_version, :docker_image_locator].each do |attr|
432       assert_equal(target_job.send(attr), new_job.send(attr))
433     end
434   end
435
436   test "reuse Job with Docker image repo+tag" do
437     filter_h = BASE_FILTERS.
438       merge("script_version" =>
439               ["=", "4fe459abe02d9b365932b8f5dc419439ab4e2577"],
440             "docker_image_locator" =>
441               ["in docker", links(:docker_image_collection_tag2).name])
442     post(:create, {
443            job: {
444              script: "hash",
445              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
446              repository: "foo",
447              script_parameters: {
448                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
449                an_integer: '1'
450              },
451            },
452            filters: filters_from_hash(filter_h),
453            find_or_create: true,
454          })
455     assert_response :success
456     new_job = assigns(:object)
457     assert_not_nil new_job
458     target_job = jobs(:previous_docker_job_run)
459     [:uuid, :script_version, :docker_image_locator].each do |attr|
460       assert_equal(target_job.send(attr), new_job.send(attr))
461     end
462   end
463
464   test "new job with unknown Docker image filter" do
465     filter_h = BASE_FILTERS.
466       merge("docker_image_locator" => ["in docker", "_nonesuchname_"])
467     post(:create, {
468            job: {
469              script: "hash",
470              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
471              repository: "foo",
472              script_parameters: {
473                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
474                an_integer: '1'
475              },
476            },
477            filters: filters_from_hash(filter_h),
478            find_or_create: true,
479          })
480     assert_response :success
481     new_job = assigns(:object)
482     assert_not_nil new_job
483     assert_not_equal(jobs(:previous_docker_job_run).uuid, new_job.uuid)
484   end
485
486   ["repository", "script"].each do |skip_key|
487     test "missing #{skip_key} filter raises an error" do
488       filter_a = filters_from_hash(BASE_FILTERS.reject { |k| k == skip_key })
489       post(:create, {
490              job: {
491                script: "hash",
492                script_version: "master",
493                repository: "foo",
494                script_parameters: {
495                  input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
496                  an_integer: '1'
497                }
498              },
499              filters: filter_a,
500              find_or_create: true,
501            })
502       assert_includes(405..599, @response.code.to_i,
503                       "bad status code with missing #{skip_key} filter")
504     end
505   end
506
507   test "find Job with script version range" do
508     get :index, filters: [["repository", "=", "foo"],
509                           ["script", "=", "hash"],
510                           ["script_version", "in git", "tag1"]]
511     assert_response :success
512     assert_not_nil assigns(:objects)
513     assert_includes(assigns(:objects).map { |job| job.uuid },
514                     jobs(:previous_job_run).uuid)
515   end
516
517   test "find Job with script version range exclusions" do
518     get :index, filters: [["repository", "=", "foo"],
519                           ["script", "=", "hash"],
520                           ["script_version", "not in git", "tag1"]]
521     assert_response :success
522     assert_not_nil assigns(:objects)
523     refute_includes(assigns(:objects).map { |job| job.uuid },
524                     jobs(:previous_job_run).uuid)
525   end
526
527   test "find Job with Docker image range" do
528     get :index, filters: [["docker_image_locator", "in docker",
529                            "arvados/apitestfixture"]]
530     assert_response :success
531     assert_not_nil assigns(:objects)
532     assert_includes(assigns(:objects).map { |job| job.uuid },
533                     jobs(:previous_docker_job_run).uuid)
534     refute_includes(assigns(:objects).map { |job| job.uuid },
535                     jobs(:previous_job_run).uuid)
536   end
537
538   test "find Job with Docker image using reader tokens" do
539     authorize_with :inactive
540     get(:index, {
541           filters: [["docker_image_locator", "in docker",
542                      "arvados/apitestfixture"]],
543           reader_tokens: [api_token(:active)],
544         })
545     assert_response :success
546     assert_not_nil assigns(:objects)
547     assert_includes(assigns(:objects).map { |job| job.uuid },
548                     jobs(:previous_docker_job_run).uuid)
549     refute_includes(assigns(:objects).map { |job| job.uuid },
550                     jobs(:previous_job_run).uuid)
551   end
552
553   test "'in docker' filter accepts arrays" do
554     get :index, filters: [["docker_image_locator", "in docker",
555                            ["_nonesuchname_", "arvados/apitestfixture"]]]
556     assert_response :success
557     assert_not_nil assigns(:objects)
558     assert_includes(assigns(:objects).map { |job| job.uuid },
559                     jobs(:previous_docker_job_run).uuid)
560     refute_includes(assigns(:objects).map { |job| job.uuid },
561                     jobs(:previous_job_run).uuid)
562   end
563
564   test "'not in docker' filter accepts arrays" do
565     get :index, filters: [["docker_image_locator", "not in docker",
566                            ["_nonesuchname_", "arvados/apitestfixture"]]]
567     assert_response :success
568     assert_not_nil assigns(:objects)
569     assert_includes(assigns(:objects).map { |job| job.uuid },
570                     jobs(:previous_job_run).uuid)
571     refute_includes(assigns(:objects).map { |job| job.uuid },
572                     jobs(:previous_docker_job_run).uuid)
573   end
574
575   def create_foo_hash_job_params(params)
576     if not params.has_key?(:find_or_create)
577       params[:find_or_create] = true
578     end
579     job_attrs = params.delete(:job) || {}
580     params[:job] = {
581       script: "hash",
582       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
583       repository: "foo",
584       script_parameters: {
585         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
586         an_integer: '1',
587       },
588     }.merge(job_attrs)
589     params
590   end
591
592   def check_new_job_created_from(params)
593     start_time = Time.now
594     post(:create, create_foo_hash_job_params(params))
595     assert_response :success
596     new_job = assigns(:object)
597     assert_not_nil new_job
598     assert_operator(start_time, :<=, new_job.created_at)
599     new_job
600   end
601
602   def check_errors_from(params)
603     post(:create, create_foo_hash_job_params(params))
604     assert_includes(405..499, @response.code.to_i)
605     errors = json_response.fetch("errors", [])
606     assert(errors.any?, "no errors assigned from #{params}")
607     refute(errors.any? { |msg| msg =~ /^#<[A-Za-z]+: / },
608            "errors include raw exception")
609     errors
610   end
611
612   # 1de84a8 is on the b1 branch, after master's tip.
613   test "new job created from unsatisfiable minimum version filter" do
614     filter_h = BASE_FILTERS.merge("script_version" => ["in git", "1de84a8"])
615     check_new_job_created_from(filters: filters_from_hash(filter_h))
616   end
617
618   test "new job created from unsatisfiable minimum version parameter" do
619     check_new_job_created_from(minimum_script_version: "1de84a8")
620   end
621
622   test "new job created from unsatisfiable minimum version attribute" do
623     check_new_job_created_from(job: {minimum_script_version: "1de84a8"})
624   end
625
626   test "graceful error from nonexistent minimum version filter" do
627     filter_h = BASE_FILTERS.merge("script_version" =>
628                                   ["in git", "__nosuchbranch__"])
629     errors = check_errors_from(filters: filters_from_hash(filter_h))
630     assert(errors.any? { |msg| msg.include? "__nosuchbranch__" },
631            "bad refspec not mentioned in error message")
632   end
633
634   test "graceful error from nonexistent minimum version parameter" do
635     errors = check_errors_from(minimum_script_version: "__nosuchbranch__")
636     assert(errors.any? { |msg| msg.include? "__nosuchbranch__" },
637            "bad refspec not mentioned in error message")
638   end
639
640   test "graceful error from nonexistent minimum version attribute" do
641     errors = check_errors_from(job: {minimum_script_version: "__nosuchbranch__"})
642     assert(errors.any? { |msg| msg.include? "__nosuchbranch__" },
643            "bad refspec not mentioned in error message")
644   end
645 end