1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
6 require 'helpers/git_test_helper'
8 class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
9 fixtures :repositories, :users, :jobs, :links, :collections
11 # See git_setup.rb for the commit log for test.git.tar
15 @controller = Arvados::V1::JobsController.new
16 authorize_with :active
19 test "reuse job with no_reuse=false" do
23 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
24 repository: "active/foo",
27 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45'
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']
37 test "reuse job with find_or_create=true" do
41 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
42 repository: "active/foo",
44 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
57 test "no reuse job with null log" do
61 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
62 repository: "active/foo",
64 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
77 test "reuse job with symbolic script_version" do
81 script_version: "tag1",
82 repository: "active/foo",
84 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
97 test "do not reuse job because no_reuse=true" do
102 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
103 repository: "active/foo",
105 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
117 [false, "false"].each do |whichfalse|
118 test "do not reuse job because find_or_create=#{whichfalse.inspect}" do
122 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
123 repository: "active/foo",
125 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
129 find_or_create: whichfalse
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']
139 test "do not reuse job because output is not readable by user" do
140 authorize_with :job_reader
144 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
145 repository: "active/foo",
147 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
160 test "test_cannot_reuse_job_no_output" do
164 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
165 repository: "active/foo",
167 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
177 test "test_reuse_job_range" do
181 minimum_script_version: "tag1",
182 script_version: "master",
183 repository: "active/foo",
185 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
196 test "cannot_reuse_job_no_minimum_given_so_must_use_specified_commit" do
200 script_version: "master",
201 repository: "active/foo",
203 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
214 test "test_cannot_reuse_job_different_input" do
218 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
219 repository: "active/foo",
221 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
232 test "test_cannot_reuse_job_different_version" do
236 script_version: "master",
237 repository: "active/foo",
239 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
250 test "test_can_reuse_job_submitted_nondeterministic" do
254 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
255 repository: "active/foo",
257 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
260 nondeterministic: true
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']
269 test "test_cannot_reuse_job_past_nondeterministic" do
273 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
274 repository: "active/foo",
276 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
287 test "test_cannot_reuse_job_no_permission" do
288 authorize_with :spectator
292 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
293 repository: "active/foo",
295 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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']
306 test "test_cannot_reuse_job_excluded" do
310 minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
311 script_version: "master",
312 repository: "active/foo",
313 exclude_script_versions: ["tag1"],
315 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
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'])
327 test "cannot reuse job with find_or_create but excluded version" do
331 script_version: "master",
332 repository: "active/foo",
334 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
338 find_or_create: true,
339 minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
340 exclude_script_versions: ["tag1"],
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'])
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)
356 'repository' => ['=', 'active/foo'],
357 'script' => ['=', 'hash'],
358 'script_version' => ['in git', 'master'],
359 'docker_image_locator' => ['=', nil],
360 'arvados_sdk_version' => ['=', nil],
363 def filters_from_hash(hash)
364 hash.each_pair.map { |name, filter| [name] + filter }
367 test "can reuse a Job based on filters" do
368 filters_hash = BASE_FILTERS.
369 merge('script_version' => ['in git', 'tag1'])
373 script_version: "master",
374 repository: "active/foo",
376 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
380 filters: filters_from_hash(filters_hash),
381 find_or_create: true,
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']
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"]]]
399 script_version: "master",
400 repository: "active/foo",
402 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
407 find_or_create: true,
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']
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"])
422 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
423 repository: "active/foo",
425 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
429 filters: filters_from_hash(filters_hash),
430 find_or_create: true,
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']
439 test "can reuse a Job with a Docker image" do
443 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
444 repository: "active/foo",
446 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
449 runtime_constraints: {
450 docker_image: 'arvados/apitestfixture',
453 find_or_create: true,
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))
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])
473 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
474 repository: "active/foo",
476 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
480 filters: filters_from_hash(filters_hash),
481 find_or_create: true,
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))
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])
501 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
502 repository: "active/foo",
504 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
508 filters: filters_from_hash(filters_hash),
509 find_or_create: true,
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))
520 test "new job with unknown Docker image filter" do
521 filters_hash = BASE_FILTERS.
522 merge("docker_image_locator" => ["in docker", "_nonesuchname_"])
526 script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
527 repository: "active/foo",
529 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
533 filters: filters_from_hash(filters_hash),
534 find_or_create: true,
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)
542 test "don't reuse job using older Docker image of same name" do
543 jobspec = {runtime_constraints: {
544 docker_image: "arvados/apitestfixture",
546 check_new_job_created_from({job: jobspec},
547 :previous_ancient_docker_image_job_run)
550 test "reuse job with Docker image that has hash name" do
551 jobspec = {runtime_constraints: {
552 docker_image: "a" * 64,
554 check_job_reused_from(jobspec, :previous_docker_job_run)
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 })
563 script_version: "master",
564 repository: "active/foo",
566 input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
571 find_or_create: true,
573 assert_includes(405..599, @response.code.to_i,
574 "bad status code with missing #{skip_key} filter")
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)
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)
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)
609 test "find Job with Docker image using reader tokens" do
610 authorize_with :inactive
612 filters: [["docker_image_locator", "in docker",
613 "arvados/apitestfixture"]],
614 reader_tokens: [api_token(:active)],
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)
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)
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)
646 JOB_SUBMIT_KEYS = [:script, :script_parameters, :script_version, :repository]
647 DEFAULT_START_JOB = :previous_job_run
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
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)]
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
666 params[:job].merge!(job_attrs)
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
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)
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)
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}")
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))
706 test "new job created from unsatisfiable minimum version parameter" do
707 check_new_job_created_from(minimum_script_version: "1de84a8")
710 test "new job created from unsatisfiable minimum version attribute" do
711 check_new_job_created_from(job: {minimum_script_version: "1de84a8"})
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")
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")
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")
734 test "don't reuse job with older Arvados SDK version specified by branch" do
735 jobspec = {runtime_constraints: {
736 arvados_sdk_version: "master",
738 check_new_job_created_from({job: jobspec},
739 :previous_job_run_with_arvados_sdk_version)
742 test "don't reuse job with older Arvados SDK version specified by commit" do
743 jobspec = {runtime_constraints: {
744 arvados_sdk_version: "ca68b24e51992e790f29df5cc4bc54ce1da4a1c2",
746 check_new_job_created_from({job: jobspec},
747 :previous_job_run_with_arvados_sdk_version)
750 test "don't reuse job with newer Arvados SDK version specified by commit" do
751 jobspec = {runtime_constraints: {
752 arvados_sdk_version: "436637c87a1d2bdbf4b624008304064b6cf0e30c",
754 check_new_job_created_from({job: jobspec},
755 :previous_job_run_with_arvados_sdk_version)
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)
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)