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

#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

#teardown_failObject

Assume that the teardown failed after this many seconds



51
52
53
# File 'lib/roby/test/teardown_plans.rb', line 51

def teardown_fail
  @teardown_fail
end

#teardown_forceObject

Force-kill all tasks after this many seconds instead of doing a clean garbage collection. The test itself will be failed when this happens



48
49
50
# File 'lib/roby/test/teardown_plans.rb', line 48

def teardown_force
  @teardown_force
end

#teardown_pollObject

Polling period of the teardown process



41
42
43
# File 'lib/roby/test/teardown_plans.rb', line 41

def teardown_poll
  @teardown_poll
end

#teardown_warnObject

Output information about the teardown process after this many seconds



44
45
46
# File 'lib/roby/test/teardown_plans.rb', line 44

def teardown_warn
  @teardown_warn
end

Instance Method Details

#clear_registered_plansObject



29
30
31
32
33
34
35
36
37
38
# File 'lib/roby/test/teardown_plans.rb', line 29

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
19
20
21
# File 'lib/roby/test/teardown_plans.rb', line 15

def initialize(name)
    super
    @teardown_poll = 0.01
    @teardown_fail = 20
    @teardown_warn = 5
    @teardown_force = 10
end

#register_plan(plan) ⇒ Object



23
24
25
26
27
# File 'lib/roby/test/teardown_plans.rb', line 23

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



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/roby/test/teardown_plans.rb', line 218

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



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/roby/test/teardown_plans.rb', line 159

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



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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/roby/test/teardown_plans.rb', line 100

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
    while now < fail_deadline
        plans = plans.map do |plan, engine, last_tasks, last_quarantine|
            if now > warn_deadline
                teardown_show_plan_state_if_changed(
                    start_time, plan, last_tasks, last_quarantine
                )
                last_tasks = plan.tasks.dup
                last_quarantine = plan.quarantined_tasks.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]
            end
        end
        plans = plans.compact
        break if plans.empty?

        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
    return true if executable_plans.all?(&:empty?)

    executable_plans
        .find_all { |p| !p.empty? }
        .each do |plan|
            teardown_show_plan_state_if_changed(
                start_time, plan, [], [], force: true
            )
        end

    false
end

#teardown_registered_plans(teardown_poll: @teardown_poll, teardown_warn: @teardown_warn, teardown_fail: @teardown_fail, teardown_force: @teardown_force) ⇒ 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: @teardown_poll)

    polling period in seconds

  • teardown_warn (Float) (defaults to: @teardown_warn)

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

  • teardown_force (Float) (defaults to: @teardown_force)

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

  • teardown_fail (Float) (defaults to: @teardown_fail)

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



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/roby/test/teardown_plans.rb', line 73

def teardown_registered_plans(
    teardown_poll: @teardown_poll, teardown_warn: @teardown_warn,
    teardown_fail: @teardown_fail, teardown_force: @teardown_force
)
    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 { |plan| teardown_clear(plan) }

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

#teardown_show_plan_state_if_changed(start_time, plan, last_tasks, last_quarantine, force: false) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/roby/test/teardown_plans.rb', line 185

def teardown_show_plan_state_if_changed(
    start_time, plan, last_tasks, last_quarantine, force: false
)
    changed_since_last_warning =
        (last_tasks != plan.tasks) ||
        (last_quarantine != plan.quarantined_tasks)

    return unless force || changed_since_last_warning

    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"

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

    Roby.warn "Quarantined tasks:"
    plan.quarantined_tasks.each do |t|
        Roby.warn "  #{t}"
    end

    nil
end