2 require 'helpers/git_test_helper'
4 class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
5 fixtures :repositories, :users, :jobs, :links, :collections
7 # See git_setup.rb for the commit log for test.git.tar
11 @controller = Arvados::V1::JobsController.new
12 authorize_with :active
15 test "reuse job with no_reuse=false" do
19 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
20 repository: "active/foo",
23 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45'
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']
33 test "reuse job with find_or_create=true" do
37 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
38 repository: "active/foo",
40 input: 'fa7aeb5140e2848d39b416daeef4ffc5+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']
53 test "reuse job with symbolic script_version" do
57 script_version: "tag1",
58 repository: "active/foo",
60 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
73 test "do not reuse job because no_reuse=true" do
78 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
79 repository: "active/foo",
81 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
93 [false, "false"].each do |whichfalse|
94 test "do not reuse job because find_or_create=#{whichfalse.inspect}" do
98 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
99 repository: "active/foo",
101 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
105 find_or_create: whichfalse
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']
115 test "do not reuse job because output is not readable by user" do
116 authorize_with :job_reader
120 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
121 repository: "active/foo",
123 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
136 test "test_cannot_reuse_job_no_output" do
140 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
141 repository: "active/foo",
143 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
153 test "test_reuse_job_range" do
157 minimum_script_version: "tag1",
158 script_version: "master",
159 repository: "active/foo",
161 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
172 test "cannot_reuse_job_no_minimum_given_so_must_use_specified_commit" do
176 script_version: "master",
177 repository: "active/foo",
179 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
190 test "test_cannot_reuse_job_different_input" do
194 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
195 repository: "active/foo",
197 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
208 test "test_cannot_reuse_job_different_version" do
212 script_version: "master",
213 repository: "active/foo",
215 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
226 test "test_can_reuse_job_submitted_nondeterministic" do
230 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
231 repository: "active/foo",
233 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
236 nondeterministic: true
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']
245 test "test_cannot_reuse_job_past_nondeterministic" do
249 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
250 repository: "active/foo",
252 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
263 test "test_cannot_reuse_job_no_permission" do
264 authorize_with :spectator
268 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
269 repository: "active/foo",
271 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
282 test "test_cannot_reuse_job_excluded" do
286 minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
287 script_version: "master",
288 repository: "active/foo",
289 exclude_script_versions: ["tag1"],
291 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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'])
303 test "cannot reuse job with find_or_create but excluded version" do
307 script_version: "master",
308 repository: "active/foo",
310 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
314 find_or_create: true,
315 minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
316 exclude_script_versions: ["tag1"],
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'])
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)
332 'repository' => ['=', 'active/foo'],
333 'script' => ['=', 'hash'],
334 'script_version' => ['in git', 'master'],
335 'docker_image_locator' => ['=', nil],
336 'arvados_sdk_version' => ['=', nil],
339 def filters_from_hash(hash)
340 hash.each_pair.map { |name, filter| [name] + filter }
343 test "can reuse a Job based on filters" do
344 filters_hash = BASE_FILTERS.
345 merge('script_version' => ['in git', 'tag1'])
349 script_version: "master",
350 repository: "active/foo",
352 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
356 filters: filters_from_hash(filters_hash),
357 find_or_create: true,
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']
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"]]]
375 script_version: "master",
376 repository: "active/foo",
378 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
383 find_or_create: true,
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']
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"])
398 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
399 repository: "active/foo",
401 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
405 filters: filters_from_hash(filters_hash),
406 find_or_create: true,
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']
415 test "can reuse a Job with a Docker image" do
419 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
420 repository: "active/foo",
422 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
425 runtime_constraints: {
426 docker_image: 'arvados/apitestfixture',
429 find_or_create: true,
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))
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])
449 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
450 repository: "active/foo",
452 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
456 filters: filters_from_hash(filters_hash),
457 find_or_create: true,
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))
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])
477 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
478 repository: "active/foo",
480 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
484 filters: filters_from_hash(filters_hash),
485 find_or_create: true,
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))
496 test "new job with unknown Docker image filter" do
497 filters_hash = BASE_FILTERS.
498 merge("docker_image_locator" => ["in docker", "_nonesuchname_"])
502 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
503 repository: "active/foo",
505 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
509 filters: filters_from_hash(filters_hash),
510 find_or_create: true,
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)
518 test "don't reuse job using older Docker image of same name" do
519 jobspec = {runtime_constraints: {
520 docker_image: "arvados/apitestfixture",
522 check_new_job_created_from({job: jobspec},
523 :previous_ancient_docker_image_job_run)
526 test "reuse job with Docker image that has hash name" do
527 jobspec = {runtime_constraints: {
528 docker_image: "a" * 64,
530 check_job_reused_from(jobspec, :previous_docker_job_run)
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 })
539 script_version: "master",
540 repository: "active/foo",
542 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
547 find_or_create: true,
549 assert_includes(405..599, @response.code.to_i,
550 "bad status code with missing #{skip_key} filter")
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)
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)
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)
585 test "find Job with Docker image using reader tokens" do
586 authorize_with :inactive
588 filters: [["docker_image_locator", "in docker",
589 "arvados/apitestfixture"]],
590 reader_tokens: [api_token(:active)],
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)
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)
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)
622 JOB_SUBMIT_KEYS = [:script, :script_parameters, :script_version, :repository]
623 DEFAULT_START_JOB = :previous_job_run
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
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)]
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
642 params[:job].merge!(job_attrs)
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
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)
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)
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}")
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))
682 test "new job created from unsatisfiable minimum version parameter" do
683 check_new_job_created_from(minimum_script_version: "1de84a8")
686 test "new job created from unsatisfiable minimum version attribute" do
687 check_new_job_created_from(job: {minimum_script_version: "1de84a8"})
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")
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")
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")
710 test "don't reuse job with older Arvados SDK version specified by branch" do
711 jobspec = {runtime_constraints: {
712 arvados_sdk_version: "master",
714 check_new_job_created_from({job: jobspec},
715 :previous_job_run_with_arvados_sdk_version)
718 test "don't reuse job with older Arvados SDK version specified by commit" do
719 jobspec = {runtime_constraints: {
720 arvados_sdk_version: "ca68b24e51992e790f29df5cc4bc54ce1da4a1c2",
722 check_new_job_created_from({job: jobspec},
723 :previous_job_run_with_arvados_sdk_version)
726 test "don't reuse job with newer Arvados SDK version specified by commit" do
727 jobspec = {runtime_constraints: {
728 arvados_sdk_version: "436637c87a1d2bdbf4b624008304064b6cf0e30c",
730 check_new_job_created_from({job: jobspec},
731 :previous_job_run_with_arvados_sdk_version)
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)
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)