mbt-gen 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/mbt-gen.rb +1774 -0
- data/lib/progress.rb +39 -0
- data/lib/regexp-to-smtlib.rb +120 -0
- data/lib/solver-lib.rb +374 -0
- metadata +65 -0
data/lib/mbt-gen.rb
ADDED
|
@@ -0,0 +1,1774 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
require 'time'
|
|
8
|
+
|
|
9
|
+
require 'nokogiri'
|
|
10
|
+
|
|
11
|
+
require_relative 'solver-lib'
|
|
12
|
+
require_relative 'progress'
|
|
13
|
+
require_relative 'regexp-to-smtlib'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CHUNK_SIZE = 10000
|
|
17
|
+
DEFAULT_Z3_PATH = "z3"
|
|
18
|
+
|
|
19
|
+
CONFIG_FILE = "config.yml"
|
|
20
|
+
OUTPUT_FILE = "result.xml"
|
|
21
|
+
CONTINUATION_FILE = 'continuation.log'
|
|
22
|
+
COMBINATION_LOG = 'combinations.log'
|
|
23
|
+
VARS_LOG = 'vars.log'
|
|
24
|
+
|
|
25
|
+
OUTPUT_DIR = "result"
|
|
26
|
+
LISTS_DIR = "lists"
|
|
27
|
+
QUERY_DIR = "queries"
|
|
28
|
+
PARAMETERIZED_QUERY_DIR = "parameterized_queries"
|
|
29
|
+
XML_TEMPLATE = ""
|
|
30
|
+
|
|
31
|
+
DEFAULT_CONFIG = {
|
|
32
|
+
:z3path => "z3",
|
|
33
|
+
:generation_system_mode => "Production",
|
|
34
|
+
:generation_system => "Production"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
USAGE = <<EOF
|
|
38
|
+
usage:
|
|
39
|
+
|
|
40
|
+
GENERATING A SINGLE MESSAGE:
|
|
41
|
+
|
|
42
|
+
ruby #{$PROGRAM_NAME} -list-validation-rules
|
|
43
|
+
- outputs a list of all validation rule names that have been specified in #{$PROGRAM_NAME}.
|
|
44
|
+
|
|
45
|
+
ruby #{$PROGRAM_NAME} <options>
|
|
46
|
+
- generates a message of the given type, which
|
|
47
|
+
- is valid XML
|
|
48
|
+
- adheres to the schema, and
|
|
49
|
+
- satisfies all validation rules specified for messages of this type.
|
|
50
|
+
where <options> is a combination of
|
|
51
|
+
-negate <validation_rule> - negates the named validation rule. This causes the resulting message to still satisfy all other validation rules, but to violate this one.
|
|
52
|
+
NOTE: the result can be found in #{OUTPUT_FILE}
|
|
53
|
+
|
|
54
|
+
GENERATING MESSAGES FOR ALL COMBINATIONS:
|
|
55
|
+
|
|
56
|
+
ruby #{$PROGRAM_NAME} -list-keys
|
|
57
|
+
- outputs a list of all keys that have been used in #{$PROGRAM_NAME} to mark fields or structures.
|
|
58
|
+
|
|
59
|
+
ruby #{$PROGRAM_NAME} -list <key>
|
|
60
|
+
- outputs a list of all fields and structures that have been marked with <key> in #{$PROGRAM_NAME}.
|
|
61
|
+
|
|
62
|
+
ruby #{$PROGRAM_NAME} -count-docs-for-key <key>
|
|
63
|
+
- calculates the number of combinations of all fields and structures marked with <key>
|
|
64
|
+
|
|
65
|
+
ruby #{$PROGRAM_NAME} -generate-docs-for-key <key> <options>
|
|
66
|
+
- generates all valid messages of the given type that can be derived from combinations of all the fields and structures marked with <key>.
|
|
67
|
+
where <options> is a combination of
|
|
68
|
+
-continue - continues a previously interrupted generation process. Must use the same <key> the the interrupted generation.ARGF
|
|
69
|
+
-max-num-docs <limit> - stops generation process after generating <limit> documents.
|
|
70
|
+
NOTE: enumerating large combination-spaces may take a long time.
|
|
71
|
+
- Use -count-docs-for-key to measure the size of the combination-space beforehand.
|
|
72
|
+
- Also: you can interrupt the generation process at any time using Ctrl+C. The intermediate result is stored in the directory #{OUTPUT_DIR} and the generation process can
|
|
73
|
+
be restarted later by adding -continue to the command line.
|
|
74
|
+
NOTE: do not expect the number of generated documents to equal the number of combinations as combinations may not have any valid message instances (for instance when they contradict the validation rules).
|
|
75
|
+
NOTE: the result can be found in the directory #{OUTPUT_DIR}
|
|
76
|
+
EOF
|
|
77
|
+
|
|
78
|
+
class FatalError < RuntimeError
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class NormalProgramTermination < RuntimeError
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def raw_field(xpath)
|
|
85
|
+
path_str = "base"
|
|
86
|
+
path_str = xpath.gsub("/", "-") if xpath
|
|
87
|
+
"field-#{path_str}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def filled_var_for_field(xpath)
|
|
91
|
+
"#{raw_field(xpath)}-filled"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def value_var_for_field(xpath)
|
|
95
|
+
"#{raw_field(xpath)}-value"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def exists_var_for_structure(xpath)
|
|
99
|
+
struct_str = "base"
|
|
100
|
+
struct_str = xpath.gsub("/", "-") if xpath
|
|
101
|
+
"struct-#{struct_str}-exists"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def size_var_for_list(xpath)
|
|
105
|
+
"#{raw_field(xpath)}-size"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def value_var_for_list(xpath, index)
|
|
109
|
+
"#{raw_field(xpath)}-index-#{index}-value"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def translate_datatype_to_SMTLIB(datatype)
|
|
113
|
+
case datatype
|
|
114
|
+
when :string
|
|
115
|
+
return "String"
|
|
116
|
+
when :int
|
|
117
|
+
return "Int"
|
|
118
|
+
when :bool
|
|
119
|
+
return "Bool"
|
|
120
|
+
when :date
|
|
121
|
+
return "Int"
|
|
122
|
+
when :timestamp
|
|
123
|
+
return "Int"
|
|
124
|
+
when :enum
|
|
125
|
+
return "String"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def translate_value_to_SMTLIB(value)
|
|
130
|
+
if value.is_a?(Integer) then
|
|
131
|
+
return value.to_s
|
|
132
|
+
elsif value.is_a?(String) then
|
|
133
|
+
return value.inspect
|
|
134
|
+
elsif value.is_a?(Date) then
|
|
135
|
+
return (value - (Time.at(0).to_date)).to_i # days since 1970.01.01
|
|
136
|
+
elsif value.is_a?(Time) then
|
|
137
|
+
return value.to_i
|
|
138
|
+
else
|
|
139
|
+
raise RuntimeError.new("cannot translate value to SMTLIB: #{value.inspect}")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def min_constraint_for_expr(expr, lower_bound)
|
|
144
|
+
"(>= #{expr} #{lower_bound})"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def max_constraint_for_expr(expr, upper_bound)
|
|
148
|
+
"(<= #{expr} #{upper_bound})"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def regex_constraint_for_expr(expr, regex)
|
|
152
|
+
"(str.in.re #{expr} #{Regexp_to_SMTLIB.translate_regexp_to_SMTLIB(regex)})"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def maxLength_contraint_for_expr(expr, maxLength)
|
|
156
|
+
"(<= (str.len #{expr}) #{maxLength})"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def oneOf_contraint_for_expr(expr, values)
|
|
160
|
+
list = values.map do |val|
|
|
161
|
+
"(= #{expr} \"#{val}\")"
|
|
162
|
+
end
|
|
163
|
+
"(or #{list.join(" ")})"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def enum_constraint_for_expr(expr, values)
|
|
167
|
+
"(or #{values.map{|x| "(= #{expr} #{translate_value_to_SMTLIB(x)})"}.join(" ")})"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def translate_field_def_to_SMTLIB_constraints(progress, solver, fielddef, varname)
|
|
171
|
+
if fielddef[:optional] == nil || fielddef[:optional] == false then
|
|
172
|
+
# field is required
|
|
173
|
+
solver.to_solver(progress, "(assert (! #{varname}-filled :named required-#{varname}))")
|
|
174
|
+
end
|
|
175
|
+
datatype = fielddef[:datatype]
|
|
176
|
+
case datatype
|
|
177
|
+
when :int
|
|
178
|
+
if fielddef[:min] then
|
|
179
|
+
solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{min_constraint_for_expr("#{varname}-value", translate_value_to_SMTLIB(fielddef[:min]))}) :named min-#{varname}))")
|
|
180
|
+
end
|
|
181
|
+
if fielddef[:max] then
|
|
182
|
+
solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{max_constraint_for_expr("#{varname}-value", translate_value_to_SMTLIB(fielddef[:max]))}) :named max-#{varname}))")
|
|
183
|
+
end
|
|
184
|
+
when :string
|
|
185
|
+
if fielddef[:regex] then
|
|
186
|
+
solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{regex_constraint_for_expr("#{varname}-value", fielddef[:regex])}) :named regex-#{varname}))")
|
|
187
|
+
end
|
|
188
|
+
# TODO: minLength?
|
|
189
|
+
if fielddef[:maxLength] then
|
|
190
|
+
solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{maxLength_contraint_for_expr("#{varname}-value", fielddef[:maxLength])}) :named maxLength-#{varname}))")
|
|
191
|
+
end
|
|
192
|
+
if fielddef[:from_list] then
|
|
193
|
+
list_filename = "#{LISTS_DIR}/#{fielddef[:from_list]}"
|
|
194
|
+
unless File.exist?(list_filename)
|
|
195
|
+
raise RuntimeError.new("could not find list file #{list_filename}.")
|
|
196
|
+
end
|
|
197
|
+
values = File.read(list_filename).split("\n").map{|v| v.chomp()}
|
|
198
|
+
solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{oneOf_contraint_for_expr("#{varname}-value", values)}) :named oneOf-#{varname}))")
|
|
199
|
+
end
|
|
200
|
+
when :enum
|
|
201
|
+
solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{enum_constraint_for_expr("#{varname}-value", fielddef[:values])}) :named enum-#{varname}))")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def translate_list_def_to_SMTLIB_constraints(progress, solver, fielddef, varname)
|
|
206
|
+
datatype = fielddef[:datatype]
|
|
207
|
+
case datatype
|
|
208
|
+
when :int
|
|
209
|
+
if fielddef[:min] then
|
|
210
|
+
fielddef[:model_maxLength].times do |i|
|
|
211
|
+
solver.to_solver(progress, "(assert (! (=> (< #{i} #{varname}-size) #{min_constraint_for_expr("#{varname}-index-#{i}-value", translate_value_to_SMTLIB(fielddef[:min]))}) :named list-min-#{varname}-list-index-#{i}))")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
if fielddef[:max] then
|
|
215
|
+
fielddef[:model_maxLength].times do |i|
|
|
216
|
+
solver.to_solver(progress, "(assert (! (=> (< #{i} #{varname}-size) #{max_constraint_for_expr("#{varname}-index-#{i}-value", translate_value_to_SMTLIB(fielddef[:max]))}) :named list-max-#{varname}-list-index-#{i}))")
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
when :string
|
|
220
|
+
if fielddef[:regex] then
|
|
221
|
+
fielddef[:model_maxLength].times do |i|
|
|
222
|
+
solver.to_solver(progress, "(assert (! (=> (< #{i} #{varname}-size) #{regex_constraint_for_expr("#{varname}-index-#{i}-value", fielddef[:regex])}) :named list-regex-#{varname}-index-#{i}))")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
# TODO: minLength?
|
|
226
|
+
if fielddef[:maxLength] then
|
|
227
|
+
fielddef[:model_maxLength].times do |i|
|
|
228
|
+
solver.to_solver(progress, "(assert (! (=> (< #{i} #{varname}-size) #{maxLength_contraint_for_expr("#{varname}-index-#{i}-value", fielddef[:maxLength])}) :named list-maxLength-#{varname}-index-#{i}))")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
when :enum
|
|
232
|
+
fielddef[:model_maxLength].times do |i|
|
|
233
|
+
solver.to_solver(progress, "(assert (! (=> (< #{i} #{varname}-size) #{enum_constraint_for_expr("#{varname}-index-#{i}-value", fielddef[:values])}) :named list-enum-#{varname}-index-#{i}))")
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def default_value_for_fielddef(fielddef)
|
|
239
|
+
datatype = fielddef[:datatype]
|
|
240
|
+
case datatype
|
|
241
|
+
when :int
|
|
242
|
+
return "0"
|
|
243
|
+
when :string
|
|
244
|
+
return "".inspect
|
|
245
|
+
when :date
|
|
246
|
+
return "0"
|
|
247
|
+
when :timestamp
|
|
248
|
+
return "0"
|
|
249
|
+
when :enum
|
|
250
|
+
return "".inspect
|
|
251
|
+
else
|
|
252
|
+
raise RuntimeError.new("unknown datatype: #{datatype.inspect}")
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def translate_structure_to_SMTLIB(progress, solver, struct, hash = {})
|
|
257
|
+
prefix = hash[:prefix]
|
|
258
|
+
config = hash[:config]
|
|
259
|
+
options = hash[:options]
|
|
260
|
+
translate_substructure_to_SMTLIB(progress, solver, struct, hash)
|
|
261
|
+
translate_validation_rules_to_SMTLIB(progress, solver, struct, prefix, config, options)
|
|
262
|
+
if options[:add_SMTLIB] then
|
|
263
|
+
solver.to_solver(progress, "; --- additional SMTLIB Code: ---")
|
|
264
|
+
solver.to_solver(progress, options[:add_SMTLIB])
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def translate_substructure_to_SMTLIB(progress, solver, struct, hash = {})
|
|
269
|
+
prefix = hash[:prefix]
|
|
270
|
+
struct_varname = hash[:struct_varname]
|
|
271
|
+
config = hash[:config]
|
|
272
|
+
options = hash[:options]
|
|
273
|
+
solver.to_solver(progress, "; --- Structure: ---")
|
|
274
|
+
struct.each do |field, definition|
|
|
275
|
+
if field == :additional_smtlib then
|
|
276
|
+
solver.to_solver(progress, "; additional SMTLIB of structure #{prefix}")
|
|
277
|
+
solver.to_solver(progress, translate_validation_rule_to_SMTLIB(definition, struct, prefix, config, options, "<additional SMTLIB of structure #{prefix}>"))
|
|
278
|
+
elsif field == :validation_rules || field == :predicates || field == :parent_link || field == :struct_xpath_prefix
|
|
279
|
+
next
|
|
280
|
+
else
|
|
281
|
+
solver.to_solver(progress, "; #{field.to_s}")
|
|
282
|
+
if definition[:type] == :field then
|
|
283
|
+
display_prefix = ""
|
|
284
|
+
display_prefix = "#{prefix}/" if prefix
|
|
285
|
+
xpath = field.to_s
|
|
286
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
287
|
+
solver.declare_const(progress, filled_var_for_field(xpath), "Bool", {:xpath => xpath})
|
|
288
|
+
solver.declare_const(progress, value_var_for_field(xpath), translate_datatype_to_SMTLIB(definition[:datatype]), {:xpath => xpath, :datatype => definition[:datatype]})
|
|
289
|
+
solver.to_solver(progress, "(assert (! (=> (not #{filled_var_for_field(xpath)}) (= #{value_var_for_field(xpath)} #{default_value_for_fielddef(definition)})) :named empty-field-has-no-value-for-#{raw_field(xpath)}))")
|
|
290
|
+
if (struct_varname)
|
|
291
|
+
solver.to_solver(progress, "(assert (! (=> (not #{struct_varname}) (not #{filled_var_for_field(xpath)})) :named empty-struct-has-no-fields-for-#{raw_field(xpath)}))")
|
|
292
|
+
end
|
|
293
|
+
translate_field_def_to_SMTLIB_constraints(progress, solver, definition, raw_field(xpath))
|
|
294
|
+
elsif definition[:type] == :list then
|
|
295
|
+
display_prefix = ""
|
|
296
|
+
display_prefix = "#{prefix}/" if prefix
|
|
297
|
+
xpath = field.to_s
|
|
298
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
299
|
+
solver.declare_const(progress, size_var_for_list(xpath), "Int", {:xpath => xpath})
|
|
300
|
+
definition[:model_maxLength].times do |i|
|
|
301
|
+
solver.declare_const(progress, value_var_for_list(xpath, i), translate_datatype_to_SMTLIB(definition[:datatype]), {:xpath => xpath, :xpath_element => definition[:xpath_element], :index => i, :datatype => definition[:datatype]})
|
|
302
|
+
solver.to_solver(progress, "(assert (! (=> (<= #{size_var_for_list(xpath)} #{i}) (= #{value_var_for_list(xpath, i)} #{default_value_for_fielddef(definition)})) :named empty-list-field-has-no-value-for-#{raw_field(xpath)}-index-#{i}))")
|
|
303
|
+
end
|
|
304
|
+
if (struct_varname)
|
|
305
|
+
solver.to_solver(progress, "(assert (! (=> (not #{struct_varname}) (= #{size_var_for_list(xpath)} 0)) :named empty-struct-has-no-fields-for-#{raw_field(xpath)}))")
|
|
306
|
+
end
|
|
307
|
+
translate_list_def_to_SMTLIB_constraints(progress, solver, definition, raw_field(xpath))
|
|
308
|
+
elsif definition[:type] == :structure then
|
|
309
|
+
xpath = field.to_s
|
|
310
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
311
|
+
solver.declare_const(progress, exists_var_for_structure(xpath), "Bool", {:xpath => xpath})
|
|
312
|
+
if (struct_varname)
|
|
313
|
+
solver.to_solver(progress, "(assert (! (=> (not #{struct_varname}) (not #{exists_var_for_structure(xpath)})) :named empty-struct-has-no-substructure-#{raw_field(xpath)}))")
|
|
314
|
+
end
|
|
315
|
+
translate_substructure_to_SMTLIB(progress, solver, definition[:ref], :prefix => xpath, :struct_varname => exists_var_for_structure(xpath), :options => options, :config => config)
|
|
316
|
+
else
|
|
317
|
+
next
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def resolve_field_path(path, relative_to_struct, prefix)
|
|
324
|
+
current = relative_to_struct
|
|
325
|
+
result = nil
|
|
326
|
+
path.split("/").each do |segment|
|
|
327
|
+
unless current then
|
|
328
|
+
raise RuntimeError.new("could not resolve segment #{segment.inspect} of field path: #{path.inspect}: resolution worked up until #{result.inspect}, but then the last segment was a field or list.")
|
|
329
|
+
end
|
|
330
|
+
field_def = current[segment.to_sym]
|
|
331
|
+
unless field_def then
|
|
332
|
+
raise RuntimeError.new("could not resolve segment #{segment.inspect} of field path: #{path.inspect}: structure #{prefix.inspect} does not have a member #{segment.to_sym.inspect}.")
|
|
333
|
+
end
|
|
334
|
+
if field_def[:type] == :structure then
|
|
335
|
+
unless field_def[:ref] then
|
|
336
|
+
raise RuntimeError.new("could not resolve field path: #{path.inspect}: substructure #{segment.inspect} of structure #{prefix.inspect} does not have a :ref.")
|
|
337
|
+
end
|
|
338
|
+
current = field_def[:ref]
|
|
339
|
+
if prefix then
|
|
340
|
+
prefix = "#{prefix}/#{segment}"
|
|
341
|
+
else
|
|
342
|
+
prefix = segment
|
|
343
|
+
end
|
|
344
|
+
elsif field_def[:type] == :field then
|
|
345
|
+
current = nil
|
|
346
|
+
result = segment
|
|
347
|
+
if prefix
|
|
348
|
+
result = "#{prefix}/#{result}"
|
|
349
|
+
end
|
|
350
|
+
elsif field_def[:type] == :list then
|
|
351
|
+
current = nil
|
|
352
|
+
result = segment
|
|
353
|
+
if prefix
|
|
354
|
+
result = "#{prefix}/#{result}"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
return result
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def resolve_field_def(path, relative_to_struct, prefix)
|
|
362
|
+
current = relative_to_struct
|
|
363
|
+
result = nil
|
|
364
|
+
path.split("/").each do |segment|
|
|
365
|
+
unless current then
|
|
366
|
+
raise RuntimeError.new("could not resolve segment #{segment.inspect} of field path: #{path.inspect}: resolution worked up until #{result.inspect}, but then the last segment was a field or list.")
|
|
367
|
+
end
|
|
368
|
+
field_def = current[segment.to_sym]
|
|
369
|
+
unless field_def then
|
|
370
|
+
raise RuntimeError.new("could not resolve segment #{segment.inspect} of field path: #{path.inspect}: structure #{prefix.inspect} does not have a member #{segment.to_sym.inspect}.")
|
|
371
|
+
end
|
|
372
|
+
if field_def[:type] == :structure then
|
|
373
|
+
unless field_def[:ref] then
|
|
374
|
+
raise RuntimeError.new("could not resolve field path: #{path.inspect}: substructure #{segment.inspect} of structure #{prefix.inspect} does not have a :ref.")
|
|
375
|
+
end
|
|
376
|
+
current = field_def[:ref]
|
|
377
|
+
if prefix then
|
|
378
|
+
prefix = "#{prefix}/#{segment}"
|
|
379
|
+
else
|
|
380
|
+
prefix = segment
|
|
381
|
+
end
|
|
382
|
+
elsif field_def[:type] == :field then
|
|
383
|
+
current = nil
|
|
384
|
+
result = field_def
|
|
385
|
+
elsif field_def[:type] == :list then
|
|
386
|
+
current = nil
|
|
387
|
+
result = field_def
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
return result
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def resolve_struct_path(path, relative_to_struct, prefix)
|
|
394
|
+
current = relative_to_struct
|
|
395
|
+
result = nil
|
|
396
|
+
path.split("/").each do |segment|
|
|
397
|
+
unless current then
|
|
398
|
+
raise RuntimeError.new("could not resolve field path: #{path.inspect} relative to struct #{relative_to_struct.inspect}")
|
|
399
|
+
end
|
|
400
|
+
if segment == "." then
|
|
401
|
+
result = current[:struct_xpath_prefix]
|
|
402
|
+
elsif segment == ".." then
|
|
403
|
+
current = current[:parent_link]
|
|
404
|
+
if prefix then
|
|
405
|
+
prefix_segs = prefix.split("/")
|
|
406
|
+
prefix = prefix_segs[0,prefix_segs.size()-1].join("/")
|
|
407
|
+
else
|
|
408
|
+
prefix = current[:struct_xpath_prefix]
|
|
409
|
+
end
|
|
410
|
+
else
|
|
411
|
+
field_def = current[segment.to_sym]
|
|
412
|
+
unless field_def then
|
|
413
|
+
raise RuntimeError.new("could not resolve segment #{segment.inspect} of field path: #{path.inspect} relative to struct #{relative_to_struct.inspect}")
|
|
414
|
+
end
|
|
415
|
+
if field_def[:type] == :structure then
|
|
416
|
+
current = field_def[:ref]
|
|
417
|
+
if prefix then
|
|
418
|
+
prefix = "#{prefix}/#{segment}"
|
|
419
|
+
else
|
|
420
|
+
prefix = segment
|
|
421
|
+
end
|
|
422
|
+
result = prefix
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
return result
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def resolve_struct_def(path, relative_to_struct, prefix, context_desc = "")
|
|
430
|
+
current = relative_to_struct
|
|
431
|
+
result = nil
|
|
432
|
+
path.split("/").each do |segment|
|
|
433
|
+
unless current then
|
|
434
|
+
raise RuntimeError.new("could not resolve field path: #{path.inspect} relative to struct #{relative_to_struct.inspect} #{context_desc}")
|
|
435
|
+
end
|
|
436
|
+
if segment == "." then
|
|
437
|
+
result = current
|
|
438
|
+
elsif segment == ".." then
|
|
439
|
+
current = current[:parent_link]
|
|
440
|
+
if prefix then
|
|
441
|
+
prefix_segs = prefix.split("/")
|
|
442
|
+
prefix = prefix_segs[0,prefix_segs.size()-1].join("/")
|
|
443
|
+
else
|
|
444
|
+
prefix = current[:struct_xpath_prefix]
|
|
445
|
+
end
|
|
446
|
+
else
|
|
447
|
+
field_def = current[segment.to_sym]
|
|
448
|
+
unless field_def then
|
|
449
|
+
raise RuntimeError.new("could not resolve segment #{segment.inspect} of field path: #{path.inspect} relative to struct #{relative_to_struct.inspect} #{context_desc}")
|
|
450
|
+
end
|
|
451
|
+
if field_def[:type] == :structure then
|
|
452
|
+
current = field_def[:ref]
|
|
453
|
+
if prefix then
|
|
454
|
+
prefix = "#{prefix}/#{segment}"
|
|
455
|
+
else
|
|
456
|
+
prefix = segment
|
|
457
|
+
end
|
|
458
|
+
result = current
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
return result
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def translate_validation_rules_to_SMTLIB(progress, solver, struct, prefix, config, options)
|
|
466
|
+
solver.to_solver(progress, "; --- Validation Rules ---")
|
|
467
|
+
translate_validation_rules_for_structure_to_SMTLIB(progress, solver, struct, prefix, config, options)
|
|
468
|
+
struct.each_pair do |key, value|
|
|
469
|
+
next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
470
|
+
if value[:type] == :structure then
|
|
471
|
+
if prefix then
|
|
472
|
+
new_prefix = "#{prefix.to_s}/#{key}"
|
|
473
|
+
else
|
|
474
|
+
new_prefix = key.to_s
|
|
475
|
+
end
|
|
476
|
+
translate_validation_rules_to_SMTLIB(progress, solver, value[:ref], new_prefix, config, options)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def translate_validation_rule_to_SMTLIB(rule, context_struct, prefix, config, options, rule_name)
|
|
482
|
+
date_now = options[:date_now]
|
|
483
|
+
timestamp_now = options[:timestamp_now]
|
|
484
|
+
return rule.gsub(/FILLED\[[^\]]*\]/) do |m|
|
|
485
|
+
unless m =~ /FILLED\[([^\]]*)\]/ then
|
|
486
|
+
raise RuntimeError.new("could not parse FILLED[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
487
|
+
end
|
|
488
|
+
field = resolve_field_path($1, context_struct, prefix)
|
|
489
|
+
filled_var_for_field(field)
|
|
490
|
+
end
|
|
491
|
+
.gsub(/VALUE\[[^\]]*\]/) do |m|
|
|
492
|
+
unless m =~ /VALUE\[([^\]]*)\]/ then
|
|
493
|
+
raise RuntimeError.new("could not parse VALUE[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
494
|
+
end
|
|
495
|
+
field = resolve_field_path($1, context_struct, prefix)
|
|
496
|
+
value_var_for_field(field)
|
|
497
|
+
end
|
|
498
|
+
.gsub(/EXIST\[[^\]]*\]/) do |m|
|
|
499
|
+
unless m =~ /EXIST\[([^\]]*)\]/ then
|
|
500
|
+
raise RuntimeError.new("could not parse EXIST[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
501
|
+
end
|
|
502
|
+
s = resolve_struct_path($1, context_struct, prefix)
|
|
503
|
+
exists_var_for_structure(s)
|
|
504
|
+
end
|
|
505
|
+
.gsub(/SIZE\[[^\]]*\]/) do |m|
|
|
506
|
+
unless m =~ /SIZE\[([^\]]*)\]/ then
|
|
507
|
+
raise RuntimeError.new("could not parse SIZE[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
508
|
+
end
|
|
509
|
+
l = resolve_field_path($1, context_struct, prefix)
|
|
510
|
+
size_var_for_list(l)
|
|
511
|
+
end
|
|
512
|
+
.gsub(/LIST_CONTAINS\[[^\]]*,[^\]]*\]/) do |m|
|
|
513
|
+
unless m =~ /LIST_CONTAINS\[([^\]]*),([^\]]*)\]/ then
|
|
514
|
+
raise RuntimeError.new("could not parse LIST_CONTAINS[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
515
|
+
end
|
|
516
|
+
l = resolve_field_path($1, context_struct, prefix)
|
|
517
|
+
l_def = resolve_field_def($1, context_struct, prefix)
|
|
518
|
+
value = $2
|
|
519
|
+
terms = []
|
|
520
|
+
l_def[:model_maxLength].times do |i|
|
|
521
|
+
terms << "(and (< #{i} #{size_var_for_list(l)}) (= #{value_var_for_list(l, i)} #{value}))"
|
|
522
|
+
end
|
|
523
|
+
"(or #{terms.join(" ")})"
|
|
524
|
+
end
|
|
525
|
+
.gsub(/LIST_VALUE_FIRST_ELEMENT\[[^\]]*\]/) do |m|
|
|
526
|
+
unless m =~ /LIST_VALUE_FIRST_ELEMENT\[([^\]]*)\]/ then
|
|
527
|
+
raise RuntimeError.new("could not parse LIST_VALUE_FIRST_ELEMENT[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
528
|
+
end
|
|
529
|
+
xpath = $1
|
|
530
|
+
l = resolve_field_path(xpath, context_struct, prefix)
|
|
531
|
+
value_var_for_list(l, 0)
|
|
532
|
+
end
|
|
533
|
+
.gsub("[Date.now]", date_now.to_s)
|
|
534
|
+
.gsub("[Timestamp.now]", timestamp_now.to_s)
|
|
535
|
+
.gsub(/PREDICATE\[[^\]]*,[^\]]*\]/) do |m|
|
|
536
|
+
unless m =~ /PREDICATE\[([^\]]*),([^\]]*)\]/ then
|
|
537
|
+
raise RuntimeError.new("could not parse PREDICATE[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
538
|
+
end
|
|
539
|
+
path = $1
|
|
540
|
+
key = $2.strip().to_sym
|
|
541
|
+
sdef = resolve_struct_def(path, context_struct, prefix, "in validation rule #{rule_name}")
|
|
542
|
+
unless sdef
|
|
543
|
+
raise RuntimeError.new("could not resolve struct path #{path.inspect} in validation rule #{rule_name}")
|
|
544
|
+
end
|
|
545
|
+
preds = sdef[:predicates]
|
|
546
|
+
mdk = preds[key]
|
|
547
|
+
if mdk then
|
|
548
|
+
if mdk[:smtlib] then
|
|
549
|
+
translate_validation_rule_to_SMTLIB(mdk[:smtlib], sdef, sdef[:struct_xpath_prefix], config, options, "#{rule_name}.predicate.#{key}[#{sdef[:struct_xpath_prefix]}]" )
|
|
550
|
+
else
|
|
551
|
+
raise RuntimeError.new("Predicate #{key} for structure #{sdef[:struct_xpath_prefix]} does not have an SMTLIB definition, but is referenced in validation rule #{rule_name}")
|
|
552
|
+
end
|
|
553
|
+
else
|
|
554
|
+
raise RuntimeError.new("No predicate #{key.inspect} defined for structure #{sdef[:struct_xpath_prefix]}, but one referenced in validation rule #{rule_name}")
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
.gsub(/CONFIG\[[^\]]*\]/) do |m|
|
|
558
|
+
unless m =~ /CONFIG\[([^\]]*)\]/ then
|
|
559
|
+
raise RuntimeError.new("could not parse LIST_CONTAINS[...]: #{m.inspect} in validation rule #{rule_name}")
|
|
560
|
+
end
|
|
561
|
+
key = $1.to_sym
|
|
562
|
+
if config.has_key?(key) then
|
|
563
|
+
"\"#{config[key]}\""
|
|
564
|
+
else
|
|
565
|
+
raise RuntimeError.new("unknown CONFIG-key #{key.inspect} encountered in validation rule #{rule_name}" )
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
.gsub(/FIELD_IN_QUERY\[[^\]]*\]/) do |m|
|
|
569
|
+
unless m =~ /FIELD_IN_QUERY\[([^\]]*),([^\]]*)\]/ then
|
|
570
|
+
raise RuntimeError.new("could not parse FIELD_IN_QUERY[..,..]: #{m.inspect} in validation rule #{rule_name}")
|
|
571
|
+
end
|
|
572
|
+
xpath = $1
|
|
573
|
+
query_filename = $2
|
|
574
|
+
fpath = resolve_field_path(xpath, context_struct, prefix)
|
|
575
|
+
fdef = resolve_field_def(xpath, context_struct, prefix)
|
|
576
|
+
values = File.read("#{QUERY_DIR}/#{query_filename.strip()}").split("\n")
|
|
577
|
+
if fdef[:datatype] == :string then
|
|
578
|
+
terms = values.filter{|l| !l.start_with?("#")}.map do |val|
|
|
579
|
+
"(= #{value_var_for_field(fpath)} \"#{val}\")"
|
|
580
|
+
end
|
|
581
|
+
elsif fdef[:datatype] == :int then
|
|
582
|
+
terms = values.filter{|l| !l.start_with?("#")}.map do |val|
|
|
583
|
+
"(= #{value_var_for_field(fpath)} #{Integer(val)})"
|
|
584
|
+
end
|
|
585
|
+
else
|
|
586
|
+
raise RuntimeError.new("FIELD_IN_QUERY[] was applied to a field of datatype #{fdef[:datatype].inspect}. However, it can only be applied to fields of datatype string or int.")
|
|
587
|
+
end
|
|
588
|
+
"(or #{terms.join(" ")})"
|
|
589
|
+
end
|
|
590
|
+
.gsub(/FIELD_IN_SMTLIB_PARAMETERIZED_QUERY\[[^\]]*\]/) do |m|
|
|
591
|
+
unless m =~ /FIELD_IN_SMTLIB_PARAMETERIZED_QUERY\[([^\]]*),([^\]]*),([^\]]*)\]/ then
|
|
592
|
+
raise RuntimeError.new("could not parse FIELD_IN_SMTLIB_PARAMETERIZED_QUERY[..,..,..]: #{m.inspect} in validation rule #{rule_name}")
|
|
593
|
+
end
|
|
594
|
+
field_xpath = $1
|
|
595
|
+
param_smtlib = $2
|
|
596
|
+
query_filename = $3
|
|
597
|
+
fpath = resolve_field_path(field_xpath.strip(), context_struct, prefix)
|
|
598
|
+
fdef = resolve_field_def(field_xpath.strip(), context_struct, prefix)
|
|
599
|
+
query_result = YAML.load_file("#{PARAMETERIZED_QUERY_DIR}/#{query_filename.strip()}")
|
|
600
|
+
terms = []
|
|
601
|
+
if fdef[:datatype] == :string then
|
|
602
|
+
terms << "(or #{query_result.keys.map { |key| "(= #{param_smtlib} \"#{key}\")" }.join(" ")})"
|
|
603
|
+
query_result.each_pair do |key, values|
|
|
604
|
+
terms << "(=> (= #{param_smtlib} \"#{key}\") (or #{values.map{ |v| "(= #{value_var_for_field(fpath)} \"#{v}\")" }.join(" ")}))"
|
|
605
|
+
end
|
|
606
|
+
else
|
|
607
|
+
raise RuntimeError.new("FIELD_IN_SMTLIB_PARAMETERIZED_QUERY[] was applied to a field of datatype #{fdef[:datatype].inspect}. However, it can only be applied to fields of datatype string.")
|
|
608
|
+
end
|
|
609
|
+
"(and #{terms.join(" ")})"
|
|
610
|
+
end
|
|
611
|
+
.gsub(/CONFIG_IN_QUERY\[[^\]]*\]/) do |m|
|
|
612
|
+
unless m =~ /CONFIG_IN_QUERY\[([^\]]*),([^\]]*)\]/ then
|
|
613
|
+
raise RuntimeError.new("could not parse CONFIG_IN_QUERY[..,..]: #{m.inspect} in validation rule #{rule_name}")
|
|
614
|
+
end
|
|
615
|
+
key = $1.to_sym
|
|
616
|
+
unless config.has_key?(key) then
|
|
617
|
+
raise RuntimeError.new("unknown CONFIG key #{key.inspect} encountered in validation rule #{rule_name}")
|
|
618
|
+
end
|
|
619
|
+
query_filename = $2
|
|
620
|
+
values = File.read("#{QUERY_DIR}/#{query_filename.strip()}").split("\n")
|
|
621
|
+
terms = values.filter{|l| !l.start_with?("#")}.map do |val|
|
|
622
|
+
"(= \"#{config[key]}\" \"#{val}\")"
|
|
623
|
+
end
|
|
624
|
+
"(or #{terms.join(" ")})"
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def translate_validation_rules_for_structure_to_SMTLIB(progress, solver, struct, prefix, config, options)
|
|
629
|
+
return unless struct[:validation_rules]
|
|
630
|
+
|
|
631
|
+
solver.to_solver(progress, "; --- Validation Rules: ---")
|
|
632
|
+
negated_rules = options[:negated_validation_rules] || []
|
|
633
|
+
solver.to_solver(progress, "; negated rules: #{negated_rules.empty? ? "<none>" : negated_rules.join(", ")}")
|
|
634
|
+
|
|
635
|
+
rules = struct[:validation_rules]
|
|
636
|
+
rules.each do |rule_name, rule|
|
|
637
|
+
rule = rule[:smtlib] # ignore the text representation
|
|
638
|
+
next unless rule # warning about this omission is the job of validate_model()
|
|
639
|
+
rule_SMTLIB = translate_validation_rule_to_SMTLIB(rule, struct, prefix, config, options, rule_name)
|
|
640
|
+
if negated_rules.include?(rule_name) then
|
|
641
|
+
rule_SMTLIB = "(not #{rule_SMTLIB})"
|
|
642
|
+
end
|
|
643
|
+
solver.to_solver(progress, "(assert (! #{rule_SMTLIB} :named validation_rule_#{rule_name}_instance_#{prefix.gsub("/", "_")}))")
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def parse_field_value(fielddef, value)
|
|
648
|
+
value = value.gsub("\n", "")
|
|
649
|
+
datatype = fielddef[:datatype]
|
|
650
|
+
case datatype
|
|
651
|
+
when :int
|
|
652
|
+
return Integer(value)
|
|
653
|
+
when :string
|
|
654
|
+
return value
|
|
655
|
+
when :enum
|
|
656
|
+
return value
|
|
657
|
+
when :date
|
|
658
|
+
return Date.parse(value)
|
|
659
|
+
when :timestamp
|
|
660
|
+
return Time.new(value)
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def solver_value_to_string(solver_value)
|
|
665
|
+
if solver_value =~ /\"(.*)\"/ then
|
|
666
|
+
return $1
|
|
667
|
+
else
|
|
668
|
+
return solver_value
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def build_document_from_model(progress, struct, model)
|
|
673
|
+
doc = Nokogiri::XML(XML_TEMPLATE)
|
|
674
|
+
add_model_struct_to_doc(progress, struct, doc, model)
|
|
675
|
+
return doc
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def add_model_struct_to_doc(progress, struct, doc, model, prefix = nil)
|
|
679
|
+
struct.each do |key, value|
|
|
680
|
+
next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
681
|
+
if value[:type] == :field then
|
|
682
|
+
if prefix then
|
|
683
|
+
xpath = "#{prefix}/#{key}"
|
|
684
|
+
else
|
|
685
|
+
xpath = key.to_s
|
|
686
|
+
end
|
|
687
|
+
model_value = model[xpath]
|
|
688
|
+
unless model_value then
|
|
689
|
+
raise RuntimeError.new("xpath '#{xpath}' not contained in model.")
|
|
690
|
+
end
|
|
691
|
+
if model_value[:filled] == true || model_value[:filled] == "true" then
|
|
692
|
+
doc.add_child("<#{key}>#{solver_value_to_string(model_value[:value])}</#{key}>")
|
|
693
|
+
end
|
|
694
|
+
elsif value[:type] == :list then
|
|
695
|
+
node = doc.add_child("<#{key}></#{key}>")
|
|
696
|
+
if node.is_a?(Nokogiri::XML::NodeSet) then
|
|
697
|
+
node = node.first
|
|
698
|
+
end
|
|
699
|
+
if prefix then
|
|
700
|
+
xpath = "#{prefix}/#{key}"
|
|
701
|
+
else
|
|
702
|
+
xpath = key.to_s
|
|
703
|
+
end
|
|
704
|
+
model_value = model[xpath]
|
|
705
|
+
xpath_element = value[:xpath_element]
|
|
706
|
+
if Integer(model_value[:size]) > 0 then
|
|
707
|
+
model_value[:value].slice(0, Integer(model_value[:size])).each do |mvalue|
|
|
708
|
+
node.add_child("<#{xpath_element}>#{solver_value_to_string(mvalue)}</#{xpath_element}>")
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
elsif value[:type] == :structure then
|
|
712
|
+
node = doc.add_child("<#{key}></#{key}>")
|
|
713
|
+
if node.is_a?(Nokogiri::XML::NodeSet) then
|
|
714
|
+
node = node.first
|
|
715
|
+
end
|
|
716
|
+
unless value[:ref] then
|
|
717
|
+
raise RuntimeError.new("struct is broken: :structure #{key.inspect} does not have a :ref")
|
|
718
|
+
end
|
|
719
|
+
if prefix then
|
|
720
|
+
new_prefix = "#{prefix}/#{key}"
|
|
721
|
+
else
|
|
722
|
+
new_prefix = key
|
|
723
|
+
end
|
|
724
|
+
add_model_struct_to_doc(progress, value[:ref], node, model, new_prefix)
|
|
725
|
+
else
|
|
726
|
+
raise RuntimeError.new("struct is broken: encountered unknown :type #{value[:type].inspect}")
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def collect_validation_rule_names(struct)
|
|
732
|
+
result = []
|
|
733
|
+
struct.each do |key, value|
|
|
734
|
+
next if key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
735
|
+
if key == :validation_rules
|
|
736
|
+
value.each do |key, value|
|
|
737
|
+
result << key
|
|
738
|
+
end
|
|
739
|
+
else
|
|
740
|
+
if value[:type] == :structure
|
|
741
|
+
result += collect_validation_rule_names(value[:ref])
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
return result
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def generate_doc(progress, message, output_filename, config, options)
|
|
749
|
+
log = options[:log] || nil
|
|
750
|
+
log.puts "generating doc #{output_filename}" if log
|
|
751
|
+
options[:solverLog] = options[:solverLog] || "#{output_filename}.smt2"
|
|
752
|
+
|
|
753
|
+
z3path = config[:z3path] || DEFAULT_Z3_PATH
|
|
754
|
+
z3 = Z3Solver.new(progress, z3path)
|
|
755
|
+
begin
|
|
756
|
+
model = z3.query_model(progress, options) do |solver|
|
|
757
|
+
solver.to_solver(progress, "(set-option :produce-unsat-cores true) ; enable generation of unsat cores")
|
|
758
|
+
solver.to_solver(progress, "")
|
|
759
|
+
log.puts "translating structure" if log
|
|
760
|
+
translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
|
|
761
|
+
solver.to_solver(progress, "")
|
|
762
|
+
end
|
|
763
|
+
ensure
|
|
764
|
+
z3.close()
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
if model then
|
|
768
|
+
result_doc = build_document_from_model(progress, message, model)
|
|
769
|
+
dir = File.dirname(output_filename)
|
|
770
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
771
|
+
File.open(output_filename, "w") do |f|
|
|
772
|
+
f.puts result_doc.to_xml()
|
|
773
|
+
end
|
|
774
|
+
progress.print_line "DONE. Generated XML was written to #{output_filename}, SMTLIB log was written to #{options[:solverLog]}"
|
|
775
|
+
return true
|
|
776
|
+
else
|
|
777
|
+
progress.print_line "FAILED. No XML document was generated. For more details, please see the SMTLIB log in #{options[:solverLog]}"
|
|
778
|
+
return false
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def z3ValueToRubyValue(value)
|
|
783
|
+
if value =~ /\"(.*)\"/ then # string
|
|
784
|
+
return $1
|
|
785
|
+
elsif value =~ /\d+/ then # int
|
|
786
|
+
return Integer(value)
|
|
787
|
+
elsif value == "true"
|
|
788
|
+
return true
|
|
789
|
+
elsif value == "false" then
|
|
790
|
+
return false
|
|
791
|
+
else
|
|
792
|
+
raise RuntimeError.new("enountered unknown type of value in z3 model: #{value.inspect}")
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def extract_key_field_values_from_model(key_fields, model)
|
|
797
|
+
result = {}
|
|
798
|
+
key_fields.each do |xpath, fieldDesc|
|
|
799
|
+
fieldInfo = model[xpath]
|
|
800
|
+
if fieldDesc[:type] == :field then
|
|
801
|
+
if fieldInfo[:filled] then
|
|
802
|
+
result[xpath] = z3ValueToRubyValue(fieldInfo[:value])
|
|
803
|
+
else
|
|
804
|
+
result[xpath] = nil
|
|
805
|
+
end
|
|
806
|
+
elsif fieldDesc[:type] == :struct || fieldDesc[:type] == :structure then
|
|
807
|
+
if fieldInfo.has_key?(:exists) then
|
|
808
|
+
result[xpath] = z3ValueToRubyValue(fieldInfo[:exists])
|
|
809
|
+
else
|
|
810
|
+
result[xpath] = false
|
|
811
|
+
end
|
|
812
|
+
else
|
|
813
|
+
raise RuntimeError.new("encountered unknown :type in solver model: #{fieldDesc[:type].inspect}")
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
return result
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def all_values_for_field(field)
|
|
820
|
+
unless field[:type] == :field then
|
|
821
|
+
raise RuntimeError.new("something that is not a field definition was passed to all_values_for_field(): #{field.inspect}")
|
|
822
|
+
end
|
|
823
|
+
unless field[:datatype] == :enum then
|
|
824
|
+
raise RuntimeError.new("all_values_for_field() is only applicable to fields of datatype enum! not #{field[:datatype].inspect}.")
|
|
825
|
+
end
|
|
826
|
+
values = field[:values].dup
|
|
827
|
+
if field[:optional] == true then
|
|
828
|
+
values << nil
|
|
829
|
+
end
|
|
830
|
+
return values
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def each_combination(fields, &block)
|
|
834
|
+
if fields.size < 1 then
|
|
835
|
+
return
|
|
836
|
+
end
|
|
837
|
+
if fields.size == 1 then
|
|
838
|
+
field = fields.first
|
|
839
|
+
values = field[1][:values].clone()
|
|
840
|
+
if field[1][:optional] == true then
|
|
841
|
+
values << nil
|
|
842
|
+
end
|
|
843
|
+
values.each do |value|
|
|
844
|
+
block.call({ field[0] => value })
|
|
845
|
+
end
|
|
846
|
+
else
|
|
847
|
+
first = fields.shift
|
|
848
|
+
values = first[1][:values].clone()
|
|
849
|
+
if first[1][:optional] == true then
|
|
850
|
+
values << nil
|
|
851
|
+
end
|
|
852
|
+
each_combination(fields) do |mapping|
|
|
853
|
+
values.each do |value|
|
|
854
|
+
block.call(mapping.merge({ first[0] => value }))
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def calc_combinations_for_key_fields(key_fields)
|
|
861
|
+
count = 1
|
|
862
|
+
key_fields.each do |field|
|
|
863
|
+
if field[1][:type] == :field then
|
|
864
|
+
if field[1][:datatype] == :enum then
|
|
865
|
+
num_values = field[1][:values].size
|
|
866
|
+
num_values += 1 if field[1][:optional]
|
|
867
|
+
elsif field[1][:datatype] == :int then
|
|
868
|
+
if field[1].has_key?(:min) and field[1].has_key?(:max) and field[1][:min] <= field[1][:max] then
|
|
869
|
+
if field[1][:optional] then
|
|
870
|
+
num_values = 2 + field[1][:max] - field[1][:min]
|
|
871
|
+
else
|
|
872
|
+
num_values = 1 + field[1][:max] - field[1][:min]
|
|
873
|
+
end
|
|
874
|
+
else
|
|
875
|
+
return "potentially infinite"
|
|
876
|
+
end
|
|
877
|
+
elsif field[1][:datatype] == :string then
|
|
878
|
+
return "potentially infinite"
|
|
879
|
+
end
|
|
880
|
+
elsif field[1][:type] == :structure then
|
|
881
|
+
num_values = 2
|
|
882
|
+
end
|
|
883
|
+
count *= num_values
|
|
884
|
+
end
|
|
885
|
+
return count
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def calc_combinations(message, key)
|
|
889
|
+
key_fields = collect_key_fields(key, message)
|
|
890
|
+
return calc_combinations_for_key_fields(key_fields)
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def read_docs_from_log_file(log_filename)
|
|
894
|
+
result = []
|
|
895
|
+
if File.exist?(log_filename) then
|
|
896
|
+
File.open(log_filename, "r") do |f|
|
|
897
|
+
f.each_line do |line|
|
|
898
|
+
doc = {}
|
|
899
|
+
if line =~ /(.*)\ <-\ \{(.*)\}/ then
|
|
900
|
+
filename = $1
|
|
901
|
+
combination = $2
|
|
902
|
+
combination.split(", ").each do |assignment|
|
|
903
|
+
if assignment =~ /(.*):\ (.*)/ then
|
|
904
|
+
key = $1
|
|
905
|
+
value = $2
|
|
906
|
+
if value =~ /\"(.*)\"/ then
|
|
907
|
+
value = $1
|
|
908
|
+
elsif value == "nil" then
|
|
909
|
+
value = nil
|
|
910
|
+
end
|
|
911
|
+
doc[key] = value
|
|
912
|
+
else
|
|
913
|
+
raise RuntimeError.new("could not parse logfile #{log_filename}: invalid assignment #{assignment.inspect}")
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
else
|
|
917
|
+
raise RuntimeError.new("could not parse logfile #{log_filename}: invalid line #{line.inspect}")
|
|
918
|
+
end
|
|
919
|
+
result << doc
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
return result
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def blocking_clause(docs, vars = docs.map{|e| e.keys}.flatten)
|
|
927
|
+
unless docs.empty?
|
|
928
|
+
codes = docs.map do |key_values|
|
|
929
|
+
value_codes = key_values.filter{|xpath,value| vars.include?(xpath) }.map do |xpath, value|
|
|
930
|
+
if value == nil then
|
|
931
|
+
"(= #{filled_var_for_field(xpath)} false)"
|
|
932
|
+
else
|
|
933
|
+
"(and (= #{filled_var_for_field(xpath)} true) (= #{value_var_for_field(xpath)} \"#{value}\"))"
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
if value_codes.empty? then
|
|
937
|
+
"true"
|
|
938
|
+
else
|
|
939
|
+
"(not (and #{value_codes.join(" ")}))"
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
return "" if codes.empty?
|
|
943
|
+
return "(assert (! (and #{codes.join(" ")}) :named blocking-clause))"
|
|
944
|
+
end
|
|
945
|
+
return nil
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
#def fixing_clause(key_values, fixed = key_value.keys)
|
|
949
|
+
# codes = []
|
|
950
|
+
# key_values.each do |xpath, value|
|
|
951
|
+
# if fixed.include?(xpath) then
|
|
952
|
+
# if value then
|
|
953
|
+
# codes << "(and (= #{filled_var_for_field(xpath)} true) (= #{value_var_for_field(xpath)} \"#{value}\"))"
|
|
954
|
+
# else
|
|
955
|
+
# codes << "(= #{filled_var_for_field(xpath)} false)"
|
|
956
|
+
# end
|
|
957
|
+
# end
|
|
958
|
+
# end
|
|
959
|
+
# if codes.empty? then
|
|
960
|
+
# return ""
|
|
961
|
+
# else
|
|
962
|
+
# return "(assert (! (and #{codes.join(" ")}) :named fixing-clause))"
|
|
963
|
+
# end
|
|
964
|
+
#end
|
|
965
|
+
|
|
966
|
+
def prepare_output_doc_filename(number, output_dir)
|
|
967
|
+
chunk_number = number / 100000
|
|
968
|
+
dirname = "#{output_dir}/#{chunk_number}"
|
|
969
|
+
unless Dir.exist?(dirname) then
|
|
970
|
+
FileUtils.mkdir_p(dirname)
|
|
971
|
+
end
|
|
972
|
+
return "#{dirname}/doc#{number}.xml"
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def switch_blocking_clause(fixed, current_value, var_xpath, blocked_values)
|
|
976
|
+
clauses = []
|
|
977
|
+
blocked_values.each do |value|
|
|
978
|
+
if value != nil then
|
|
979
|
+
clauses << "(not (and (= #{filled_var_for_field(var_xpath)} true) (= #{value_var_for_field(var_xpath)} \"#{value}\")))"
|
|
980
|
+
else
|
|
981
|
+
clauses << "(not (= #{filled_var_for_field(var_xpath)} false))"
|
|
982
|
+
end
|
|
983
|
+
end
|
|
984
|
+
if fixed then
|
|
985
|
+
if current_value != nil then
|
|
986
|
+
result = "; switch fixing clause\n(assert (! (and (= #{filled_var_for_field(var_xpath)} true) (= #{value_var_for_field(var_xpath)} \"#{current_value}\")) :named switch_fixing_clause))"
|
|
987
|
+
else
|
|
988
|
+
result = "; switch fixing clause\n(assert (! (= #{filled_var_for_field(var_xpath)} false) :named switch_fixing_clause))"
|
|
989
|
+
end
|
|
990
|
+
else
|
|
991
|
+
result = ""
|
|
992
|
+
end
|
|
993
|
+
unless clauses.empty? then
|
|
994
|
+
result = result + "\n; switch blocking clause\n(assert (! (and #{clauses.join(" ")}) :named switch_blocking_clause))"
|
|
995
|
+
end
|
|
996
|
+
return result
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def values_for_key_field(field)
|
|
1000
|
+
if field[:type] == :field then
|
|
1001
|
+
if field[:datatype] == :enum then
|
|
1002
|
+
if field[:optional] then
|
|
1003
|
+
return field[:values] + [nil]
|
|
1004
|
+
else
|
|
1005
|
+
return field[:values]
|
|
1006
|
+
end
|
|
1007
|
+
elsif field[:datatype] == :int then
|
|
1008
|
+
if field.has_key?(:min) and field.has_key?(:max) and field[:min] <= field[:max] then
|
|
1009
|
+
return (field[:min]..field[:max]).to_a
|
|
1010
|
+
else
|
|
1011
|
+
raise RuntimeError.new("cannot determine values for unbounded :int field")
|
|
1012
|
+
end
|
|
1013
|
+
else
|
|
1014
|
+
raise RuntimeError.new("cannot determine values for datatype #{field[:datatype].inspect}")
|
|
1015
|
+
end
|
|
1016
|
+
elsif field[:type] == :structure then
|
|
1017
|
+
if field[:optional] then
|
|
1018
|
+
return [true, false]
|
|
1019
|
+
else
|
|
1020
|
+
return [true]
|
|
1021
|
+
end
|
|
1022
|
+
else
|
|
1023
|
+
raise RuntimeError.new("Encountered unknown :type in model: #{field[:type].inspect} - keys can only mark fields or structures.")
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def fixing_clause(xpath, switched_info)
|
|
1028
|
+
if switched_info[:current_value] == nil then
|
|
1029
|
+
return "(assert (! (= #{filled_var_for_field(xpath)} false) :named fixing-clause-#{raw_field(xpath)}))"
|
|
1030
|
+
else
|
|
1031
|
+
return "(assert (! (and (= #{filled_var_for_field(xpath)} true) (= #{value_var_for_field(xpath)} \"#{switched_info[:current_value]}\")) :named fixing-clause-#{raw_field(xpath)}))"
|
|
1032
|
+
end
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def collect_key_fields(fkey, struct, prefix = nil)
|
|
1036
|
+
result = {}
|
|
1037
|
+
struct.each do |key, value|
|
|
1038
|
+
next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
1039
|
+
if value[:type] == :structure
|
|
1040
|
+
if value.has_key?(:keys) and value[:keys].include?(fkey) then
|
|
1041
|
+
xpath = key.to_s
|
|
1042
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
1043
|
+
result[xpath] = value.clone().filter{|k,v| [:type, :optional, :datatype, :values].include?(k)}
|
|
1044
|
+
end
|
|
1045
|
+
if prefix then
|
|
1046
|
+
new_prefix = "#{prefix}/#{key}"
|
|
1047
|
+
else
|
|
1048
|
+
new_prefix = key
|
|
1049
|
+
end
|
|
1050
|
+
result = result.merge(collect_key_fields(fkey, value[:ref], new_prefix))
|
|
1051
|
+
elsif value[:type] == :field and value.has_key?(:keys) and value[:keys].include?(fkey) then
|
|
1052
|
+
xpath = key.to_s
|
|
1053
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
1054
|
+
result[xpath] = value.clone().filter{|k,v| [:type, :optional, :datatype, :values, :min, :max].include?(k)}
|
|
1055
|
+
end
|
|
1056
|
+
end
|
|
1057
|
+
return result
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def collect_key_fields_and_strctures(fkey, struct, prefix = nil)
|
|
1061
|
+
fields = []
|
|
1062
|
+
structures = []
|
|
1063
|
+
struct.each do |key, value|
|
|
1064
|
+
next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
1065
|
+
if value[:type] == :structure
|
|
1066
|
+
if value.has_key?(:keys) and value[:keys].include?(fkey) then
|
|
1067
|
+
xpath = key.to_s
|
|
1068
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
1069
|
+
structures << xpath
|
|
1070
|
+
end
|
|
1071
|
+
if prefix then
|
|
1072
|
+
new_prefix = "#{prefix}/#{key}"
|
|
1073
|
+
else
|
|
1074
|
+
new_prefix = key
|
|
1075
|
+
end
|
|
1076
|
+
result_fields, result_structures = collect_key_fields_and_strctures(fkey, value[:ref], new_prefix)
|
|
1077
|
+
fields += result_fields
|
|
1078
|
+
structures += result_structures
|
|
1079
|
+
elsif value[:type] == :field and value.has_key?(:keys) and value[:keys].include?(fkey) then
|
|
1080
|
+
xpath = key.to_s
|
|
1081
|
+
xpath = "#{prefix}/#{xpath}" if prefix
|
|
1082
|
+
fields << xpath
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
return fields, structures
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
def collect_keys(struct)
|
|
1089
|
+
result = []
|
|
1090
|
+
struct.each do |key, value|
|
|
1091
|
+
next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
1092
|
+
if value[:type] == :structure
|
|
1093
|
+
if value.has_key?(:keys) then
|
|
1094
|
+
result += value[:keys]
|
|
1095
|
+
end
|
|
1096
|
+
if value[:ref] then
|
|
1097
|
+
result += collect_keys(value[:ref])
|
|
1098
|
+
else
|
|
1099
|
+
raise RuntimeError.new("Model error: encountered structure without :ref: key=#{key.inspect} value=#{value.inspect}")
|
|
1100
|
+
end
|
|
1101
|
+
elsif value[:type] == :field and value.has_key?(:keys) then
|
|
1102
|
+
result += value[:keys]
|
|
1103
|
+
end
|
|
1104
|
+
end
|
|
1105
|
+
result.uniq
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def collect_validation_rules(struct)
|
|
1109
|
+
result = []
|
|
1110
|
+
struct.each do |key, value|
|
|
1111
|
+
next if key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
1112
|
+
if key == :validation_rules then
|
|
1113
|
+
result += value.keys
|
|
1114
|
+
else
|
|
1115
|
+
if value[:type] == :structure
|
|
1116
|
+
if value[:ref] then
|
|
1117
|
+
result += collect_validation_rules(value[:ref])
|
|
1118
|
+
else
|
|
1119
|
+
raise RuntimeError.new("Model error: encountered structure without :ref: key=#{key.inspect} value=#{value.inspect}")
|
|
1120
|
+
end
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
end
|
|
1124
|
+
result.uniq
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
def list_keys(message)
|
|
1128
|
+
keys = collect_keys(message)
|
|
1129
|
+
puts "keys: #{keys.join(", ")}"
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
def list_fields_and_structures(message, fkey)
|
|
1133
|
+
fields, structures = collect_key_fields_and_strctures(fkey, message)
|
|
1134
|
+
puts "fields for key #{fkey}:"
|
|
1135
|
+
fields.each { |f| puts " #{f}" }
|
|
1136
|
+
puts "structures for key #{fkey}:"
|
|
1137
|
+
structures.each { |s| puts " #{s}" }
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
def list_validation_rules(message)
|
|
1141
|
+
rules = collect_validation_rules(message)
|
|
1142
|
+
puts "Validation Rules:"
|
|
1143
|
+
rules.each { |r| puts " #{r}" }
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
class CoreEnumerationAlgo
|
|
1147
|
+
|
|
1148
|
+
def initialize()
|
|
1149
|
+
@num_tries = 0
|
|
1150
|
+
@num_docs = 0
|
|
1151
|
+
@on_new_doc = nil
|
|
1152
|
+
@on_unsat = nil
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
# fix values of all variables left from switched
|
|
1157
|
+
def enumerate_fixing_clause(assignment, vars, switched)
|
|
1158
|
+
clauses = []
|
|
1159
|
+
switched.times do |index|
|
|
1160
|
+
var_xpath = vars[index][0]
|
|
1161
|
+
var_def = vars[index][1]
|
|
1162
|
+
if var_def[:type] == :field then
|
|
1163
|
+
if assignment[var_xpath] == nil then
|
|
1164
|
+
clauses << "(assert (! (= #{filled_var_for_field(var_xpath)} false) :named fixing_clause_#{index}))"
|
|
1165
|
+
else
|
|
1166
|
+
clauses << "(assert (! (and (= #{filled_var_for_field(var_xpath)} true) (= #{value_var_for_field(var_xpath)} #{value_of_type(assignment[var_xpath], var_def[:datatype])})) :named fixing_clause_#{index}))"
|
|
1167
|
+
end
|
|
1168
|
+
elsif var_def[:type] == :structure then
|
|
1169
|
+
clauses << "(assert (! (= #{exists_var_for_structure(var_xpath)} #{assignment[var_xpath].inspect}) :named fixing_clause_#{index}))"
|
|
1170
|
+
end
|
|
1171
|
+
end
|
|
1172
|
+
"; fixing clauses:\n#{clauses.join("\n")}"
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
def summarize_fixing_clause(assignment, vars, switched)
|
|
1176
|
+
fixed = switched.times.to_a.map do |index|
|
|
1177
|
+
var_xpath = vars[index][0]
|
|
1178
|
+
"#{var_xpath}=#{assignment[var_xpath].inspect}"
|
|
1179
|
+
end
|
|
1180
|
+
"fixed(#{fixed.join(", ")})"
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
def value_of_type(value, type)
|
|
1184
|
+
if type == :string or type == :enum then
|
|
1185
|
+
"\"#{value}\""
|
|
1186
|
+
elsif type == :int then
|
|
1187
|
+
value.to_s
|
|
1188
|
+
end
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
# prevent the var from taking values that already have been enumerated exhaustively
|
|
1192
|
+
def enumerate_blocking_clause(var_xpath, var_def, blocked)
|
|
1193
|
+
clauses = []
|
|
1194
|
+
num = 0
|
|
1195
|
+
if var_def[:type] == :field then
|
|
1196
|
+
blocked.each do |value|
|
|
1197
|
+
if value == nil then
|
|
1198
|
+
clauses << "(assert (! (not (= #{filled_var_for_field(var_xpath)} false)) :named blocking_clause_#{num}))"
|
|
1199
|
+
else
|
|
1200
|
+
clauses << "(assert (! (not (and (= #{filled_var_for_field(var_xpath)} true) (= #{value_var_for_field(var_xpath)} #{value_of_type(value, var_def[:datatype])}))) :named blocking_clause_#{num}))"
|
|
1201
|
+
end
|
|
1202
|
+
num += 1
|
|
1203
|
+
end
|
|
1204
|
+
elsif var_def[:type] == :structure then
|
|
1205
|
+
blocked.each do |value|
|
|
1206
|
+
clauses << "(assert (! (not (= #{exists_var_for_structure(var_xpath)} #{value.inspect})) :named blocking_clause_#{num}))"
|
|
1207
|
+
num += 1
|
|
1208
|
+
end
|
|
1209
|
+
end
|
|
1210
|
+
"; blocking clauses:\n#{clauses.join("\n")}"
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def summarize_blocking_clause(var_xpath, blocked)
|
|
1214
|
+
"blocked(#{var_xpath})=[#{blocked.map{|v| v.inspect}.join(", ")}]"
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
def generate_sparse_doc(progress, message, key, config, options, code, output_doc_filename)
|
|
1218
|
+
additional_options = {}
|
|
1219
|
+
if code then
|
|
1220
|
+
additional_options = { :add_SMTLIB => code }
|
|
1221
|
+
end
|
|
1222
|
+
return generate_doc_return_key_values(progress, message, key, output_doc_filename, config, options.merge(additional_options))
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def log_filename_for_output_filename(output_filename)
|
|
1226
|
+
"#{output_filename}.smt2"
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def generate_doc_return_key_values(progress, message, key, output_filename, config, options)
|
|
1230
|
+
log = options[:log] || nil
|
|
1231
|
+
log.puts "generating doc #{output_filename}" if log
|
|
1232
|
+
options[:solverLog] = log_filename_for_output_filename(output_filename)
|
|
1233
|
+
|
|
1234
|
+
key_fields = collect_key_fields(key, message)
|
|
1235
|
+
|
|
1236
|
+
z3path = config[:z3path] || DEFAULT_Z3_PATH
|
|
1237
|
+
z3 = Z3Solver.new(progress, z3path)
|
|
1238
|
+
begin
|
|
1239
|
+
model = z3.query_model(progress, options) do |solver|
|
|
1240
|
+
solver.to_solver(progress, "(set-option :produce-unsat-cores true) ; enable generation of unsat cores")
|
|
1241
|
+
solver.to_solver(progress, "")
|
|
1242
|
+
log.puts "translating structure" if log
|
|
1243
|
+
translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
|
|
1244
|
+
solver.to_solver(progress, "")
|
|
1245
|
+
end
|
|
1246
|
+
ensure
|
|
1247
|
+
z3.close()
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
if model then
|
|
1251
|
+
result_doc = build_document_from_model(progress, message, model)
|
|
1252
|
+
dir = File.dirname(output_filename)
|
|
1253
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
1254
|
+
File.open(output_filename, "w") do |f|
|
|
1255
|
+
f.puts result_doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
|
|
1256
|
+
end
|
|
1257
|
+
progress.print_line "DONE. Generated XML was written to #{output_filename}, SMTLIB log was written to #{options[:solverLog]}"
|
|
1258
|
+
return extract_key_field_values_from_model(key_fields, model)
|
|
1259
|
+
else
|
|
1260
|
+
progress.print_line "FAILED. No XML document was generated. For more details, please see the SMTLIB log in #{options[:solverLog]}"
|
|
1261
|
+
return nil
|
|
1262
|
+
end
|
|
1263
|
+
end
|
|
1264
|
+
|
|
1265
|
+
def store_continuation_point(doc_number, state, assignment, new_assignment, filename)
|
|
1266
|
+
File.open(filename, "w") do |f|
|
|
1267
|
+
f.puts("#{doc_number};#{@num_docs};state(#{state.map{|s| if s then "[#{s.map{|v| v.inspect}.join(",")}]" else "nil" end}.join("#")});assignment(#{assignment.to_a.map{|k,v| "#{k}:#{v.inspect}"}.join(",")});new_assignment(#{new_assignment.to_a.map{|k,v| "#{k}:#{v.inspect}"}.join(",")})")
|
|
1268
|
+
end
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
def self.parse_state(state_str)
|
|
1272
|
+
unless state_str =~ /state\((.*)\)/
|
|
1273
|
+
return nil
|
|
1274
|
+
end
|
|
1275
|
+
split = $1.split("#")
|
|
1276
|
+
split.map do |e|
|
|
1277
|
+
if e == "nil" then
|
|
1278
|
+
nil
|
|
1279
|
+
elsif e =~ /\[(.*)\]/ then
|
|
1280
|
+
$1.split(",").map do |i|
|
|
1281
|
+
if i.strip == "nil" then
|
|
1282
|
+
nil
|
|
1283
|
+
elsif i.strip == "true" then
|
|
1284
|
+
true
|
|
1285
|
+
elsif i.strip == "false" then
|
|
1286
|
+
false
|
|
1287
|
+
elsif i.strip =~ /\"(.*)\"/ then
|
|
1288
|
+
$1
|
|
1289
|
+
end
|
|
1290
|
+
end
|
|
1291
|
+
end
|
|
1292
|
+
end
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
def self.parse_assignment(str)
|
|
1296
|
+
unless str =~ /assignment\((.*)\)/
|
|
1297
|
+
return nil
|
|
1298
|
+
end
|
|
1299
|
+
split = $1.split(",")
|
|
1300
|
+
result = {}
|
|
1301
|
+
split.each do |e|
|
|
1302
|
+
s2 = e.split(":")
|
|
1303
|
+
key = s2[0]
|
|
1304
|
+
value = s2[1]
|
|
1305
|
+
if value == "nil" then
|
|
1306
|
+
result[key] = nil
|
|
1307
|
+
elsif value == "true" then
|
|
1308
|
+
result[key] = true
|
|
1309
|
+
elsif value == "false" then
|
|
1310
|
+
result[key] = false
|
|
1311
|
+
elsif value =~ /\"(.*)\"/
|
|
1312
|
+
result[key] = $1
|
|
1313
|
+
end
|
|
1314
|
+
end
|
|
1315
|
+
result
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
def self.parse_new_assignment(str)
|
|
1319
|
+
unless str =~ /new_assignment\((.*)\)/
|
|
1320
|
+
return nil
|
|
1321
|
+
end
|
|
1322
|
+
split = $1.split(",")
|
|
1323
|
+
result = {}
|
|
1324
|
+
split.each do |e|
|
|
1325
|
+
s2 = e.split(":")
|
|
1326
|
+
key = s2[0]
|
|
1327
|
+
value = s2[1]
|
|
1328
|
+
if value == "nil" then
|
|
1329
|
+
result[key] = nil
|
|
1330
|
+
elsif value == "true" then
|
|
1331
|
+
result[key] = true
|
|
1332
|
+
elsif value == "false" then
|
|
1333
|
+
result[key] = false
|
|
1334
|
+
elsif value =~ /\"(.*)\"/
|
|
1335
|
+
result[key] = $1
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
result
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1341
|
+
def self.parse_continuation_point(line)
|
|
1342
|
+
split = line.split(";")
|
|
1343
|
+
doc_number = Integer(split[0])
|
|
1344
|
+
num_docs = Integer(split[1])
|
|
1345
|
+
state = CoreEnumerationAlgo.parse_state(split[2])
|
|
1346
|
+
assignment = CoreEnumerationAlgo.parse_assignment(split[3])
|
|
1347
|
+
new_assignment = CoreEnumerationAlgo.parse_assignment(split[4])
|
|
1348
|
+
return [doc_number, num_docs, state, assignment, new_assignment]
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
# TODO: logging
|
|
1352
|
+
|
|
1353
|
+
def display_combination_space(assignment, vars, switched, blocked)
|
|
1354
|
+
# enumerate_fixing_clause(assignment, vars, switched) + enumerate_blocking_clause(vars[switched][0], vars[switched][1], blocked)
|
|
1355
|
+
|
|
1356
|
+
# => all vars left of switched are fixed to their assigned values (when the value is nil, then they are empty)
|
|
1357
|
+
# => the switched var is prevented to have any of the blocked values
|
|
1358
|
+
res = switched.times.to_a.map do |index|
|
|
1359
|
+
var_xpath = vars[index][0]
|
|
1360
|
+
"#{var_xpath}=#{assignment[var_xpath].inspect}"
|
|
1361
|
+
end + ["#{vars[switched][0]}|#{blocked.map{|v| v.inspect}.join(",")}"]
|
|
1362
|
+
"{#{res.join(", ")}}"
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
# core algo
|
|
1366
|
+
def enumerate(progress, switched, vars, assignment, message, key, output_dir, config, options, state = [nil] * vars.size())
|
|
1367
|
+
enumerate(progress, switched+1, vars, assignment, message, key, output_dir, config, options, state) if switched < vars.size()-1
|
|
1368
|
+
is_finite_state = keys_are_finite_state(vars)
|
|
1369
|
+
if state[switched] == nil then
|
|
1370
|
+
blocked = [assignment[vars[switched][0]]]
|
|
1371
|
+
state[switched] = blocked
|
|
1372
|
+
else
|
|
1373
|
+
blocked = state[switched] + [assignment[vars[switched][0]]]
|
|
1374
|
+
state[switched] = blocked
|
|
1375
|
+
end
|
|
1376
|
+
found = true
|
|
1377
|
+
while found do
|
|
1378
|
+
found = false
|
|
1379
|
+
# - fix all vars left of switched to their values in assignment
|
|
1380
|
+
# - block switched from obtaining the values in blocked
|
|
1381
|
+
# progress.print_line "DEBUG: #{summarize_fixing_clause(assignment, vars, switched)}, #{summarize_blocking_clause(vars[switched][0], blocked)}"
|
|
1382
|
+
clauses = enumerate_fixing_clause(assignment, vars, switched) + enumerate_blocking_clause(vars[switched][0], vars[switched][1], blocked)
|
|
1383
|
+
output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
|
|
1384
|
+
new_assignment = generate_sparse_doc(progress, message, key, config, options, clauses, output_doc_filename)
|
|
1385
|
+
generated_doc_number = @num_tries
|
|
1386
|
+
store_continuation_point(generated_doc_number, state, assignment, new_assignment, "#{output_dir}/#{CONTINUATION_FILE}")
|
|
1387
|
+
if is_finite_state then
|
|
1388
|
+
progress.report_progress(determine_progress(state, vars))
|
|
1389
|
+
else
|
|
1390
|
+
progress.report_progress(nil)
|
|
1391
|
+
end
|
|
1392
|
+
@num_tries += 1
|
|
1393
|
+
if new_assignment then # sat
|
|
1394
|
+
@num_docs += 1
|
|
1395
|
+
@on_new_doc.call(generated_doc_number, output_doc_filename, new_assignment) if @on_new_doc
|
|
1396
|
+
if switched < vars.size()-1 then
|
|
1397
|
+
enumerate(progress, switched+1, vars, new_assignment, message, key, output_dir, config, options, state)
|
|
1398
|
+
end
|
|
1399
|
+
blocked << new_assignment[vars[switched][0]]
|
|
1400
|
+
state[switched] = blocked
|
|
1401
|
+
found = true
|
|
1402
|
+
else # unsat
|
|
1403
|
+
@on_unsat.call(generated_doc_number, log_filename_for_output_filename(output_doc_filename), display_combination_space(assignment, vars, switched, blocked)) if @on_unsat
|
|
1404
|
+
found = false
|
|
1405
|
+
end
|
|
1406
|
+
end
|
|
1407
|
+
state[switched] = nil
|
|
1408
|
+
end
|
|
1409
|
+
|
|
1410
|
+
# NOTE: can only be called when all key-fields are finite-state
|
|
1411
|
+
def is_final_state(state, new_assignment, key_fields_list)
|
|
1412
|
+
return state[1,state.size()] == [nil] * (state.size()-1) &&
|
|
1413
|
+
state[0].to_set == values_for_key_field(key_fields_list[0][1]).to_set &&
|
|
1414
|
+
new_assignment.empty?
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
# NOTE: can only be called when all key-fields are finite-state
|
|
1418
|
+
def determine_progress(state, vars)
|
|
1419
|
+
combinations_per_value = [nil] * vars.size()
|
|
1420
|
+
comb = 1
|
|
1421
|
+
vars.size().times.to_a.reverse_each do |i|
|
|
1422
|
+
combinations_per_value[i] = comb
|
|
1423
|
+
comb = values_for_key_field(vars[i][1]).size() * comb
|
|
1424
|
+
end
|
|
1425
|
+
|
|
1426
|
+
blocked_combinations = 0
|
|
1427
|
+
state.size().times do |i|
|
|
1428
|
+
if state[i] then
|
|
1429
|
+
blocked_combinations += state[i].size() * combinations_per_value[i]
|
|
1430
|
+
end
|
|
1431
|
+
end
|
|
1432
|
+
blocked_combinations
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
def num_combinations(key_def)
|
|
1436
|
+
if key_def[:type] == :field then
|
|
1437
|
+
if key_def[:datatype] == :enum then
|
|
1438
|
+
key_def[:values].size() + (key_def[:optional] ? 1 : 0)
|
|
1439
|
+
elsif key_def[:datatype] == :int then
|
|
1440
|
+
if key_def.has_key?(:min) and key_def.has_key?(:max) and key_def[:min] <= key_def[:max] then
|
|
1441
|
+
1 + key_def[:max] - key_def[:min] + (key_def[:optional] ? 1 : 0)
|
|
1442
|
+
else
|
|
1443
|
+
nil
|
|
1444
|
+
end
|
|
1445
|
+
else
|
|
1446
|
+
nil
|
|
1447
|
+
end
|
|
1448
|
+
elsif key_def[:type] == :structure then
|
|
1449
|
+
2
|
|
1450
|
+
end
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
def keys_are_finite_state(list)
|
|
1454
|
+
list.each do |field|
|
|
1455
|
+
name = field[0]
|
|
1456
|
+
if field[1][:type] == :field then
|
|
1457
|
+
if field[1][:datatype] == :enum then
|
|
1458
|
+
# enums are finite-state
|
|
1459
|
+
elsif field[1][:datatype] == :int and field[1].has_key?(:min) and field[1].has_key?(:max) and field[1][:min] <= field[1][:max] then
|
|
1460
|
+
# ints are finite-state when both bounds are given and :min <= :max
|
|
1461
|
+
else
|
|
1462
|
+
return false
|
|
1463
|
+
end
|
|
1464
|
+
end
|
|
1465
|
+
end
|
|
1466
|
+
return true
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1469
|
+
def generate_docs_for_key(message, key, output_dir, config, options)
|
|
1470
|
+
|
|
1471
|
+
number_of_combinations = calc_combinations(message, key)
|
|
1472
|
+
if number_of_combinations.is_a?(String) then
|
|
1473
|
+
progress = ProgressBar.new(100000000000000)
|
|
1474
|
+
else
|
|
1475
|
+
progress = ProgressBar.new(number_of_combinations)
|
|
1476
|
+
end
|
|
1477
|
+
begin
|
|
1478
|
+
ensure
|
|
1479
|
+
end
|
|
1480
|
+
progress.report_progress(0.0)
|
|
1481
|
+
progress.print_line "generating sparse documents efficiently..."
|
|
1482
|
+
progress.print_line "there is a theoretical maximum of #{number_of_combinations} combinations."
|
|
1483
|
+
progress.print_line "Note, however, that we are generating only those documents that are valid, which are usually much less."
|
|
1484
|
+
|
|
1485
|
+
progress.print_line "preparing output dir #{output_dir}..."
|
|
1486
|
+
if options[:continue] then
|
|
1487
|
+
unless Dir.exist?(output_dir) then
|
|
1488
|
+
raise FatalError.new("FATAL: directory #{output_dir} not found. There seems to be nothing to continue from...")
|
|
1489
|
+
end
|
|
1490
|
+
log_mode = "a"
|
|
1491
|
+
else
|
|
1492
|
+
unless Dir.exist?(output_dir) then
|
|
1493
|
+
FileUtils.mkdir_p(output_dir)
|
|
1494
|
+
end
|
|
1495
|
+
log_mode = "w"
|
|
1496
|
+
end
|
|
1497
|
+
log_filename = "#{output_dir}/#{COMBINATION_LOG}"
|
|
1498
|
+
|
|
1499
|
+
key_fields = collect_key_fields(key, message)
|
|
1500
|
+
key_fields_list = key_fields.each_pair.to_a
|
|
1501
|
+
key_fields_list.sort! {|a,b| num_combinations(b[1]) <=> num_combinations(a[1]) }
|
|
1502
|
+
|
|
1503
|
+
if options[:continue] then
|
|
1504
|
+
stored_key_fields_str = File.read("#{output_dir}/#{VARS_LOG}")
|
|
1505
|
+
unless key_fields_list.to_yaml == stored_key_fields_str then
|
|
1506
|
+
raise FatalError.new("vars are differing! please specify the same key in order to use -continue!")
|
|
1507
|
+
end
|
|
1508
|
+
progress.print_line "continuing enumerating documents for key #{key}!"
|
|
1509
|
+
else
|
|
1510
|
+
# store vars
|
|
1511
|
+
File.open("#{output_dir}/#{VARS_LOG}", "w") do |f|
|
|
1512
|
+
f.puts(key_fields_list.to_yaml)
|
|
1513
|
+
end
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1516
|
+
#progress.print_line "DEBUG: key_fields_list=#{key_fields_list.inspect}"
|
|
1517
|
+
|
|
1518
|
+
cont_filename = "#{output_dir}/#{CONTINUATION_FILE}"
|
|
1519
|
+
if options[:continue] then
|
|
1520
|
+
unless File.exist?(cont_filename) then
|
|
1521
|
+
raise FatalError.new("FATAL: file #{cont_filename} not found. There seems to be no continuation point to continue from...")
|
|
1522
|
+
end
|
|
1523
|
+
doc_number, num_docs, state, assignment, new_assignment = CoreEnumerationAlgo.parse_continuation_point(File.read(cont_filename).chomp)
|
|
1524
|
+
if is_final_state(state, new_assignment, key_fields_list) then
|
|
1525
|
+
progress.report_progress(number_of_combinations)
|
|
1526
|
+
raise FatalError.new("FATAL: The enumeration is finished. There is nothing left to do. Resuming the enumeration hence does not make sense.")
|
|
1527
|
+
end
|
|
1528
|
+
progress.print_line "RESUMING enumeration from the following continuation point:"
|
|
1529
|
+
progress.print_line "|doc_number=#{doc_number}"
|
|
1530
|
+
progress.print_line "|num_docs=#{num_docs}"
|
|
1531
|
+
progress.print_line "|state=#{state.inspect}"
|
|
1532
|
+
progress.print_line "|assignment=#{assignment.inspect}"
|
|
1533
|
+
progress.print_line "|new_assignment=#{new_assignment.inspect}"
|
|
1534
|
+
if keys_are_finite_state(key_fields_list) then
|
|
1535
|
+
progress.report_progress(determine_progress(state, key_fields_list))
|
|
1536
|
+
else
|
|
1537
|
+
progress.report_progress(nil)
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
if new_assignment && !new_assignment.empty? then
|
|
1541
|
+
assignment = new_assignment
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
# restore state at continuation point
|
|
1545
|
+
@num_tries = doc_number+1 # we do not want to overwrite the last document written before the STOP
|
|
1546
|
+
number_of_combinations += 1 # account for this in the safety mechanism
|
|
1547
|
+
@num_docs = num_docs
|
|
1548
|
+
doc_counter = 0
|
|
1549
|
+
File.open(log_filename, log_mode) do |log|
|
|
1550
|
+
|
|
1551
|
+
@on_new_doc = Proc.new { |num_doc, doc_filename, combination|
|
|
1552
|
+
# combination logging
|
|
1553
|
+
log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
|
|
1554
|
+
|
|
1555
|
+
doc_counter += 1
|
|
1556
|
+
if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
|
|
1557
|
+
raise NormalProgramTermination.new("-max-num-docs exceeded. Stopping!")
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
# safety mechanism
|
|
1561
|
+
if @num_docs > number_of_combinations then
|
|
1562
|
+
raise RuntimeError.new("This should never happen! Number of combinations exceeded. #{@num_docs} documents generated, but there are only #{number_of_combinations} possible combinations of the key vars.")
|
|
1563
|
+
end
|
|
1564
|
+
}
|
|
1565
|
+
@on_unsat = Proc.new { |num_doc, log_filename, combination_str|
|
|
1566
|
+
log.puts "CONTRADICTION (see: #{log_filename}) <- #{combination_str}"
|
|
1567
|
+
progress.print_line "doc#{num_doc} NOT generated"
|
|
1568
|
+
progress.print_line "No more valid combinations of the free vars could be found. -> Switching"
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
# core algo - resume from state
|
|
1572
|
+
enumerate(progress, 0, key_fields_list, assignment, message, key, output_dir, config, options, state)
|
|
1573
|
+
end
|
|
1574
|
+
else
|
|
1575
|
+
# init safety mechanism
|
|
1576
|
+
@num_tries = 0
|
|
1577
|
+
@num_docs = 0
|
|
1578
|
+
doc_counter = 0
|
|
1579
|
+
# first try - a freely generated document
|
|
1580
|
+
File.open(log_filename, log_mode) do |log|
|
|
1581
|
+
output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
|
|
1582
|
+
key_values = generate_doc_return_key_values(progress, message, key, output_doc_filename, config, options)
|
|
1583
|
+
log.puts "#{output_doc_filename} <- {#{key_values.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
|
|
1584
|
+
doc_counter += 1
|
|
1585
|
+
@num_tries += 1
|
|
1586
|
+
@on_new_doc = Proc.new { |num_doc, doc_filename, combination|
|
|
1587
|
+
# combination logging
|
|
1588
|
+
log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
|
|
1589
|
+
|
|
1590
|
+
doc_counter += 1
|
|
1591
|
+
if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
|
|
1592
|
+
raise NormalProgramTermination.new("-max-num-docs exceeded. Stopping!")
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1595
|
+
# safety mechanism
|
|
1596
|
+
unless number_of_combinations.is_a?(String) then
|
|
1597
|
+
if @num_docs > number_of_combinations then
|
|
1598
|
+
raise RuntimeError.new("This should never happen! Number of combinations exceeded. #{@num_docs} documents generated, but there are only #{number_of_combinations} possible combinations of the key vars.")
|
|
1599
|
+
end
|
|
1600
|
+
end
|
|
1601
|
+
}
|
|
1602
|
+
@on_unsat = Proc.new { |num_doc, log_filename, combination_str|
|
|
1603
|
+
log.puts "CONTRADICTION (see: #{log_filename}) <- #{combination_str}"
|
|
1604
|
+
progress.print_line "doc#{num_doc} NOT generated"
|
|
1605
|
+
progress.print_line "No more valid combinations of the free vars could be found. -> Switching"
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
# core algo - start enumeration
|
|
1609
|
+
enumerate(progress, 0, key_fields_list, key_values, message, key, output_dir, config, options)
|
|
1610
|
+
end
|
|
1611
|
+
|
|
1612
|
+
progress.print_line "No more valid documents could be found. -> Exiting"
|
|
1613
|
+
progress.report_progress(number_of_combinations)
|
|
1614
|
+
end
|
|
1615
|
+
end
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
def introduce_parent_links(message, parent = nil, prefix = nil)
|
|
1619
|
+
if parent != nil then
|
|
1620
|
+
message[:parent_link] = parent
|
|
1621
|
+
end
|
|
1622
|
+
if prefix != nil then
|
|
1623
|
+
message[:struct_xpath_prefix] = prefix
|
|
1624
|
+
end
|
|
1625
|
+
message.each_pair do |key, value|
|
|
1626
|
+
next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
1627
|
+
if value[:type] == :structure then
|
|
1628
|
+
if prefix then
|
|
1629
|
+
new_prefix = "#{prefix}/#{key}"
|
|
1630
|
+
else
|
|
1631
|
+
new_prefix = key
|
|
1632
|
+
end
|
|
1633
|
+
introduce_parent_links(value[:ref], message, new_prefix)
|
|
1634
|
+
end
|
|
1635
|
+
end
|
|
1636
|
+
end
|
|
1637
|
+
|
|
1638
|
+
def validate_model(message, prefix=nil)
|
|
1639
|
+
|
|
1640
|
+
message.each_pair do |key, value|
|
|
1641
|
+
if key == :validation_rules then
|
|
1642
|
+
value.each_pair do |vr_key, vr_value|
|
|
1643
|
+
unless vr_value[:text] && vr_value[:text] != "" then
|
|
1644
|
+
puts "INFO: Model Problem: validation rule #{vr_key} does not have a textual representation."
|
|
1645
|
+
end
|
|
1646
|
+
unless vr_value[:smtlib] && vr_value[:smtlib] != "" then
|
|
1647
|
+
puts "WARN: Model Problem: validation rule #{vr_key} does not have an SMTLIB representation. It will hence be omitted!"
|
|
1648
|
+
end
|
|
1649
|
+
end
|
|
1650
|
+
elsif key == :predicates then
|
|
1651
|
+
value.each_pair do |mkey, mvalue|
|
|
1652
|
+
next unless mvalue
|
|
1653
|
+
unless mvalue[:text] && mvalue[:text] != "" then
|
|
1654
|
+
puts "INFO: Model Problem: structure #{prefix}'s predicate #{mkey} does not have a textual representation."
|
|
1655
|
+
end
|
|
1656
|
+
unless mvalue[:smtlib] && mvalue[:smtlib] != "" then
|
|
1657
|
+
puts "WARN: Model Problem: structure #{prefix}'s predicate #{mkey} does not have an SMTLIB representation. It will be regarded as always true."
|
|
1658
|
+
end
|
|
1659
|
+
end
|
|
1660
|
+
elsif key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
|
|
1661
|
+
next
|
|
1662
|
+
else
|
|
1663
|
+
if value[:type] == :structure then
|
|
1664
|
+
unless value[:ref] then
|
|
1665
|
+
raise RuntimeError.new("Model Problem: encountered structure without a :ref (key=#{key.inspect}, value=#{value.inspect})")
|
|
1666
|
+
end
|
|
1667
|
+
if prefix then
|
|
1668
|
+
new_prefix = "#{prefix}/#{key}"
|
|
1669
|
+
else
|
|
1670
|
+
new_prefix = key
|
|
1671
|
+
end
|
|
1672
|
+
validate_model(value[:ref], new_prefix)
|
|
1673
|
+
elsif value[:type] == :field then
|
|
1674
|
+
unless value[:datatype] then
|
|
1675
|
+
raise RuntimeError.new("Model Problem: encountered field without a :datatype (key=#{key.inspect}, value=#{value.inspect})")
|
|
1676
|
+
end
|
|
1677
|
+
if value[:datatype] == :enum then
|
|
1678
|
+
unless value[:values] then
|
|
1679
|
+
raise RuntimeError.new("Model Problem: encountered enum field without a :values (key=#{key.inspect}, value=#{value.inspect})")
|
|
1680
|
+
end
|
|
1681
|
+
end
|
|
1682
|
+
end
|
|
1683
|
+
end
|
|
1684
|
+
end
|
|
1685
|
+
end
|
|
1686
|
+
|
|
1687
|
+
def generate(message, argv, message_config_filename = nil)
|
|
1688
|
+
if ARGV[0] == "--help" || ARGV[0] == "-help" || ARGV[0] == "help" then
|
|
1689
|
+
puts USAGE
|
|
1690
|
+
exit(0)
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
options = { :negated_validation_rules => [], :date_now => (Date.today - Time.at(0).to_date).to_i, :timestamp_now => Time.now().to_i }
|
|
1694
|
+
|
|
1695
|
+
opt = argv.shift
|
|
1696
|
+
while opt do
|
|
1697
|
+
if opt == "-negate" then
|
|
1698
|
+
rule = argv.shift
|
|
1699
|
+
options[:negated_validation_rules] << rule
|
|
1700
|
+
elsif opt == "-list-keys" then
|
|
1701
|
+
options[:list_keys] = true
|
|
1702
|
+
elsif opt == "-list" then
|
|
1703
|
+
options[:list] = argv.shift
|
|
1704
|
+
elsif opt == "-list-validation-rules" then
|
|
1705
|
+
options[:list_validation_rules] = true
|
|
1706
|
+
elsif opt == "-generate-docs-from" then
|
|
1707
|
+
options[:combination_source] = argv.shift
|
|
1708
|
+
elsif opt == "-skip" then
|
|
1709
|
+
options[:skip] = Integer(argv.shift)
|
|
1710
|
+
elsif opt == "-generate-docs-for-key" then
|
|
1711
|
+
options[:docs_for_key] = argv.shift
|
|
1712
|
+
elsif opt == "-count-docs-for-key" then
|
|
1713
|
+
options[:count_docs_for_key] = argv.shift
|
|
1714
|
+
elsif opt == "-continue" then
|
|
1715
|
+
options[:continue] = true
|
|
1716
|
+
elsif opt == "-max-num-docs" then
|
|
1717
|
+
options[:max_num_docs] = Integer(argv.shift)
|
|
1718
|
+
puts "flag -max-num-docs used. Will stop after generating #{options[:max_num_docs]} documents."
|
|
1719
|
+
else
|
|
1720
|
+
puts "FATAL: unknown option #{opt.inspect}."
|
|
1721
|
+
puts USAGE
|
|
1722
|
+
raise FatalError.new("")
|
|
1723
|
+
end
|
|
1724
|
+
opt = argv.shift
|
|
1725
|
+
end
|
|
1726
|
+
|
|
1727
|
+
unless options[:negated_validation_rules].empty? then
|
|
1728
|
+
puts "negating rules: #{options[:negated_validation_rules].join(", ")}"
|
|
1729
|
+
|
|
1730
|
+
rules = collect_validation_rule_names(message)
|
|
1731
|
+
unknown_negated_rules = options[:negated_validation_rules].filter { |rule_name| !rules.include?(rule_name) }
|
|
1732
|
+
unless unknown_negated_rules.empty? then
|
|
1733
|
+
throw RuntimeError.new("unknown negated rules #{unknown_negated_rules.join(", ")}")
|
|
1734
|
+
end
|
|
1735
|
+
|
|
1736
|
+
end
|
|
1737
|
+
|
|
1738
|
+
if File.exist?(CONFIG_FILE) then
|
|
1739
|
+
config = YAML.load_file(CONFIG_FILE)
|
|
1740
|
+
else
|
|
1741
|
+
config = DEFAULT_CONFIG
|
|
1742
|
+
File.open(CONFIG_FILE, "w") do |f|
|
|
1743
|
+
f.puts(config.to_yaml)
|
|
1744
|
+
end
|
|
1745
|
+
puts "No config file found. Default config written to #{CONFIG_FILE}"
|
|
1746
|
+
end
|
|
1747
|
+
if message_config_filename then
|
|
1748
|
+
message_config = YAML.load_file(message_config_filename)
|
|
1749
|
+
config = config.merge(message_config)
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
introduce_parent_links(message)
|
|
1753
|
+
|
|
1754
|
+
if options[:count_docs_for_key] then
|
|
1755
|
+
puts "there are #{calc_combinations(message, options[:count_docs_for_key])} combinations."
|
|
1756
|
+
elsif options[:docs_for_key] then
|
|
1757
|
+
puts "using #{options[:date_now]} as [Date.now]"
|
|
1758
|
+
puts "using #{options[:timestamp_now]} as [Timestamp.now]"
|
|
1759
|
+
validate_model(message)
|
|
1760
|
+
CoreEnumerationAlgo.new().generate_docs_for_key(message, options[:docs_for_key], OUTPUT_DIR, config, options)
|
|
1761
|
+
elsif options[:list_keys] then
|
|
1762
|
+
list_keys(message)
|
|
1763
|
+
elsif options[:list] then
|
|
1764
|
+
list_fields_and_structures(message, options[:list])
|
|
1765
|
+
elsif options[:list_validation_rules] then
|
|
1766
|
+
list_validation_rules(message)
|
|
1767
|
+
else
|
|
1768
|
+
puts "using #{options[:date_now]} as [Date.now]"
|
|
1769
|
+
puts "using #{options[:timestamp_now]} as [Timestamp.now]"
|
|
1770
|
+
validate_model(message)
|
|
1771
|
+
progress = ProgressBar.new(1)
|
|
1772
|
+
generate_doc(progress, message, OUTPUT_FILE, config, options)
|
|
1773
|
+
end
|
|
1774
|
+
end
|