atp 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7ca6eaefd174b135c5b39a8b2d287f24cd890814
4
- data.tar.gz: 3c379de6d014a88c5dbec902ec85eda2d4d1abd6
3
+ metadata.gz: 0318ad5ca67b39da3b87b038434eaa85b1ee2a1d
4
+ data.tar.gz: 63bdc11c210bba2219097d14b3c5aa55678a8c8f
5
5
  SHA512:
6
- metadata.gz: ec297a8c950f05a49eaa17c359d5c35378aea3e9a1e39f88f3c72906968658dc22e61590357f83b462f25dab699a1577d7d334efdb26d126b0f3694a159eaa9f
7
- data.tar.gz: 1fc8899484368c2fc172801ba5bae1968fd9488d52420d0f0c30e1275a9db41783ab2886536317ca5744e2b7222badee59a9e6d5006d373b827f6e90d3bb6d80
6
+ metadata.gz: ae4da77cd22cc197e3ecf80af390de270b71c6a00cfcae3c49345156b9840c5f6429520707c29c36dffb89d54ec50b8af710257c93f23b872d44b5a10fb8aad6
7
+ data.tar.gz: 6ee881a43ee9cf5015860b50719d6d07c461f545180d967319d8ada891a36cd47adf8ac48a5ef277c79494bfbc0a54dfdddb77d1bbaac77a70dc8a6d67b63f24
@@ -1,7 +1,7 @@
1
1
  module ATP
2
2
  MAJOR = 0
3
3
  MINOR = 2
4
- BUGFIX = 0
4
+ BUGFIX = 1
5
5
  DEV = nil
6
6
 
7
7
  VERSION = [MAJOR, MINOR, BUGFIX].join(".") + (DEV ? ".pre#{DEV}" : '')
data/lib/atp.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  require 'origen'
2
2
  require_relative '../config/application.rb'
3
+
3
4
  module ATP
4
5
  autoload :Program, 'atp/program'
5
6
  autoload :Flow, 'atp/flow'
6
7
  autoload :Processor, 'atp/processor'
8
+ autoload :Validator, 'atp/validator'
7
9
  autoload :Runner, 'atp/runner'
8
10
  autoload :Formatter, 'atp/formatter'
9
11
  autoload :Parser, 'atp/parser'
@@ -22,6 +24,7 @@ module ATP
22
24
  # and to implement the flow control API
23
25
  module Processors
24
26
  autoload :Condition, 'atp/processors/condition'
27
+ autoload :ConditionExtractor, 'atp/processors/condition_extractor'
25
28
  autoload :Relationship, 'atp/processors/relationship'
26
29
  autoload :PreCleaner, 'atp/processors/pre_cleaner'
27
30
  autoload :PostCleaner, 'atp/processors/post_cleaner'
@@ -34,6 +37,8 @@ module ATP
34
37
  # Validators are run on the processed AST to check it for common errors or
35
38
  # logical issues that will prevent it being rendered to a test program format
36
39
  module Validators
40
+ autoload :DuplicateIDs, 'atp/validators/duplicate_ids'
41
+ autoload :MissingIDs, 'atp/validators/missing_ids'
37
42
  autoload :Condition, 'atp/validators/condition'
38
43
  end
39
44
 
@@ -4,6 +4,7 @@ module ATP
4
4
  include Factories
5
5
 
6
6
  attr_reader :context
7
+ attr_accessor :source_file, :source_line_number
7
8
 
8
9
  def flow
9
10
  n0(:flow)
@@ -13,8 +14,13 @@ module ATP
13
14
  n(:name, str.to_s)
14
15
  end
15
16
 
16
- def log(str)
17
- n(:log, str.to_s)
17
+ def log(str, options = {})
18
+ test = n(:log, str.to_s)
19
+ if options[:conditions]
20
+ apply_conditions(test, options[:conditions])
21
+ else
22
+ test
23
+ end
18
24
  end
19
25
 
20
26
  def render(str)
@@ -41,6 +47,24 @@ module ATP
41
47
  n(:test_executed, id, executed, node)
42
48
  end
43
49
 
50
+ def enable_flow_flag(var, options = {})
51
+ test = n(:enable_flow_flag, var)
52
+ if options[:conditions]
53
+ apply_conditions(test, options[:conditions])
54
+ else
55
+ test
56
+ end
57
+ end
58
+
59
+ def disable_flow_flag(var, options = {})
60
+ test = n(:disable_flow_flag, var)
61
+ if options[:conditions]
62
+ apply_conditions(test, options[:conditions])
63
+ else
64
+ test
65
+ end
66
+ end
67
+
44
68
  def group(group_name, nodes, options = {})
45
69
  children = [name(group_name)]
46
70
 
@@ -59,8 +83,13 @@ module ATP
59
83
  end
60
84
  end
61
85
 
62
- def cz(setup, node)
63
- n(:cz, setup, node)
86
+ def cz(setup, node, options = {})
87
+ test = n(:cz, setup, node)
88
+ if options[:conditions]
89
+ apply_conditions(test, options[:conditions])
90
+ else
91
+ test
92
+ end
64
93
  end
65
94
 
66
95
  def new_context
@@ -199,7 +228,13 @@ module ATP
199
228
  children << n(:bin, options[:bin]) if options[:bin]
200
229
  children << n(:softbin, options[:softbin]) if options[:softbin]
201
230
  children << n(:description, options[:description]) if options[:description]
202
- n(:set_result, *children)
231
+ result = n(:set_result, *children)
232
+
233
+ if options[:conditions]
234
+ apply_conditions(result, options[:conditions])
235
+ else
236
+ result
237
+ end
203
238
  end
204
239
 
205
240
  def number(val)
@@ -2,11 +2,14 @@ module ATP
2
2
  module AST
3
3
  module Factories
4
4
  def n(type, *children)
5
- ATP::AST::Node.new(type, children)
5
+ options = children.last.is_a?(Hash) ? children.pop : {}
6
+ options[:file] ||= options.delete(:source_file) || try(:source_file)
7
+ options[:line_number] ||= options.delete(:source_line_number) || try(:source_line_number)
8
+ ATP::AST::Node.new(type, children, options)
6
9
  end
7
10
 
8
- def n0(type)
9
- ATP::AST::Node.new(type, [])
11
+ def n0(type, options = {})
12
+ n(type, options)
10
13
  end
11
14
  end
12
15
  end
@@ -4,6 +4,8 @@ module ATP
4
4
  class Node < ::AST::Node
5
5
  include Factories
6
6
 
7
+ attr_reader :file, :line_number
8
+
7
9
  def initialize(type, children = [], properties = {})
8
10
  # Always use strings instead of symbols in the AST, makes serializing
9
11
  # back and forward to a string easier
@@ -11,6 +13,14 @@ module ATP
11
13
  super type, children, properties
12
14
  end
13
15
 
16
+ def source
17
+ if file
18
+ "#{file}:#{line_number}"
19
+ else
20
+ '<Sorry, lost the source file info, please include an example if you report as a bug>'
21
+ end
22
+ end
23
+
14
24
  # Create a new node from the given S-expression (a string)
15
25
  def self.from_sexp(sexp)
16
26
  @parser ||= Parser.new
@@ -2,12 +2,13 @@ module ATP
2
2
  # Implements the main user API for building and interacting
3
3
  # with an abstract test program
4
4
  class Flow
5
- attr_reader :program
5
+ attr_reader :program, :name
6
6
  # Returns the raw AST
7
7
  attr_reader :raw
8
8
 
9
- def initialize(program)
9
+ def initialize(program, name = nil)
10
10
  @program = program
11
+ @name = name
11
12
  @builder = AST::Builder.new
12
13
  @raw = builder.flow
13
14
  end
@@ -16,6 +17,8 @@ module ATP
16
17
  # used to build and represent the given test flow
17
18
  def ast
18
19
  ast = Processors::PreCleaner.new.process(raw)
20
+ Validators::DuplicateIDs.new(self).process(ast)
21
+ Validators::MissingIDs.new(self).process(ast)
19
22
  ast = Processors::Condition.new.process(ast)
20
23
  ast = Processors::Relationship.new.process(ast)
21
24
  ast = Processors::PostCleaner.new.process(ast)
@@ -44,60 +47,86 @@ module ATP
44
47
  # @option options [Hash] :on_pass What action to take if the test passes
45
48
  # @option options [Hash] :conditions What conditions must be met to execute the test
46
49
  def test(instance, options = {})
50
+ extract_meta!(options)
47
51
  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)
52
+ t = apply_open_conditions(options) do |options|
53
+ # Allows any continue, bin, or soft bin argument passed in at the options top-level to be assumed
54
+ # to be the action to take if the test fails
55
+ if b = options.delete(:bin)
56
+ options[:on_fail] ||= {}
57
+ options[:on_fail][:bin] = b
58
+ end
59
+ if b = options.delete(:softbin) || b = options.delete(:sbin) || b = options.delete(:soft_bin)
60
+ options[:on_fail] ||= {}
61
+ options[:on_fail][:softbin] = b
71
62
  end
63
+ if options.delete(:continue)
64
+ options[:on_fail] ||= {}
65
+ options[:on_fail][:continue] = true
66
+ end
67
+ builder.test(instance, options)
72
68
  end
73
69
  append(t) unless r
74
70
  t
75
71
  end
76
72
 
77
73
  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)
74
+ extract_meta!(options)
75
+ t = apply_open_conditions(options) do |options|
76
+ fail 'A :type option set to :pass or :fail is required when calling bin' unless options[:type]
77
+ options[:bin] = number
78
+ options[:softbin] ||= options[:soft_bin] || options[:sbin]
79
+ builder.set_result(options[:type], options)
80
+ end
81
+ append(t)
82
82
  end
83
83
 
84
84
  def cz(instance, cz_setup, options = {})
85
- options[:return] = true
86
- append(builder.cz(cz_setup, test(instance, options)))
85
+ extract_meta!(options)
86
+ t = apply_open_conditions(options) do |options|
87
+ conditions = options.delete(:conditions)
88
+ options[:return] = true
89
+ builder.cz(cz_setup, test(instance, options), conditions: conditions)
90
+ end
91
+ append(t)
87
92
  end
88
93
  alias_method :characterize, :cz
89
94
 
90
95
  # Append a log message line to the flow
91
96
  def log(message, options = {})
92
- append builder.log(message)
97
+ extract_meta!(options)
98
+ t = apply_open_conditions(options) do |options|
99
+ builder.log(message, options)
100
+ end
101
+ append(t)
102
+ end
103
+
104
+ # Enable a flow control variable
105
+ def enable(var, options = {})
106
+ extract_meta!(options)
107
+ t = apply_open_conditions(options) do |options|
108
+ builder.enable_flow_flag(var, options)
109
+ end
110
+ append(t)
111
+ end
112
+
113
+ # Disable a flow control variable
114
+ def disable(var, options = {})
115
+ extract_meta!(options)
116
+ t = apply_open_conditions(options) do |options|
117
+ builder.disable_flow_flag(var, options)
118
+ end
119
+ append(t)
93
120
  end
94
121
 
95
122
  # Insert explicitly rendered content in to the flow
96
123
  def render(str, options = {})
124
+ extract_meta!(options)
97
125
  append builder.render(str)
98
126
  end
99
127
 
100
128
  def with_condition(options)
129
+ extract_meta!(options)
101
130
  open_conditions.push(options)
102
131
  yield
103
132
  open_conditions.pop
@@ -112,6 +141,25 @@ module ATP
112
141
 
113
142
  private
114
143
 
144
+ def apply_open_conditions(options)
145
+ if options[:context] == :current
146
+ options[:conditions] = builder.context[:conditions]
147
+ end
148
+ builder.new_context
149
+ t = yield(options)
150
+ unless options[:context] == :current
151
+ open_conditions.each do |conditions|
152
+ t = builder.apply_conditions(t, conditions)
153
+ end
154
+ end
155
+ t
156
+ end
157
+
158
+ def extract_meta!(options)
159
+ builder.source_file = options.delete(:source_file) if options[:source_file]
160
+ builder.source_line_number = options.delete(:source_line_number) if options[:source_line_number]
161
+ end
162
+
115
163
  # For testing
116
164
  def raw=(ast)
117
165
  @raw = ast
@@ -11,6 +11,10 @@ module ATP
11
11
  class Processor
12
12
  include ::AST::Processor::Mixin
13
13
 
14
+ def run(node)
15
+ process(node)
16
+ end
17
+
14
18
  def process(node)
15
19
  if node.respond_to?(:to_ast)
16
20
  super(node)
@@ -65,7 +65,9 @@ module ATP
65
65
  children = node.children.dup
66
66
  name = children.shift
67
67
  state = children.shift
68
+ remove_condition << node
68
69
  children = optimize_siblings(n(:temp, children))
70
+ remove_condition.pop
69
71
  if condition_to_be_removed?(node)
70
72
  process_all(children)
71
73
  else
@@ -79,7 +81,9 @@ module ATP
79
81
  def on_condition(node)
80
82
  children = node.children.dup
81
83
  name = children.shift
84
+ remove_condition << node
82
85
  children = optimize_siblings(n(:temp, children))
86
+ remove_condition.pop
83
87
  if condition_to_be_removed?(node)
84
88
  process_all(children)
85
89
  else
@@ -100,7 +104,7 @@ module ATP
100
104
  end
101
105
 
102
106
  def condition_to_be_removed?(node)
103
- remove_condition.last && equal_conditions?(remove_condition.last, node)
107
+ remove_condition.any? { |c| equal_conditions?(c, node) }
104
108
  end
105
109
 
106
110
  def equal_conditions?(node1, node2)
@@ -118,11 +122,17 @@ module ATP
118
122
  end
119
123
 
120
124
  def on_flow(node)
121
- node.updated(nil, optimize_siblings(node))
125
+ # The extract_common_embedded_conditions method can probably do the whole job,
126
+ # but it might get a little complicated with regards to optimizing adjacent groups,
127
+ # so have left the original logic to have the first crack and deal with the groups
128
+ # for now.
129
+ nodes = optimize_siblings(node)
130
+ nodes = extract_common_embedded_conditions(nodes)
131
+ node.updated(nil, nodes)
122
132
  end
123
133
 
124
134
  def on_members(node)
125
- node.updated(nil, optimize_siblings(node))
135
+ node.updated(nil, extract_common_embedded_conditions(optimize_siblings(node)))
126
136
  end
127
137
 
128
138
  def optimize_siblings(top_node)
@@ -166,6 +176,46 @@ module ATP
166
176
  children.flatten
167
177
  end
168
178
 
179
+ def extract_common_embedded_conditions(nodes)
180
+ nodes = [nodes] unless nodes.is_a?(Array)
181
+ result = []
182
+ cond_a = nil
183
+ test_a = nil
184
+ ConditionExtractor.new.run(nodes).each do |cond_b, test_b|
185
+ if cond_a
186
+ common = cond_a & cond_b
187
+ if common.empty?
188
+ result << combine(cond_a, extract_common_embedded_conditions(test_a))
189
+ cond_a = cond_b
190
+ test_a = test_b
191
+ else
192
+ a = combine(cond_a - common, test_a)
193
+ b = combine(cond_b - common, test_b)
194
+ cond_a = common
195
+ test_a = [a, b].flatten
196
+ end
197
+ else
198
+ cond_a = cond_b
199
+ test_a = test_b
200
+ end
201
+ end
202
+ if nodes == [test_a]
203
+ nodes
204
+ else
205
+ result << combine(cond_a, extract_common_embedded_conditions(test_a))
206
+ result.flatten
207
+ end
208
+ end
209
+
210
+ def combine(conditions, node)
211
+ if conditions && !conditions.empty?
212
+ conditions.reverse_each do |n|
213
+ node = n.updated(nil, n.children + (node.is_a?(Array) ? node : [node]))
214
+ end
215
+ end
216
+ node
217
+ end
218
+
169
219
  def remove_condition
170
220
  @remove_condition ||= []
171
221
  end
@@ -0,0 +1,46 @@
1
+ module ATP
2
+ module Processors
3
+ class ConditionExtractor < Processor
4
+ attr_reader :results, :conditions
5
+
6
+ def run(nodes)
7
+ @results = []
8
+ @conditions = []
9
+ process_all(nodes)
10
+ @results
11
+ end
12
+
13
+ def on_boolean_condition(node)
14
+ children = node.children.dup
15
+ name = children.shift
16
+ state = children.shift
17
+ conditions << node.updated(nil, [name, state])
18
+ process_all(children)
19
+ conditions.pop
20
+ end
21
+ alias_method :on_flow_flag, :on_boolean_condition
22
+ alias_method :on_test_result, :on_boolean_condition
23
+ alias_method :on_test_executed, :on_boolean_condition
24
+
25
+ def on_condition(node)
26
+ children = node.children.dup
27
+ name = children.shift
28
+ conditions << node.updated(nil, [name])
29
+ process_all(children)
30
+ conditions.pop
31
+ end
32
+ alias_method :on_job, :on_condition
33
+
34
+ def on_test(node)
35
+ results << [conditions.uniq, node]
36
+ end
37
+ alias_method :on_group, :on_test
38
+ alias_method :on_log, :on_test
39
+ alias_method :on_enable_flow_flag, :on_test
40
+ alias_method :on_disable_flow_flag, :on_test
41
+ alias_method :on_cz, :on_test
42
+ alias_method :on_set_result, :on_test
43
+ alias_method :on_render, :on_test
44
+ end
45
+ end
46
+ end
@@ -8,12 +8,20 @@ module ATP
8
8
  @group_ids = []
9
9
  end
10
10
 
11
+ # Make all IDs lower cased symbols
11
12
  def on_id(node)
12
- id = node.to_a.first
13
- id = id.to_s.downcase.to_sym
14
- node.updated(nil, [id])
13
+ id = node.to_a[0]
14
+ node.updated(nil, [clean(id)])
15
15
  end
16
16
 
17
+ # Make all ID references use the lower case symbols
18
+ def on_test_executed(node)
19
+ children = node.children.dup
20
+ children[0] = clean(children[0])
21
+ node.updated(nil, process_all(children))
22
+ end
23
+ alias_method :on_test_result, :on_test_executed
24
+
17
25
  def on_group(node)
18
26
  if id = node.children.find { |n| n.type == :id }
19
27
  @group_ids << process(id).value
@@ -38,6 +46,14 @@ module ATP
38
46
  end
39
47
  node.updated(nil, process_all(children))
40
48
  end
49
+
50
+ def clean(id)
51
+ if id.is_a?(Array)
52
+ id.map { |i| clean(i) }
53
+ else
54
+ id.to_s.downcase.to_sym
55
+ end
56
+ end
41
57
  end
42
58
  end
43
59
  end
@@ -2,7 +2,7 @@ module ATP
2
2
  # Program is the top-level container for a collection of test flows
3
3
  class Program
4
4
  def flow(name)
5
- flows[name] ||= Flow.new(self)
5
+ flows[name] ||= Flow.new(self, name)
6
6
  end
7
7
 
8
8
  def flows
@@ -0,0 +1,24 @@
1
+ require 'ast'
2
+ module ATP
3
+ class Validator < Processor
4
+ attr_reader :flow
5
+
6
+ def initialize(flow)
7
+ @flow = flow
8
+ end
9
+
10
+ def process(node)
11
+ if @top_level_called
12
+ super
13
+ else
14
+ @top_level_called = true
15
+ setup
16
+ super(node)
17
+ exit 1 if on_completion
18
+ end
19
+ end
20
+
21
+ def setup
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ module ATP
2
+ module Validators
3
+ class DuplicateIDs < Validator
4
+ def on_completion
5
+ if @duplicate_ids
6
+ @duplicate_ids.each do |id, nodes|
7
+ Origen.log.error "Test ID #{id} is defined more than once in flow #{flow.name}:"
8
+ nodes.each do |node|
9
+ Origen.log.error " #{node.source}"
10
+ end
11
+ end
12
+ true
13
+ end
14
+ end
15
+
16
+ def on_id(node)
17
+ @existing_ids ||= {}
18
+ id = node.value
19
+ if @existing_ids[id]
20
+ @duplicate_ids ||= {}
21
+ if @duplicate_ids[id]
22
+ @duplicate_ids[id] << node
23
+ else
24
+ @duplicate_ids[id] = [@existing_ids[id], node]
25
+ end
26
+ else
27
+ @existing_ids[id] = node
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ module ATP
2
+ module Validators
3
+ class MissingIDs < Validator
4
+ def setup
5
+ @referenced_ids = {}
6
+ @present_ids ||= {}.with_indifferent_access
7
+ @referenced_early = {}.with_indifferent_access
8
+ end
9
+
10
+ def on_completion
11
+ failed = false
12
+ @referenced_ids.each do |id, nodes|
13
+ unless @present_ids[id]
14
+ Origen.log.error "Test ID #{id} is referenced in flow #{flow.name} in the following lines, but it is never defined:"
15
+ nodes.each do |node|
16
+ Origen.log.error " #{node.source}"
17
+ end
18
+ failed = true
19
+ @referenced_early.delete(id)
20
+ end
21
+ end
22
+ @referenced_early.each do |id, nodes|
23
+ Origen.log.error "Test ID #{id} is referenced in flow #{flow.name} in the following line(s):"
24
+ nodes.each do |node|
25
+ Origen.log.error " #{node.source}"
26
+ end
27
+ Origen.log.error 'but it was not defined until later:'
28
+ Origen.log.error " #{@present_ids[id].first.source}"
29
+ failed = true
30
+ end
31
+ failed
32
+ end
33
+
34
+ def on_id(node)
35
+ id = node.value
36
+ @present_ids[id] ||= []
37
+ @present_ids[id] << node
38
+ end
39
+
40
+ def on_test_executed(node)
41
+ ids = node.to_a[0]
42
+ [ids].flatten.each do |id|
43
+ @referenced_ids[id] ||= []
44
+ @referenced_ids[id] << node
45
+ unless @present_ids[id]
46
+ @referenced_early[id] ||= []
47
+ @referenced_early[id] << node
48
+ end
49
+ end
50
+ process_all(node)
51
+ end
52
+ alias_method :on_test_result, :on_test_executed
53
+ end
54
+ end
55
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen McGinty
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-20 00:00:00.000000000 Z
11
+ date: 2016-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: origen
@@ -78,12 +78,16 @@ files:
78
78
  - lib/atp/parser.rb
79
79
  - lib/atp/processor.rb
80
80
  - lib/atp/processors/condition.rb
81
+ - lib/atp/processors/condition_extractor.rb
81
82
  - lib/atp/processors/post_cleaner.rb
82
83
  - lib/atp/processors/pre_cleaner.rb
83
84
  - lib/atp/processors/relationship.rb
84
85
  - lib/atp/program.rb
85
86
  - lib/atp/runner.rb
87
+ - lib/atp/validator.rb
86
88
  - lib/atp/validators/condition.rb
89
+ - lib/atp/validators/duplicate_ids.rb
90
+ - lib/atp/validators/missing_ids.rb
87
91
  - lib/tasks/atp.rake
88
92
  - templates/web/archive.md.erb
89
93
  - templates/web/contact.md.erb