Merge branch '8784-dir-listings'
[arvados.git] / services / api / test / functional / arvados / v1 / job_reuse_controller_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'test_helper'
6 require 'helpers/git_test_helper'
7
8 class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
9   fixtures :repositories, :users, :jobs, :links, :collections
10
11   # See git_setup.rb for the commit log for test.git.tar
12   include GitTestHelper
13
14   setup do
15     @controller = Arvados::V1::JobsController.new
16     authorize_with :active
17   end
18
19   test "reuse job with no_reuse=false" do
20     post :create, job: {
21       no_reuse: false,
22       script: "hash",
23       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
24       repository: "active/foo",
25       script_parameters: {
26         an_integer: '1',
27         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45'
28       }
29     }
30     assert_response :success
31     assert_not_nil assigns(:object)
32     new_job = JSON.parse(@response.body)
33     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
34     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
35   end
36
37   test "reuse job with find_or_create=true" do
38     post :create, {
39       job: {
40         script: "hash",
41         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
42         repository: "active/foo",
43         script_parameters: {
44           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
45           an_integer: '1'
46         }
47       },
48       find_or_create: true
49     }
50     assert_response :success
51     assert_not_nil assigns(:object)
52     new_job = JSON.parse(@response.body)
53     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
54     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
55   end
56
57   test "no reuse job with null log" do
58     post :create, {
59       job: {
60         script: "hash",
61         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
62         repository: "active/foo",
63         script_parameters: {
64           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
65           an_integer: '3'
66         }
67       },
68       find_or_create: true
69     }
70     assert_response :success
71     assert_not_nil assigns(:object)
72     new_job = JSON.parse(@response.body)
73     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqq3', new_job['uuid']
74     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
75   end
76
77   test "reuse job with symbolic script_version" do
78     post :create, {
79       job: {
80         script: "hash",
81         script_version: "tag1",
82         repository: "active/foo",
83         script_parameters: {
84           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
85           an_integer: '1'
86         }
87       },
88       find_or_create: true
89     }
90     assert_response :success
91     assert_not_nil assigns(:object)
92     new_job = JSON.parse(@response.body)
93     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
94     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
95   end
96
97   test "do not reuse job because no_reuse=true" do
98     post :create, {
99       job: {
100         no_reuse: true,
101         script: "hash",
102         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
103         repository: "active/foo",
104         script_parameters: {
105           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
106           an_integer: '1'
107         }
108       }
109     }
110     assert_response :success
111     assert_not_nil assigns(:object)
112     new_job = JSON.parse(@response.body)
113     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
114     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
115   end
116
117   [false, "false"].each do |whichfalse|
118     test "do not reuse job because find_or_create=#{whichfalse.inspect}" do
119       post :create, {
120         job: {
121           script: "hash",
122           script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
123           repository: "active/foo",
124           script_parameters: {
125             input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
126             an_integer: '1'
127           }
128         },
129         find_or_create: whichfalse
130       }
131       assert_response :success
132       assert_not_nil assigns(:object)
133       new_job = JSON.parse(@response.body)
134       assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
135       assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
136     end
137   end
138
139   test "do not reuse job because output is not readable by user" do
140     authorize_with :job_reader
141     post :create, {
142       job: {
143         script: "hash",
144         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
145         repository: "active/foo",
146         script_parameters: {
147           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
148           an_integer: '1'
149         }
150       },
151       find_or_create: true
152     }
153     assert_response :success
154     assert_not_nil assigns(:object)
155     new_job = JSON.parse(@response.body)
156     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
157     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
158   end
159
160   test "test_cannot_reuse_job_no_output" do
161     post :create, job: {
162       no_reuse: false,
163       script: "hash",
164       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
165       repository: "active/foo",
166       script_parameters: {
167         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
168         an_integer: '2'
169       }
170     }
171     assert_response :success
172     assert_not_nil assigns(:object)
173     new_job = JSON.parse(@response.body)
174     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykppp', new_job['uuid']
175   end
176
177   test "test_reuse_job_range" do
178     post :create, job: {
179       no_reuse: false,
180       script: "hash",
181       minimum_script_version: "tag1",
182       script_version: "master",
183       repository: "active/foo",
184       script_parameters: {
185         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
186         an_integer: '1'
187       }
188     }
189     assert_response :success
190     assert_not_nil assigns(:object)
191     new_job = JSON.parse(@response.body)
192     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
193     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
194   end
195
196   test "cannot_reuse_job_no_minimum_given_so_must_use_specified_commit" do
197     post :create, job: {
198       no_reuse: false,
199       script: "hash",
200       script_version: "master",
201       repository: "active/foo",
202       script_parameters: {
203         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
204         an_integer: '1'
205       }
206     }
207     assert_response :success
208     assert_not_nil assigns(:object)
209     new_job = JSON.parse(@response.body)
210     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
211     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
212   end
213
214   test "test_cannot_reuse_job_different_input" do
215     post :create, job: {
216       no_reuse: false,
217       script: "hash",
218       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
219       repository: "active/foo",
220       script_parameters: {
221         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
222         an_integer: '2'
223       }
224     }
225     assert_response :success
226     assert_not_nil assigns(:object)
227     new_job = JSON.parse(@response.body)
228     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
229     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
230   end
231
232   test "test_cannot_reuse_job_different_version" do
233     post :create, job: {
234       no_reuse: false,
235       script: "hash",
236       script_version: "master",
237       repository: "active/foo",
238       script_parameters: {
239         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
240         an_integer: '2'
241       }
242     }
243     assert_response :success
244     assert_not_nil assigns(:object)
245     new_job = JSON.parse(@response.body)
246     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
247     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
248   end
249
250   test "test_can_reuse_job_submitted_nondeterministic" do
251     post :create, job: {
252       no_reuse: false,
253       script: "hash",
254       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
255       repository: "active/foo",
256       script_parameters: {
257         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
258         an_integer: '1'
259       },
260       nondeterministic: true
261     }
262     assert_response :success
263     assert_not_nil assigns(:object)
264     new_job = JSON.parse(@response.body)
265     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
266     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
267   end
268
269   test "test_cannot_reuse_job_past_nondeterministic" do
270     post :create, job: {
271       no_reuse: false,
272       script: "hash2",
273       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
274       repository: "active/foo",
275       script_parameters: {
276         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
277         an_integer: '1'
278       }
279     }
280     assert_response :success
281     assert_not_nil assigns(:object)
282     new_job = JSON.parse(@response.body)
283     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykyyy', new_job['uuid']
284     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
285   end
286
287   test "test_cannot_reuse_job_no_permission" do
288     authorize_with :spectator
289     post :create, job: {
290       no_reuse: false,
291       script: "hash",
292       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
293       repository: "active/foo",
294       script_parameters: {
295         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
296         an_integer: '1'
297       }
298     }
299     assert_response :success
300     assert_not_nil assigns(:object)
301     new_job = JSON.parse(@response.body)
302     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
303     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
304   end
305
306   test "test_cannot_reuse_job_excluded" do
307     post :create, job: {
308       no_reuse: false,
309       script: "hash",
310       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
311       script_version: "master",
312       repository: "active/foo",
313       exclude_script_versions: ["tag1"],
314       script_parameters: {
315         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
316         an_integer: '1'
317       }
318     }
319     assert_response :success
320     assert_not_nil assigns(:object)
321     new_job = JSON.parse(@response.body)
322     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
323     assert_not_equal('4fe459abe02d9b365932b8f5dc419439ab4e2577',
324                      new_job['script_version'])
325   end
326
327   test "cannot reuse job with find_or_create but excluded version" do
328     post :create, {
329       job: {
330         script: "hash",
331         script_version: "master",
332         repository: "active/foo",
333         script_parameters: {
334           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
335           an_integer: '1'
336         }
337       },
338       find_or_create: true,
339       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
340       exclude_script_versions: ["tag1"],
341     }
342     assert_response :success
343     assert_not_nil assigns(:object)
344     new_job = JSON.parse(@response.body)
345     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
346     assert_not_equal('4fe459abe02d9b365932b8f5dc419439ab4e2577',
347                      new_job['script_version'])
348   end
349
350   test "cannot reuse job when hash-like branch includes newer commit" do
351     check_new_job_created_from({job: {script_version: "738783"}},
352                                :previous_job_run_superseded_by_hash_branch)
353   end
354
355   BASE_FILTERS = {
356     'repository' => ['=', 'active/foo'],
357     'script' => ['=', 'hash'],
358     'script_version' => ['in git', 'master'],
359     'docker_image_locator' => ['=', nil],
360     'arvados_sdk_version' => ['=', nil],
361   }
362
363   def filters_from_hash(hash)
364     hash.each_pair.map { |name, filter| [name] + filter }
365   end
366
367   test "can reuse a Job based on filters" do
368     filters_hash = BASE_FILTERS.
369       merge('script_version' => ['in git', 'tag1'])
370     post(:create, {
371            job: {
372              script: "hash",
373              script_version: "master",
374              repository: "active/foo",
375              script_parameters: {
376                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
377                an_integer: '1'
378              }
379            },
380            filters: filters_from_hash(filters_hash),
381            find_or_create: true,
382          })
383     assert_response :success
384     assert_not_nil assigns(:object)
385     new_job = JSON.parse(@response.body)
386     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
387     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
388   end
389
390   test "can not reuse a Job based on filters" do
391     filters = filters_from_hash(BASE_FILTERS
392                                   .reject { |k| k == 'script_version' })
393     filters += [["script_version", "in git",
394                  "31ce37fe365b3dc204300a3e4c396ad333ed0556"],
395                 ["script_version", "not in git", ["tag1"]]]
396     post(:create, {
397            job: {
398              script: "hash",
399              script_version: "master",
400              repository: "active/foo",
401              script_parameters: {
402                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
403                an_integer: '1'
404              }
405            },
406            filters: filters,
407            find_or_create: true,
408          })
409     assert_response :success
410     assert_not_nil assigns(:object)
411     new_job = JSON.parse(@response.body)
412     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
413     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
414   end
415
416   test "can not reuse a Job based on arbitrary filters" do
417     filters_hash = BASE_FILTERS.
418       merge("created_at" => ["<", "2010-01-01T00:00:00Z"])
419     post(:create, {
420            job: {
421              script: "hash",
422              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
423              repository: "active/foo",
424              script_parameters: {
425                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
426                an_integer: '1'
427              }
428            },
429            filters: filters_from_hash(filters_hash),
430            find_or_create: true,
431          })
432     assert_response :success
433     assert_not_nil assigns(:object)
434     new_job = JSON.parse(@response.body)
435     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
436     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
437   end
438
439   test "can reuse a Job with a Docker image" do
440     post(:create, {
441            job: {
442              script: "hash",
443              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
444              repository: "active/foo",
445              script_parameters: {
446                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
447                an_integer: '1'
448              },
449              runtime_constraints: {
450                docker_image: 'arvados/apitestfixture',
451              }
452            },
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 "can reuse a Job with a Docker image hash filter" do
465     filters_hash = BASE_FILTERS.
466       merge("script_version" =>
467               ["=", "4fe459abe02d9b365932b8f5dc419439ab4e2577"],
468             "docker_image_locator" =>
469               ["in docker", links(:docker_image_collection_hash).name])
470     post(:create, {
471            job: {
472              script: "hash",
473              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
474              repository: "active/foo",
475              script_parameters: {
476                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
477                an_integer: '1'
478              },
479            },
480            filters: filters_from_hash(filters_hash),
481            find_or_create: true,
482          })
483     assert_response :success
484     new_job = assigns(:object)
485     assert_not_nil new_job
486     target_job = jobs(:previous_docker_job_run)
487     [:uuid, :script_version, :docker_image_locator].each do |attr|
488       assert_equal(target_job.send(attr), new_job.send(attr))
489     end
490   end
491
492   test "reuse Job with Docker image repo+tag" do
493     filters_hash = BASE_FILTERS.
494       merge("script_version" =>
495               ["=", "4fe459abe02d9b365932b8f5dc419439ab4e2577"],
496             "docker_image_locator" =>
497               ["in docker", links(:docker_image_collection_tag2).name])
498     post(:create, {
499            job: {
500              script: "hash",
501              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
502              repository: "active/foo",
503              script_parameters: {
504                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
505                an_integer: '1'
506              },
507            },
508            filters: filters_from_hash(filters_hash),
509            find_or_create: true,
510          })
511     assert_response :success
512     new_job = assigns(:object)
513     assert_not_nil new_job
514     target_job = jobs(:previous_docker_job_run)
515     [:uuid, :script_version, :docker_image_locator].each do |attr|
516       assert_equal(target_job.send(attr), new_job.send(attr))
517     end
518   end
519
520   test "new job with unknown Docker image filter" do
521     filters_hash = BASE_FILTERS.
522       merge("docker_image_locator" => ["in docker", "_nonesuchname_"])
523     post(:create, {
524            job: {
525              script: "hash",
526              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
527              repository: "active/foo",
528              script_parameters: {
529                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
530                an_integer: '1'
531              },
532            },
533            filters: filters_from_hash(filters_hash),
534            find_or_create: true,
535          })
536     assert_response :success
537     new_job = assigns(:object)
538     assert_not_nil new_job
539     assert_not_equal(jobs(:previous_docker_job_run).uuid, new_job.uuid)
540   end
541
542   test "don't reuse job using older Docker image of same name" do
543     jobspec = {runtime_constraints: {
544         docker_image: "arvados/apitestfixture",
545       }}
546     check_new_job_created_from({job: jobspec},
547                                :previous_ancient_docker_image_job_run)
548   end
549
550   test "reuse job with Docker image that has hash name" do
551     jobspec = {runtime_constraints: {
552         docker_image: "a" * 64,
553       }}
554     check_job_reused_from(jobspec, :previous_docker_job_run)
555   end
556
557   ["repository", "script"].each do |skip_key|
558     test "missing #{skip_key} filter raises an error" do
559       filters = filters_from_hash(BASE_FILTERS.reject { |k| k == skip_key })
560       post(:create, {
561              job: {
562                script: "hash",
563                script_version: "master",
564                repository: "active/foo",
565                script_parameters: {
566                  input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
567                  an_integer: '1'
568                }
569              },
570              filters: filters,
571              find_or_create: true,
572            })
573       assert_includes(405..599, @response.code.to_i,
574                       "bad status code with missing #{skip_key} filter")
575     end
576   end
577
578   test "find Job with script version range" do
579     get :index, filters: [["repository", "=", "active/foo"],
580                           ["script", "=", "hash"],
581                           ["script_version", "in git", "tag1"]]
582     assert_response :success
583     assert_not_nil assigns(:objects)
584     assert_includes(assigns(:objects).map { |job| job.uuid },
585                     jobs(:previous_job_run).uuid)
586   end
587
588   test "find Job with script version range exclusions" do
589     get :index, filters: [["repository", "=", "active/foo"],
590                           ["script", "=", "hash"],
591                           ["script_version", "not in git", "tag1"]]
592     assert_response :success
593     assert_not_nil assigns(:objects)
594     refute_includes(assigns(:objects).map { |job| job.uuid },
595                     jobs(:previous_job_run).uuid)
596   end
597
598   test "find Job with Docker image range" do
599     get :index, filters: [["docker_image_locator", "in docker",
600                            "arvados/apitestfixture"]]
601     assert_response :success
602     assert_not_nil assigns(:objects)
603     assert_includes(assigns(:objects).map { |job| job.uuid },
604                     jobs(:previous_docker_job_run).uuid)
605     refute_includes(assigns(:objects).map { |job| job.uuid },
606                     jobs(:previous_job_run).uuid)
607   end
608
609   test "find Job with Docker image using reader tokens" do
610     authorize_with :inactive
611     get(:index, {
612           filters: [["docker_image_locator", "in docker",
613                      "arvados/apitestfixture"]],
614           reader_tokens: [api_token(:active)],
615         })
616     assert_response :success
617     assert_not_nil assigns(:objects)
618     assert_includes(assigns(:objects).map { |job| job.uuid },
619                     jobs(:previous_docker_job_run).uuid)
620     refute_includes(assigns(:objects).map { |job| job.uuid },
621                     jobs(:previous_job_run).uuid)
622   end
623
624   test "'in docker' filter accepts arrays" do
625     get :index, filters: [["docker_image_locator", "in docker",
626                            ["_nonesuchname_", "arvados/apitestfixture"]]]
627     assert_response :success
628     assert_not_nil assigns(:objects)
629     assert_includes(assigns(:objects).map { |job| job.uuid },
630                     jobs(:previous_docker_job_run).uuid)
631     refute_includes(assigns(:objects).map { |job| job.uuid },
632                     jobs(:previous_job_run).uuid)
633   end
634
635   test "'not in docker' filter accepts arrays" do
636     get :index, filters: [["docker_image_locator", "not in docker",
637                            ["_nonesuchname_", "arvados/apitestfixture"]]]
638     assert_response :success
639     assert_not_nil assigns(:objects)
640     assert_includes(assigns(:objects).map { |job| job.uuid },
641                     jobs(:previous_job_run).uuid)
642     refute_includes(assigns(:objects).map { |job| job.uuid },
643                     jobs(:previous_docker_job_run).uuid)
644   end
645
646   JOB_SUBMIT_KEYS = [:script, :script_parameters, :script_version, :repository]
647   DEFAULT_START_JOB = :previous_job_run
648
649   def create_job_params(params, start_from=DEFAULT_START_JOB)
650     if not params.has_key?(:find_or_create)
651       params[:find_or_create] = true
652     end
653     job_attrs = params.delete(:job) || {}
654     start_job = jobs(start_from)
655     params[:job] = Hash[JOB_SUBMIT_KEYS.map do |key|
656                           [key, start_job.send(key)]
657                         end]
658     params[:job][:runtime_constraints] =
659       job_attrs.delete(:runtime_constraints) || {}
660     { arvados_sdk_version: :arvados_sdk_version,
661       docker_image_locator: :docker_image }.each do |method, constraint_key|
662       if constraint_value = start_job.send(method)
663         params[:job][:runtime_constraints][constraint_key] ||= constraint_value
664       end
665     end
666     params[:job].merge!(job_attrs)
667     params
668   end
669
670   def create_job_from(params, start_from)
671     post(:create, create_job_params(params, start_from))
672     assert_response :success
673     new_job = assigns(:object)
674     assert_not_nil new_job
675     new_job
676   end
677
678   def check_new_job_created_from(params, start_from=DEFAULT_START_JOB)
679     start_time = Time.now
680     new_job = create_job_from(params, start_from)
681     assert_operator(start_time, :<=, new_job.created_at)
682     new_job
683   end
684
685   def check_job_reused_from(params, start_from)
686     new_job = create_job_from(params, start_from)
687     assert_equal(jobs(start_from).uuid, new_job.uuid)
688   end
689
690   def check_errors_from(params, start_from=DEFAULT_START_JOB)
691     post(:create, create_job_params(params, start_from))
692     assert_includes(405..499, @response.code.to_i)
693     errors = json_response.fetch("errors", [])
694     assert(errors.any?, "no errors assigned from #{params}")
695     refute(errors.any? { |msg| msg =~ /^#<[A-Za-z]+: / },
696            "errors include raw exception: #{errors.inspect}")
697     errors
698   end
699
700   # 1de84a8 is on the b1 branch, after master's tip.
701   test "new job created from unsatisfiable minimum version filter" do
702     filters_hash = BASE_FILTERS.merge("script_version" => ["in git", "1de84a8"])
703     check_new_job_created_from(filters: filters_from_hash(filters_hash))
704   end
705
706   test "new job created from unsatisfiable minimum version parameter" do
707     check_new_job_created_from(minimum_script_version: "1de84a8")
708   end
709
710   test "new job created from unsatisfiable minimum version attribute" do
711     check_new_job_created_from(job: {minimum_script_version: "1de84a8"})
712   end
713
714   test "graceful error from nonexistent minimum version filter" do
715     filters_hash = BASE_FILTERS.merge("script_version" =>
716                                       ["in git", "__nosuchbranch__"])
717     errors = check_errors_from(filters: filters_from_hash(filters_hash))
718     assert(errors.any? { |msg| msg.include? "__nosuchbranch__" },
719            "bad refspec not mentioned in error message")
720   end
721
722   test "graceful error from nonexistent minimum version parameter" do
723     errors = check_errors_from(minimum_script_version: "__nosuchbranch__")
724     assert(errors.any? { |msg| msg.include? "__nosuchbranch__" },
725            "bad refspec not mentioned in error message")
726   end
727
728   test "graceful error from nonexistent minimum version attribute" do
729     errors = check_errors_from(job: {minimum_script_version: "__nosuchbranch__"})
730     assert(errors.any? { |msg| msg.include? "__nosuchbranch__" },
731            "bad refspec not mentioned in error message")
732   end
733
734   test "don't reuse job with older Arvados SDK version specified by branch" do
735     jobspec = {runtime_constraints: {
736         arvados_sdk_version: "master",
737       }}
738     check_new_job_created_from({job: jobspec},
739                                :previous_job_run_with_arvados_sdk_version)
740   end
741
742   test "don't reuse job with older Arvados SDK version specified by commit" do
743     jobspec = {runtime_constraints: {
744         arvados_sdk_version: "ca68b24e51992e790f29df5cc4bc54ce1da4a1c2",
745       }}
746     check_new_job_created_from({job: jobspec},
747                                :previous_job_run_with_arvados_sdk_version)
748   end
749
750   test "don't reuse job with newer Arvados SDK version specified by commit" do
751     jobspec = {runtime_constraints: {
752         arvados_sdk_version: "436637c87a1d2bdbf4b624008304064b6cf0e30c",
753       }}
754     check_new_job_created_from({job: jobspec},
755                                :previous_job_run_with_arvados_sdk_version)
756   end
757
758   test "reuse job from arvados_sdk_version git filters" do
759     prev_job = jobs(:previous_job_run_with_arvados_sdk_version)
760     filters_hash = BASE_FILTERS.
761       merge("arvados_sdk_version" => ["in git", "commit2"],
762             "docker_image_locator" => ["=", prev_job.docker_image_locator])
763     filters_hash.delete("script_version")
764     params = create_job_params(filters: filters_from_hash(filters_hash))
765     post(:create, params)
766     assert_response :success
767     assert_equal(prev_job.uuid, assigns(:object).uuid)
768   end
769
770   test "create new job because of arvados_sdk_version 'not in git' filters" do
771     filters_hash = BASE_FILTERS.reject { |k| k == "script_version" }
772     filters = filters_from_hash(filters_hash)
773     # Allow anything from the root commit, but before commit 2.
774     filters += [["arvados_sdk_version", "in git", "436637c8"],
775                 ["arvados_sdk_version", "not in git", "00634b2b"]]
776     check_new_job_created_from(filters: filters)
777   end
778 end