mbt-gen 0.0.2 → 0.0.3

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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mbt-gen.rb +332 -49
  3. data/lib/solver-lib.rb +85 -18
  4. metadata +3 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55d2d7fd4933678bc3dac436056891c26984272a85fd6ea42e4ef9d947ee89f1
4
- data.tar.gz: 8a8f055137928c6b814f6a12acc7481095c47f37ed73fe8ed7b7872db8c2de9b
3
+ metadata.gz: 056ec1e0d6d661c5c4a092e4a7e879f610a7524dfa63fb7faf9779d8fc4bfc6e
4
+ data.tar.gz: 20f715037f76a5e23dd5407416018ff7c2cbf3c64a616c6fe32117bc0b411a20
5
5
  SHA512:
6
- metadata.gz: b2f497291c2f9c80123d6c21c4e533f2d260216053e21606ba118a0fde71c0b5acebe7a5092fa1671ef7f02337f866659f08eef3f029174bfbcf49c67193a92e
7
- data.tar.gz: a63cd52e1af07b675f02b31c7457e0d1e91c20217b4066c66efc2b4e6bc04ed7a8ef15d2d4763e70ab9cc2caa6942804461512de58650d927830aeab3342258a
6
+ metadata.gz: 7cfca600b07970cc752fa96da6e9dc1052af81024e94c87f581b49632cb414e13d6e71c05dfbb7dc51eab907e3f0ef504ac880994b59cd83347a038c8709fbd5
7
+ data.tar.gz: 2ba5fa686c5a8894431e714481953ea2c4989de0825e14c526e273ba10e798250003492195b5163a69ebfe53e1cba7d9b243d9f9c5ae82806484b3026a4e793a
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,6 +805,12 @@ 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)
749
815
  log = options[:log] || nil
750
816
  log.puts "generating doc #{output_filename}" if log
@@ -754,8 +820,7 @@ def generate_doc(progress, message, output_filename, config, options)
754
820
  z3 = Z3Solver.new(progress, z3path)
755
821
  begin
756
822
  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, "")
823
+ set_standard_options(progress, solver)
759
824
  log.puts "translating structure" if log
760
825
  translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
761
826
  solver.to_solver(progress, "")
@@ -1043,6 +1108,20 @@ def list_validation_rules(message)
1043
1108
  puts "Validation Rules:"
1044
1109
  rules.each { |r| puts " #{r}" }
1045
1110
  end
1111
+
1112
+ def value_of_type(value, type)
1113
+ if type == :string or type == :enum then
1114
+ "\"#{value}\""
1115
+ elsif type == :int then
1116
+ value.to_s
1117
+ elsif type == :date then
1118
+ (Date.parse(value) - Time.at(0).to_date).to_i.to_s
1119
+ elsif type == :timestamp then
1120
+ (Time.new(value) - Time.at(0)).to_i.to_s
1121
+ else
1122
+ raise RuntimeError.new("encountered unknown :datatype #{type.inspect}")
1123
+ end
1124
+ end
1046
1125
 
1047
1126
  class CoreEnumerationAlgo
1048
1127
 
@@ -1081,14 +1160,6 @@ class CoreEnumerationAlgo
1081
1160
  "fixed(#{fixed.join(", ")})"
1082
1161
  end
1083
1162
 
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
1163
  # prevent the var from taking values that already have been enumerated exhaustively
1093
1164
  def enumerate_blocking_clause(var_xpath, var_def, blocked)
1094
1165
  clauses = []
@@ -1138,8 +1209,7 @@ class CoreEnumerationAlgo
1138
1209
  z3 = Z3Solver.new(progress, z3path)
1139
1210
  begin
1140
1211
  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, "")
1212
+ set_standard_options(progress, solver)
1143
1213
  log.puts "translating structure" if log
1144
1214
  translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
1145
1215
  solver.to_solver(progress, "")
@@ -1259,7 +1329,7 @@ class CoreEnumerationAlgo
1259
1329
  res = switched.times.to_a.map do |index|
1260
1330
  var_xpath = vars[index][0]
1261
1331
  "#{var_xpath}=#{assignment[var_xpath].inspect}"
1262
- end + ["#{vars[switched][0]}|#{blocked.map{|v| v.inspect}.join(",")}"]
1332
+ end + ["#{vars[switched][0]}\{#{blocked.map{|v| v.inspect}.join(",")}}"]
1263
1333
  "{#{res.join(", ")}}"
1264
1334
  end
1265
1335
 
@@ -1414,8 +1484,6 @@ class CoreEnumerationAlgo
1414
1484
  end
1415
1485
  end
1416
1486
 
1417
- #progress.print_line "DEBUG: key_fields_list=#{key_fields_list.inspect}"
1418
-
1419
1487
  cont_filename = "#{output_dir}/#{CONTINUATION_FILE}"
1420
1488
  if options[:continue] then
1421
1489
  unless File.exist?(cont_filename) then
@@ -1451,7 +1519,7 @@ class CoreEnumerationAlgo
1451
1519
 
1452
1520
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1453
1521
  # combination logging
1454
- log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
1522
+ log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1455
1523
 
1456
1524
  doc_counter += 1
1457
1525
  if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
@@ -1481,12 +1549,12 @@ class CoreEnumerationAlgo
1481
1549
  File.open(log_filename, log_mode) do |log|
1482
1550
  output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
1483
1551
  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(", ")}}"
1552
+ log.puts "#{output_doc_filename} <- {#{key_values.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1485
1553
  doc_counter += 1
1486
1554
  @num_tries += 1
1487
1555
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1488
1556
  # combination logging
1489
- log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")}}"
1557
+ log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1490
1558
 
1491
1559
  doc_counter += 1
1492
1560
  if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
@@ -1585,6 +1653,205 @@ def validate_model(message, prefix=nil)
1585
1653
  end
1586
1654
  end
1587
1655
 
1656
+ def validate_doc_against_schema(struct, doc)
1657
+ root = doc.root
1658
+ sdef = struct[root.name.to_sym]
1659
+ unless sdef[:type] == :structure then
1660
+ puts "ERROR: schema validation failed: root element should be a structure"
1661
+ end
1662
+ # first, check that each doc element is defined for the struct
1663
+ validate_doc_struct_against_schema(sdef[:ref], root, root.name)
1664
+ # second, check that all required elements from the struct are actually present in the doc
1665
+ validate_schema_against_doc_struct(sdef[:ref], root, root.name)
1666
+ end
1667
+
1668
+ def validate_doc_struct_against_schema(struct, doc, prefix = nil)
1669
+ doc.children.to_a.filter{|x| x.is_a?(Nokogiri::XML::Element) }.each do |c|
1670
+ fdef = struct[c.name.to_sym]
1671
+ if prefix then
1672
+ xpath = "#{prefix}/#{c.name}"
1673
+ else
1674
+ xpath = c.name
1675
+ end
1676
+ if fdef
1677
+ if fdef[:type] == :structure then
1678
+ return unless validate_doc_struct_against_schema(fdef[:ref], c, xpath)
1679
+ end
1680
+ else
1681
+ if prefix then
1682
+ xpath = "#{prefix}/#{c.name}"
1683
+ else
1684
+ xpath = c.name
1685
+ end
1686
+ raise FatalError.new("ERROR: schema validation failed: #{c.name} of #{xpath} is present in document, but not defined in schema.")
1687
+ end
1688
+ end
1689
+ end
1690
+
1691
+ def validate_schema_against_doc_struct(struct, doc, prefix = nil)
1692
+ struct.each do |key, value|
1693
+ next if key == :additional_smtlib || key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix
1694
+ if prefix then
1695
+ xpath = "#{prefix}/#{key}"
1696
+ else
1697
+ xpath = key
1698
+ end
1699
+ if value[:type] == :structure then
1700
+ node = doc.at_xpath(key.to_s)
1701
+ if node then
1702
+ validate_schema_against_doc_struct(value[:ref], node, xpath)
1703
+ else
1704
+ if value[:optional] == false then
1705
+ raise FatalError.new("ERROR: schema validation failed: non-optional structure #{xpath} not found in doc.")
1706
+ end
1707
+ end
1708
+ elsif value[:type] == :field then
1709
+ if value[:optional] == false then
1710
+ unless doc.at_xpath(key.to_s) then
1711
+ raise FatalError.new("ERROR: schema validation failed: non-optional field #{xpath} not found in doc.")
1712
+ end
1713
+ end
1714
+ elsif value[:type] == :list then
1715
+ if value[:optional] == false then
1716
+ unless doc.at_xpath(key.to_s) then
1717
+ raise FatalError.new("ERROR: schema validation failed: non-optional list #{xpath} not found in doc.")
1718
+ end
1719
+ end
1720
+ list = doc.at_xpath(key.to_s)
1721
+ if list then
1722
+ list.children.to_a.each do |c|
1723
+ next unless c.is_a?(Nokogiri::XML::Element)
1724
+ unless c.name == value[:xpath_element] then
1725
+ raise FatalError.new("ERROR: schema validation failed: encountered element #{c.name} in list #{xpath}, which should only contain #{value[:xpath_element]} elements.")
1726
+ end
1727
+ end
1728
+ end
1729
+ else
1730
+ raise RuntimeError.new("Model error: encountered element #{xpath} of unknown type: #{value[:type]}.")
1731
+ end
1732
+ end
1733
+ end
1734
+
1735
+ def translate_doc_to_SMTLIB(progress, solver, struct, doc, hash = {})
1736
+ options = hash[:options]
1737
+ config = hash[:config]
1738
+
1739
+ solver.to_solver(progress, "; --- Document: ---")
1740
+ translate_struct_in_doc_to_SMTLIB(progress, solver, struct, doc, nil, hash)
1741
+ end
1742
+
1743
+ def translate_struct_in_doc_to_SMTLIB(progress, solver, struct, doc, prefix, hash = {})
1744
+ options = hash[:options]
1745
+ config = hash[:config]
1746
+
1747
+ struct.each do |key, value|
1748
+ next if key == :additional_smtlib || key == :validation_rules || key == :predicates || key == :parent_link || key == :struct_xpath_prefix
1749
+ if prefix then
1750
+ xpath = "#{prefix}/#{key.to_s}"
1751
+ else
1752
+ xpath = key.to_s
1753
+ end
1754
+ if value[:type] == :structure then
1755
+ node = doc.at_xpath(xpath)
1756
+ if node then
1757
+ info = {
1758
+ :assertion_type => :doc_structure_exists,
1759
+ :xpath => xpath,
1760
+ }
1761
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1762
+ solver.associateAssertionIDWith(assertionID, info)
1763
+ solver.to_solver(progress, "(assert (! (= #{exists_var_for_structure(xpath)} true) :named #{assertionID}))")
1764
+
1765
+ translate_struct_in_doc_to_SMTLIB(progress, solver, value[:ref], doc, xpath, hash)
1766
+ else
1767
+ info = {
1768
+ :assertion_type => :doc_structure_not_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)} false) :named #{assertionID}))")
1774
+ end
1775
+ elsif value[:type] == :field then
1776
+ node = doc.at_xpath(xpath)
1777
+ if node then
1778
+ fvalue = node.text
1779
+ info = {
1780
+ :assertion_type => :doc_field_filled,
1781
+ :xpath => xpath,
1782
+ :value => fvalue
1783
+ }
1784
+ fvalue_str = value_of_type(fvalue, value[:datatype])
1785
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1786
+ solver.associateAssertionIDWith(assertionID, info)
1787
+ solver.to_solver(progress, "(assert (! (and (= #{filled_var_for_field(xpath)} true) (= #{value_var_for_field(xpath)} #{fvalue_str})) :named #{assertionID}))")
1788
+ else
1789
+ info = {
1790
+ :assertion_type => :doc_field_empty,
1791
+ :xpath => xpath,
1792
+ }
1793
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1794
+ solver.associateAssertionIDWith(assertionID, info)
1795
+ solver.to_solver(progress, "(assert (! (= #{filled_var_for_field(xpath)} false) :named #{assertionID}))")
1796
+ end
1797
+ elsif value[:type] == :list then
1798
+ node = doc.at_xpath(xpath)
1799
+ if node then
1800
+ fvalues = []
1801
+ node.children.to_a.each do |c|
1802
+ next unless c.is_a?(Nokogiri::XML::Element)
1803
+ fvalues << c.text
1804
+ end
1805
+ info = {
1806
+ :assertion_type => :doc_list_filled,
1807
+ :xpath => xpath,
1808
+ :values => fvalues
1809
+ }
1810
+ fvalue_str = value_of_type(fvalues, value[:datatype])
1811
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1812
+ solver.associateAssertionIDWith(assertionID, info)
1813
+ clauses = []
1814
+ fvalues.each_with_index do |fvalue, index|
1815
+ clauses << "(= #{value_var_for_list(xpath,index)} #{value_of_type(fvalue, value[:datatype])})"
1816
+ end
1817
+ solver.to_solver(progress, "(assert (! (and (= #{filled_var_for_field(xpath)} true) (= #{size_var_for_list(xpath)} #{fvalues.size}) #{clauses.join(' ')}) :named #{assertionID}))")
1818
+ else
1819
+ info = {
1820
+ :assertion_type => :doc_field_empty,
1821
+ :xpath => xpath,
1822
+ }
1823
+ assertionID = "doc-#{xpath.gsub('/', '-')}"
1824
+ solver.associateAssertionIDWith(assertionID, info)
1825
+ solver.to_solver(progress, "(assert (! (= #{filled_var_for_field(xpath)} false) :named #{assertionID}))")
1826
+ end
1827
+ else
1828
+ raise RuntimeError.new("Model Error: encountered unknown type #{value[:type].inspect}")
1829
+ end
1830
+ end
1831
+ end
1832
+
1833
+ def validate_doc(progress, struct, doc, options, config)
1834
+ options[:solverLog] = "validation.smt2"
1835
+
1836
+ z3path = config[:z3path] || DEFAULT_Z3_PATH
1837
+ z3 = Z3Solver.new(progress, z3path)
1838
+ begin
1839
+ result = z3.query(progress, options) do |solver|
1840
+ set_standard_options(progress, solver)
1841
+ translate_structure_to_SMTLIB(progress, solver, struct, :options => options, :config => config)
1842
+ translate_doc_to_SMTLIB(progress, solver, struct, doc, :options => options, :config => config)
1843
+ solver.to_solver(progress, "")
1844
+ end
1845
+ unless result == true then
1846
+ raise FatalError.new("VALIDATION FAILED! SMTLIB log was written to #{options[:solverLog]}.\n\n#{result}")
1847
+ end
1848
+ ensure
1849
+ z3.close()
1850
+ end
1851
+
1852
+ return true
1853
+ end
1854
+
1588
1855
  def generate(message, argv, message_config_filename = nil)
1589
1856
  if ARGV[0] == "--help" || ARGV[0] == "-help" || ARGV[0] == "help" then
1590
1857
  puts USAGE
@@ -1617,6 +1884,12 @@ def generate(message, argv, message_config_filename = nil)
1617
1884
  elsif opt == "-max-num-docs" then
1618
1885
  options[:max_num_docs] = Integer(argv.shift)
1619
1886
  puts "flag -max-num-docs used. Will stop after generating #{options[:max_num_docs]} documents."
1887
+ elsif opt == "-validate" then
1888
+ if options.has_key?(:validate_files) then
1889
+ options[:validate_files] << argv.shift
1890
+ else
1891
+ options[:validate_files] = [argv.shift]
1892
+ end
1620
1893
  else
1621
1894
  puts "FATAL: unknown option #{opt.inspect}."
1622
1895
  puts USAGE
@@ -1652,7 +1925,17 @@ def generate(message, argv, message_config_filename = nil)
1652
1925
 
1653
1926
  introduce_parent_links(message)
1654
1927
 
1655
- if options[:count_docs_for_key] then
1928
+ if options[:validate_files] then
1929
+ puts "using #{options[:date_now]} as [Date.now]"
1930
+ puts "using #{options[:timestamp_now]} as [Timestamp.now]"
1931
+ options[:validate_files].each do |filename|
1932
+ puts "validating file #{filename}"
1933
+ doc = Nokogiri::XML(File.read(filename))
1934
+ validate_doc_against_schema(message, doc)
1935
+ progress = ProgressBar.new(1)
1936
+ validate_doc(progress, message, doc, options, config)
1937
+ end
1938
+ elsif options[:count_docs_for_key] then
1656
1939
  puts "there are #{calc_combinations(message, options[:count_docs_for_key])} combinations."
1657
1940
  elsif options[:docs_for_key] then
1658
1941
  puts "using #{options[:date_now]} as [Date.now]"
data/lib/solver-lib.rb CHANGED
@@ -113,6 +113,13 @@ class SolverSession
113
113
  return nextAssertionID
114
114
  end
115
115
 
116
+ def associateAssertionIDWith(assertionID, hash)
117
+ if @associations.has_key?(assertionID) then
118
+ raise RuntimeError.new("AssertionID #{assertionID.inspect} already has info associated with it!")
119
+ end
120
+ @associations[assertionID] = hash
121
+ end
122
+
116
123
  def getAssociationForAssertionID(assertionID)
117
124
  return @associations[assertionID]
118
125
  end
@@ -159,32 +166,89 @@ class Z3Solver
159
166
  Thread.kill(@z3thr)
160
167
  end
161
168
 
162
- def query(&block)
163
- solver = SolverSession.new(@z3in, @z3outAndErr)
169
+ def query(progress, options, &block)
170
+ unless options[:solverLog]
171
+ raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
172
+ end
173
+ progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
174
+ solver = SolverSession.new(@z3in, @z3outAndErr, options[:solverLog])
164
175
 
165
176
  block.call(solver)
166
177
 
167
- solver.toSolver "(check-sat)"
168
- result = solver.fromSolver()
178
+ solver.to_solver(progress, "(check-sat)")
179
+ result = solver.from_solver(progress)
180
+ explanation = true
169
181
  if result.chomp == "sat"
170
182
  # model is not contradictory -> continue
171
183
  elsif result.chomp == "unsat"
172
184
  # model is contradictory -> add a message
173
185
 
174
- solver.toSolver("(get-unsat-core)")
175
- unsat_core = solver.fromSolver()
176
- explanation = build_explanation_based_on_unsat_core(solver, sid, unsat_core)
177
- messages << {:type => :issue, :text => "Model is contradictory", :explanation => explanation}
178
- solver.writeProtocolToFile("contradictions.smt2")
186
+ solver.to_solver(progress, "(get-unsat-core)")
187
+ unsat_core = solver.from_solver(progress)
188
+ explanation = parse_unsat_core(solver, unsat_core)
179
189
  else
180
- handleSolverError(solver, result)
190
+ handleSolverError(progress, solver, options, result)
181
191
  end
182
192
 
183
- messages = []
184
- solver.toSolver "(reset)"
185
- solver.toSolver "(set-logic ALL)"
186
- solver.toSolver "(set-option :produce-unsat-cores true)"
187
- return messages
193
+ solver.to_solver(progress, "(reset)")
194
+ solver.to_solver(progress, "(set-logic ALL)")
195
+ solver.to_solver(progress, "(set-option :produce-unsat-cores true)")
196
+ return explanation
197
+ end
198
+
199
+ def display_assertion(assertion, solver)
200
+ if assertion =~ /validation_rule_(.*)_instance_(.*)/ then
201
+ return "Validation rule #{$1} of structure #{$2.gsub("_", "/")}"
202
+ else
203
+ info = solver.getAssociationForAssertionID(assertion)
204
+ if info then
205
+ if info[:assertion_type] == :doc_field_filled then
206
+ return "Field #{info[:xpath]} contains value #{info[:value].inspect} in document"
207
+ elsif info[:assertion_type] == :doc_field_empty then
208
+ return "Field #{info[:xpath]} is empty in document"
209
+ elsif info[:assertion_type] == :doc_structure_exists then
210
+ return "Structure #{info[:xpath]} exists in document"
211
+ elsif info[:assertion_type] == :doc_structure_not_exists then
212
+ return "Structure #{info[:xpath]} does not exist in document"
213
+ elsif info[:assertion_type] == :constraint then
214
+ if info[:constraint_type] == :min then
215
+ return "Field #{info[:xpath]} has a minimum value of #{info[:value]}."
216
+ elsif info[:constraint_type] == :max then
217
+ return "Field #{info[:xpath]} has a maximum value of #{info[:value]}."
218
+ elsif info[:constraint_type] == :required then
219
+ return "Field #{info[:xpath]} is required."
220
+ else
221
+ p info
222
+ raise RuntimeError.new("encountered unknown :constraint_type #{info[:constraint_type].inspect} for assertionID #{assertion.inspect}")
223
+ end
224
+ else
225
+ raise RuntimeError.new("encountered unknown :assertion_type #{info[:assertion_type].inspect} for assertionID #{assertion.inspect}")
226
+ end
227
+ else
228
+ return assertion
229
+ end
230
+ end
231
+ end
232
+
233
+ def parse_unsat_core(solver, unsat_core)
234
+ unsat_core = unsat_core.chomp
235
+ # TODO: implement
236
+ unless unsat_core =~ /\((.*)\)/ then
237
+ raise RuntimeError.new("could not parse unsat core: #{unsat_core.inspect}")
238
+ end
239
+ list = $1.split(" ")
240
+
241
+ result = "EXPLANATION:\n"
242
+ result += "The following values\n"
243
+
244
+ list.filter{|a| a.start_with?("doc-")}.each do |assertion|
245
+ result += "- #{display_assertion(assertion, solver)} (#{assertion})\n"
246
+ end
247
+ result += "\nviolate the following constraints\n"
248
+ list.filter{|a| !a.start_with?("doc-")}.each do |assertion|
249
+ result += "- #{display_assertion(assertion, solver)} (#{assertion})\n"
250
+ end
251
+ return result
188
252
  end
189
253
 
190
254
  # main entry point!
@@ -249,10 +313,13 @@ class Z3Solver
249
313
  end
250
314
  end
251
315
  elsif varname.end_with?("-size") then
252
- unless model[xpath][:type] == :list then
253
- raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a list, not a #{model[xpath][:type]}.")
316
+ unless model[xpath][:type] == :list || model[xpath][:type] == :field then
317
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a list or a field, not a #{model[xpath][:type]}.")
318
+ end
319
+ if model[xpath][:type] == :field then
320
+ model[xpath][:type] = :list
254
321
  end
255
- model[xpath][:value] = value
322
+ model[xpath][:size] = Integer(value)
256
323
  elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
257
324
  unless model[xpath][:type] == :struct then
258
325
  raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a struct, not a #{model[xpath][:type]}.")
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbt-gen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - FM-enthusiast
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-04-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: nokogiri
@@ -42,7 +41,6 @@ licenses:
42
41
  metadata:
43
42
  bug_tracker_uri: https://codeberg.org/FM-enthusiast/MBT-gen/issues
44
43
  source_code_uri: https://codeberg.org/FM-enthusiast/MBT-gen
45
- post_install_message:
46
44
  rdoc_options: []
47
45
  require_paths:
48
46
  - lib
@@ -58,8 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
56
  version: '0'
59
57
  requirements:
60
58
  - an SMT-solver. Z3 is recommended, it can be obtained from https://github.com/Z3Prover/z3
61
- rubygems_version: 3.5.22
62
- signing_key:
59
+ rubygems_version: 3.6.7
63
60
  specification_version: 4
64
61
  summary: test data generator for Model-based testing
65
62
  test_files: []