mbt-gen 0.0.2 → 0.0.4

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mbt-gen.rb +451 -150
  3. data/lib/progress.rb +47 -5
  4. data/lib/solver-lib.rb +245 -98
  5. metadata +2 -2
data/lib/mbt-gen.rb CHANGED
@@ -73,6 +73,11 @@ GENERATING MESSAGES FOR ALL COMBINATIONS:
73
73
  be restarted later by adding -continue to the command line.
74
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
75
  NOTE: the result can be found in the directory #{OUTPUT_DIR}
76
+
77
+ VALIDATING THE MODEL:
78
+
79
+ ruby #{$PROGRAM_NAME} -validate <file.xml>
80
+ - reads in the given XML file and validates it against the model - that is, it checks whether it conforms to the specified XML schema and to the validation rules.
76
81
  EOF
77
82
 
78
83
  class FatalError < RuntimeError
@@ -167,38 +172,100 @@ def enum_constraint_for_expr(expr, values)
167
172
  "(or #{values.map{|x| "(= #{expr} #{translate_value_to_SMTLIB(x)})"}.join(" ")})"
168
173
  end
169
174
 
170
- def translate_field_def_to_SMTLIB_constraints(progress, solver, fielddef, varname)
175
+ def translate_field_def_to_SMTLIB_constraints(progress, solver, fielddef, xpath)
176
+ varname = raw_field(xpath)
171
177
  if fielddef[:optional] == nil || fielddef[:optional] == false then
172
178
  # field is required
173
- solver.to_solver(progress, "(assert (! #{varname}-filled :named required-#{varname}))")
179
+ assertionID = "required-#{varname}"
180
+ info = {
181
+ :assertion_type => :constraint,
182
+ :xpath => xpath,
183
+ :constraint_type => :required
184
+ }
185
+ solver.associateAssertionIDWith(assertionID, info)
186
+ solver.to_solver(progress, "(assert (! #{varname}-filled :named #{assertionID}))")
174
187
  end
175
188
  datatype = fielddef[:datatype]
176
189
  case datatype
177
190
  when :int
178
191
  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}))")
192
+ assertionID = "min-#{varname}"
193
+ info = {
194
+ :assertion_type => :constraint,
195
+ :xpath => xpath,
196
+ :datatype => :int,
197
+ :value => fielddef[:min],
198
+ :constraint_type => :min
199
+ }
200
+ solver.associateAssertionIDWith(assertionID, info)
201
+ solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{min_constraint_for_expr("#{varname}-value", translate_value_to_SMTLIB(fielddef[:min]))}) :named #{assertionID}))")
180
202
  end
181
203
  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}))")
204
+ assertionID = "max-#{varname}"
205
+ info = {
206
+ :assertion_type => :constraint,
207
+ :xpath => xpath,
208
+ :datatype => :int,
209
+ :value => fielddef[:max],
210
+ :constraint_type => :max
211
+ }
212
+ solver.associateAssertionIDWith(assertionID, info)
213
+ solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{max_constraint_for_expr("#{varname}-value", translate_value_to_SMTLIB(fielddef[:max]))}) :named #{assertionID}))")
183
214
  end
184
215
  when :string
185
216
  if fielddef[:regex] then
186
- solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{regex_constraint_for_expr("#{varname}-value", fielddef[:regex])}) :named regex-#{varname}))")
217
+ assertionID = "regex-#{varname}"
218
+ info = {
219
+ :assertion_type => :constraint,
220
+ :xpath => xpath,
221
+ :datatype => :string,
222
+ :pattern => fielddef[:regex],
223
+ :constraint_type => :regex
224
+ }
225
+ solver.associateAssertionIDWith(assertionID, info)
226
+ solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{regex_constraint_for_expr("#{varname}-value", fielddef[:regex])}) :named #{assertionID}))")
187
227
  end
188
228
  # TODO: minLength?
189
229
  if fielddef[:maxLength] then
190
- solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{maxLength_contraint_for_expr("#{varname}-value", fielddef[:maxLength])}) :named maxLength-#{varname}))")
230
+ assertionID = "maxLength-#{varname}"
231
+ info = {
232
+ :assertion_type => :constraint,
233
+ :xpath => xpath,
234
+ :datatype => :string,
235
+ :value => fielddef[:maxLength],
236
+ :constraint_type => :maxLength
237
+ }
238
+ solver.associateAssertionIDWith(assertionID, info)
239
+ solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{maxLength_contraint_for_expr("#{varname}-value", fielddef[:maxLength])}) :named #{assertionID}))")
191
240
  end
192
241
  if fielddef[:from_list] then
242
+ assertionID = "oneOf-#{varname}"
193
243
  list_filename = "#{LISTS_DIR}/#{fielddef[:from_list]}"
244
+ info = {
245
+ :assertion_type => :constraint,
246
+ :xpath => xpath,
247
+ :datatype => :string,
248
+ :filename => list_filename,
249
+ :constraint_type => :from_list
250
+ }
194
251
  unless File.exist?(list_filename)
195
252
  raise RuntimeError.new("could not find list file #{list_filename}.")
196
253
  end
197
254
  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}))")
255
+ solver.associateAssertionIDWith(assertionID, info)
256
+ solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{oneOf_contraint_for_expr("#{varname}-value", values)}) :named #{assertionID}))")
199
257
  end
200
258
  when :enum
201
- solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{enum_constraint_for_expr("#{varname}-value", fielddef[:values])}) :named enum-#{varname}))")
259
+ assertionID = "enum-#{varname}"
260
+ info = {
261
+ :assertion_type => :constraint,
262
+ :xpath => xpath,
263
+ :datatype => :enum,
264
+ :values => fielddef[:values],
265
+ :constraint_type => :values
266
+ }
267
+ solver.associateAssertionIDWith(assertionID, info)
268
+ solver.to_solver(progress, "(assert (! (=> #{varname}-filled #{enum_constraint_for_expr("#{varname}-value", fielddef[:values])}) :named #{assertionID}))")
202
269
  end
203
270
  end
204
271
 
@@ -290,13 +357,15 @@ def translate_substructure_to_SMTLIB(progress, solver, struct, hash = {})
290
357
  if (struct_varname)
291
358
  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
359
  end
293
- translate_field_def_to_SMTLIB_constraints(progress, solver, definition, raw_field(xpath))
360
+ translate_field_def_to_SMTLIB_constraints(progress, solver, definition, xpath)
294
361
  elsif definition[:type] == :list then
295
362
  display_prefix = ""
296
363
  display_prefix = "#{prefix}/" if prefix
297
364
  xpath = field.to_s
298
365
  xpath = "#{prefix}/#{xpath}" if prefix
366
+ solver.declare_const(progress, filled_var_for_field(xpath), "Bool", {:xpath => xpath})
299
367
  solver.declare_const(progress, size_var_for_list(xpath), "Int", {:xpath => xpath})
368
+ solver.to_solver(progress, "(assert (! (=> (not #{filled_var_for_field(xpath)}) (= #{size_var_for_list(xpath)} 0)) :named empty-list-field-has-zero-size-#{raw_field(xpath)}))")
300
369
  definition[:model_maxLength].times do |i|
301
370
  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
371
  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}))")
@@ -678,13 +747,13 @@ end
678
747
  def add_model_struct_to_doc(progress, struct, doc, model, prefix = nil)
679
748
  struct.each do |key, value|
680
749
  next if key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix || key == :additional_smtlib
750
+ if prefix then
751
+ xpath = "#{prefix}/#{key}"
752
+ else
753
+ xpath = key.to_s
754
+ end
755
+ model_value = model[xpath]
681
756
  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
757
  unless model_value then
689
758
  raise RuntimeError.new("xpath '#{xpath}' not contained in model.")
690
759
  end
@@ -696,12 +765,6 @@ def add_model_struct_to_doc(progress, struct, doc, model, prefix = nil)
696
765
  if node.is_a?(Nokogiri::XML::NodeSet) then
697
766
  node = node.first
698
767
  end
699
- if prefix then
700
- xpath = "#{prefix}/#{key}"
701
- else
702
- xpath = key.to_s
703
- end
704
- model_value = model[xpath]
705
768
  xpath_element = value[:xpath_element]
706
769
  if Integer(model_value[:size]) > 0 then
707
770
  model_value[:value].slice(0, Integer(model_value[:size])).each do |mvalue|
@@ -709,19 +772,16 @@ def add_model_struct_to_doc(progress, struct, doc, model, prefix = nil)
709
772
  end
710
773
  end
711
774
  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
775
  unless value[:ref] then
717
776
  raise RuntimeError.new("struct is broken: :structure #{key.inspect} does not have a :ref")
718
777
  end
719
- if prefix then
720
- new_prefix = "#{prefix}/#{key}"
721
- else
722
- new_prefix = key
778
+ if model_value[:exists] == true || model_value[:exists] == "true" then
779
+ node = doc.add_child("<#{key}></#{key}>")
780
+ if node.is_a?(Nokogiri::XML::NodeSet) then
781
+ node = node.first
782
+ end
783
+ add_model_struct_to_doc(progress, value[:ref], node, model, xpath)
723
784
  end
724
- add_model_struct_to_doc(progress, value[:ref], node, model, new_prefix)
725
785
  else
726
786
  raise RuntimeError.new("struct is broken: encountered unknown :type #{value[:type].inspect}")
727
787
  end
@@ -745,23 +805,24 @@ def collect_validation_rule_names(struct)
745
805
  return result
746
806
  end
747
807
 
808
+ def set_standard_options(progress, solver)
809
+ solver.to_solver(progress, "(set-option :produce-unsat-cores true) ; enable generation of unsat cores")
810
+ solver.to_solver(progress, "(set-option :smt.core.minimize true) ; ensure that unsat cores are minimal")
811
+ solver.to_solver(progress, "")
812
+ end
813
+
748
814
  def generate_doc(progress, message, output_filename, config, options)
815
+ raise RuntimeError.new("option :solverFactory is required") unless options[:solverFactory]
749
816
  log = options[:log] || nil
750
817
  log.puts "generating doc #{output_filename}" if log
751
818
  options[:solverLog] = options[:solverLog] || "#{output_filename}.smt2"
752
819
 
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()
820
+ solverFactory = options[:solverFactory]
821
+ model = solverFactory.query_model(options) do |solver|
822
+ set_standard_options(progress, solver)
823
+ log.puts "translating structure" if log
824
+ translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
825
+ solver.to_solver(progress, "")
765
826
  end
766
827
 
767
828
  if model then
@@ -860,6 +921,9 @@ end
860
921
 
861
922
  def calc_combinations(message, key)
862
923
  key_fields = collect_key_fields(key, message)
924
+ if key_fields.empty?
925
+ raise FatalError.new("unknown key: #{key}")
926
+ end
863
927
  return calc_combinations_for_key_fields(key_fields)
864
928
  end
865
929
 
@@ -1043,6 +1107,20 @@ def list_validation_rules(message)
1043
1107
  puts "Validation Rules:"
1044
1108
  rules.each { |r| puts " #{r}" }
1045
1109
  end
1110
+
1111
+ def value_of_type(value, type)
1112
+ if type == :string or type == :enum then
1113
+ "\"#{value}\""
1114
+ elsif type == :int then
1115
+ value.to_s
1116
+ elsif type == :date then
1117
+ (Date.parse(value) - Time.at(0).to_date).to_i.to_s
1118
+ elsif type == :timestamp then
1119
+ (Time.new(value) - Time.at(0)).to_i.to_s
1120
+ else
1121
+ raise RuntimeError.new("encountered unknown :datatype #{type.inspect}")
1122
+ end
1123
+ end
1046
1124
 
1047
1125
  class CoreEnumerationAlgo
1048
1126
 
@@ -1081,14 +1159,6 @@ class CoreEnumerationAlgo
1081
1159
  "fixed(#{fixed.join(", ")})"
1082
1160
  end
1083
1161
 
1084
- def value_of_type(value, type)
1085
- if type == :string or type == :enum then
1086
- "\"#{value}\""
1087
- elsif type == :int then
1088
- value.to_s
1089
- end
1090
- end
1091
-
1092
1162
  # prevent the var from taking values that already have been enumerated exhaustively
1093
1163
  def enumerate_blocking_clause(var_xpath, var_def, blocked)
1094
1164
  clauses = []
@@ -1128,24 +1198,19 @@ class CoreEnumerationAlgo
1128
1198
  end
1129
1199
 
1130
1200
  def generate_doc_return_key_values(progress, message, key, output_filename, config, options)
1201
+ raise RuntimeError.new("option :solverFactory is required.") unless options[:solverFactory]
1131
1202
  log = options[:log] || nil
1132
1203
  log.puts "generating doc #{output_filename}" if log
1133
1204
  options[:solverLog] = log_filename_for_output_filename(output_filename)
1134
1205
 
1135
1206
  key_fields = collect_key_fields(key, message)
1136
1207
 
1137
- z3path = config[:z3path] || DEFAULT_Z3_PATH
1138
- z3 = Z3Solver.new(progress, z3path)
1139
- begin
1140
- model = z3.query_model(progress, options) do |solver|
1141
- solver.to_solver(progress, "(set-option :produce-unsat-cores true) ; enable generation of unsat cores")
1142
- solver.to_solver(progress, "")
1143
- log.puts "translating structure" if log
1144
- translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
1145
- solver.to_solver(progress, "")
1146
- end
1147
- ensure
1148
- z3.close()
1208
+ solverFactory = options[:solverFactory]
1209
+ model = solverFactory.query_model(options) do |solver|
1210
+ set_standard_options(progress, solver)
1211
+ log.puts "translating structure" if log
1212
+ translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
1213
+ solver.to_solver(progress, "")
1149
1214
  end
1150
1215
 
1151
1216
  if model then
@@ -1163,9 +1228,12 @@ class CoreEnumerationAlgo
1163
1228
  end
1164
1229
  end
1165
1230
 
1166
- def store_continuation_point(doc_number, state, assignment, new_assignment, filename)
1231
+ def store_continuation_point(doc_number, state, assignment, new_assignment, filename, options, time)
1167
1232
  File.open(filename, "w") do |f|
1168
- 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(",")})")
1233
+ state_str = "state(#{state.map{|s| if s then "[#{s.map{|v| v.inspect}.join(",")}]" else "nil" end}.join("#")})"
1234
+ assignment_str = "assignment(#{assignment.to_a.map{|k,v| "#{k}:#{v.inspect}"}.join(",")})"
1235
+ new_assignment_str = "new_assignment(#{new_assignment.to_a.map{|k,v| "#{k}:#{v.inspect}"}.join(",")})"
1236
+ f.puts("#{doc_number};#{@num_docs};#{state_str};#{assignment_str};#{new_assignment_str};#{time}")
1169
1237
  end
1170
1238
  end
1171
1239
 
@@ -1246,7 +1314,8 @@ class CoreEnumerationAlgo
1246
1314
  state = CoreEnumerationAlgo.parse_state(split[2])
1247
1315
  assignment = CoreEnumerationAlgo.parse_assignment(split[3])
1248
1316
  new_assignment = CoreEnumerationAlgo.parse_assignment(split[4])
1249
- return [doc_number, num_docs, state, assignment, new_assignment]
1317
+ processessing_time = Integer(split[5])
1318
+ return [doc_number, num_docs, state, assignment, new_assignment, processessing_time]
1250
1319
  end
1251
1320
 
1252
1321
  # TODO: logging
@@ -1259,16 +1328,17 @@ class CoreEnumerationAlgo
1259
1328
  res = switched.times.to_a.map do |index|
1260
1329
  var_xpath = vars[index][0]
1261
1330
  "#{var_xpath}=#{assignment[var_xpath].inspect}"
1262
- end + ["#{vars[switched][0]}|#{blocked.map{|v| v.inspect}.join(",")}"]
1331
+ end + ["#{vars[switched][0]}\{#{blocked.map{|v| v.inspect}.join(",")}}"]
1263
1332
  "{#{res.join(", ")}}"
1264
1333
  end
1334
+
1265
1335
 
1266
1336
  # core algo
1267
- def enumerate(progress, switched, vars, assignment, message, key, output_dir, config, options, state = [nil] * vars.size())
1268
- enumerate(progress, switched+1, vars, assignment, message, key, output_dir, config, options, state) if switched < vars.size()-1
1337
+ def enumerate(progress, switched, vars, assignment, message, key, output_dir, config, options, state = [nil] * vars.size(), start_time, prior_processing_time)
1338
+ enumerate(progress, switched+1, vars, assignment, message, key, output_dir, config, options, state, start_time, prior_processing_time) if switched < vars.size()-1
1269
1339
  is_finite_state = keys_are_finite_state(vars)
1270
1340
  if state[switched] == nil then
1271
- blocked = [assignment[vars[switched][0]]]
1341
+ blocked = [assignment[vars[switched][0]]]
1272
1342
  state[switched] = blocked
1273
1343
  else
1274
1344
  blocked = state[switched] + [assignment[vars[switched][0]]]
@@ -1284,24 +1354,33 @@ class CoreEnumerationAlgo
1284
1354
  output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
1285
1355
  new_assignment = generate_sparse_doc(progress, message, key, config, options, clauses, output_doc_filename)
1286
1356
  generated_doc_number = @num_tries
1287
- store_continuation_point(generated_doc_number, state, assignment, new_assignment, "#{output_dir}/#{CONTINUATION_FILE}")
1357
+ time = prior_processing_time + (Time.now().to_i - options[:timestamp_now])
1358
+ store_continuation_point(generated_doc_number, state, assignment, new_assignment, "#{output_dir}/#{CONTINUATION_FILE}", options, time)
1359
+ progress.print_line "STORING continuation point"
1360
+ progress.print_line "|time=#{time}"
1361
+ progress.print_line "|doc_number=#{generated_doc_number}"
1362
+ progress.print_line "|num_docs=#{@num_docs}"
1363
+ progress.print_line "|state=#{state.inspect}"
1364
+ progress.print_line "|assignment=#{assignment.inspect}"
1365
+ progress.print_line "|new_assignment=#{new_assignment.inspect}"
1366
+
1288
1367
  if is_finite_state then
1289
- progress.report_progress(determine_progress(state, vars))
1368
+ progress.report_progress(determine_progress(state, vars), start_time, prior_processing_time, @num_docs)
1290
1369
  else
1291
- progress.report_progress(nil)
1370
+ progress.report_progress(nil, start_time, prior_processing_time, @num_docs)
1292
1371
  end
1293
1372
  @num_tries += 1
1294
1373
  if new_assignment then # sat
1295
1374
  @num_docs += 1
1296
1375
  @on_new_doc.call(generated_doc_number, output_doc_filename, new_assignment) if @on_new_doc
1297
1376
  if switched < vars.size()-1 then
1298
- enumerate(progress, switched+1, vars, new_assignment, message, key, output_dir, config, options, state)
1377
+ enumerate(progress, switched+1, vars, new_assignment, message, key, output_dir, config, options, state, start_time, prior_processing_time)
1299
1378
  end
1300
1379
  blocked << new_assignment[vars[switched][0]]
1301
1380
  state[switched] = blocked
1302
1381
  found = true
1303
1382
  else # unsat
1304
- @on_unsat.call(generated_doc_number, log_filename_for_output_filename(output_doc_filename), display_combination_space(assignment, vars, switched, blocked)) if @on_unsat
1383
+ @on_unsat.call(generated_doc_number, log_filename_for_output_filename(output_doc_filename), display_combination_space(assignment, vars, switched, blocked), switched) if @on_unsat
1305
1384
  found = false
1306
1385
  end
1307
1386
  end
@@ -1367,18 +1446,17 @@ class CoreEnumerationAlgo
1367
1446
  return true
1368
1447
  end
1369
1448
 
1370
- def generate_docs_for_key(message, key, output_dir, config, options)
1449
+ def generate_docs_for_key(progress, message, key, output_dir, config, options)
1371
1450
 
1451
+ prior_processing_time = 0
1452
+ start_time = options[:timestamp_now]
1372
1453
  number_of_combinations = calc_combinations(message, key)
1373
1454
  if number_of_combinations.is_a?(String) then
1374
- progress = ProgressBar.new(100000000000000)
1455
+ progress.set_max(100000000000000)
1375
1456
  else
1376
- progress = ProgressBar.new(number_of_combinations)
1377
- end
1378
- begin
1379
- ensure
1457
+ progress.set_max(number_of_combinations)
1380
1458
  end
1381
- progress.report_progress(0.0)
1459
+ progress.report_progress(0.0, start_time, prior_processing_time, 0)
1382
1460
  progress.print_line "generating sparse documents efficiently..."
1383
1461
  progress.print_line "there is a theoretical maximum of #{number_of_combinations} combinations."
1384
1462
  progress.print_line "Note, however, that we are generating only those documents that are valid, which are usually much less."
@@ -1414,28 +1492,27 @@ class CoreEnumerationAlgo
1414
1492
  end
1415
1493
  end
1416
1494
 
1417
- #progress.print_line "DEBUG: key_fields_list=#{key_fields_list.inspect}"
1418
-
1419
1495
  cont_filename = "#{output_dir}/#{CONTINUATION_FILE}"
1420
1496
  if options[:continue] then
1421
1497
  unless File.exist?(cont_filename) then
1422
1498
  raise FatalError.new("FATAL: file #{cont_filename} not found. There seems to be no continuation point to continue from...")
1423
1499
  end
1424
- doc_number, num_docs, state, assignment, new_assignment = CoreEnumerationAlgo.parse_continuation_point(File.read(cont_filename).chomp)
1500
+ doc_number, num_docs, state, assignment, new_assignment, prior_processing_time = CoreEnumerationAlgo.parse_continuation_point(File.read(cont_filename).chomp)
1425
1501
  if is_final_state(state, new_assignment, key_fields_list) then
1426
- progress.report_progress(number_of_combinations)
1502
+ progress.report_progress(number_of_combinations, start_time, prior_processing_time, num_docs)
1427
1503
  raise FatalError.new("FATAL: The enumeration is finished. There is nothing left to do. Resuming the enumeration hence does not make sense.")
1428
1504
  end
1429
1505
  progress.print_line "RESUMING enumeration from the following continuation point:"
1506
+ progress.print_line "|time=#{prior_processing_time}"
1430
1507
  progress.print_line "|doc_number=#{doc_number}"
1431
1508
  progress.print_line "|num_docs=#{num_docs}"
1432
1509
  progress.print_line "|state=#{state.inspect}"
1433
1510
  progress.print_line "|assignment=#{assignment.inspect}"
1434
1511
  progress.print_line "|new_assignment=#{new_assignment.inspect}"
1435
1512
  if keys_are_finite_state(key_fields_list) then
1436
- progress.report_progress(determine_progress(state, key_fields_list))
1513
+ progress.report_progress(determine_progress(state, key_fields_list), start_time, prior_processing_time, num_docs)
1437
1514
  else
1438
- progress.report_progress(nil)
1515
+ progress.report_progress(nil, start_time, prior_processing_time, num_docs)
1439
1516
  end
1440
1517
 
1441
1518
  if new_assignment && !new_assignment.empty? then
@@ -1446,15 +1523,13 @@ class CoreEnumerationAlgo
1446
1523
  @num_tries = doc_number+1 # we do not want to overwrite the last document written before the STOP
1447
1524
  number_of_combinations += 1 # account for this in the safety mechanism
1448
1525
  @num_docs = num_docs
1449
- doc_counter = 0
1450
1526
  File.open(log_filename, log_mode) do |log|
1451
1527
 
1452
1528
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1453
1529
  # combination logging
1454
- log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
1530
+ log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1455
1531
 
1456
- doc_counter += 1
1457
- if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
1532
+ if options[:max_num_docs] && @num_docs >= options[:max_num_docs] then
1458
1533
  raise NormalProgramTermination.new("-max-num-docs exceeded. Stopping!")
1459
1534
  end
1460
1535
 
@@ -1463,33 +1538,27 @@ class CoreEnumerationAlgo
1463
1538
  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.")
1464
1539
  end
1465
1540
  }
1466
- @on_unsat = Proc.new { |num_doc, log_filename, combination_str|
1541
+ @on_unsat = Proc.new { |num_doc, log_filename, combination_str, switched|
1467
1542
  log.puts "CONTRADICTION (see: #{log_filename}) <- #{combination_str}"
1468
1543
  progress.print_line "doc#{num_doc} NOT generated"
1469
- progress.print_line "No more valid combinations of the free vars could be found. -> Switching"
1544
+ progress.print_line "No more valid combinations of the free vars could be found. -> Switching (var #{switched})"
1470
1545
  }
1471
1546
 
1472
1547
  # core algo - resume from state
1473
- enumerate(progress, 0, key_fields_list, assignment, message, key, output_dir, config, options, state)
1548
+ enumerate(progress, 0, key_fields_list, assignment, message, key, output_dir, config, options, state, start_time, prior_processing_time)
1474
1549
  end
1475
1550
  else
1476
1551
  # init safety mechanism
1477
1552
  @num_tries = 0
1478
1553
  @num_docs = 0
1479
- doc_counter = 0
1480
1554
  # first try - a freely generated document
1481
1555
  File.open(log_filename, log_mode) do |log|
1482
1556
  output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
1483
- key_values = generate_doc_return_key_values(progress, message, key, output_doc_filename, config, options)
1484
- log.puts "#{output_doc_filename} <- {#{key_values.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
1485
- doc_counter += 1
1486
- @num_tries += 1
1487
1557
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1488
1558
  # combination logging
1489
- log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
1559
+ log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1490
1560
 
1491
- doc_counter += 1
1492
- if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
1561
+ if options[:max_num_docs] && @num_docs >= options[:max_num_docs] then
1493
1562
  raise NormalProgramTermination.new("-max-num-docs exceeded. Stopping!")
1494
1563
  end
1495
1564
 
@@ -1500,19 +1569,28 @@ class CoreEnumerationAlgo
1500
1569
  end
1501
1570
  end
1502
1571
  }
1503
- @on_unsat = Proc.new { |num_doc, log_filename, combination_str|
1572
+ @on_unsat = Proc.new { |num_doc, log_filename, combination_str, switched|
1504
1573
  log.puts "CONTRADICTION (see: #{log_filename}) <- #{combination_str}"
1505
1574
  progress.print_line "doc#{num_doc} NOT generated"
1506
- progress.print_line "No more valid combinations of the free vars could be found. -> Switching"
1575
+ progress.print_line "No more valid combinations of the free vars could be found. -> Switching (var #{switched})"
1507
1576
  }
1577
+ key_values = generate_doc_return_key_values(progress, message, key, output_doc_filename, config, options)
1578
+ @num_tries += 1
1579
+ if key_values then
1580
+ @num_docs += 1
1581
+ @on_new_doc.call(@num_tries, output_doc_filename, key_values)
1582
+ else
1583
+ # NOTE: this only occurs when the model is inconsistent and has no valid documents
1584
+ progress.print_line("ERROR: Model inconsistent - no valid documents could be found.")
1585
+ @on_unsat.call(@num_tries, log_filename_for_output_filename(output_doc_filename), "{}", 0)
1586
+ end
1508
1587
 
1509
1588
  # core algo - start enumeration
1510
- enumerate(progress, 0, key_fields_list, key_values, message, key, output_dir, config, options)
1589
+ enumerate(progress, 0, key_fields_list, key_values, message, key, output_dir, config, options, start_time, prior_processing_time)
1511
1590
  end
1512
-
1513
- progress.print_line "No more valid documents could be found. -> Exiting"
1514
- progress.report_progress(number_of_combinations)
1515
1591
  end
1592
+ progress.print_line "No more valid documents could be found. -> Exiting"
1593
+ progress.report_progress(number_of_combinations, start_time, prior_processing_time, @num_docs)
1516
1594
  end
1517
1595
  end
1518
1596
 
@@ -1585,6 +1663,211 @@ def validate_model(message, prefix=nil)
1585
1663
  end
1586
1664
  end
1587
1665
 
1666
+ def validate_doc_against_schema(struct, doc)
1667
+ root = doc.root
1668
+ sdef = struct[root.name.to_sym]
1669
+ unless sdef[:type] == :structure then
1670
+ puts "ERROR: schema validation failed: root element should be a structure"
1671
+ end
1672
+ # first, check that each doc element is defined for the struct
1673
+ validate_doc_struct_against_schema(sdef[:ref], root, root.name)
1674
+ # second, check that all required elements from the struct are actually present in the doc
1675
+ validate_schema_against_doc_struct(sdef[:ref], root, root.name)
1676
+ end
1677
+
1678
+ def validate_doc_struct_against_schema(struct, doc, prefix = nil)
1679
+ doc.children.to_a.filter{|x| x.is_a?(Nokogiri::XML::Element) }.each do |c|
1680
+ fdef = struct[c.name.to_sym]
1681
+ if prefix then
1682
+ xpath = "#{prefix}/#{c.name}"
1683
+ else
1684
+ xpath = c.name
1685
+ end
1686
+ if fdef
1687
+ if fdef[:type] == :structure then
1688
+ return unless validate_doc_struct_against_schema(fdef[:ref], c, xpath)
1689
+ end
1690
+ else
1691
+ if prefix then
1692
+ xpath = "#{prefix}/#{c.name}"
1693
+ else
1694
+ xpath = c.name
1695
+ end
1696
+ raise FatalError.new("ERROR: schema validation failed: #{c.name} of #{xpath} is present in document, but not defined in schema.")
1697
+ end
1698
+ end
1699
+ end
1700
+
1701
+ def validate_schema_against_doc_struct(struct, doc, prefix = nil)
1702
+ struct.each do |key, value|
1703
+ next if key == :additional_smtlib || key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix
1704
+ if prefix then
1705
+ xpath = "#{prefix}/#{key}"
1706
+ else
1707
+ xpath = key
1708
+ end
1709
+ if value[:type] == :structure then
1710
+ node = doc.at_xpath(key.to_s)
1711
+ if node then
1712
+ validate_schema_against_doc_struct(value[:ref], node, xpath)
1713
+ else
1714
+ if value[:optional] == false then
1715
+ raise FatalError.new("ERROR: schema validation failed: non-optional structure #{xpath} not found in doc.")
1716
+ end
1717
+ end
1718
+ elsif value[:type] == :field then
1719
+ if value[:optional] == false then
1720
+ unless doc.at_xpath(key.to_s) then
1721
+ raise FatalError.new("ERROR: schema validation failed: non-optional field #{xpath} not found in doc.")
1722
+ end
1723
+ end
1724
+ elsif value[:type] == :list then
1725
+ if value[:optional] == false then
1726
+ unless doc.at_xpath(key.to_s) then
1727
+ raise FatalError.new("ERROR: schema validation failed: non-optional list #{xpath} not found in doc.")
1728
+ end
1729
+ end
1730
+ list = doc.at_xpath(key.to_s)
1731
+ if list then
1732
+ list.children.to_a.each do |c|
1733
+ next unless c.is_a?(Nokogiri::XML::Element)
1734
+ unless c.name == value[:xpath_element] then
1735
+ raise FatalError.new("ERROR: schema validation failed: encountered element #{c.name} in list #{xpath}, which should only contain #{value[:xpath_element]} elements.")
1736
+ end
1737
+ end
1738
+ end
1739
+ else
1740
+ raise RuntimeError.new("Model error: encountered element #{xpath} of unknown type: #{value[:type]}.")
1741
+ end
1742
+ end
1743
+ end
1744
+
1745
+ def translate_doc_to_SMTLIB(progress, solver, struct, doc, hash = {})
1746
+ options = hash[:options]
1747
+ config = hash[:config]
1748
+
1749
+ solver.to_solver(progress, "; --- Document: ---")
1750
+ translate_struct_in_doc_to_SMTLIB(progress, solver, struct, doc, nil, hash)
1751
+ end
1752
+
1753
+ def translate_struct_in_doc_to_SMTLIB(progress, solver, struct, doc, prefix, hash = {})
1754
+ options = hash[:options]
1755
+ config = hash[:config]
1756
+
1757
+ struct.each do |key, value|
1758
+ next if key == :additional_smtlib || key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix
1759
+ if prefix then
1760
+ xpath = "#{prefix}/#{key.to_s}"
1761
+ else
1762
+ xpath = key.to_s
1763
+ end
1764
+ if value[:type] == :structure then
1765
+ node = doc.at_xpath(xpath)
1766
+ if node then
1767
+ info = {
1768
+ :assertion_type => :doc_structure_exists,
1769
+ :xpath => xpath,
1770
+ }
1771
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1772
+ solver.associateAssertionIDWith(assertionID, info)
1773
+ solver.to_solver(progress, "(assert (! (= #{exists_var_for_structure(xpath)} true) :named #{assertionID}))")
1774
+
1775
+ translate_struct_in_doc_to_SMTLIB(progress, solver, value[:ref], doc, xpath, hash)
1776
+ else
1777
+ info = {
1778
+ :assertion_type => :doc_structure_not_exists,
1779
+ :xpath => xpath,
1780
+ }
1781
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1782
+ solver.associateAssertionIDWith(assertionID, info)
1783
+ solver.to_solver(progress, "(assert (! (= #{exists_var_for_structure(xpath)} false) :named #{assertionID}))")
1784
+ end
1785
+ elsif value[:type] == :field then
1786
+ node = doc.at_xpath(xpath)
1787
+ if node then
1788
+ fvalue = node.text
1789
+ info = {
1790
+ :assertion_type => :doc_field_filled,
1791
+ :xpath => xpath,
1792
+ :value => fvalue
1793
+ }
1794
+ fvalue_str = value_of_type(fvalue, value[:datatype])
1795
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1796
+ solver.associateAssertionIDWith(assertionID, info)
1797
+ solver.to_solver(progress, "(assert (! (and (= #{filled_var_for_field(xpath)} true) (= #{value_var_for_field(xpath)} #{fvalue_str})) :named #{assertionID}))")
1798
+ else
1799
+ info = {
1800
+ :assertion_type => :doc_field_empty,
1801
+ :xpath => xpath,
1802
+ }
1803
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1804
+ solver.associateAssertionIDWith(assertionID, info)
1805
+ solver.to_solver(progress, "(assert (! (= #{filled_var_for_field(xpath)} false) :named #{assertionID}))")
1806
+ end
1807
+ elsif value[:type] == :list then
1808
+ node = doc.at_xpath(xpath)
1809
+ if node then
1810
+ fvalues = []
1811
+ node.children.to_a.each do |c|
1812
+ next unless c.is_a?(Nokogiri::XML::Element)
1813
+ fvalues << c.text
1814
+ end
1815
+ info = {
1816
+ :assertion_type => :doc_list_filled,
1817
+ :xpath => xpath,
1818
+ :values => fvalues
1819
+ }
1820
+ fvalue_str = value_of_type(fvalues, value[:datatype])
1821
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1822
+ solver.associateAssertionIDWith(assertionID, info)
1823
+ clauses = []
1824
+ fvalues.each_with_index do |fvalue, index|
1825
+ clauses << "(= #{value_var_for_list(xpath,index)} #{value_of_type(fvalue, value[:datatype])})"
1826
+ end
1827
+ solver.to_solver(progress, "(assert (! (and (= #{filled_var_for_field(xpath)} true) (= #{size_var_for_list(xpath)} #{fvalues.size}) #{clauses.join(' ')}) :named #{assertionID}))")
1828
+ else
1829
+ info = {
1830
+ :assertion_type => :doc_field_empty,
1831
+ :xpath => xpath,
1832
+ }
1833
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1834
+ solver.associateAssertionIDWith(assertionID, info)
1835
+ solver.to_solver(progress, "(assert (! (= #{filled_var_for_field(xpath)} false) :named #{assertionID}))")
1836
+ end
1837
+ else
1838
+ raise RuntimeError.new("Model Error: encountered unknown type #{value[:type].inspect}")
1839
+ end
1840
+ end
1841
+ end
1842
+
1843
+ def validate_doc(progress, struct, doc, options, config)
1844
+ raise RuntimeError.new("option :solverFactory is required") unless options[:solverFactory]
1845
+ options[:solverLog] = "validation.smt2"
1846
+
1847
+ solverFactory = options[:solverFactory]
1848
+ result = solverFactory.query(options) do |solver|
1849
+ set_standard_options(progress, solver)
1850
+ translate_structure_to_SMTLIB(progress, solver, struct, :options => options, :config => config)
1851
+ translate_doc_to_SMTLIB(progress, solver, struct, doc, :options => options, :config => config)
1852
+ solver.to_solver(progress, "")
1853
+ end
1854
+ unless result == true then
1855
+ raise FatalError.new("VALIDATION FAILED! SMTLIB log was written to #{options[:solverLog]}.\n\n#{result}")
1856
+ end
1857
+
1858
+ return true
1859
+ end
1860
+
1861
+ def generate_unused_logfile_name()
1862
+ number = 0
1863
+ result = "logs/#{number}.log"
1864
+ while File.exist?(result) do
1865
+ number += 1
1866
+ result = "logs/#{number}.log"
1867
+ end
1868
+ result
1869
+ end
1870
+
1588
1871
  def generate(message, argv, message_config_filename = nil)
1589
1872
  if ARGV[0] == "--help" || ARGV[0] == "-help" || ARGV[0] == "help" then
1590
1873
  puts USAGE
@@ -1617,6 +1900,12 @@ def generate(message, argv, message_config_filename = nil)
1617
1900
  elsif opt == "-max-num-docs" then
1618
1901
  options[:max_num_docs] = Integer(argv.shift)
1619
1902
  puts "flag -max-num-docs used. Will stop after generating #{options[:max_num_docs]} documents."
1903
+ elsif opt == "-validate" then
1904
+ if options.has_key?(:validate_files) then
1905
+ options[:validate_files] << argv.shift
1906
+ else
1907
+ options[:validate_files] = [argv.shift]
1908
+ end
1620
1909
  else
1621
1910
  puts "FATAL: unknown option #{opt.inspect}."
1622
1911
  puts USAGE
@@ -1625,51 +1914,63 @@ def generate(message, argv, message_config_filename = nil)
1625
1914
  opt = argv.shift
1626
1915
  end
1627
1916
 
1628
- unless options[:negated_validation_rules].empty? then
1629
- puts "negating rules: #{options[:negated_validation_rules].join(", ")}"
1917
+ ProgressBar.logging(1, generate_unused_logfile_name()) do |progress|
1918
+ unless options[:negated_validation_rules].empty? then
1919
+ progress.print_line "negating rules: #{options[:negated_validation_rules].join(", ")}"
1920
+
1921
+ rules = collect_validation_rule_names(message)
1922
+ unknown_negated_rules = options[:negated_validation_rules].filter { |rule_name| !rules.include?(rule_name) }
1923
+ unless unknown_negated_rules.empty? then
1924
+ throw RuntimeError.new("unknown negated rules #{unknown_negated_rules.join(", ")}")
1925
+ end
1630
1926
 
1631
- rules = collect_validation_rule_names(message)
1632
- unknown_negated_rules = options[:negated_validation_rules].filter { |rule_name| !rules.include?(rule_name) }
1633
- unless unknown_negated_rules.empty? then
1634
- throw RuntimeError.new("unknown negated rules #{unknown_negated_rules.join(", ")}")
1635
1927
  end
1636
1928
 
1637
- end
1929
+ if File.exist?(CONFIG_FILE) then
1930
+ config = YAML.load_file(CONFIG_FILE)
1931
+ else
1932
+ config = DEFAULT_CONFIG
1933
+ File.open(CONFIG_FILE, "w") do |f|
1934
+ f.puts(config.to_yaml)
1935
+ end
1936
+ progress.print_line "No config file found. Default config written to #{CONFIG_FILE}"
1937
+ end
1938
+ if message_config_filename then
1939
+ message_config = YAML.load_file(message_config_filename)
1940
+ config = config.merge(message_config)
1941
+ end
1638
1942
 
1639
- if File.exist?(CONFIG_FILE) then
1640
- config = YAML.load_file(CONFIG_FILE)
1641
- else
1642
- config = DEFAULT_CONFIG
1643
- File.open(CONFIG_FILE, "w") do |f|
1644
- f.puts(config.to_yaml)
1645
- end
1646
- puts "No config file found. Default config written to #{CONFIG_FILE}"
1647
- end
1648
- if message_config_filename then
1649
- message_config = YAML.load_file(message_config_filename)
1650
- config = config.merge(message_config)
1651
- end
1652
-
1653
- introduce_parent_links(message)
1654
-
1655
- if options[:count_docs_for_key] then
1656
- puts "there are #{calc_combinations(message, options[:count_docs_for_key])} combinations."
1657
- elsif options[:docs_for_key] then
1658
- puts "using #{options[:date_now]} as [Date.now]"
1659
- puts "using #{options[:timestamp_now]} as [Timestamp.now]"
1660
- validate_model(message)
1661
- CoreEnumerationAlgo.new().generate_docs_for_key(message, options[:docs_for_key], OUTPUT_DIR, config, options)
1662
- elsif options[:list_keys] then
1663
- list_keys(message)
1664
- elsif options[:list] then
1665
- list_fields_and_structures(message, options[:list])
1666
- elsif options[:list_validation_rules] then
1667
- list_validation_rules(message)
1668
- else
1669
- puts "using #{options[:date_now]} as [Date.now]"
1670
- puts "using #{options[:timestamp_now]} as [Timestamp.now]"
1671
- validate_model(message)
1672
- progress = ProgressBar.new(1)
1673
- generate_doc(progress, message, OUTPUT_FILE, config, options)
1943
+ introduce_parent_links(message)
1944
+ z3path = config[:z3path] || DEFAULT_Z3_PATH
1945
+ options[:solverFactory] = SolverFactory.new(progress, z3path)
1946
+
1947
+ if options[:validate_files] then
1948
+ progress.print_line "using #{options[:date_now]} as [Date.now]"
1949
+ progress.print_line "using #{options[:timestamp_now]} as [Timestamp.now]"
1950
+ options[:validate_files].each do |filename|
1951
+ progress.print_line "validating file #{filename}"
1952
+ doc = Nokogiri::XML(File.read(filename))
1953
+ validate_doc_against_schema(message, doc)
1954
+ validate_doc(progress, message, doc, options, config)
1955
+ end
1956
+ elsif options[:count_docs_for_key] then
1957
+ progress.print_line "there are #{calc_combinations(message, options[:count_docs_for_key])} combinations."
1958
+ elsif options[:docs_for_key] then
1959
+ progress.print_line "using #{options[:date_now]} as [Date.now]"
1960
+ progress.print_line "using #{options[:timestamp_now]} as [Timestamp.now]"
1961
+ validate_model(message)
1962
+ CoreEnumerationAlgo.new().generate_docs_for_key(progress, message, options[:docs_for_key], OUTPUT_DIR, config, options)
1963
+ elsif options[:list_keys] then
1964
+ list_keys(message)
1965
+ elsif options[:list] then
1966
+ list_fields_and_structures(message, options[:list])
1967
+ elsif options[:list_validation_rules] then
1968
+ list_validation_rules(message)
1969
+ else
1970
+ progress.print_line "using #{options[:date_now]} as [Date.now]"
1971
+ progress.print_line "using #{options[:timestamp_now]} as [Timestamp.now]"
1972
+ validate_model(message)
1973
+ generate_doc(progress, message, OUTPUT_FILE, config, options)
1974
+ end
1674
1975
  end
1675
1976
  end