Module: Roby::Test::TeardownPlans

Includes:
ExpectExecution
Included in:
Roby::Test, Spec
Defined in:
lib/roby/test/teardown_plans.rb

Overview

Implementation of the teardown procedure

The main method #teardown_registered_plans is used by tests on teardown to attempt to clean up running tasks, and handle corner cases (i.e. tasks that do not want to be stopped) as best as possible

Defined Under Namespace

Classes: TeardownFailedError

Constant Summary

Constants included from ExpectExecution

ExpectExecution::SETUP_METHODS

Instance Attribute Summary collapse

Attributes included from ExpectExecution

#expect_execution_default_timeout

Instance Method Summary collapse

Methods included from ExpectExecution

#add_expectations, #execute, #execute_one_cycle, #expect_execution, #reset_current_expect_execution, #setup_current_expect_execution

Instance Attribute Details

#default_teardown_pollObject

Returns the value of attribute default_teardown_poll.



37
38
39
# File 'lib/roby/test/teardown_plans.rb', line 37

def default_teardown_poll
  @default_teardown_poll
end

#registered_plansObject (readonly)

Returns the value of attribute registered_plans.



11
12
13
# File 'lib/roby/test/teardown_plans.rb', line 11

def registered_plans
  @registered_plans
end

Instance Method Details

#clear_registered_plansObject



26
27
28
29
30
31
32
33
34
35
# File 'lib/roby/test/teardown_plans.rb', line 26

def clear_registered_plans
    registered_plans.each do |p|
        if p.respond_to?(:execution_engine)
            p.execution_engine.killall
            p.execution_engine.reset
            execute(plan: p) { p.clear }
        end
    end
    registered_plans.clear
end

#initialize(name) ⇒ Object



15
16
17
18
# File 'lib/roby/test/teardown_plans.rb', line 15

def initialize(name)
    super
    @default_teardown_poll = 0.01
end

#register_plan(plan) ⇒ Object



20
21
22
23
24
# File 'lib/roby/test/teardown_plans.rb', line 20

def register_plan(plan)
    raise "registering nil plan" unless plan

    (@registered_plans ||= []) << plan
end

#teardown_clear(plan) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Try to cleanly kill all running tasks in the registered plans

Returns:

  • (Boolean)

    true if successful, false otherwise



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/roby/test/teardown_plans.rb', line 196

def teardown_clear(plan)
    if plan.tasks.any? { |t| t.starting? || t.running? }
        Roby.warn(
            "failed to teardown: #{plan} has #{plan.tasks.size} "\
            "tasks and #{plan.free_events.size} events, "\
            "#{plan.quarantined_tasks.size} of which are in quarantine"
        )

        unless plan.execution_engine
            Roby.warn "this is most likely because this plan "\
                      "does not have an execution engine. Either "\
                      "add one or clear the plan in the tests"
        end
    end

    execute(plan: plan) { plan.clear }

    if (engine = plan.execution_engine)
        engine.clear
        engine.emitted_events.clear
    end

    unless plan.transactions.empty?
        Roby.warn "  #{plan.transactions.size} transactions left "\
                    "attached to the plan"
        plan.transactions.each(&:discard_transaction)
    end

    nil
end

#teardown_forced_killall(teardown_warn_counter, teardown_fail_counter, teardown_poll) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Force-kill all that can be

This clears all dependency relations between tasks to let the garbage collector get them unordered, and force-kills the execution agents



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/roby/test/teardown_plans.rb', line 138

def teardown_forced_killall(
    teardown_warn_counter, teardown_fail_counter, teardown_poll
)
    registered_plans.each do |plan|
        execution_agent_g =
            plan.task_relation_graph_for(TaskStructure::ExecutionAgent)
        to_stop = execution_agent_g.each_edge.find_all do |_, child, _|
            child.running? && !child.stop_event.pending? &&
                child.stop_event.controlable?
        end

        execute(plan: plan) do
            plan.each_task do |t|
                t.clear_relations(
                    remove_internal: false, remove_strong: false
                )
            end
            to_stop.each { |_, child, _| child.stop! }
        end
    end

    teardown_killall(
        teardown_warn_counter, teardown_fail_counter, teardown_poll
    )
end

#teardown_killall(teardown_warn, teardown_fail, teardown_poll) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Try to cleanly kill all running tasks in the registered plans

Returns:

  • (Boolean)

    true if successful, false otherwise



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/roby/test/teardown_plans.rb', line 89

def teardown_killall(
    teardown_warn, teardown_fail, teardown_poll
)
    executable_plans = registered_plans.find_all(&:executable?)
    plans = executable_plans.map do |p|
        [p, p.execution_engine, Set.new, Set.new]
    end

    start_time = now = Time.now
    warn_deadline = now + teardown_warn
    fail_deadline = now + teardown_fail
    until plans.empty? || (now > fail_deadline)
        plans = plans.map do |plan, engine, last_tasks, last_quarantine|
            plan_quarantine = plan.quarantined_tasks
            if now > warn_deadline
                teardown_warn(start_time, plan, last_tasks, last_quarantine)
                last_tasks = plan.tasks.dup
                last_quarantine = plan_quarantine.dup
            end
            engine.killall

            quarantine_and_dependencies =
                plan.compute_useful_tasks(plan.quarantined_tasks)

            if quarantine_and_dependencies.size != plan.tasks.size
                [plan, engine, last_tasks, last_quarantine]
            elsif !quarantine_and_dependencies.empty?
                teardown_warn(start_time, plan, last_tasks, last_quarantine)
                nil
            end
        end
        plans = plans.compact
        sleep teardown_poll

        now = Time.now
    end

    # NOTE: this is NOT plan.empty?. We stop processing plans that
    # are made of quarantined tasks and their dependencies, but
    # still report an error when they exist
    executable_plans.all?(&:empty?)
end

#teardown_registered_plans(teardown_poll: default_teardown_poll, teardown_warn: 5, teardown_fail: 20, teardown_force: teardown_fail / 2) ⇒ Object

Clear all plans registered with #registered_plans

It first attempts an orderly shutdown, then goes to try to force-stop all the tasks that can and will finally clear the data structure without caring for running tasks (something that’s bad in principle, but is usually fine during unit tests)

For instance, the default of teardown_force=10 and teardown_fail=20 will try an orderly stop for 10 seconds and a forced stop for 10s.

Parameters:

  • teardown_poll (Float) (defaults to: default_teardown_poll)

    polling period in seconds

  • teardown_warn (Float) (defaults to: 5)

    warn that something is wrong (holding up cleanup) after this many seconds

  • teardown_force (Float) (defaults to: teardown_fail / 2)

    try to force-kill tasks after this many seconds from the start

  • teardown_fail (Float) (defaults to: 20)

    stop trying to stop tasks and clear the data structure after this many seconds from the start



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/roby/test/teardown_plans.rb', line 59

def teardown_registered_plans(
    teardown_poll: default_teardown_poll,
    teardown_warn: 5, teardown_fail: 20, teardown_force: teardown_fail / 2
)
    old_gc_roby_logger_level = Roby.logger.level
    return if registered_plans.all?(&:empty?)

    success = teardown_killall(teardown_warn, teardown_force, teardown_poll)

    unless success
        Roby.warn "clean teardown failed, trying to force-kill all tasks"
        teardown_forced_killall(
            teardown_warn, (teardown_fail - teardown_force), teardown_poll
        )
    end

    registered_plans.each do |plan|
        teardown_clear(plan)
    end

    raise TeardownFailedError, "failed to tear down plan" unless success
ensure
    Roby.logger.level = old_gc_roby_logger_level
end

#teardown_warn(start_time, plan, last_tasks, last_quarantine) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/roby/test/teardown_plans.rb', line 164

def teardown_warn(start_time, plan, last_tasks, last_quarantine)
    if last_tasks != plan.tasks || last_quarantine != plan.quarantined_tasks
        duration = Integer(Time.now - start_time)
        Roby.warn "trying to shut down #{plan} for #{duration}s after "\
                  "#{self.class.name}##{name}, "\
                  "quarantine=#{plan.quarantined_tasks.size} tasks, "\
                  "tasks=#{plan.tasks.size} tasks"
    end

    if last_tasks != plan.tasks
        Roby.warn "Known tasks:"
        plan.tasks.each do |t|
            Roby.warn "  #{t} running=#{t.running?} finishing=#{t.finishing?}"
        end
    end

    plan_quarantine = plan.quarantined_tasks
    if last_quarantine != plan_quarantine
        Roby.warn "Quarantined tasks:"
        plan_quarantine.each do |t|
            Roby.warn "  #{t}"
        end
    end

    nil
end