Class: Toaster::StateTransitionGraph
- Inherits:
-
Object
- Object
- Toaster::StateTransitionGraph
- Defined in:
- lib/toaster/state/state_transition_graph.rb
Constant Summary collapse
- META_PROPERTIES =
["__task_num__", "__original__", "__combo__"]
Instance Attribute Summary collapse
-
#avoid_circles ⇒ Object
Returns the value of attribute avoid_circles.
-
#edges(include_start_and_end_nodes = false) ⇒ Object
readonly
Returns the value of attribute edges.
-
#end_node ⇒ Object
readonly
Returns the value of attribute end_node.
-
#ignore_properties ⇒ Object
readonly
Returns the value of attribute ignore_properties.
-
#nodes(include_terminal_nodes = false) ⇒ Object
readonly
Returns the value of attribute nodes.
-
#start_node ⇒ Object
readonly
Returns the value of attribute start_node.
-
#task_executions ⇒ Object
readonly
Returns the value of attribute task_executions.
-
#tasks ⇒ Object
readonly
Returns the value of attribute tasks.
Class Method Summary collapse
- .build_graph_for_automation(automation, coverage_goal = nil) ⇒ Object
- .build_graph_for_tasks(tasks, coverage_goal = nil, ignore_properties = []) ⇒ Object
- .build_graph_for_test_suite(test_suite) ⇒ Object
Instance Method Summary collapse
- #add_state(state_props) ⇒ Object
-
#build_states_for_task_executions(insert_nil_prestate_prop_for_insertions = false) ⇒ Object
Build the basic graph nodes as a permutation over the pre-states and post-states of task executions monitored by the system.
- #connect(node1, node2, conditions = nil, transition = nil) ⇒ Object
- #connect_to_matching_prestate(node, num_tasks_back) ⇒ Object
- #connect_to_successor_recursive(node, consider_task_convergence = false) ⇒ Object
- #extend_for_combination_set(combinations) ⇒ Object
-
#extend_for_combinations(combination_length, successive = false) ⇒ Object
Add graph nodes and edges to satisfy different task combination test coverage criteria.
-
#extend_for_idempodence(num_tasks, only_connect_to_start = true) ⇒ Object
Add graph nodes and edges to satisfy the idempotence test coverage criterion for a given number of tasks.
- #extend_for_skippings(combination_length, successive = false) ⇒ Object
- #get_state(state_props) ⇒ Object
-
#initialize ⇒ StateTransitionGraph
constructor
A new instance of StateTransitionGraph.
- #to_simple_json(include_start_and_end_nodes = true) ⇒ Object
Constructor Details
#initialize ⇒ StateTransitionGraph
Returns a new instance of StateTransitionGraph.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/toaster/state/state_transition_graph.rb', line 25 def initialize @start_node = StateNodeInitial.new @end_node = StateNodeFinal.new @tasks = [] @ignore_properties = [] @task_executions = {} # set of all nodes @nodes = Set.new # set of nodes without start nodes and end nodes @nodes_without_terminals = Set.new # set of all edges @edges = Set.new # set of edges which connect "actual" state nodes, i.e., # not including edges containing a start node or end node @edges_without_terminal_nodes = Set.new # avoid circles in the generated graph..? @avoid_circles = false end |
Instance Attribute Details
#avoid_circles ⇒ Object
Returns the value of attribute avoid_circles.
21 22 23 |
# File 'lib/toaster/state/state_transition_graph.rb', line 21 def avoid_circles @avoid_circles end |
#edges(include_start_and_end_nodes = false) ⇒ Object (readonly)
Returns the value of attribute edges.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def edges @edges end |
#end_node ⇒ Object (readonly)
Returns the value of attribute end_node.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def end_node @end_node end |
#ignore_properties ⇒ Object (readonly)
Returns the value of attribute ignore_properties.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def ignore_properties @ignore_properties end |
#nodes(include_terminal_nodes = false) ⇒ Object (readonly)
Returns the value of attribute nodes.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def nodes @nodes end |
#start_node ⇒ Object (readonly)
Returns the value of attribute start_node.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def start_node @start_node end |
#task_executions ⇒ Object (readonly)
Returns the value of attribute task_executions.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def task_executions @task_executions end |
#tasks ⇒ Object (readonly)
Returns the value of attribute tasks.
19 20 21 |
# File 'lib/toaster/state/state_transition_graph.rb', line 19 def tasks @tasks end |
Class Method Details
.build_graph_for_automation(automation, coverage_goal = nil) ⇒ Object
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 |
# File 'lib/toaster/state/state_transition_graph.rb', line 93 def self.build_graph_for_automation(automation, coverage_goal=nil) coverage_goal = TestCoverageGoal.new if !coverage_goal if !automation raise "Cannot build graph for nil automation." end ignore_properties = [] # dup here to avoid updates of active record ignore_properties.concat(automation.ignore_properties.to_a.dup) additional_ignores = SystemState.read_ignore_properties() additional_ignores = [] if !additional_ignores ignore_properties.concat(additional_ignores) puts "DEBUG: ignore_properties: #{ignore_properties}" # list of tasks TimeStamp.add(nil, "get_tasks") tasks = automation.get_globally_executed_tasks TimeStamp.add_and_print("load automation tasks", nil, "get_tasks") # let's make sure the list of tasks is index-based (i.e., not a Set) # (because we want to do index-based lookaheads in the tasks array later on..) tasks = tasks.to_a # STG instance graph = build_graph_for_tasks(tasks, coverage_goal, ignore_properties) return graph end |
.build_graph_for_tasks(tasks, coverage_goal = nil, ignore_properties = []) ⇒ Object
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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/toaster/state/state_transition_graph.rb', line 121 def self.build_graph_for_tasks(tasks, coverage_goal=nil, ignore_properties=[]) coverage_goal = TestCoverageGoal.new if !coverage_goal TimeStamp.add(nil, "build_graph") graph = StateTransitionGraph.new() graph.avoid_circles = coverage_goal.repeat_N.to_i >= 0 graph.ignore_properties.concat(ignore_properties) graph.tasks.concat(tasks) tasks.each do |t| t.global_executions.each do |exe| if exe.success graph.task_executions[t] = [] if !graph.task_executions[t] graph.task_executions[t] << exe end end end TimeStamp.add_and_print("load task executions for list of #{tasks.size} tasks.", nil, "build_graph") TimeStamp.add(nil, "build_graph_states") graph.build_states_for_task_executions() TimeStamp.add_and_print("build states from task executions", nil, "build_graph_states") idem_numtasks = coverage_goal.idempotence if idem_numtasks idem_numtasks = [idem_numtasks] if !idem_numtasks.kind_of?(Array) TimeStamp.add(nil, "extend_idem") idem_numtasks.each do |idem| if tasks.size >= idem graph.extend_for_idempodence(idem, coverage_goal.only_connect_to_start) end end TimeStamp.add_and_print("extend graph for idempotence", nil, "extend_idem") end TimeStamp.add(nil, "extend_comb") coverage_goal.combinations.each do |type,lengths| if type == CombinationCoverage::COMBINE_N lengths.each do |n| graph.extend_for_combinations(n, false) end elsif type == CombinationCoverage::COMBINE_N_SUCCESSIVE lengths.each do |n| graph.extend_for_combinations(n, true) end elsif type == CombinationCoverage::SKIP_N lengths.each do |n| graph.extend_for_skippings(n, false) end elsif type == CombinationCoverage::SKIP_N_SUCCESSIVE lengths.each do |n| graph.extend_for_skippings(n, true) end end end TimeStamp.add_and_print("extend graph for combinations", nil, "extend_comb") return graph end |
.build_graph_for_test_suite(test_suite) ⇒ Object
89 90 91 |
# File 'lib/toaster/state/state_transition_graph.rb', line 89 def self.build_graph_for_test_suite(test_suite) return build_graph_for_automation(test_suite.automation, test_suite.coverage_goal) end |
Instance Method Details
#add_state(state_props) ⇒ Object
82 83 84 85 86 87 |
# File 'lib/toaster/state/state_transition_graph.rb', line 82 def add_state(state_props) n = StateNode.new(state_props) @nodes.add(n) @nodes_without_terminals.add(n) return n end |
#build_states_for_task_executions(insert_nil_prestate_prop_for_insertions = false) ⇒ Object
Build the basic graph nodes as a permutation over the pre-states and post-states of task executions monitored by the system.
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 212 213 214 215 216 217 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 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/toaster/state/state_transition_graph.rb', line 185 def build_states_for_task_executions(insert_nil_prestate_prop_for_insertions=false) # node front, i.e., list of temporary child nodes as # we iterate through the tasks and build the graph node_front = [start_node()] count = 0 puts "INFO: Building graph states for #{tasks.size} tasks." # nested loop 1 tasks.each do |t| count += 1 #puts "INFO: Handling task ##{count} of #{tasks.size}. Node front size: #{node_front.size}" execs = task_executions[t] # initialize the hash with a single element that # represents an "empty pre-state" with an "empty set of conditions". # This makes sure that we always execute the innermost # loop at least once, which is actually only relevant # for the last task which has no "next pre-states"... trans_next_desired_pre_states = { {} => Set.new } # make a look-ahead to the pre-states of the next task in our sequence if count < tasks.size t_next = tasks[count] execs_next = task_executions[t_next] transitions_next = t_next.global_state_transitions(execs_next, insert_nil_prestate_prop_for_insertions) # insert artificial transition, for automations which have not been executed before if transitions_next.empty? transitions_next << StateTransition.new end trans_next_desired_pre_states = StateTransitionGraph.prestates_from_transitions(transitions_next) end #puts "trans_next_desired_pre_states size: #{trans_next_desired_pre_states.size}" transitions = t.global_state_transitions(execs, insert_nil_prestate_prop_for_insertions) # insert artificial transition, for automations which have not been executed before if transitions.empty? transitions << StateTransition.new end prestates = t.global_pre_states_reduced(transitions) desired_prestates_in_tests = prestates log "desired_prestates_in_tests #{desired_prestates_in_tests.inspect}" # are we handling the first task? if count == 1 desired_prestates_in_tests.each do |pre| # remove ignored properties remove_ignore_props(pre) # artificially introduce __task_num__ parameter, because # we want to obtain a nice, cycle-free "left-to-right" # graph that grows as we iterate through the tasks n = get_state(pre.merge({"__task_num__" => count-1, "__original__" => true})) n.succeeding_task = t connect(start_node, n) node_front << n end node_front.delete(start_node()) end # copy and clear node front list node_front_copy = node_front node_front = [] trans_post_states = StateTransitionGraph.poststates_from_transitions(transitions) # nested loop 2 trans_post_states.each do |post,trans_conditions| # Since post states are determined from the state changes # of state transitions, we might get into a situation where # the repeated execution of a task (because we test idempotence) # does not result in any state changes and hence we receive an # empty post state... # This would be incorrect and would blow up our STG! # Hence, we only continue here if either # - the post state is not empty, OR # - there is only one (probably empty) post state if !post.empty? || trans_post_states.size == 1 # remove ignored properties remove_ignore_props(post) # nested loop 3 node_front_copy.each do |node| # nested loop 4 trans_next_desired_pre_states.each do |pre_next,trans_conditions_next| new_state = {} StateTransitionGraph.state_merge!(new_state, node.properties) StateTransitionGraph.state_merge!(new_state, pre_next) StateTransitionGraph.state_merge!(new_state, post) # again, additionally introduce __task_num__ parameter StateTransitionGraph.state_merge!(new_state, {"__task_num__" => count, "__original__" => true}) n = get_state(new_state) n.preceding_task = t if count < tasks.size n.succeeding_task = tasks[count] end connect(node, n, trans_conditions) if !node_front.include?(n) && StateTransitionGraph.node_satisfies_poststate(n, post) node_front << n end end end end end # are we handling the last task? if count >= tasks.size node_front.each do |n| connect(n, end_node()) end end end return self end |
#connect(node1, node2, conditions = nil, transition = nil) ⇒ Object
44 45 46 47 48 49 50 51 52 |
# File 'lib/toaster/state/state_transition_graph.rb', line 44 def connect(node1, node2, conditions=nil, transition=nil) edge = TransitionEdge.new(node1, node2, conditions, transition) @edges.add(edge) if node1.succeeding_task @edges_without_terminal_nodes.add(edge) end node1.outgoing.add(edge) node2.incoming.add(edge) end |
#connect_to_matching_prestate(node, num_tasks_back) ⇒ Object
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/toaster/state/state_transition_graph.rb', line 349 def connect_to_matching_prestate(node, num_tasks_back) preceding_task = get_preceding_task(node, num_tasks_back) if preceding_task prestate_node = nil if !@avoid_circles # find non-conflicting pre-state for this post-state prestates = prestate_nodes_of_task(preceding_task) non_conflicting_node = nil prestates.each do |pre| if !pre.conflicts_with?(node, META_PROPERTIES) non_conflicting_node = pre break end end prestate_node = non_conflicting_node else # always create a new state node prestate_node = nil end is_new_node = false if !prestate_node state_props = node.properties.dup # decrease property "__task_num__" state_props["__task_num__"] -= num_tasks_back + 1 # remove property "__original__" state_props.delete("__original__") prestate_node = add_state(state_props) prestate_node.succeeding_task = preceding_task prestate_node.preceding_task = nil is_new_node = true #connect_to_prestate_recursively(new_node) end #connect node to new node connect(node, prestate_node, nil) # connect new node to successor node (recursively) if is_new_node connect_to_successor_recursive(prestate_node) end #end else # connect to start_node # TODO needed? #connect(@start_node, node, nil) end end |
#connect_to_successor_recursive(node, consider_task_convergence = false) ⇒ Object
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 |
# File 'lib/toaster/state/state_transition_graph.rb', line 403 def connect_to_successor_recursive(node, consider_task_convergence=false) next_task = node.succeeding_task # find non-conflicting pre-state of the next task states = poststate_nodes_of_task(next_task) non_conflicting_node = nil if !@avoid_circles states.each do |state| if !state.conflicts_with?(node, META_PROPERTIES) non_conflicting_node = state break end end end # connect to new node if non_conflicting_node connect(node, non_conflicting_node) else state_props = node.properties.dup # increase property "__task_num__" by 1 state_props["__task_num__"] += 1 # remove property "__original__" state_props.delete("__original__") if consider_task_convergence # TODO needed? raise "not implemented" # add properties which represent the state change of the task execution #execs = task_executions[next_task] #transitions = next_task.global_state_transitions(execs) #poststates = next_task.global_post_states_reduced(transitions) #state_props.merge!(poststates) end new_node = add_state(state_props) new_node.preceding_task = node.succeeding_task new_node.succeeding_task = get_task_after(node.succeeding_task) #connect node to new node connect(node, new_node) if new_node.succeeding_task connect_to_successor_recursive(new_node) else connect(new_node, @end_node) end end end |
#extend_for_combination_set(combinations) ⇒ Object
469 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 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 |
# File 'lib/toaster/state/state_transition_graph.rb', line 469 def extend_for_combination_set(combinations) combinations.each do |combo| node_front = [start_node] combo_id = Util.generate_short_uid() combo.each do |task| node_front_copy = node_front.dup node_front = [] node_front_copy.each do |node| task_prestates = prestate_nodes_of_task_reachable_from_node(task, node) task_prestates.each do |prestate_node| prestate_node_copy = prestate_node if combo.index(task) <= 0 prestate_node = get_state(prestate_node.properties.merge("__combo__" => combo_id)) elsif combo.index(task) >= combo.size - 1 node = get_state(node.properties.merge("__combo__" => combo_id)) else node = get_state(node.properties.merge("__combo__" => combo_id)) prestate_node = get_state(prestate_node.properties.merge("__combo__" => combo_id)) end connect(node, prestate_node, nil) node_front << prestate_node_copy end end end # finally, connect all nodes in the "node front" to the end node node_front.each do |node| connect(node, @end_node, nil) end end end |
#extend_for_combinations(combination_length, successive = false) ⇒ Object
Add graph nodes and edges to satisfy different task combination test coverage criteria. Args:
- *combination_length*: length of the task combinations to be covered.
- *only_successive*: whether the combinations have to be successive tasks.
457 458 459 460 461 |
# File 'lib/toaster/state/state_transition_graph.rb', line 457 def extend_for_combinations(combination_length, successive=false) #puts "INFO generating combinations of length #{combination_length} for list with #{tasks.size} tasks, only_successive=#{successive}" combinations = Combinatorial.combine(tasks, combination_length, successive) extend_for_combination_set(combinations) end |
#extend_for_idempodence(num_tasks, only_connect_to_start = true) ⇒ Object
Add graph nodes and edges to satisfy the idempotence test coverage criterion for a given number of tasks.
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'lib/toaster/state/state_transition_graph.rb', line 324 def extend_for_idempodence(num_tasks, only_connect_to_start=true) nodes_before = @nodes_without_terminals.size puts "DEBUG: Extending graph with #{nodes_before} nodes " + "for idempotence of length=#{num_tasks}, only_connect_to_start=#{only_connect_to_start}" @nodes_without_terminals.dup.each do |n| if original_node?(n) if n.succeeding_task task_index = tasks.index(n.succeeding_task) if !only_connect_to_start || task_index == num_tasks connect_to_matching_prestate(n, num_tasks - 1) end else task_index = tasks.index(n.preceding_task) + 1 if !only_connect_to_start || task_index == num_tasks #puts "DEBUG: Connecting node #{n} to matching prestate, num_tasks=#{num_tasks}" connect_to_matching_prestate(n, num_tasks - 1) end end end end nodes_after = @nodes_without_terminals.size end |
#extend_for_skippings(combination_length, successive = false) ⇒ Object
463 464 465 466 467 |
# File 'lib/toaster/state/state_transition_graph.rb', line 463 def extend_for_skippings(combination_length, successive=false) #puts "INFO generating 'skippings' of length #{combination_length} for list with #{tasks.size} tasks, only_successive=#{successive}" combinations = Combinatorial.skip(tasks, combination_length, successive) extend_for_combination_set(combinations) end |
#get_state(state_props) ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/toaster/state/state_transition_graph.rb', line 64 def get_state(state_props) return nil if !state_props # remove ignored properties from map remove_ignore_props(state_props) hash_code = state_props.hash # search for existing node @nodes_without_terminals.each do |n| # first compare hash code if hash_code == n.properties.hash # now compare actual properties if n.properties.eql?(state_props) return n end end end return add_state(state_props) end |
#to_simple_json(include_start_and_end_nodes = true) ⇒ Object
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 |
# File 'lib/toaster/state/state_transition_graph.rb', line 516 def to_simple_json(include_start_and_end_nodes=true) if @nodes.size > 1000 puts "WARN: Graph contains #{@nodes.size} nodes. Aborting request." return "{}" end json = {} nodes = (json["nodes"] = []) edges = (json["edges"] = []) node_ids = {} if include_start_and_end_nodes node_ids[@start_node] = "__start__" node_ids[@end_node] = "__end__" nodes << { "ID" => "__start__", "name" => "Start", "column" => 1, "content" => "" } nodes << { "ID" => "__end__", "name" => "End", "column" => @tasks.size + 3, "content" => "" } end counter = 0 # build list of nodes @nodes.each do |n| counter += 1 node_ids[n] = "n#{counter}" props = n.properties.dup task_num = props["__task_num__"] META_PROPERTIES.each do |mp| props.delete(mp) end name = "State #{counter}" content = MarkupUtil.to_pretty_json(props)[1..-2].strip nodes << { "ID" => node_ids[n], "name" => name, "content" => content, "column" => task_num + 2 } end # build list of edges nodes_with_start = @nodes_without_terminals.dup nodes_with_start << @start_node nodes_with_start.each do |n| n.outgoing.each do |e| from = node_ids[e.node_from] to = node_ids[e.node_to] if from && to task = e.represented_task link = $cgi && task ? l('auto' => '', 'task' => task.id, 't' => 'tasks') : "" label = task ? task.name : "" label_short = task ? ("t#{@tasks.index(task) + 1}") : "" edges << { "from" => from, "to" => to, "href" => link, "label" => label, "label_short" => label_short } end end end return MarkupUtil.to_pretty_json(json) end |