mbt-gen 0.0.3 → 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 +147 -129
  3. data/lib/progress.rb +47 -5
  4. data/lib/solver-lib.rb +228 -148
  5. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 056ec1e0d6d661c5c4a092e4a7e879f610a7524dfa63fb7faf9779d8fc4bfc6e
4
- data.tar.gz: 20f715037f76a5e23dd5407416018ff7c2cbf3c64a616c6fe32117bc0b411a20
3
+ metadata.gz: 8b69ab00ef224ac052d2f5897cce4c620c72a3630f9e6da9eaabb7cc7277904f
4
+ data.tar.gz: ad905f43225a8680663ec4068a2acd65845090e238da9c8b902008e9c118e1e8
5
5
  SHA512:
6
- metadata.gz: 7cfca600b07970cc752fa96da6e9dc1052af81024e94c87f581b49632cb414e13d6e71c05dfbb7dc51eab907e3f0ef504ac880994b59cd83347a038c8709fbd5
7
- data.tar.gz: 2ba5fa686c5a8894431e714481953ea2c4989de0825e14c526e273ba10e798250003492195b5163a69ebfe53e1cba7d9b243d9f9c5ae82806484b3026a4e793a
6
+ metadata.gz: 2d1d2d4393f92efad0eb54968c3029aa7030551ce296483890b4b7dbe57cdb3ba8fe4224ef7cd2d6dacc0f28df482b14eb3f373b6e4394e45478ccee4da3147a
7
+ data.tar.gz: ffc27db25c445aa64b856a0b2763085cbb6469675fdd5bf89730d5e6cd6a626a615abe5be2ac1b7f02d69972e8b6ab11b0b7d270c13340ffc70c8442a64914ff
data/lib/mbt-gen.rb CHANGED
@@ -812,21 +812,17 @@ def set_standard_options(progress, solver)
812
812
  end
813
813
 
814
814
  def generate_doc(progress, message, output_filename, config, options)
815
+ raise RuntimeError.new("option :solverFactory is required") unless options[:solverFactory]
815
816
  log = options[:log] || nil
816
817
  log.puts "generating doc #{output_filename}" if log
817
818
  options[:solverLog] = options[:solverLog] || "#{output_filename}.smt2"
818
819
 
819
- z3path = config[:z3path] || DEFAULT_Z3_PATH
820
- z3 = Z3Solver.new(progress, z3path)
821
- begin
822
- model = z3.query_model(progress, options) do |solver|
823
- set_standard_options(progress, solver)
824
- log.puts "translating structure" if log
825
- translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
826
- solver.to_solver(progress, "")
827
- end
828
- ensure
829
- 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, "")
830
826
  end
831
827
 
832
828
  if model then
@@ -925,6 +921,9 @@ end
925
921
 
926
922
  def calc_combinations(message, key)
927
923
  key_fields = collect_key_fields(key, message)
924
+ if key_fields.empty?
925
+ raise FatalError.new("unknown key: #{key}")
926
+ end
928
927
  return calc_combinations_for_key_fields(key_fields)
929
928
  end
930
929
 
@@ -1199,23 +1198,19 @@ class CoreEnumerationAlgo
1199
1198
  end
1200
1199
 
1201
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]
1202
1202
  log = options[:log] || nil
1203
1203
  log.puts "generating doc #{output_filename}" if log
1204
1204
  options[:solverLog] = log_filename_for_output_filename(output_filename)
1205
1205
 
1206
1206
  key_fields = collect_key_fields(key, message)
1207
1207
 
1208
- z3path = config[:z3path] || DEFAULT_Z3_PATH
1209
- z3 = Z3Solver.new(progress, z3path)
1210
- begin
1211
- model = z3.query_model(progress, options) do |solver|
1212
- set_standard_options(progress, solver)
1213
- log.puts "translating structure" if log
1214
- translate_structure_to_SMTLIB(progress, solver, message, :options => options, :config => config)
1215
- solver.to_solver(progress, "")
1216
- end
1217
- ensure
1218
- 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, "")
1219
1214
  end
1220
1215
 
1221
1216
  if model then
@@ -1233,9 +1228,12 @@ class CoreEnumerationAlgo
1233
1228
  end
1234
1229
  end
1235
1230
 
1236
- 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)
1237
1232
  File.open(filename, "w") do |f|
1238
- 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}")
1239
1237
  end
1240
1238
  end
1241
1239
 
@@ -1316,7 +1314,8 @@ class CoreEnumerationAlgo
1316
1314
  state = CoreEnumerationAlgo.parse_state(split[2])
1317
1315
  assignment = CoreEnumerationAlgo.parse_assignment(split[3])
1318
1316
  new_assignment = CoreEnumerationAlgo.parse_assignment(split[4])
1319
- 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]
1320
1319
  end
1321
1320
 
1322
1321
  # TODO: logging
@@ -1332,13 +1331,14 @@ class CoreEnumerationAlgo
1332
1331
  end + ["#{vars[switched][0]}\{#{blocked.map{|v| v.inspect}.join(",")}}"]
1333
1332
  "{#{res.join(", ")}}"
1334
1333
  end
1334
+
1335
1335
 
1336
1336
  # core algo
1337
- def enumerate(progress, switched, vars, assignment, message, key, output_dir, config, options, state = [nil] * vars.size())
1338
- 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
1339
1339
  is_finite_state = keys_are_finite_state(vars)
1340
1340
  if state[switched] == nil then
1341
- blocked = [assignment[vars[switched][0]]]
1341
+ blocked = [assignment[vars[switched][0]]]
1342
1342
  state[switched] = blocked
1343
1343
  else
1344
1344
  blocked = state[switched] + [assignment[vars[switched][0]]]
@@ -1354,24 +1354,33 @@ class CoreEnumerationAlgo
1354
1354
  output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
1355
1355
  new_assignment = generate_sparse_doc(progress, message, key, config, options, clauses, output_doc_filename)
1356
1356
  generated_doc_number = @num_tries
1357
- 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
+
1358
1367
  if is_finite_state then
1359
- progress.report_progress(determine_progress(state, vars))
1368
+ progress.report_progress(determine_progress(state, vars), start_time, prior_processing_time, @num_docs)
1360
1369
  else
1361
- progress.report_progress(nil)
1370
+ progress.report_progress(nil, start_time, prior_processing_time, @num_docs)
1362
1371
  end
1363
1372
  @num_tries += 1
1364
1373
  if new_assignment then # sat
1365
1374
  @num_docs += 1
1366
1375
  @on_new_doc.call(generated_doc_number, output_doc_filename, new_assignment) if @on_new_doc
1367
1376
  if switched < vars.size()-1 then
1368
- 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)
1369
1378
  end
1370
1379
  blocked << new_assignment[vars[switched][0]]
1371
1380
  state[switched] = blocked
1372
1381
  found = true
1373
1382
  else # unsat
1374
- @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
1375
1384
  found = false
1376
1385
  end
1377
1386
  end
@@ -1437,18 +1446,17 @@ class CoreEnumerationAlgo
1437
1446
  return true
1438
1447
  end
1439
1448
 
1440
- def generate_docs_for_key(message, key, output_dir, config, options)
1449
+ def generate_docs_for_key(progress, message, key, output_dir, config, options)
1441
1450
 
1451
+ prior_processing_time = 0
1452
+ start_time = options[:timestamp_now]
1442
1453
  number_of_combinations = calc_combinations(message, key)
1443
1454
  if number_of_combinations.is_a?(String) then
1444
- progress = ProgressBar.new(100000000000000)
1455
+ progress.set_max(100000000000000)
1445
1456
  else
1446
- progress = ProgressBar.new(number_of_combinations)
1457
+ progress.set_max(number_of_combinations)
1447
1458
  end
1448
- begin
1449
- ensure
1450
- end
1451
- progress.report_progress(0.0)
1459
+ progress.report_progress(0.0, start_time, prior_processing_time, 0)
1452
1460
  progress.print_line "generating sparse documents efficiently..."
1453
1461
  progress.print_line "there is a theoretical maximum of #{number_of_combinations} combinations."
1454
1462
  progress.print_line "Note, however, that we are generating only those documents that are valid, which are usually much less."
@@ -1489,21 +1497,22 @@ class CoreEnumerationAlgo
1489
1497
  unless File.exist?(cont_filename) then
1490
1498
  raise FatalError.new("FATAL: file #{cont_filename} not found. There seems to be no continuation point to continue from...")
1491
1499
  end
1492
- 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)
1493
1501
  if is_final_state(state, new_assignment, key_fields_list) then
1494
- progress.report_progress(number_of_combinations)
1502
+ progress.report_progress(number_of_combinations, start_time, prior_processing_time, num_docs)
1495
1503
  raise FatalError.new("FATAL: The enumeration is finished. There is nothing left to do. Resuming the enumeration hence does not make sense.")
1496
1504
  end
1497
1505
  progress.print_line "RESUMING enumeration from the following continuation point:"
1506
+ progress.print_line "|time=#{prior_processing_time}"
1498
1507
  progress.print_line "|doc_number=#{doc_number}"
1499
1508
  progress.print_line "|num_docs=#{num_docs}"
1500
1509
  progress.print_line "|state=#{state.inspect}"
1501
1510
  progress.print_line "|assignment=#{assignment.inspect}"
1502
1511
  progress.print_line "|new_assignment=#{new_assignment.inspect}"
1503
1512
  if keys_are_finite_state(key_fields_list) then
1504
- 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)
1505
1514
  else
1506
- progress.report_progress(nil)
1515
+ progress.report_progress(nil, start_time, prior_processing_time, num_docs)
1507
1516
  end
1508
1517
 
1509
1518
  if new_assignment && !new_assignment.empty? then
@@ -1514,15 +1523,13 @@ class CoreEnumerationAlgo
1514
1523
  @num_tries = doc_number+1 # we do not want to overwrite the last document written before the STOP
1515
1524
  number_of_combinations += 1 # account for this in the safety mechanism
1516
1525
  @num_docs = num_docs
1517
- doc_counter = 0
1518
1526
  File.open(log_filename, log_mode) do |log|
1519
1527
 
1520
1528
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1521
1529
  # combination logging
1522
1530
  log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1523
1531
 
1524
- doc_counter += 1
1525
- 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
1526
1533
  raise NormalProgramTermination.new("-max-num-docs exceeded. Stopping!")
1527
1534
  end
1528
1535
 
@@ -1531,33 +1538,27 @@ class CoreEnumerationAlgo
1531
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.")
1532
1539
  end
1533
1540
  }
1534
- @on_unsat = Proc.new { |num_doc, log_filename, combination_str|
1541
+ @on_unsat = Proc.new { |num_doc, log_filename, combination_str, switched|
1535
1542
  log.puts "CONTRADICTION (see: #{log_filename}) <- #{combination_str}"
1536
1543
  progress.print_line "doc#{num_doc} NOT generated"
1537
- 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})"
1538
1545
  }
1539
1546
 
1540
1547
  # core algo - resume from state
1541
- 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)
1542
1549
  end
1543
1550
  else
1544
1551
  # init safety mechanism
1545
1552
  @num_tries = 0
1546
1553
  @num_docs = 0
1547
- doc_counter = 0
1548
1554
  # first try - a freely generated document
1549
1555
  File.open(log_filename, log_mode) do |log|
1550
1556
  output_doc_filename = prepare_output_doc_filename(@num_tries, output_dir)
1551
- key_values = generate_doc_return_key_values(progress, message, key, output_doc_filename, config, options)
1552
- log.puts "#{output_doc_filename} <- {#{key_values.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1553
- doc_counter += 1
1554
- @num_tries += 1
1555
1557
  @on_new_doc = Proc.new { |num_doc, doc_filename, combination|
1556
1558
  # combination logging
1557
1559
  log.puts "#{doc_filename} <- {#{combination.map{|k,v| "#{k}=#{v.inspect}"}.join(", ")}}"
1558
1560
 
1559
- doc_counter += 1
1560
- 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
1561
1562
  raise NormalProgramTermination.new("-max-num-docs exceeded. Stopping!")
1562
1563
  end
1563
1564
 
@@ -1568,19 +1569,28 @@ class CoreEnumerationAlgo
1568
1569
  end
1569
1570
  end
1570
1571
  }
1571
- @on_unsat = Proc.new { |num_doc, log_filename, combination_str|
1572
+ @on_unsat = Proc.new { |num_doc, log_filename, combination_str, switched|
1572
1573
  log.puts "CONTRADICTION (see: #{log_filename}) <- #{combination_str}"
1573
1574
  progress.print_line "doc#{num_doc} NOT generated"
1574
- 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})"
1575
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
1576
1587
 
1577
1588
  # core algo - start enumeration
1578
- 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)
1579
1590
  end
1580
-
1581
- progress.print_line "No more valid documents could be found. -> Exiting"
1582
- progress.report_progress(number_of_combinations)
1583
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)
1584
1594
  end
1585
1595
  end
1586
1596
 
@@ -1831,27 +1841,33 @@ def translate_struct_in_doc_to_SMTLIB(progress, solver, struct, doc, prefix, has
1831
1841
  end
1832
1842
 
1833
1843
  def validate_doc(progress, struct, doc, options, config)
1844
+ raise RuntimeError.new("option :solverFactory is required") unless options[:solverFactory]
1834
1845
  options[:solverLog] = "validation.smt2"
1835
1846
 
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()
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}")
1850
1856
  end
1851
1857
 
1852
1858
  return true
1853
1859
  end
1854
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
+
1855
1871
  def generate(message, argv, message_config_filename = nil)
1856
1872
  if ARGV[0] == "--help" || ARGV[0] == "-help" || ARGV[0] == "help" then
1857
1873
  puts USAGE
@@ -1898,61 +1914,63 @@ def generate(message, argv, message_config_filename = nil)
1898
1914
  opt = argv.shift
1899
1915
  end
1900
1916
 
1901
- unless options[:negated_validation_rules].empty? then
1902
- 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
1903
1926
 
1904
- rules = collect_validation_rule_names(message)
1905
- unknown_negated_rules = options[:negated_validation_rules].filter { |rule_name| !rules.include?(rule_name) }
1906
- unless unknown_negated_rules.empty? then
1907
- throw RuntimeError.new("unknown negated rules #{unknown_negated_rules.join(", ")}")
1908
1927
  end
1909
1928
 
1910
- 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
1911
1942
 
1912
- if File.exist?(CONFIG_FILE) then
1913
- config = YAML.load_file(CONFIG_FILE)
1914
- else
1915
- config = DEFAULT_CONFIG
1916
- File.open(CONFIG_FILE, "w") do |f|
1917
- f.puts(config.to_yaml)
1918
- end
1919
- puts "No config file found. Default config written to #{CONFIG_FILE}"
1920
- end
1921
- if message_config_filename then
1922
- message_config = YAML.load_file(message_config_filename)
1923
- config = config.merge(message_config)
1924
- end
1925
-
1926
- introduce_parent_links(message)
1927
-
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
1939
- puts "there are #{calc_combinations(message, options[:count_docs_for_key])} combinations."
1940
- elsif options[:docs_for_key] then
1941
- puts "using #{options[:date_now]} as [Date.now]"
1942
- puts "using #{options[:timestamp_now]} as [Timestamp.now]"
1943
- validate_model(message)
1944
- CoreEnumerationAlgo.new().generate_docs_for_key(message, options[:docs_for_key], OUTPUT_DIR, config, options)
1945
- elsif options[:list_keys] then
1946
- list_keys(message)
1947
- elsif options[:list] then
1948
- list_fields_and_structures(message, options[:list])
1949
- elsif options[:list_validation_rules] then
1950
- list_validation_rules(message)
1951
- else
1952
- puts "using #{options[:date_now]} as [Date.now]"
1953
- puts "using #{options[:timestamp_now]} as [Timestamp.now]"
1954
- validate_model(message)
1955
- progress = ProgressBar.new(1)
1956
- 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
1957
1975
  end
1958
1976
  end
data/lib/progress.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ require 'fileutils'
4
+
3
5
  class Progress
4
6
 
5
- def initialize()
7
+ def initialize(logfilename)
6
8
  @last_line = ""
9
+ dir = File.dirname(logfilename)
10
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
11
+ @logfile = File.open(logfilename, "w")
7
12
  end
8
13
 
9
14
  def update_last_line(last_line)
@@ -13,27 +18,64 @@ class Progress
13
18
  end
14
19
 
15
20
  def print_line(line)
21
+ @logfile.puts(line)
22
+ @logfile.flush()
16
23
  print "\033[M#{line}\n#{@last_line}"
17
24
  $stdout.flush()
18
25
  end
19
26
  end
20
27
 
21
28
  class ProgressBar < Progress
22
- def initialize(max)
29
+ def initialize(max, logfilename)
30
+ super(logfilename)
23
31
  @max = max.to_f
24
32
  end
25
33
 
34
+ def set_max(value)
35
+ @max = value.to_f
36
+ end
37
+
26
38
  def render_bar(current, length)
27
39
  bars = (current.to_f * length.to_f).floor
28
40
  "=" * bars + ">" + " " * (length - bars)
29
41
  end
30
42
 
31
- def report_progress(progress)
43
+ def processing_time(start_time, prior_processessing_time)
44
+ total_sec = prior_processessing_time + (Time.now().to_i - start_time)
45
+ sec = total_sec
46
+ days = sec / (3600*24)
47
+ sec -= days * (3600*24)
48
+ hours = sec / 3600
49
+ sec -= hours * 3600
50
+ min = sec / 60
51
+ sec -= min * 60
52
+ if days > 0 then
53
+ return format("%d:%02d:%02d:%02d", days, hours, min, sec)
54
+ else
55
+ if hours > 0 then
56
+ return format("%02d:%02d:%02d", hours, min, sec)
57
+ else
58
+ return format("%02d:%02d", min, sec)
59
+ end
60
+ end
61
+ end
62
+
63
+ def report_progress(progress, start_time, prior_processessing_time, num_docs)
64
+ time_str = processing_time(start_time, prior_processessing_time)
32
65
  if progress then
33
66
  current = (progress.to_f / @max)
34
- update_last_line("[#{render_bar(current, 40)}] combinations covered: #{progress}/#{@max} (#{format("%.8f", current * 100.0)}%)")
67
+ update_last_line("#{time_str} [#{render_bar(current, 40)}] combinations covered: #{progress}/#{@max} (#{format("%.8f", current * 100.0)}%), docs generated: #{num_docs}")
35
68
  else
36
- update_last_line("[... ? ... ? ... ? ...] combinations covered: ?/∞ (?%)")
69
+ update_last_line("#{time_str} [... ? ... ? ... ? ...] combinations covered: ?/∞ (?%), docs generated: #{num_docs}")
70
+ end
71
+ end
72
+
73
+ def self.logging(max, logfile, &block)
74
+ progress = ProgressBar.new(max, logfile)
75
+ begin
76
+ block.call(progress)
77
+ ensure
78
+ progress.print_line("Output written to #{logfile}")
37
79
  end
38
80
  end
39
81
  end
data/lib/solver-lib.rb CHANGED
@@ -5,9 +5,9 @@ require 'open3'
5
5
 
6
6
  class SolverSession
7
7
 
8
- def initialize(z3in, z3outAndErr, logFileName)
9
- @z3in = z3in
10
- @z3outAndErr = z3outAndErr
8
+ def initialize(solverFactory, logFileName)
9
+ @solverFactory = solverFactory
10
+ @solver = @solverFactory.new_solver()
11
11
  @protocol_filename = logFileName
12
12
  @protocol = File.open(@protocol_filename, "w")
13
13
  @assertCounter = -1
@@ -15,13 +15,26 @@ class SolverSession
15
15
  @consts = {}
16
16
  end
17
17
 
18
+ def switch_solver(progress)
19
+ @solver.close()
20
+ @solver = @solverFactory.new_solver()
21
+ # re-feed the session protocol into the new solver to get it up-to-date
22
+ @protocol.close()
23
+ File.open(@protocol_filename, "r") do |protocol|
24
+ protocol.each_line do |line|
25
+ to_solver(progress, line)
26
+ end
27
+ end
28
+ @protocol = File.open(@protocol_filename, "a")
29
+ end
30
+
18
31
  def emptyReadBuffer(progress)
19
32
  empty = false
20
33
  while !empty do
21
34
  begin
22
35
  c = ""
23
- @z3outAndErr.read_nonblock(1, c)
24
- line = c + @z3outAndErr.gets
36
+ @solver.z3outAndErr.read_nonblock(1, c)
37
+ line = c + @solver.z3outAndErr.gets
25
38
  checkLineForSolverError(line, progress)
26
39
  @protocol.puts("; Solver Response: #{line}")
27
40
  rescue Errno::EWOULDBLOCK
@@ -45,16 +58,27 @@ class SolverSession
45
58
 
46
59
  def to_solver(progress, line)
47
60
  emptyReadBuffer(progress);
48
- @protocol.puts(line)
49
- begin
50
- @z3in.puts(line)
51
- rescue Errno::EINVAL
52
- progress.print_line "ERROR: could not write the line #{line.inspect} to the solver."
53
- writeProtocolToFile("failure.smt2")
54
- rescue Errno::EPIPE
55
- writeProtocolToFile("failure.smt2")
56
- throw RuntimeError.new("Broken Pipe")
61
+ success = false
62
+ while (!success) do
63
+ begin
64
+ @solver.z3in.puts(line)
65
+ success = true
66
+ rescue Errno::EINVAL => e
67
+ progress.print_line("DEBUG: Invalid argument to puts(): #{line.inspect}")
68
+ progress.print_line("An Error occured: #{e.message}")
69
+ e.backtrace.each { |stline| progress.print_line(stline) }
70
+ progress.print_line("=> Switching Solver")
71
+ switch_solver(progress)
72
+ progress.print_line("=> Switch successful. Trying again.")
73
+ rescue Errno::EPIPE => e
74
+ progress.print_line("An Error occured: #{e.message}")
75
+ e.backtrace.each { |stline| progress.print_line(stline) }
76
+ progress.print_line("=> Switching Solver")
77
+ switch_solver(progress)
78
+ progress.print_line("=> Switch successful. Trying again.")
79
+ end
57
80
  end
81
+ @protocol.puts(line)
58
82
  end
59
83
 
60
84
  def checkLineForSolverError(line, progress)
@@ -65,14 +89,14 @@ class SolverSession
65
89
 
66
90
 
67
91
  def from_solver(progress)
68
- result = @z3outAndErr.gets
92
+ result = @solver.z3outAndErr.gets(chomp: true)
69
93
  checkLineForSolverError(result, progress)
70
94
  @protocol.puts("; Solver Response: #{result}")
71
95
  return result
72
96
  end
73
97
 
74
98
  def replace_strings(line)
75
- line.gsub(/"([^"\\]|\\.)*"/, "")
99
+ line.gsub(/"[^"]*"/, "")
76
100
  end
77
101
 
78
102
  def read_multiline_from_solver()
@@ -80,21 +104,23 @@ class SolverSession
80
104
  for_counting = ""
81
105
  @protocol.puts
82
106
  @protocol.puts("; BEGIN Multiline Solver Response:")
83
- line = @z3outAndErr.gets
107
+ line = @solver.z3outAndErr.gets(chomp: true)
84
108
  result += line
85
- for_counting += replace_strings(line)
109
+ for_counting = replace_strings(line)
86
110
  @protocol.puts("; #{line}")
87
111
 
88
112
  num_opening = for_counting.count("(")
89
113
  num_closing = for_counting.count(")")
90
- while num_opening > num_closing do
91
- line = @z3outAndErr.gets
92
- result += line
93
- for_counting += replace_strings(line)
94
- @protocol.puts("; #{line}")
95
-
96
- num_opening = for_counting.count("(")
97
- num_closing = for_counting.count(")")
114
+ while num_opening > num_closing && line != nil do
115
+ line = @solver.z3outAndErr.gets(chomp: true)
116
+ if line
117
+ result += line
118
+ for_counting = replace_strings(line)
119
+ @protocol.puts("; #{line}")
120
+
121
+ num_opening += for_counting.count("(")
122
+ num_closing += for_counting.count(")")
123
+ end
98
124
  end
99
125
  @protocol.puts("; END Multiline Solver Response")
100
126
  return result
@@ -133,150 +159,103 @@ class SolverSession
133
159
  def get_const_info(name)
134
160
  @consts[name]
135
161
  end
162
+
163
+ def parse_unsat_core(unsat_core)
164
+ @solver.parse_unsat_core(unsat_core, self)
165
+ end
166
+
167
+ def handleSolverError(progress, options, result)
168
+ @solver.handleSolverError(progress, self, options, result)
169
+ end
136
170
  end
137
171
 
138
- class Z3Solver
172
+ class SolverFactory
173
+
174
+ attr_reader :z3path, :z3version
139
175
 
140
176
  def initialize(progress, z3path)
141
- z3version = `#{z3path} --version`.chomp()
142
- if $?.exitstatus == 0 then
177
+ @progress = progress
178
+ z3version = nil
179
+ exitstatus = nil
180
+ begin
181
+ z3version = `#{z3path} --version`.chomp()
182
+ exitstatus = $?.exitstatus
183
+ rescue Errno::ENOENT
184
+ end
185
+ if exitstatus == 0 && z3version != nil then
143
186
  progress.print_line "INFO: Using #{z3version} found in z3path=#{z3path}"
144
- @z3in, @z3outAndErr, @z3thr = Open3.popen2e(z3path, '-in', '-smt2')
145
- Process.detach(@z3thr.pid)
187
+ @z3path = z3path
188
+ @z3version = z3version
146
189
  else
147
190
  progress.print_line "WARN: could not find z3 in z3path=#{z3path.inspect}"
148
191
  `which z3`
149
192
  if $?.exitstatus == 0 then
150
193
  z3version = `z3 --version`.chomp()
151
194
  progress.print_line "However, #{z3version} is availlable on the PATH. Using this one."
152
- @z3in, @z3outAndErr, @z3thr = Open3.popen2e('z3', '-in', '-smt2')
153
- Process.detach(@z3thr.pid)
195
+ @z3path = 'z3'
196
+ @z3version = z3version
154
197
  else
155
- raise RuntimeError.new("ERROR: unable to find Z3. Exiting! (Please provide its location in the config file using the key :z3path)")
198
+ raise RuntimeError.new("ERROR: unable to find Z3 solver. Exiting! (Please provide its location in the config file using the key :z3path)")
156
199
  end
157
200
  end
158
- @z3in.sync = true
159
-
160
- @sessionIdCounter = 0
161
201
  end
162
202
 
163
- def close()
164
- @z3in.close()
165
- @z3outAndErr.close()
166
- Thread.kill(@z3thr)
203
+ def new_solver()
204
+ Z3Solver.new(@progress, @z3path)
167
205
  end
168
206
 
169
- def query(progress, options, &block)
207
+ def query(options, &block)
170
208
  unless options[:solverLog]
171
209
  raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
172
210
  end
173
- progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
174
- solver = SolverSession.new(@z3in, @z3outAndErr, options[:solverLog])
211
+ @progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
175
212
 
176
- block.call(solver)
213
+ session = SolverSession.new(self, options[:solverLog])
214
+ block.call(session)
177
215
 
178
- solver.to_solver(progress, "(check-sat)")
179
- result = solver.from_solver(progress)
216
+ session.to_solver(@progress, "(check-sat)")
217
+ result = session.from_solver(@progress)
180
218
  explanation = true
181
- if result.chomp == "sat"
219
+ if result == "sat"
182
220
  # model is not contradictory -> continue
183
- elsif result.chomp == "unsat"
221
+ elsif result == "unsat"
184
222
  # model is contradictory -> add a message
185
223
 
186
- solver.to_solver(progress, "(get-unsat-core)")
187
- unsat_core = solver.from_solver(progress)
188
- explanation = parse_unsat_core(solver, unsat_core)
224
+ session.to_solver(@progress, "(get-unsat-core)")
225
+ unsat_core = session.from_solver(@progress)
226
+ explanation = session.parse_unsat_core(unsat_core)
189
227
  else
190
- handleSolverError(progress, solver, options, result)
228
+ session.handleSolverError(@progress, options, result)
191
229
  end
192
230
 
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
231
  return explanation
197
232
  end
198
233
 
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
252
- end
253
-
254
234
  # main entry point!
255
- def query_model(progress, options, &block)
235
+ def query_model(options, &block)
256
236
  unless options[:solverLog]
257
237
  raise RuntimeError.new("No solverLog specified, but the option :solverLog is required!")
258
238
  end
259
- progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
260
- solver = SolverSession.new(@z3in, @z3outAndErr, options[:solverLog])
239
+ @progress.print_line("calling solver (solver-log is written to #{options[:solverLog]})")
240
+ session = SolverSession.new(self, options[:solverLog])
261
241
 
262
- block.call(solver)
263
-
264
- solver.to_solver(progress, "(check-sat)")
265
- result = solver.from_solver(progress)
242
+ block.call(session)
243
+ session.to_solver(@progress, "(check-sat)")
244
+ result = session.from_solver(@progress)
266
245
  if result.chomp == "sat"
267
246
  # model is not contradictory -> extract model
268
247
  const_list = []
269
248
  const_info = {}
270
- solver.for_each_const do |name, info|
249
+ session.for_each_const do |name, info|
271
250
  const_list << name
272
251
  const_info[name] = info
273
252
  end
274
- solver.to_solver(progress, "(get-value (#{const_list.join(" ")}))")
275
- model_str = solver.read_multiline_from_solver()
253
+ session.to_solver(@progress, "(get-value (#{const_list.join(" ")}))")
254
+ model_str = session.read_multiline_from_solver()
276
255
  model = {}
277
- foreach_field_in_model_str(model_str, solver, const_info) do |varname, value, info|
256
+ Z3Solver.foreach_field_in_model_str(model_str, const_info) do |varname, value, info|
278
257
  unless info then
279
- progress.print_line "WARN: no info availlable for varname=#{varname.inspect}, value=#{value.inspect}"
258
+ @progress.print_line "WARN: no info availlable for varname=#{varname.inspect}, value=#{value.inspect}"
280
259
  end
281
260
  xpath = info[:xpath]
282
261
  if model.has_key?(xpath) then
@@ -285,7 +264,7 @@ class Z3Solver
285
264
  raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a field, not a #{model[xpath][:type]}.")
286
265
  end
287
266
  unless value == "true" or value == "false" then
288
- raise RuntimeError.new("inconsistent model value filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
267
+ raise RuntimeError.new("inconsistent model value: filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
289
268
  end
290
269
  if value == "true" then
291
270
  model[xpath][:filled] = true
@@ -314,7 +293,7 @@ class Z3Solver
314
293
  end
315
294
  elsif varname.end_with?("-size") then
316
295
  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]}.")
296
+ raise RuntimeError.new("inconsistent model: xpath #{xpath} should be a list or a field, not a #{model[xpath][:type].inspect}.")
318
297
  end
319
298
  if model[xpath][:type] == :field then
320
299
  model[xpath][:type] = :list
@@ -329,7 +308,7 @@ class Z3Solver
329
308
  else
330
309
  if varname.end_with?("-filled") then
331
310
  unless value == "true" or value == "false" then
332
- raise RuntimeError.new("inconsistent model value filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
311
+ raise RuntimeError.new("inconsistent model value: filled-value of #{xpath} should be a Bool, but was #{value.inspect}.")
333
312
  end
334
313
  if value == "true" then
335
314
  model[xpath] = { :type => :field, :filled => true }
@@ -341,7 +320,7 @@ class Z3Solver
341
320
  model[xpath] = { :type => :field, :value => value }
342
321
  elsif varname.start_with?("struct-") && varname.end_with?("-exists") then
343
322
  unless value == "true" or value == "false" then
344
- raise RuntimeError.new("inconsistent model value exists-value of struct #{xpath} should be a Bool, but was #{value.inspect}.")
323
+ raise RuntimeError.new("inconsistent model value: exists-value of struct #{xpath} should be a Bool, but was #{value.inspect}.")
345
324
  end
346
325
  model[xpath] = { :type => :struct, :exists => value }
347
326
  elsif varname.end_with?("-size") then
@@ -353,21 +332,27 @@ class Z3Solver
353
332
  elsif result.chomp == "unsat"
354
333
  # model is contradictory -> add a message
355
334
 
356
- solver.to_solver(progress, "(get-unsat-core)")
357
- unsat_core = solver.from_solver(progress)
358
- solver.writeProtocolToFile(progress, options[:solverLog])
359
- progress.print_line "Solver reported a contradiction! please see #{options[:solverLog]} for further information."
335
+ session.to_solver(@progress, "(get-unsat-core)")
336
+ unsat_core = session.from_solver(@progress)
337
+ session.writeProtocolToFile(@progress, options[:solverLog])
338
+ @progress.print_line "Solver reported a contradiction! please see #{options[:solverLog]} for further information."
360
339
  return nil
361
340
  else
362
- handleSolverError(progress, solver, options, result)
341
+ session.handleSolverError(@progress, options, result)
363
342
  end
364
343
 
365
- messages = []
366
- solver.to_solver "(reset)"
367
- solver.to_solver "(set-logic ALL)"
368
- solver.to_solver "(set-option :produce-unsat-cores true)"
344
+ return nil
345
+ end
369
346
 
370
- return messages
347
+ def convert_solver_value(type, datatype, value, xpath)
348
+ check_type(type, value, xpath)
349
+ if datatype == :date then
350
+ return (Time.at(0).to_date + Integer(value)).to_s
351
+ elsif datatype == :timestamp then
352
+ return Time.at(Integer(value)).to_s
353
+ else
354
+ return value
355
+ end
371
356
  end
372
357
 
373
358
  def check_type(type, value, xpath)
@@ -388,24 +373,119 @@ class Z3Solver
388
373
  end
389
374
  end
390
375
 
391
- def convert_solver_value(type, datatype, value, xpath)
392
- check_type(type, value, xpath)
393
- if datatype == :date then
394
- return (Time.at(0).to_date + Integer(value)).to_s
395
- elsif datatype == :timestamp then
396
- return Time.at(Integer(value)).to_s
376
+ end
377
+
378
+ class Z3Solver
379
+
380
+ attr_reader :z3in, :z3outAndErr
381
+
382
+ def initialize(progress, z3path)
383
+ @progress = progress
384
+ @z3in, @z3outAndErr, @z3thr = Open3.popen2e(z3path, '-in', '-smt2')
385
+ Process.detach(@z3thr.pid)
386
+ @z3in.sync = true
387
+ @sessionIdCounter = 0
388
+ end
389
+
390
+ def close()
391
+ begin
392
+ @z3in.close() unless @z3in.closed?
393
+ rescue Errno::EINVAL
394
+ end
395
+ begin
396
+ @z3outAndErr.close() unless @z3outAndErr.closed?
397
+ rescue Errno::EINVAL
398
+ end
399
+ Thread.kill(@z3thr)
400
+ end
401
+
402
+ def display_assertion(assertion, session)
403
+ if assertion =~ /validation_rule_(.*)_instance_(.*)/ then
404
+ return "Validation rule #{$1} of structure #{$2.gsub("_", "/")}"
397
405
  else
398
- return value
406
+ info = session.getAssociationForAssertionID(assertion)
407
+ if info then
408
+ if info[:assertion_type] == :doc_field_filled then
409
+ return "Field #{info[:xpath]} contains value #{info[:value].inspect} in document"
410
+ elsif info[:assertion_type] == :doc_field_empty then
411
+ return "Field #{info[:xpath]} is empty in document"
412
+ elsif info[:assertion_type] == :doc_structure_exists then
413
+ return "Structure #{info[:xpath]} exists in document"
414
+ elsif info[:assertion_type] == :doc_structure_not_exists then
415
+ return "Structure #{info[:xpath]} does not exist in document"
416
+ elsif info[:assertion_type] == :constraint then
417
+ if info[:constraint_type] == :min then
418
+ return "Field #{info[:xpath]} has a minimum value of #{info[:value]}."
419
+ elsif info[:constraint_type] == :max then
420
+ return "Field #{info[:xpath]} has a maximum value of #{info[:value]}."
421
+ elsif info[:constraint_type] == :required then
422
+ return "Field #{info[:xpath]} is required."
423
+ else
424
+ p info
425
+ raise RuntimeError.new("encountered unknown :constraint_type #{info[:constraint_type].inspect} for assertionID #{assertion.inspect}")
426
+ end
427
+ else
428
+ raise RuntimeError.new("encountered unknown :assertion_type #{info[:assertion_type].inspect} for assertionID #{assertion.inspect}")
429
+ end
430
+ else
431
+ return assertion
432
+ end
433
+ end
434
+ end
435
+
436
+ def parse_unsat_core(unsat_core, session)
437
+ unless unsat_core =~ /\((.*)\)/ then
438
+ raise RuntimeError.new("could not parse unsat core: #{unsat_core.inspect}")
439
+ end
440
+ list = $1.split(" ")
441
+
442
+ result = "EXPLANATION:\n"
443
+ result += "The following values\n"
444
+
445
+ list.filter{|a| a.start_with?("doc-")}.each do |assertion|
446
+ result += "- #{display_assertion(assertion, session)} (#{assertion})\n"
447
+ end
448
+ result += "\nviolate the following constraints\n"
449
+ list.filter{|a| !a.start_with?("doc-")}.each do |assertion|
450
+ result += "- #{display_assertion(assertion, session)} (#{assertion})\n"
451
+ end
452
+ return result
453
+ end
454
+
455
+ def self.find_matching_bracket(str)
456
+ i = 0
457
+ open = 0
458
+ in_str = false
459
+ while open <= 0 do
460
+ if str[i] == '(' and !in_str then
461
+ open += 1
462
+ elsif str[i] == '"' then
463
+ in_str = !in_str
464
+ end
465
+ i += 1
466
+ end
467
+ while open > 0 do
468
+ if str[i] == '(' && !in_str then
469
+ open += 1
470
+ elsif str[i] == ')' && !in_str then
471
+ open -= 1
472
+ elsif str[i] == '"' then
473
+ in_str = !in_str
474
+ end
475
+ i += 1
399
476
  end
477
+ str[0,i]
400
478
  end
401
479
 
402
- def foreach_field_in_model_str(str, solver, const_info, &block)
480
+ def self.foreach_field_in_model_str(str, const_info, &block)
403
481
  unless str =~ /^\((.*)\)$/m
404
482
  raise RuntimeError.new("could not parse model: #{str.inspect}")
405
483
  end
406
484
  str = $1
407
485
 
408
- str.split("\n").each do |line|
486
+ while str.size() > 0 do
487
+ line = Z3Solver.find_matching_bracket(str)
488
+ str = str[line.size()..]
409
489
  unless line =~ /\(([^\ ]*)\ (.*)\)/
410
490
  raise RuntimeError.new("could not parse model line: #{line.inspect}")
411
491
  end
@@ -423,8 +503,8 @@ class Z3Solver
423
503
  return nil
424
504
  end
425
505
 
426
- def handleSolverError(progress, solver, options, result)
427
- solver.writeProtocolToFile(progress, options[:solverLog])
506
+ def handleSolverError(progress, session, options, result)
507
+ session.writeProtocolToFile(progress, options[:solverLog])
428
508
  raise RuntimeError.new("Solver Error: Solver returned '#{result}', for more information see #{options[:solverLog]}")
429
509
  end
430
510
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbt-gen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - FM-enthusiast
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-18 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: nokogiri
@@ -41,6 +42,7 @@ licenses:
41
42
  metadata:
42
43
  bug_tracker_uri: https://codeberg.org/FM-enthusiast/MBT-gen/issues
43
44
  source_code_uri: https://codeberg.org/FM-enthusiast/MBT-gen
45
+ post_install_message:
44
46
  rdoc_options: []
45
47
  require_paths:
46
48
  - lib
@@ -56,7 +58,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
58
  version: '0'
57
59
  requirements:
58
60
  - an SMT-solver. Z3 is recommended, it can be obtained from https://github.com/Z3Prover/z3
59
- rubygems_version: 3.6.7
61
+ rubygems_version: 3.5.22
62
+ signing_key:
60
63
  specification_version: 4
61
64
  summary: test data generator for Model-based testing
62
65
  test_files: []