mbt-gen 0.0.1 → 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 +335 -151
  3. data/lib/solver-lib.rb +87 -28
  4. metadata +6 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ac020a4fa0dd1933cbc568d8d378919d0136525a7172830add366461a7fbda3
4
- data.tar.gz: b3da4c6ef18e9542e9efc96db915537197663a391f43e7102749cdccf6ae7f72
3
+ metadata.gz: 056ec1e0d6d661c5c4a092e4a7e879f610a7524dfa63fb7faf9779d8fc4bfc6e
4
+ data.tar.gz: 20f715037f76a5e23dd5407416018ff7c2cbf3c64a616c6fe32117bc0b411a20
5
5
  SHA512:
6
- metadata.gz: 4d653ef8c58e941b0b5aa513eb67cafbec0b54c3b103e5506cbab5c041c63afff153ad1b469c1486e1775f64834b25d0de4a97ea34ea8d12742d82d8d556d609
7
- data.tar.gz: 418fc3e0151700e49d5141add6d38ab3557af788e9b7aa6e97b3c599d487ae1736516fcc623a71b8b4af2102fcf0362cca888d9c49c02eefc19925d165e5d4fc
6
+ metadata.gz: 7cfca600b07970cc752fa96da6e9dc1052af81024e94c87f581b49632cb414e13d6e71c05dfbb7dc51eab907e3f0ef504ac880994b59cd83347a038c8709fbd5
7
+ data.tar.gz: 2ba5fa686c5a8894431e714481953ea2c4989de0825e14c526e273ba10e798250003492195b5163a69ebfe53e1cba7d9b243d9f9c5ae82806484b3026a4e793a
data/lib/mbt-gen.rb CHANGED
@@ -18,9 +18,9 @@ DEFAULT_Z3_PATH = "z3"
18
18
 
19
19
  CONFIG_FILE = "config.yml"
20
20
  OUTPUT_FILE = "result.xml"
21
- CONTINUATION_FILE = 'continuation.log'
22
- COMBINATION_LOG = 'combinations.log'
23
- VARS_LOG = 'vars.log'
21
+ CONTINUATION_FILE = "continuation.log"
22
+ COMBINATION_LOG = "combinations.log"
23
+ VARS_LOG = "vars.log"
24
24
 
25
25
  OUTPUT_DIR = "result"
26
26
  LISTS_DIR = "lists"
@@ -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, "")
@@ -830,33 +895,6 @@ def all_values_for_field(field)
830
895
  return values
831
896
  end
832
897
 
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
898
  def calc_combinations_for_key_fields(key_fields)
861
899
  count = 1
862
900
  key_fields.each do |field|
@@ -923,46 +961,6 @@ def read_docs_from_log_file(log_filename)
923
961
  return result
924
962
  end
925
963
 
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
964
  def prepare_output_doc_filename(number, output_dir)
967
965
  chunk_number = number / 100000
968
966
  dirname = "#{output_dir}/#{chunk_number}"
@@ -972,30 +970,6 @@ def prepare_output_doc_filename(number, output_dir)
972
970
  return "#{dirname}/doc#{number}.xml"
973
971
  end
974
972
 
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
973
  def values_for_key_field(field)
1000
974
  if field[:type] == :field then
1001
975
  if field[:datatype] == :enum then
@@ -1024,14 +998,6 @@ def values_for_key_field(field)
1024
998
  end
1025
999
  end
1026
1000
 
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
1001
  def collect_key_fields(fkey, struct, prefix = nil)
1036
1002
  result = {}
1037
1003
  struct.each do |key, value|
@@ -1142,6 +1108,20 @@ def list_validation_rules(message)
1142
1108
  puts "Validation Rules:"
1143
1109
  rules.each { |r| puts " #{r}" }
1144
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
1145
1125
 
1146
1126
  class CoreEnumerationAlgo
1147
1127
 
@@ -1180,14 +1160,6 @@ class CoreEnumerationAlgo
1180
1160
  "fixed(#{fixed.join(", ")})"
1181
1161
  end
1182
1162
 
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
1163
  # prevent the var from taking values that already have been enumerated exhaustively
1192
1164
  def enumerate_blocking_clause(var_xpath, var_def, blocked)
1193
1165
  clauses = []
@@ -1237,8 +1209,7 @@ class CoreEnumerationAlgo
1237
1209
  z3 = Z3Solver.new(progress, z3path)
1238
1210
  begin
1239
1211
  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, "")
1212
+ set_standard_options(progress, solver)
1242
1213
  log.puts "translating structure" if log
1243
1214
  translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
1244
1215
  solver.to_solver(progress, "")
@@ -1358,7 +1329,7 @@ class CoreEnumerationAlgo
1358
1329
  res = switched.times.to_a.map do |index|
1359
1330
  var_xpath = vars[index][0]
1360
1331
  "#{var_xpath}=#{assignment[var_xpath].inspect}"
1361
- end + ["#{vars[switched][0]}|#{blocked.map{|v| v.inspect}.join(",")}"]
1332
+ end + ["#{vars[switched][0]}\{#{blocked.map{|v| v.inspect}.join(",")}}"]
1362
1333
  "{#{res.join(", ")}}"
1363
1334
  end
1364
1335
 
@@ -1513,8 +1484,6 @@ class CoreEnumerationAlgo
1513
1484
  end
1514
1485
  end
1515
1486
 
1516
- #progress.print_line "DEBUG: key_fields_list=#{key_fields_list.inspect}"
1517
-
1518
1487
  cont_filename = "#{output_dir}/#{CONTINUATION_FILE}"
1519
1488
  if options[:continue] then
1520
1489
  unless File.exist?(cont_filename) then
@@ -1550,7 +1519,7 @@ class CoreEnumerationAlgo
1550
1519
 
1551
1520
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1552
1521
  # combination logging
1553
- 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(", ")}}"
1554
1523
 
1555
1524
  doc_counter += 1
1556
1525
  if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
@@ -1580,12 +1549,12 @@ class CoreEnumerationAlgo
1580
1549
  File.open(log_filename, log_mode) do |log|
1581
1550
  output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
1582
1551
  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(", ")}}"
1552
+ log.puts "#{output_doc_filename} <- {#{key_values.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1584
1553
  doc_counter += 1
1585
1554
  @num_tries += 1
1586
1555
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1587
1556
  # combination logging
1588
- 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(", ")}}"
1589
1558
 
1590
1559
  doc_counter += 1
1591
1560
  if options[:max_num_docs] && doc_counter >= options[:max_num_docs] then
@@ -1684,6 +1653,205 @@ def validate_model(message, prefix=nil)
1684
1653
  end
1685
1654
  end
1686
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
+
1687
1855
  def generate(message, argv, message_config_filename = nil)
1688
1856
  if ARGV[0] == "--help" || ARGV[0] == "-help" || ARGV[0] == "help" then
1689
1857
  puts USAGE
@@ -1716,6 +1884,12 @@ def generate(message, argv, message_config_filename = nil)
1716
1884
  elsif opt == "-max-num-docs" then
1717
1885
  options[:max_num_docs] = Integer(argv.shift)
1718
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
1719
1893
  else
1720
1894
  puts "FATAL: unknown option #{opt.inspect}."
1721
1895
  puts USAGE
@@ -1751,7 +1925,17 @@ def generate(message, argv, message_config_filename = nil)
1751
1925
 
1752
1926
  introduce_parent_links(message)
1753
1927
 
1754
- 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
1755
1939
  puts "there are #{calc_combinations(message, options[:count_docs_for_key])} combinations."
1756
1940
  elsif options[:docs_for_key] then
1757
1941
  puts "using #{options[:date_now]} as [Date.now]"
data/lib/solver-lib.rb CHANGED
@@ -1,14 +1,6 @@
1
- require 'open3'
2
-
3
- FTYPE_MANDATORY = 0
4
- FTYPE_OPTIONAL = 1
5
-
6
- SUBF_AND = 0
7
- SUBF_OR = 1
8
- SUBF_XOR = 2
9
-
10
- LOG_DIR = 'logs'
1
+ #!/usr/bin/ruby
11
2
 
3
+ require 'open3'
12
4
 
13
5
 
14
6
  class SolverSession
@@ -121,6 +113,13 @@ class SolverSession
121
113
  return nextAssertionID
122
114
  end
123
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
+
124
123
  def getAssociationForAssertionID(assertionID)
125
124
  return @associations[assertionID]
126
125
  end
@@ -167,32 +166,89 @@ class Z3Solver
167
166
  Thread.kill(@z3thr)
168
167
  end
169
168
 
170
- def query(&block)
171
- 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])
172
175
 
173
176
  block.call(solver)
174
177
 
175
- solver.toSolver "(check-sat)"
176
- result = solver.fromSolver()
178
+ solver.to_solver(progress, "(check-sat)")
179
+ result = solver.from_solver(progress)
180
+ explanation = true
177
181
  if result.chomp == "sat"
178
182
  # model is not contradictory -> continue
179
183
  elsif result.chomp == "unsat"
180
184
  # model is contradictory -> add a message
181
185
 
182
- solver.toSolver("(get-unsat-core)")
183
- unsat_core = solver.fromSolver()
184
- explanation = build_explanation_based_on_unsat_core(solver, sid, unsat_core)
185
- messages << {:type => :issue, :text => "Model is contradictory", :explanation => explanation}
186
- 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)
187
189
  else
188
- handleSolverError(solver, result)
190
+ handleSolverError(progress, solver, options, result)
189
191
  end
190
192
 
191
- messages = []
192
- solver.toSolver "(reset)"
193
- solver.toSolver "(set-logic ALL)"
194
- solver.toSolver "(set-option :produce-unsat-cores true)"
195
- 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
196
252
  end
197
253
 
198
254
  # main entry point!
@@ -257,10 +313,13 @@ class Z3Solver
257
313
  end
258
314
  end
259
315
  elsif varname.end_with?("-size") then
260
- unless model[xpath][:type] == :list then
261
- 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
262
321
  end
263
- model[xpath][:value] = value
322
+ model[xpath][:size] = Integer(value)
264
323
  elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
265
324
  unless model[xpath][:type] == :struct then
266
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.1
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-23 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
@@ -36,13 +35,12 @@ files:
36
35
  - lib/progress.rb
37
36
  - lib/regexp-to-smtlib.rb
38
37
  - lib/solver-lib.rb
39
- homepage: https://github.com/FM-enthusiast/MBT-gen
38
+ homepage: https://codeberg.org/FM-enthusiast/MBT-gen
40
39
  licenses:
41
40
  - GPL-3.0-only
42
41
  metadata:
43
- bug_tracker_uri: https://github.com/FM-enthusiast/MBT-gen/issues
44
- source_code_uri: https://github.com/FM-enthusiast/MBT-gen
45
- post_install_message:
42
+ bug_tracker_uri: https://codeberg.org/FM-enthusiast/MBT-gen/issues
43
+ source_code_uri: https://codeberg.org/FM-enthusiast/MBT-gen
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: []