Module: Roby::TaskStructure::Dependency::Extension

Defined in:
lib/roby/task_structure/dependency.rb

Constant Summary collapse

FIND_THROUGH_METHOD_MISSING =
{ "_child" => :find_child_from_role }.freeze
HAS_THROUGH_METHOD_MISSING =
{ "_child" => :has_role? }.freeze

Instance Method Summary collapse

Instance Method Details

#added_child(child, info) ⇒ Object

Set up the event gathering needed by Dependency.check_structure



668
669
670
671
# File 'lib/roby/task_structure/dependency.rb', line 668

def added_child(child, info) # :nodoc:
    super
    relation_graphs[Dependency].update_triggers_for(self, child, info)
end

#child_from_role(role_name, validate = true) ⇒ Object

Returns the child whose role is role_name

If validate is true (the default), raises ArgumentError if there is none. Otherwise, returns nil. This argument is meant only to avoid the costly operation of raising an exception in cases it is expected that the role may not exist.



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/roby/task_structure/dependency.rb', line 438

def child_from_role(role_name, validate = true)
    unless validate
        Roby.warn_deprecated "#child_from_role(name, false) has been replaced by #find_child_from_role"
    end

    child = find_child_from_role(role_name)
    if !child && validate
        known_children = {}
        each_out_neighbour_merged(Dependency, intrusive: false) do |myself, child|
            myself[child, Dependency][:roles].each do |role|
                known_children[role] = child
            end
        end
        raise Roby::NoSuchChild.new(self, role_name, known_children), "#{self} has no child with the role '#{role_name}'"
    end
    child
end

#childrenObject

The set of child objects in the Dependency relation



340
341
342
# File 'lib/roby/task_structure/dependency.rb', line 340

def children
    child_objects(Dependency)
end

#depended_upon_by?(obj) ⇒ Boolean

True if obj is a parent of this object in the hierarchy relation (+obj+ is realized by self)

Returns:

  • (Boolean)


317
318
319
# File 'lib/roby/task_structure/dependency.rb', line 317

def depended_upon_by?(obj)
    parent_object?(obj, Dependency)
end

#depends_on(task, options = {}) ⇒ Object

Adds task as a child of self in the Dependency relation. The following options are allowed:

success

the list of success events. The default is [:success]

failure

the list of failing events. The default is [:failed]

model

a [task_model, arguments] pair which defines the task model the parent is expecting. The default value is to get these parameters from task

The success set describes the events of the child task that are required by the parent task. More specifically, the child task remains useful for the parent task as long as none of these events are emitted. By default, it is the success event. Of course, an error condition is encountered when all events of success become unreachable. In addition, the relation is removed if the remove_when_done flag is set to true (false by default).

The failure set describes the events of the child task which are an error condition from the parent task point of view.

In both error cases, a ChildFailedError exception is raised.



552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
# File 'lib/roby/task_structure/dependency.rb', line 552

def depends_on(task, options = {})
    if task.respond_to?(:as_plan)
        task = task.as_plan
    end
    if task == self
        raise ArgumentError, "cannot add a dependency of a task to itself"
    end

    options = Dependency.validate_options options,
                                          model: [task.provided_models, task.meaningful_arguments],
                                          success: :success.to_unbound_task_predicate,
                                          failure: false.to_unbound_task_predicate,
                                          remove_when_done: true,
                                          consider_in_pending: true,
                                          roles: nil,
                                          role: nil

    # We accept
    #
    #   model
    #   [model1, model2]
    #   [model1, arguments]
    #   [[model1, model2], arguments]
    if !options[:model].respond_to?(:to_ary)
        options[:model] = [Array(options[:model]), {}]
    elsif options[:model].size == 2
        unless options[:model].first.respond_to?(:to_ary)
            if options[:model].last.kind_of?(Hash)
                options[:model] = [Array(options[:model].first), options[:model].last]
            else
                options[:model] = [options[:model], {}]
            end
        end
    elsif !options[:model].first.respond_to?(:to_ary)
        options[:model] = [Array(options[:model]), {}]
    end

    roles = options[:roles] || Set.new
    if role = options.delete(:role)
        roles << role.to_str
    end
    roles = roles.map(&:to_str)
    options[:roles] = roles.to_set

    if options[:success].nil?
        options[:success] = []
    end
    options[:success] = Array[*options[:success]]
        .map(&:to_unbound_task_predicate)
        .inject(&:or)

    if options[:failure].nil?
        options[:failure] = []
    end
    options[:failure] = Array[*options[:failure]]
        .map(&:to_unbound_task_predicate)
        .inject(&:or)

    # options[:success] ||= false.to_unbound_task_predicate
    # options[:failure] ||= false.to_unbound_task_predicate

    # Validate failure and success event names
    if options[:success]
        not_there = options[:success].required_events
            .find_all { |name| !task.has_event?(name) }
        unless not_there.empty?
            raise ArgumentError, "#{task} does not have the following events: #{not_there.join(', ')}"
        end
    end

    if options[:failure]
        not_there = options[:failure].required_events
            .find_all { |name| !task.has_event?(name) }
        unless not_there.empty?
            raise ArgumentError, "#{task} does not have the following events: #{not_there.join(', ')}"
        end
    end

    # There is no positive events in success. Behind the scenes, it
    # actually means that the task does not have to start (since nothing
    # in :success would become unreachable)
    #
    # Add !:start in failure
    unless options[:success]
        not_started = :start.to_unbound_task_predicate.never
        if options[:failure]
            options[:failure] = not_started.or(options[:failure])
        else
            options[:failure] = not_started
        end
    end

    required_model, required_args = *options[:model]
    if !required_args.respond_to?(:to_hash)
        raise ArgumentError, "argument specification must be a hash, got #{required_args} (#{required_args.class})"
    elsif !task.fullfills?(required_model, required_args)
        raise ArgumentError, "task #{task} does not fullfill the provided model #{options[:model]}"
    end

    # Check if there is already a dependency link. If it is the case,
    # merge the options. Otherwise, just add.
    options.freeze
    options.each_value(&:freeze)
    add_child(task, options)
    task
end

#depends_on?(obj, recursive: false) ⇒ Boolean

True if obj is a child of this object in the hierarchy relation. If recursive is true, take into account the whole subgraph. Otherwise, only direct children are checked.

Returns:

  • (Boolean)


324
325
326
327
328
329
330
331
332
# File 'lib/roby/task_structure/dependency.rb', line 324

def depends_on?(obj, recursive: false)
    if recursive
        relation_graph_for(Dependency)
            .depth_first_visit(obj) { |v| return true if v == obj }
        false
    else
        child_object?(obj, Dependency)
    end
end

#each_fullfilled_model(&block) ⇒ Array<Model<Task>,TaskService>

Enumerates the models that are fullfilled by this task

Returns:

See Also:



808
809
810
# File 'lib/roby/task_structure/dependency.rb', line 808

def each_fullfilled_model(&block)
    fullfilled_model[0].each(&block)
end

#each_role(&block) ⇒ Object

Enumerates all the roles this task has



365
366
367
368
369
370
371
372
373
# File 'lib/roby/task_structure/dependency.rb', line 365

def each_role(&block)
    unless block_given?
        return enum_for(:each_role, &block)
    end

    each_parent_object(Dependency) do |parent|
        yield(parent, parent.roles_of(self))
    end
end

#explicit_fullfilled_modelnil, Object

Returns an explicitly set #fullfilled_model

Returns:



775
776
777
778
779
780
781
782
# File 'lib/roby/task_structure/dependency.rb', line 775

def explicit_fullfilled_model
    if explicit = @fullfilled_model
        explicit
    elsif explicit = self.model.explicit_fullfilled_model
        tasks, tags = explicit.partition { |m| m <= Roby::Task }
        [tasks.first || Roby::Task, tags, {}]
    end
end

#explicit_fullfilled_model?Boolean

True if #fullfilled_model has been set on this task or on this task's model

Returns:

  • (Boolean)


766
767
768
# File 'lib/roby/task_structure/dependency.rb', line 766

def explicit_fullfilled_model?
    !!explicit_fullfilled_model
end

#find_child_from_role(role_name) ⇒ nil, Task

Returns the child whose role is role_name

Returns:

  • (nil, Task)

    the task if a dependency with the given role is found, and nil otherwise



418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/roby/task_structure/dependency.rb', line 418

def find_child_from_role(role_name)
    each_out_neighbour_merged(Dependency, intrusive: false) do |myself, child|
        roles = myself[child, Dependency][:roles]
        if roles.include?(role_name)
            if plan
                return plan[child]
            else
                return child
            end
        end
    end
    nil
end

#find_through_method_missing(m, args) ⇒ Object



836
837
838
839
840
# File 'lib/roby/task_structure/dependency.rb', line 836

def find_through_method_missing(m, args)
    MetaRuby::DSLs.find_through_method_missing(
        self, m, args, FIND_THROUGH_METHOD_MISSING
    ) || super
end

#first_childrenObject

Return the set of this task children for which the :start event has no parent in CausalLinks



675
676
677
678
679
680
681
682
683
684
685
686
687
# File 'lib/roby/task_structure/dependency.rb', line 675

def first_children
    result = Set.new

    causal_link_graph = plan.event_relation_graph_for(EventStructure::CausalLink)
    relation_graph_for(Dependency).depth_first_visit(self) do |task|
        next if task == self

        if task != self && causal_link_graph.root?(task.start_event)
            result << task
        end
    end
    result
end

#fullfilled_model(Array<Model<Task>,Model<TaskService>>,{String=>Object}]

The list of models and arguments that this task fullfilles

If there is a task model in the list of models, it is always the first element of the model set

Beware that, for historical reasons, this is not the same format than #fullfilled_model=

Returns:



736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
# File 'lib/roby/task_structure/dependency.rb', line 736

def fullfilled_model
    if current_model = explicit_fullfilled_model
        has_value = true
    else
        current_model = [Roby::Task, [], {}]
    end

    each_in_neighbour_merged(Dependency, intrusive: false) do |myself, parent|
        has_value = true

        required_models, required_arguments = parent[myself, Dependency][:model]
        current_model = Dependency.merge_fullfilled_model(current_model,
                                                          required_models, required_arguments)
    end

    if !has_value
        model = self.model.fullfilled_model.find_all { |m| m <= Roby::Task }.min
        [[model], self.meaningful_arguments]
    else
        model, tags, arguments = *current_model
        tags = tags.dup
        tags.unshift model
        [tags, arguments]
    end
end

#fullfilled_model=(model) ⇒ Object

Sets a base model specification that must be met by this task

In normal operations, the fullfilled model returned by #fullfilled_model is computed from the dependency relations in which self is a child.

However, this fails in case self is a root task in the dependency relation. Moreover, it might be handy to over-constrain the model computed through the dependency relation.

In both cases, a model can be specified explicitely by setting the fullfilled_model attribute. The value has to be

[task_model, [tag1, tag2, ...], task_arguments]

For instance, a completely non-constrained model would be

[Roby::Task, [], {}]

This parameter can be set model-wide by using #fullfilled_model= on the class object



710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
# File 'lib/roby/task_structure/dependency.rb', line 710

def fullfilled_model=(model)
    unless model[0].kind_of?(Class)
        raise ArgumentError, "expected a task model as first element, got #{model[0]}"
    end
    if !model[1].respond_to?(:to_ary)
        raise ArgumentError, "expected an array as second element, got #{model[1]}"
    elsif !model[1].all? { |t| t.kind_of?(Roby::Models::TaskServiceModel) }
        raise ArgumentError, "expected an array of model tags as second element, got #{model[1]}"
    end

    unless model[2].respond_to?(:to_hash)
        raise ArgumentError, "expected a hash as third element, got #{model[2]}"
    end

    @fullfilled_model = model
end

#has_role?(role_name) ⇒ Boolean

Returns:

  • (Boolean)


379
380
381
# File 'lib/roby/task_structure/dependency.rb', line 379

def has_role?(role_name)
    !!find_child_from_role(role_name)
end

#has_through_method_missing?(m) ⇒ Boolean

Returns:

  • (Boolean)


830
831
832
833
834
# File 'lib/roby/task_structure/dependency.rb', line 830

def has_through_method_missing?(m)
    MetaRuby::DSLs.has_through_method_missing?(
        self, m, HAS_THROUGH_METHOD_MISSING
    ) || super
end

#parent_taskObject

Returns the single parent task for this task

If there is more than one parent or no parent at all, raise an exception



347
348
349
350
351
352
353
354
355
356
# File 'lib/roby/task_structure/dependency.rb', line 347

def parent_task
    parents = each_parent_task.to_a
    if parents.size > 1
        raise ArgumentError, "#{self} has #{parents.size} parents (#{parents.map(&:to_s).join(', ')}. A single parent was expected"
    elsif parents.empty?
        raise ArgumentError, "#{self} has no parents. A single parent was expected"
    end

    parents.first
end

#parentsObject

The set of parent objects in the Dependency relation



335
336
337
# File 'lib/roby/task_structure/dependency.rb', line 335

def parents
    parent_objects(Dependency)
end

#provided_modelsArray<Models::Task,TaskService>

Returns the set of models this task is providing by itself

It differs from #fullfilled_model because it is not considering the models that are required because of the dependency relation

Returns:

See Also:



791
792
793
794
795
796
797
798
799
800
801
802
# File 'lib/roby/task_structure/dependency.rb', line 791

def provided_models
    if model = explicit_fullfilled_model
        [model[0]] + model[1]
    else
        models = self.model.fullfilled_model
        if (task_class = models.find { |m| m.kind_of?(Class) })
            [task_class] + models.find_all { |m| !task_class.has_ancestor?(m) }
        else
            models
        end
    end
end

#remove_dependency(task_or_role) ⇒ Object



659
660
661
662
663
664
665
# File 'lib/roby/task_structure/dependency.rb', line 659

def remove_dependency(task_or_role)
    if task_or_role.respond_to?(:to_str)
        remove_child(child_from_role(task_or_role))
    else
        remove_child(task_or_role)
    end
end

#remove_finished_childrenObject

Remove all children that have successfully finished



813
814
815
816
817
818
819
820
821
822
823
824
825
# File 'lib/roby/task_structure/dependency.rb', line 813

def remove_finished_children
    # We call #to_a to get a copy of children, since we will remove
    # children in the block. Note that we can't use #delete_if here
    # since #children is a relation enumerator (not the relation list
    # itself)
    children = each_child.to_a
    for child in children
        child, info = child
        if info[:success].evaluate(child)
            remove_child(child)
        end
    end
end

#remove_roles(child, *roles, remove_child_when_empty: true) ⇒ Boolean

Remove a given role this task's child

Parameters:

  • child (Task)

    the child task

  • roles (Array<String>)

    the roles that should be removed

  • remove_child_when_empty (Boolean) (defaults to: true)

    if true (the default), the child will be removed from this task's children if the set of roles is empty

Returns:

  • (Boolean)

    true if the child is still a child of this task after the call, and false otherwise

Raises:

  • (ArgumentError)

    if the child does not have the expected role



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/roby/task_structure/dependency.rb', line 393

def remove_roles(child, *roles, remove_child_when_empty: true)
    dependency_info = self[child, Dependency].dup
    child_roles = dependency_info[:roles].dup
    roles.each do |r|
        unless child_roles.include?(r)
            raise ArgumentError, "#{r} is not a role of #{child} with respect to #{self}"
        end

        child_roles.delete(r)
    end

    if child_roles.empty? && remove_child_when_empty
        remove_child(child)
        false
    else
        dependency_info[:roles] = child_roles
        self[child, Dependency] = dependency_info
        true
    end
end

#resolve_role_path(*path) ⇒ Object

Returns a task in the dependency hierarchy of this task by following the roles. path is an array of role names, and the method will follow the trail until the desired task

Raises ArgumentError if the child does not exist

See #role_path to get a role path for a specific task



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/roby/task_structure/dependency.rb', line 463

def resolve_role_path(*path)
    if path.size == 1 && path[0].respond_to?(:to_ary)
        path = path[0]
    end
    # Special case for ease of use in algorithms
    if path.empty?
        return self
    end

    up_until_now = []
    path.inject(self) do |task, role|
        up_until_now << role
        unless (next_task = task.find_child_from_role(role))
            raise ArgumentError, "the child #{up_until_now.join('.')} of #{task} does not exist"
        end

        next_task
    end
end

#role_paths(task, validate = true) ⇒ Object

Returns a set role paths that lead to task when starting from self

A role path is an array of roles that lead to task when starting by self.

I.e. if ['role1', 'role2', 'role3'] is a role path from self to +task, it means that

task1 = self.child_from_role('role1')
task2 = task1.child_from_role('role2')
task  = task2.child_from_role('role3')

The method returns a set of role paths, as there may be multiple paths leading from self to task

See #resolve_role_path to get a task from its role path



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/roby/task_structure/dependency.rb', line 499

def role_paths(task, validate = true)
    if task == self
        return []
    end

    result = []
    task.each_role do |parent, roles|
        if parent == self
            new_paths = roles.map { |r| [r] }
        elsif heads = role_paths(parent, false)
            heads.each do |h|
                roles.each do |t|
                    result << (h.dup << t)
                end
            end
        end
        if new_paths
            result.concat(new_paths)
        end
    end

    if result.empty?
        if validate
            raise ArgumentError, "#{task} can not be reached from #{self}"
        end

        return
    end
    result
end

#rolesObject



375
376
377
# File 'lib/roby/task_structure/dependency.rb', line 375

def roles
    each_role.map { |_, roles| roles.to_a }.flatten.to_set
end

#roles_of(child) ⇒ Object

Returns the set of roles that child has



359
360
361
362
# File 'lib/roby/task_structure/dependency.rb', line 359

def roles_of(child)
    info = self[child, Dependency]
    info[:roles]
end