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



613
614
615
616
# File 'lib/roby/task_structure/dependency.rb', line 613

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.



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

def child_from_role(role_name, validate = true)
    if !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 = Hash.new
        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



295
# File 'lib/roby/task_structure/dependency.rb', line 295

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)


278
# File 'lib/roby/task_structure/dependency.rb', line 278

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.



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
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
# File 'lib/roby/task_structure/dependency.rb', line 499

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]), Hash.new]
    elsif options[:model].size == 2
        if !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], Hash.new]
            end
        end
    elsif !options[:model].first.respond_to?(:to_ary)
        options[:model] = [Array(options[:model]), Hash.new]
    end

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

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

    if options[:failure].nil?
        options[:failure] = []
    end
    options[:failure] = Array[*options[:failure]].
        map { |predicate| predicate.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) }
        if !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) }
        if !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
    if !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.
    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)


283
284
285
286
287
288
289
290
291
# File 'lib/roby/task_structure/dependency.rb', line 283

def depends_on?(obj, recursive: false)
    if recursive
        relation_graph_for(Dependency).
            depth_first_visit(obj) { |v| return true if v == obj }
        return 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:



747
748
749
# File 'lib/roby/task_structure/dependency.rb', line 747

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

#each_role(&block) ⇒ Object

Enumerates all the roles this task has



316
317
318
319
320
321
322
323
# File 'lib/roby/task_structure/dependency.rb', line 316

def each_role(&block)
    if !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:



717
718
719
720
721
722
723
724
# File 'lib/roby/task_structure/dependency.rb', line 717

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, Hash.new]
    end
end

#explicit_fullfilled_model?Boolean

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

Returns:

  • (Boolean)


708
709
710
# File 'lib/roby/task_structure/dependency.rb', line 708

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



367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/roby/task_structure/dependency.rb', line 367

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



770
771
772
773
# File 'lib/roby/task_structure/dependency.rb', line 770

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



620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/roby/task_structure/dependency.rb', line 620

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]



679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
# File 'lib/roby/task_structure/dependency.rb', line 679

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



654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/roby/task_structure/dependency.rb', line 654

def fullfilled_model=(model)
    if !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

    if !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)


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

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

#has_through_method_missing?(m) ⇒ Boolean

Returns:

  • (Boolean)


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

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



299
300
301
302
303
304
305
306
307
# File 'lib/roby/task_structure/dependency.rb', line 299

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



293
# File 'lib/roby/task_structure/dependency.rb', line 293

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:



733
734
735
736
737
738
739
740
741
# File 'lib/roby/task_structure/dependency.rb', line 733

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

#remove_dependency(task_or_role) ⇒ Object



604
605
606
607
608
609
610
# File 'lib/roby/task_structure/dependency.rb', line 604

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



752
753
754
755
756
757
758
759
760
761
762
763
764
# File 'lib/roby/task_structure/dependency.rb', line 752

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



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/roby/task_structure/dependency.rb', line 343

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|
        if !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



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/roby/task_structure/dependency.rb', line 412

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
        if !(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



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/roby/task_structure/dependency.rb', line 447

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



325
326
327
# File 'lib/roby/task_structure/dependency.rb', line 325

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



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

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