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.
- checksums.yaml +4 -4
- data/lib/mbt-gen.rb +147 -129
- data/lib/progress.rb +47 -5
- data/lib/solver-lib.rb +228 -148
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b69ab00ef224ac052d2f5897cce4c620c72a3630f9e6da9eaabb7cc7277904f
|
|
4
|
+
data.tar.gz: ad905f43225a8680663ec4068a2acd65845090e238da9c8b902008e9c118e1e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1455
|
+
progress.set_max(100000000000000)
|
|
1445
1456
|
else
|
|
1446
|
-
progress
|
|
1457
|
+
progress.set_max(number_of_combinations)
|
|
1447
1458
|
end
|
|
1448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
|
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(
|
|
9
|
-
@
|
|
10
|
-
@
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
172
|
+
class SolverFactory
|
|
173
|
+
|
|
174
|
+
attr_reader :z3path, :z3version
|
|
139
175
|
|
|
140
176
|
def initialize(progress, z3path)
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
@
|
|
145
|
-
|
|
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
|
-
@
|
|
153
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
@z3outAndErr.close()
|
|
166
|
-
Thread.kill(@z3thr)
|
|
203
|
+
def new_solver()
|
|
204
|
+
Z3Solver.new(@progress, @z3path)
|
|
167
205
|
end
|
|
168
206
|
|
|
169
|
-
def query(
|
|
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
|
-
|
|
213
|
+
session = SolverSession.new(self, options[:solverLog])
|
|
214
|
+
block.call(session)
|
|
177
215
|
|
|
178
|
-
|
|
179
|
-
result =
|
|
216
|
+
session.to_solver(@progress, "(check-sat)")
|
|
217
|
+
result = session.from_solver(@progress)
|
|
180
218
|
explanation = true
|
|
181
|
-
if result
|
|
219
|
+
if result == "sat"
|
|
182
220
|
# model is not contradictory -> continue
|
|
183
|
-
elsif result
|
|
221
|
+
elsif result == "unsat"
|
|
184
222
|
# model is contradictory -> add a message
|
|
185
223
|
|
|
186
|
-
|
|
187
|
-
unsat_core =
|
|
188
|
-
explanation = parse_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,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
249
|
+
session.for_each_const do |name, info|
|
|
271
250
|
const_list << name
|
|
272
251
|
const_info[name] = info
|
|
273
252
|
end
|
|
274
|
-
|
|
275
|
-
model_str =
|
|
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,
|
|
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
|
-
|
|
357
|
-
unsat_core =
|
|
358
|
-
|
|
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,
|
|
341
|
+
session.handleSolverError(@progress, options, result)
|
|
363
342
|
end
|
|
364
343
|
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
427
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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: []
|