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

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

Instance Method Summary collapse

Instance Method Details

#added_child(child, info) ⇒ Object

Set up the event gathering needed by Dependency.check_structure



639
640
641
642
# File 'lib/roby/task_structure/dependency.rb', line 639

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.



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/roby/task_structure/dependency.rb', line 409

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



311
312
313
# File 'lib/roby/task_structure/dependency.rb', line 311

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)


288
289
290
# File 'lib/roby/task_structure/dependency.rb', line 288

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.



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
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
# File 'lib/roby/task_structure/dependency.rb', line 523

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)


295
296
297
298
299
300
301
302
303
# File 'lib/roby/task_structure/dependency.rb', line 295

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:



779
780
781
# File 'lib/roby/task_structure/dependency.rb', line 779

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

#each_role(&block) ⇒ Object

Enumerates all the roles this task has



336
337
338
339
340
341
342
343
344
# File 'lib/roby/task_structure/dependency.rb', line 336

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:



746
747
748
749
750
751
752
753
# File 'lib/roby/task_structure/dependency.rb', line 746

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)


737
738
739
# File 'lib/roby/task_structure/dependency.rb', line 737

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



389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/roby/task_structure/dependency.rb', line 389

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



803
804
805
806
# File 'lib/roby/task_structure/dependency.rb', line 803

def find_through_method_missing(m, args)
    MetaRuby::DSLs.find_through_method_missing(
        self, m, args, "_child" => :find_child_from_role)
end

#first_childrenObject

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



646
647
648
649
650
651
652
653
654
655
656
657
658
# File 'lib/roby/task_structure/dependency.rb', line 646

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:

  • ((Array<Model<Task>,Model<TaskService>>,{String=>Object}])

    (Array<Model<Task>,Model<TaskService>>,String=>Object]



707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
# File 'lib/roby/task_structure/dependency.rb', line 707

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



681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/roby/task_structure/dependency.rb', line 681

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)


350
351
352
# File 'lib/roby/task_structure/dependency.rb', line 350

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

#has_through_method_missing?(m) ⇒ Boolean

Returns:

  • (Boolean)


798
799
800
801
# File 'lib/roby/task_structure/dependency.rb', line 798

def has_through_method_missing?(m)
    MetaRuby::DSLs.has_through_method_missing?(
        self, m, "_child" => :has_role?)
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



318
319
320
321
322
323
324
325
326
327
# File 'lib/roby/task_structure/dependency.rb', line 318

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



306
307
308
# File 'lib/roby/task_structure/dependency.rb', line 306

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:



762
763
764
765
766
767
768
769
770
771
772
773
# File 'lib/roby/task_structure/dependency.rb', line 762

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



630
631
632
633
634
635
636
# File 'lib/roby/task_structure/dependency.rb', line 630

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



784
785
786
787
788
789
790
791
792
793
794
795
796
# File 'lib/roby/task_structure/dependency.rb', line 784

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



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/roby/task_structure/dependency.rb', line 364

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



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

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



470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/roby/task_structure/dependency.rb', line 470

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



346
347
348
# File 'lib/roby/task_structure/dependency.rb', line 346

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



330
331
332
333
# File 'lib/roby/task_structure/dependency.rb', line 330

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