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