atp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/atp/flow.rb ADDED
@@ -0,0 +1,140 @@
1
+ module ATP
2
+ # Implements the main user API for building and interacting
3
+ # with an abstract test program
4
+ class Flow
5
+ attr_reader :program
6
+ # Returns the raw AST
7
+ attr_reader :raw
8
+
9
+ def initialize(program)
10
+ @program = program
11
+ @builder = AST::Builder.new
12
+ @raw = builder.flow
13
+ end
14
+
15
+ # Returns a processed/optimized AST, this is the one that should be
16
+ # used to build and represent the given test flow
17
+ def ast
18
+ ast = Processors::PreCleaner.new.process(raw)
19
+ ast = Processors::Condition.new.process(ast)
20
+ ast = Processors::Relationship.new.process(ast)
21
+ ast = Processors::PostCleaner.new.process(ast)
22
+ end
23
+
24
+ # Group all tests generated within the given block
25
+ #
26
+ # @example
27
+ # flow.group "RAM Tests" do
28
+ # flow.test ...
29
+ # flow.test ...
30
+ # end
31
+ def group(name, options = {})
32
+ open_groups.push([])
33
+ yield
34
+ append builder.group(name, open_groups.pop, options)
35
+ end
36
+
37
+ # Add a test line to the flow
38
+ #
39
+ # @param [String, Symbol] the name of the test
40
+ # @param [Hash] options a hash to describe the test's attributes
41
+ # @option options [Symbol] :id A unique test ID
42
+ # @option options [String] :description A description of what the test does, usually formatted in markdown
43
+ # @option options [Hash] :on_fail What action to take if the test fails, e.g. assign a bin
44
+ # @option options [Hash] :on_pass What action to take if the test passes
45
+ # @option options [Hash] :conditions What conditions must be met to execute the test
46
+ def test(instance, options = {})
47
+ r = options.delete(:return)
48
+ if options[:context] == :current
49
+ options[:conditions] = builder.context[:conditions]
50
+ end
51
+ # Allows any continue, bin, or soft bin argument passed in at the options top-level to be assumed
52
+ # to be the action to take if the test fails
53
+ if b = options.delete(:bin)
54
+ options[:on_fail] ||= {}
55
+ options[:on_fail][:bin] = b
56
+ end
57
+ if b = options.delete(:softbin) || b = options.delete(:sbin) || b = options.delete(:soft_bin)
58
+ options[:on_fail] ||= {}
59
+ options[:on_fail][:softbin] = b
60
+ end
61
+ if options.delete(:continue)
62
+ options[:on_fail] ||= {}
63
+ options[:on_fail][:continue] = true
64
+ end
65
+ builder.new_context
66
+
67
+ t = builder.test(instance, options)
68
+ unless options[:context] == :current
69
+ open_conditions.each do |conditions|
70
+ t = builder.apply_conditions(t, conditions)
71
+ end
72
+ end
73
+ append(t) unless r
74
+ t
75
+ end
76
+
77
+ def bin(number, options = {})
78
+ fail 'A :type option set to :pass or :fail is required when calling bin' unless options[:type]
79
+ options[:bin] = number
80
+ options[:softbin] ||= options[:soft_bin] || options[:sbin]
81
+ append builder.set_result(options[:type], options)
82
+ end
83
+
84
+ def cz(instance, cz_setup, options = {})
85
+ options[:return] = true
86
+ append(builder.cz(cz_setup, test(instance, options)))
87
+ end
88
+ alias_method :characterize, :cz
89
+
90
+ # Append a log message line to the flow
91
+ def log(message, options = {})
92
+ append builder.log(message)
93
+ end
94
+
95
+ # Insert explicitly rendered content in to the flow
96
+ def render(str, options = {})
97
+ append builder.render(str)
98
+ end
99
+
100
+ def with_condition(options)
101
+ open_conditions.push(options)
102
+ yield
103
+ open_conditions.pop
104
+ end
105
+ alias_method :with_conditions, :with_condition
106
+
107
+ # Execute the given flow in the console
108
+ def run(options = {})
109
+ Formatters::Datalog.run_and_format(ast, options)
110
+ nil
111
+ end
112
+
113
+ private
114
+
115
+ # For testing
116
+ def raw=(ast)
117
+ @raw = ast
118
+ end
119
+
120
+ def open_conditions
121
+ @open_conditions ||= []
122
+ end
123
+
124
+ def open_groups
125
+ @open_groups ||= []
126
+ end
127
+
128
+ def append(node)
129
+ if open_groups.empty?
130
+ @raw = @raw.updated(nil, @raw.children + [node])
131
+ else
132
+ open_groups.last << node
133
+ end
134
+ end
135
+
136
+ def builder
137
+ @builder
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,20 @@
1
+ module ATP
2
+ class Formatter < Processor
3
+ def format(node, options = {})
4
+ process(node)
5
+ end
6
+
7
+ def self.format(node, options = {})
8
+ new.format(node, options)
9
+ end
10
+
11
+ def self.run_and_format(node, options = {})
12
+ ast = Runner.new.run(node, options)
13
+ format(ast, options)
14
+ end
15
+
16
+ def self.run(*args)
17
+ run_and_format(*args)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ module ATP
2
+ module Formatters
3
+ # Returns the executed flow as a string of test names. This
4
+ # is mainly intended to be used for testing the runner.
5
+ class Basic < Formatter
6
+ def format(node, options = {})
7
+ @output = ''
8
+ process(node)
9
+ @output
10
+ end
11
+
12
+ def on_test(node)
13
+ @output += node.to_h[:name][0]
14
+ @output += "\n"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module ATP
2
+ module Formatters
3
+ # Outputs the given AST to something resembling an ATE datalog,
4
+ # this can optionally be rendered to a file or the console (the default).
5
+ class Datalog < Formatter
6
+ def on_flow(node)
7
+ puts 'Number Result Test Name Pin Channel Low Measured High Force Loc'
8
+ process_all(node.children)
9
+ end
10
+
11
+ def on_test(node)
12
+ t = node.to_h
13
+ str = "#{t[:number]}".ljust(11)
14
+ str += "#{t[:failed] ? 'FAIL' : 'PASS'}".ljust(9)
15
+ str += "#{t[:name][0]}".ljust(20)
16
+ puts str
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/atp/not.rb ADDED
@@ -0,0 +1,13 @@
1
+ module ATP
2
+ class NOT
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def inspect
10
+ "NOT[#{value}]"
11
+ end
12
+ end
13
+ end
data/lib/atp/or.rb ADDED
@@ -0,0 +1,11 @@
1
+ module ATP
2
+ class OR < ::Array
3
+ def initialize(*vals)
4
+ vals.flatten.each { |v| self << v }
5
+ end
6
+
7
+ def inspect
8
+ "OR#{super}"
9
+ end
10
+ end
11
+ end
data/lib/atp/parser.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'sexpistol'
2
+ module ATP
3
+ class Parser < Sexpistol
4
+ def initialize
5
+ self.ruby_keyword_literals = true
6
+ end
7
+
8
+ def string_to_ast(string)
9
+ to_sexp(parse_string(string))
10
+ end
11
+
12
+ def to_sexp(ast_array)
13
+ children = ast_array.map do |item|
14
+ if item.is_a?(Array)
15
+ to_sexp(item)
16
+ else
17
+ item
18
+ end
19
+ end
20
+ type = children.shift
21
+ return type if type.is_a?(ATP::AST::Node)
22
+ type = type.to_s.gsub('-', '_').to_sym
23
+ ATP::AST::Node.new(type, children)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ require 'ast'
2
+ module ATP
3
+ # The base processor, this provides a default handler for
4
+ # all node types and will not make any changes to the AST,
5
+ # i.e. an equivalent AST will be returned by the process method.
6
+ #
7
+ # Child classes of this should be used to implement additional
8
+ # processors to modify or otherwise work with the AST.
9
+ #
10
+ # @see http://www.rubydoc.info/gems/ast/2.0.0/AST/Processor
11
+ class Processor
12
+ include ::AST::Processor::Mixin
13
+
14
+ def process(node)
15
+ if node.respond_to?(:to_ast)
16
+ super(node)
17
+ else
18
+ node
19
+ end
20
+ end
21
+
22
+ def handler_missing(node)
23
+ node.updated(nil, process_all(node.children))
24
+ end
25
+
26
+ def n(type, children)
27
+ ATP::AST::Node.new(type, children)
28
+ end
29
+
30
+ def n0(type)
31
+ n(type, [])
32
+ end
33
+
34
+ def n1(type, arg)
35
+ n(type, [arg])
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,174 @@
1
+ module ATP
2
+ module Processors
3
+ # This optimizes the condition nodes such that any adjacent flow nodes that
4
+ # have the same condition, will be grouped together under a single condition
5
+ # wrapper.
6
+ #
7
+ # For example this AST:
8
+ #
9
+ # (flow
10
+ # (group "g1"
11
+ # (test
12
+ # (name "test1"))
13
+ # (flow-flag "bitmap" true
14
+ # (test
15
+ # (name "test2"))))
16
+ # (flow-flag "bitmap" true
17
+ # (group "g1"
18
+ # (flow-flag "x" true
19
+ # (test
20
+ # (name "test3")))
21
+ # (flow-flag "y" true
22
+ # (flow-flag "x" true
23
+ # (test
24
+ # (name "test4")))))))
25
+ #
26
+ # Will be optimized to this:
27
+ #
28
+ # (flow
29
+ # (group "g1"
30
+ # (test
31
+ # (name "test1"))
32
+ # (flow-flag "bitmap" true
33
+ # (test
34
+ # (name "test2"))
35
+ # (flow-flag "x" true
36
+ # (test
37
+ # (name "test3"))
38
+ # (flow-flag "y" true
39
+ # (test
40
+ # (name "test4")))))))
41
+ #
42
+ class Condition < Processor
43
+ CONDITION_NODES = [:flow_flag, :test_result, :test_executed, :group, :job]
44
+
45
+ def process(node)
46
+ # Bit of a hack - To get all of the nested conditions optimized away it is necessary
47
+ # to execute this recursively a few times. This guard ensures that the recursion is
48
+ # only performed on the top-level and not on every process operation.
49
+ if @top_level_called
50
+ super
51
+ else
52
+ @top_level_called = true
53
+ ast1 = nil
54
+ ast2 = node
55
+ while ast1 != ast2
56
+ ast1 = super(ast2)
57
+ ast2 = super(ast1)
58
+ end
59
+ @top_level_called = false
60
+ ast1
61
+ end
62
+ end
63
+
64
+ def on_boolean_condition(node)
65
+ children = node.children.dup
66
+ name = children.shift
67
+ state = children.shift
68
+ children = optimize_siblings(n(:temp, children))
69
+ if condition_to_be_removed?(node)
70
+ process_all(children)
71
+ else
72
+ node.updated(nil, [name, state] + process_all(children))
73
+ end
74
+ end
75
+ alias_method :on_flow_flag, :on_boolean_condition
76
+ alias_method :on_test_result, :on_boolean_condition
77
+ alias_method :on_test_executed, :on_boolean_condition
78
+
79
+ def on_condition(node)
80
+ children = node.children.dup
81
+ name = children.shift
82
+ children = optimize_siblings(n(:temp, children))
83
+ if condition_to_be_removed?(node)
84
+ process_all(children)
85
+ else
86
+ node.updated(nil, [name] + process_all(children))
87
+ end
88
+ end
89
+ alias_method :on_group, :on_condition
90
+ alias_method :on_job, :on_condition
91
+
92
+ # Returns true if the given node contains the given condition within
93
+ # its immediate children
94
+ def has_condition?(condition, node)
95
+ ([node] + node.children.to_a).any? do |n|
96
+ if n.is_a?(ATP::AST::Node)
97
+ equal_conditions?(condition, n)
98
+ end
99
+ end
100
+ end
101
+
102
+ def condition_to_be_removed?(node)
103
+ remove_condition.last && equal_conditions?(remove_condition.last, node)
104
+ end
105
+
106
+ def equal_conditions?(node1, node2)
107
+ if node1.type == node2.type
108
+ if node1.type == :group || node1.type == :job
109
+ node1.to_a.take(1) == node2.to_a.take(1)
110
+ else
111
+ node1.to_a.take(2) == node2.to_a.take(2)
112
+ end
113
+ end
114
+ end
115
+
116
+ def condition?(node)
117
+ node.is_a?(ATP::AST::Node) && CONDITION_NODES.include?(node.type)
118
+ end
119
+
120
+ def on_flow(node)
121
+ node.updated(nil, optimize_siblings(node))
122
+ end
123
+
124
+ def on_members(node)
125
+ node.updated(nil, optimize_siblings(node))
126
+ end
127
+
128
+ def optimize_siblings(top_node)
129
+ children = []
130
+ unprocessed_children = []
131
+ current = nil
132
+ last = top_node.children.size - 1
133
+ top_node.to_a.each_with_index do |node, i|
134
+ # If a condition has been identified in a previous node
135
+ if current
136
+ process_nodes = false
137
+ # If this node has the current condition, then buffer it for later processing
138
+ # and continue to the next node
139
+ if has_condition?(current, node)
140
+ unprocessed_children << node
141
+ node = nil
142
+ else
143
+ process_nodes = true
144
+ end
145
+ if process_nodes || i == last
146
+ remove_condition << current
147
+ current_children = current.children + [process_all(unprocessed_children)].flatten
148
+ unprocessed_children = []
149
+ remove_condition.pop
150
+ children << process(current.updated(nil, current_children))
151
+ if node && (!condition?(node) || i == last)
152
+ current = nil
153
+ children << process(node)
154
+ else
155
+ current = node
156
+ end
157
+ end
158
+ else
159
+ if condition?(node) && i != last
160
+ current = node
161
+ else
162
+ children << process(node)
163
+ end
164
+ end
165
+ end
166
+ children.flatten
167
+ end
168
+
169
+ def remove_condition
170
+ @remove_condition ||= []
171
+ end
172
+ end
173
+ end
174
+ end