factbase 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.0pdd.yml +1 -1
- data/.github/workflows/actionlint.yml +1 -1
- data/.github/workflows/benchmark.yml +64 -0
- data/.github/workflows/codecov.yml +4 -2
- data/.github/workflows/copyrights.yml +2 -2
- data/.github/workflows/markdown-lint.yml +1 -1
- data/.github/workflows/pdd.yml +1 -1
- data/.github/workflows/rake.yml +2 -2
- data/.github/workflows/xcop.yml +1 -1
- data/.github/workflows/yamllint.yml +1 -1
- data/.gitignore +1 -1
- data/.rubocop.yml +6 -1
- data/.rultor.yml +2 -2
- data/.simplecov +1 -1
- data/.yamllint.yml +9 -4
- data/Gemfile +9 -7
- data/Gemfile.lock +99 -78
- data/LICENSE.txt +1 -1
- data/README.md +33 -0
- data/Rakefile +6 -1
- data/benchmarks/simple.rb +96 -0
- data/factbase.gemspec +1 -1
- data/lib/factbase/accum.rb +3 -3
- data/lib/factbase/fact.rb +6 -4
- data/lib/factbase/flatten.rb +2 -2
- data/lib/factbase/inv.rb +2 -2
- data/lib/factbase/looged.rb +6 -5
- data/lib/factbase/pre.rb +15 -4
- data/lib/factbase/query.rb +16 -8
- data/lib/factbase/query_once.rb +71 -0
- data/lib/factbase/rules.rb +3 -3
- data/lib/factbase/syntax.rb +13 -8
- data/lib/factbase/tee.rb +2 -2
- data/lib/factbase/term.rb +33 -5
- data/lib/factbase/term_once.rb +84 -0
- data/lib/factbase/terms/aggregates.rb +4 -4
- data/lib/factbase/terms/aliases.rb +5 -5
- data/lib/factbase/terms/casting.rb +2 -2
- data/lib/factbase/terms/debug.rb +2 -2
- data/lib/factbase/terms/defn.rb +2 -2
- data/lib/factbase/terms/logical.rb +3 -3
- data/lib/factbase/terms/math.rb +2 -2
- data/lib/factbase/terms/meta.rb +2 -2
- data/lib/factbase/terms/ordering.rb +2 -2
- data/lib/factbase/terms/strings.rb +2 -2
- data/lib/factbase/terms/system.rb +2 -2
- data/lib/factbase/to_json.rb +2 -2
- data/lib/factbase/to_xml.rb +2 -2
- data/lib/factbase/to_yaml.rb +2 -2
- data/lib/factbase.rb +16 -6
- data/test/factbase/terms/test_aggregates.rb +6 -6
- data/test/factbase/terms/test_aliases.rb +6 -6
- data/test/factbase/terms/test_casting.rb +6 -6
- data/test/factbase/terms/test_debug.rb +53 -0
- data/test/factbase/terms/test_defn.rb +15 -15
- data/test/factbase/terms/test_logical.rb +15 -13
- data/test/factbase/terms/test_math.rb +42 -42
- data/test/factbase/terms/test_meta.rb +87 -0
- data/test/factbase/terms/test_ordering.rb +45 -0
- data/test/factbase/terms/test_strings.rb +8 -8
- data/test/factbase/terms/test_system.rb +6 -6
- data/test/factbase/test_accum.rb +17 -8
- data/test/factbase/test_fact.rb +22 -22
- data/test/factbase/test_flatten.rb +6 -6
- data/test/factbase/test_inv.rb +3 -3
- data/test/factbase/test_looged.rb +13 -13
- data/test/factbase/test_pre.rb +16 -2
- data/test/factbase/test_query.rb +17 -17
- data/test/factbase/test_rules.rb +11 -8
- data/test/factbase/test_syntax.rb +29 -9
- data/test/factbase/test_tee.rb +18 -9
- data/test/factbase/test_term.rb +18 -18
- data/test/factbase/test_to_json.rb +4 -4
- data/test/factbase/test_to_xml.rb +12 -14
- data/test/factbase/test_to_yaml.rb +3 -3
- data/test/test__helper.rb +2 -2
- data/test/test_factbase.rb +198 -18
- metadata +12 -5
data/test/factbase/test_term.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Yegor Bugayenko
|
3
|
+
# Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
#
|
5
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -25,34 +25,34 @@ require_relative '../../lib/factbase/term'
|
|
25
25
|
|
26
26
|
# Term test.
|
27
27
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
28
|
-
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
28
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
29
29
|
# License:: MIT
|
30
30
|
class TestTerm < Minitest::Test
|
31
31
|
def test_false_matching
|
32
|
-
t = Factbase::Term.new(:never, [])
|
33
|
-
|
32
|
+
t = Factbase::Term.new(Factbase.new, :never, [])
|
33
|
+
refute(t.evaluate(fact('foo' => [100]), []))
|
34
34
|
end
|
35
35
|
|
36
36
|
def test_size_matching
|
37
|
-
t = Factbase::Term.new(:size, [:foo])
|
37
|
+
t = Factbase::Term.new(Factbase.new, :size, [:foo])
|
38
38
|
assert_equal(3, t.evaluate(fact('foo' => [42, 12, -90]), []))
|
39
39
|
assert_equal(0, t.evaluate(fact('bar' => 100), []))
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_exists_matching
|
43
|
-
t = Factbase::Term.new(:exists, [:foo])
|
43
|
+
t = Factbase::Term.new(Factbase.new, :exists, [:foo])
|
44
44
|
assert(t.evaluate(fact('foo' => [42, 12, -90]), []))
|
45
|
-
|
45
|
+
refute(t.evaluate(fact('bar' => 100), []))
|
46
46
|
end
|
47
47
|
|
48
48
|
def test_absent_matching
|
49
|
-
t = Factbase::Term.new(:absent, [:foo])
|
49
|
+
t = Factbase::Term.new(Factbase.new, :absent, [:foo])
|
50
50
|
assert(t.evaluate(fact('z' => [42, 12, -90]), []))
|
51
|
-
|
51
|
+
refute(t.evaluate(fact('foo' => 100), []))
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_type_matching
|
55
|
-
t = Factbase::Term.new(:type, [:foo])
|
55
|
+
t = Factbase::Term.new(Factbase.new, :type, [:foo])
|
56
56
|
assert_equal('Integer', t.evaluate(fact('foo' => 42), []))
|
57
57
|
assert_equal('Integer', t.evaluate(fact('foo' => [42]), []))
|
58
58
|
assert_equal('Array', t.evaluate(fact('foo' => [1, 2, 3]), []))
|
@@ -64,30 +64,30 @@ class TestTerm < Minitest::Test
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def test_past
|
67
|
-
t = Factbase::Term.new(:prev, [:foo])
|
67
|
+
t = Factbase::Term.new(Factbase.new, :prev, [:foo])
|
68
68
|
assert_nil(t.evaluate(fact('foo' => 4), []))
|
69
69
|
assert_equal([4], t.evaluate(fact('foo' => 5), []))
|
70
70
|
end
|
71
71
|
|
72
72
|
def test_at
|
73
|
-
t = Factbase::Term.new(:at, [1, :foo])
|
73
|
+
t = Factbase::Term.new(Factbase.new, :at, [1, :foo])
|
74
74
|
assert_nil(t.evaluate(fact('foo' => 4), []))
|
75
75
|
assert_equal(5, t.evaluate(fact('foo' => [4, 5]), []))
|
76
76
|
end
|
77
77
|
|
78
78
|
def test_report_missing_term
|
79
|
-
t = Factbase::Term.new(:something, [])
|
80
|
-
msg = assert_raises do
|
79
|
+
t = Factbase::Term.new(Factbase.new, :something, [])
|
80
|
+
msg = assert_raises(StandardError) do
|
81
81
|
t.evaluate(fact, [])
|
82
82
|
end.message
|
83
|
-
|
83
|
+
assert_includes(msg, 'not defined at (something)', msg)
|
84
84
|
end
|
85
85
|
|
86
86
|
def test_report_other_error
|
87
|
-
t = Factbase::Term.new(:at, [])
|
88
|
-
msg = assert_raises do
|
87
|
+
t = Factbase::Term.new(Factbase.new, :at, [])
|
88
|
+
msg = assert_raises(StandardError) do
|
89
89
|
t.evaluate(fact, [])
|
90
90
|
end.message
|
91
|
-
|
91
|
+
assert_includes(msg, 'at (at)', msg)
|
92
92
|
end
|
93
93
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Yegor Bugayenko
|
3
|
+
# Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
#
|
5
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -27,7 +27,7 @@ require_relative '../../lib/factbase/to_json'
|
|
27
27
|
|
28
28
|
# Test.
|
29
29
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
30
|
-
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
31
31
|
# License:: MIT
|
32
32
|
class TestToJSON < Minitest::Test
|
33
33
|
def test_simple_rendering
|
@@ -37,7 +37,7 @@ class TestToJSON < Minitest::Test
|
|
37
37
|
f.foo = 256
|
38
38
|
to = Factbase::ToJSON.new(fb)
|
39
39
|
json = JSON.parse(to.json)
|
40
|
-
|
40
|
+
assert_equal(256, json[0]['foo'][1])
|
41
41
|
end
|
42
42
|
|
43
43
|
def test_sort_keys
|
@@ -47,6 +47,6 @@ class TestToJSON < Minitest::Test
|
|
47
47
|
f.b = 1
|
48
48
|
f.a = 256
|
49
49
|
json = Factbase::ToJSON.new(fb).json
|
50
|
-
|
50
|
+
assert_includes(json, '{"a":256,"b":1,"c":42}', json)
|
51
51
|
end
|
52
52
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Yegor Bugayenko
|
3
|
+
# Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
#
|
5
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -27,7 +27,7 @@ require_relative '../../lib/factbase/to_xml'
|
|
27
27
|
|
28
28
|
# Test.
|
29
29
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
30
|
-
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
31
31
|
# License:: MIT
|
32
32
|
class TestToXML < Minitest::Test
|
33
33
|
def test_simple_rendering
|
@@ -35,11 +35,9 @@ class TestToXML < Minitest::Test
|
|
35
35
|
fb.insert.t = Time.now
|
36
36
|
to = Factbase::ToXML.new(fb)
|
37
37
|
xml = Nokogiri::XML.parse(to.xml)
|
38
|
-
|
39
|
-
|
40
|
-
xml.xpath('/fb/f/t/text()').text
|
41
|
-
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/
|
42
|
-
)
|
38
|
+
refute_empty(xml.xpath('/fb/f[t]'))
|
39
|
+
assert_match(
|
40
|
+
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, xml.xpath('/fb/f/t/text()').text
|
43
41
|
)
|
44
42
|
end
|
45
43
|
|
@@ -48,7 +46,7 @@ class TestToXML < Minitest::Test
|
|
48
46
|
fb.insert.t = "\uffff < > & ' \""
|
49
47
|
to = Factbase::ToXML.new(fb)
|
50
48
|
xml = Nokogiri::XML.parse(to.xml)
|
51
|
-
|
49
|
+
refute_empty(xml.xpath('/fb/f[t]'))
|
52
50
|
end
|
53
51
|
|
54
52
|
def test_meta_data_presence
|
@@ -56,8 +54,8 @@ class TestToXML < Minitest::Test
|
|
56
54
|
fb.insert.x = 42
|
57
55
|
to = Factbase::ToXML.new(fb)
|
58
56
|
xml = Nokogiri::XML.parse(to.xml)
|
59
|
-
|
60
|
-
|
57
|
+
refute_empty(xml.xpath('/fb[@version]'))
|
58
|
+
refute_empty(xml.xpath('/fb[@size]'))
|
61
59
|
end
|
62
60
|
|
63
61
|
def test_to_xml_with_short_names
|
@@ -67,8 +65,8 @@ class TestToXML < Minitest::Test
|
|
67
65
|
f.f = 2
|
68
66
|
to = Factbase::ToXML.new(fb)
|
69
67
|
xml = Nokogiri::XML.parse(to.xml)
|
70
|
-
|
71
|
-
|
68
|
+
refute_empty(xml.xpath('/fb/f/type'))
|
69
|
+
refute_empty(xml.xpath('/fb/f/f'))
|
72
70
|
end
|
73
71
|
|
74
72
|
def test_show_types_as_attributes
|
@@ -89,7 +87,7 @@ class TestToXML < Minitest::Test
|
|
89
87
|
'/fb/f/d[@t="T"]',
|
90
88
|
'/fb/f/e/v[@t="S"]',
|
91
89
|
'/fb/f/e/v[@t="I"]'
|
92
|
-
].each { |x|
|
90
|
+
].each { |x| refute_empty(xml.xpath(x), out) }
|
93
91
|
end
|
94
92
|
|
95
93
|
def test_sorts_keys
|
@@ -102,7 +100,7 @@ class TestToXML < Minitest::Test
|
|
102
100
|
to = Factbase::ToXML.new(fb)
|
103
101
|
xml = Nokogiri::XML.parse(to.xml)
|
104
102
|
%w[a c t x].each_with_index do |e, i|
|
105
|
-
|
103
|
+
refute_empty(xml.xpath("/fb/f/#{e}[count(preceding-sibling::*) = #{i}]"), e)
|
106
104
|
end
|
107
105
|
end
|
108
106
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Yegor Bugayenko
|
3
|
+
# Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
#
|
5
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -27,7 +27,7 @@ require_relative '../../lib/factbase/to_yaml'
|
|
27
27
|
|
28
28
|
# Test.
|
29
29
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
30
|
-
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
30
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
31
31
|
# License:: MIT
|
32
32
|
class TestToYAML < Minitest::Test
|
33
33
|
def test_simple_rendering
|
@@ -51,6 +51,6 @@ class TestToYAML < Minitest::Test
|
|
51
51
|
f.a = 256
|
52
52
|
f.c = 10
|
53
53
|
yaml = Factbase::ToYAML.new(fb).yaml
|
54
|
-
|
54
|
+
assert_includes(yaml, "a: 256\n b: 42\n c: 10", yaml)
|
55
55
|
end
|
56
56
|
end
|
data/test/test__helper.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Yegor Bugayenko
|
3
|
+
# Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
#
|
5
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -37,6 +37,6 @@ Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|
|
37
37
|
class Minitest::Test
|
38
38
|
def fact(map = {})
|
39
39
|
require 'factbase/fact'
|
40
|
-
Factbase::Fact.new(Mutex.new, map)
|
40
|
+
Factbase::Fact.new(Factbase.new, Mutex.new, map)
|
41
41
|
end
|
42
42
|
end
|
data/test/test_factbase.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Yegor Bugayenko
|
3
|
+
# Copyright (c) 2024-2025 Yegor Bugayenko
|
4
4
|
#
|
5
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -22,6 +22,7 @@
|
|
22
22
|
|
23
23
|
require 'minitest/autorun'
|
24
24
|
require 'loog'
|
25
|
+
require 'threads'
|
25
26
|
require_relative '../lib/factbase'
|
26
27
|
require_relative '../lib/factbase/rules'
|
27
28
|
require_relative '../lib/factbase/inv'
|
@@ -30,7 +31,7 @@ require_relative '../lib/factbase/looged'
|
|
30
31
|
|
31
32
|
# Factbase main module test.
|
32
33
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
33
|
-
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
34
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
34
35
|
# License:: MIT
|
35
36
|
class TestFactbase < Minitest::Test
|
36
37
|
def test_injects_data_correctly
|
@@ -48,13 +49,22 @@ class TestFactbase < Minitest::Test
|
|
48
49
|
assert_equal([2, 3], maps[1]['bar'])
|
49
50
|
end
|
50
51
|
|
52
|
+
def test_query_many_times
|
53
|
+
fb = Factbase.new
|
54
|
+
total = 5
|
55
|
+
total.times { fb.insert }
|
56
|
+
total.times do
|
57
|
+
assert_equal(5, fb.query('(always)').each.to_a.size)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
51
61
|
def test_simple_setting
|
52
62
|
fb = Factbase.new
|
53
63
|
fb.insert
|
54
64
|
fb.insert.bar = 88
|
55
65
|
found = 0
|
56
66
|
fb.query('(exists bar)').each do |f|
|
57
|
-
|
67
|
+
assert_predicate(f.bar, :positive?)
|
58
68
|
f.foo = 42
|
59
69
|
assert_equal(42, f.foo)
|
60
70
|
found += 1
|
@@ -87,17 +97,17 @@ class TestFactbase < Minitest::Test
|
|
87
97
|
File.binwrite(f.path, f1.export)
|
88
98
|
f2.import(File.binread(f.path))
|
89
99
|
end
|
90
|
-
assert_equal(1, f2.query('(eq foo 42)').each.to_a.
|
100
|
+
assert_equal(1, f2.query('(eq foo 42)').each.to_a.size)
|
91
101
|
end
|
92
102
|
|
93
103
|
def test_reads_from_empty_file
|
94
104
|
fb = Factbase.new
|
95
105
|
Tempfile.open do |f|
|
96
106
|
File.binwrite(f.path, '')
|
97
|
-
|
98
|
-
assert_raises do
|
107
|
+
assert_includes(
|
108
|
+
assert_raises(StandardError) do
|
99
109
|
fb.import(File.binread(f.path))
|
100
|
-
end.message
|
110
|
+
end.message, 'cannot load a factbase'
|
101
111
|
)
|
102
112
|
end
|
103
113
|
end
|
@@ -121,10 +131,10 @@ class TestFactbase < Minitest::Test
|
|
121
131
|
|
122
132
|
def test_txn_returns_boolean
|
123
133
|
fb = Factbase.new
|
124
|
-
|
125
|
-
|
134
|
+
assert_kind_of(FalseClass, fb.txn { true })
|
135
|
+
assert_kind_of(TrueClass, fb.txn(&:insert))
|
126
136
|
assert(fb.txn { |fbt| fbt.insert.bar = 42 })
|
127
|
-
|
137
|
+
refute(fb.txn { |fbt| fbt.query('(always)').each.to_a })
|
128
138
|
assert(fb.txn { |fbt| fbt.query('(always)').each { |f| f.hello = 33 } })
|
129
139
|
assert(fb.txn { |fbt| fbt.query('(always)').each.to_a[0].zzz = 33 })
|
130
140
|
end
|
@@ -136,13 +146,13 @@ class TestFactbase < Minitest::Test
|
|
136
146
|
fbt.insert.z = 42
|
137
147
|
end
|
138
148
|
assert_equal(2, fb.size)
|
139
|
-
|
140
|
-
assert_raises do
|
149
|
+
assert_includes(
|
150
|
+
assert_raises(StandardError) do
|
141
151
|
fb.txn do |fbt|
|
142
152
|
fbt.insert.foo = 42
|
143
153
|
throw 'intentionally'
|
144
154
|
end
|
145
|
-
end.message
|
155
|
+
end.message, 'intentionally'
|
146
156
|
)
|
147
157
|
assert_equal(2, fb.size)
|
148
158
|
end
|
@@ -163,12 +173,12 @@ class TestFactbase < Minitest::Test
|
|
163
173
|
fb.insert.bar = 3
|
164
174
|
fb.insert.foo = 5
|
165
175
|
assert_equal(2, fb.size)
|
166
|
-
|
167
|
-
assert_raises do
|
176
|
+
assert_includes(
|
177
|
+
assert_raises(StandardError) do
|
168
178
|
fb.txn do |fbt|
|
169
179
|
fbt.insert.foo = 42
|
170
180
|
end
|
171
|
-
end.message
|
181
|
+
end.message, 'oops'
|
172
182
|
)
|
173
183
|
assert_equal(2, fb.size)
|
174
184
|
end
|
@@ -185,7 +195,7 @@ class TestFactbase < Minitest::Test
|
|
185
195
|
d.txn do |fbt|
|
186
196
|
fbt.insert.bar = 455
|
187
197
|
end
|
188
|
-
assert_raises do
|
198
|
+
assert_raises(StandardError) do
|
189
199
|
d.txn do |fbt|
|
190
200
|
fbt.insert
|
191
201
|
throw 'oops'
|
@@ -216,7 +226,177 @@ class TestFactbase < Minitest::Test
|
|
216
226
|
fbt.insert.bar = 33
|
217
227
|
raise Factbase::Rollback
|
218
228
|
end
|
219
|
-
|
229
|
+
refute(modified)
|
220
230
|
assert_equal(0, fb.query('(always)').each.to_a.size)
|
221
231
|
end
|
232
|
+
|
233
|
+
def test_concurrent_inserts
|
234
|
+
fb = Factbase.new
|
235
|
+
Threads.new(100).assert do
|
236
|
+
fact = fb.insert
|
237
|
+
fact.foo = 42
|
238
|
+
fact.bar = 49
|
239
|
+
fact.value = fact.foo * fact.bar
|
240
|
+
end
|
241
|
+
assert_equal(100, fb.size)
|
242
|
+
assert_equal(100, fb.query('(eq foo 42)').each.to_a.size)
|
243
|
+
assert_equal(100, fb.query('(eq bar 49)').each.to_a.size)
|
244
|
+
assert_equal(100, fb.query("(eq value #{42 * 49})").each.to_a.size)
|
245
|
+
end
|
246
|
+
|
247
|
+
def test_different_values_when_concurrent_inserts
|
248
|
+
fb = Factbase.new
|
249
|
+
Threads.new(100).assert do |i|
|
250
|
+
fb.insert.foo = i
|
251
|
+
end
|
252
|
+
assert_equal(100, fb.size)
|
253
|
+
Threads.new(100) do |i|
|
254
|
+
f = fb.query("(eq foo #{i})").each.to_a
|
255
|
+
assert_equal(1, f.count)
|
256
|
+
assert_equal(i, f.first.foo)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# @todo #98:1h I assumed that the test `test_different_properties_when_concurrent_inserts` would be passed.
|
261
|
+
# I see like this:
|
262
|
+
# ```
|
263
|
+
# [2024-08-22 21:14:53.962] ERROR -- Expected: 1
|
264
|
+
# Actual: 0: nil
|
265
|
+
# [2024-08-22 21:14:53.962] ERROR -- Expected: 1
|
266
|
+
# Actual: 0: nil
|
267
|
+
# test_different_properties_when_concurrent_inserts ERROR (0.01s)
|
268
|
+
# Minitest::UnexpectedError: RuntimeError: Only 0 out of 5 threads completed successfully
|
269
|
+
# /home/suban/.rbenv/versions/3.3.4/lib/ruby/gems/3.3.0/gems/threads-0.4.0/lib/threads.rb:73:in `assert'
|
270
|
+
# test/test_factbase.rb:265:in `test_different_properties_when_concurrent_inserts'
|
271
|
+
# ```
|
272
|
+
def test_different_properties_when_concurrent_inserts
|
273
|
+
skip('Does not work')
|
274
|
+
fb = Factbase.new
|
275
|
+
Threads.new(5).assert do |i|
|
276
|
+
fb.insert.send(:"prop_#{i}=", i)
|
277
|
+
end
|
278
|
+
assert_equal(5, fb.size)
|
279
|
+
Threads.new(5).assert do |i|
|
280
|
+
prop = "prop_#{i}"
|
281
|
+
f = fb.query("(eq #{prop} #{i})").each.to_a
|
282
|
+
assert_equal(1, f.count)
|
283
|
+
assert_equal(i, f.first.send(prop.to_sym))
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# @todo #98:1h I assumed that the test `test_concurrent_transactions_inserts` would be passed.
|
288
|
+
# I see like this:
|
289
|
+
# ```
|
290
|
+
# Expected: 100
|
291
|
+
# Actual: 99
|
292
|
+
# D:/a/factbase/factbase/test/test_factbase.rb:281:in `test_concurrent_transactions_inserts'
|
293
|
+
# ```
|
294
|
+
# See details here https://github.com/yegor256/factbase/actions/runs/10492255419/job/29068637032
|
295
|
+
def test_concurrent_transactions_inserts
|
296
|
+
skip('Does not work')
|
297
|
+
fb = Factbase.new
|
298
|
+
Threads.new(100).assert do |i|
|
299
|
+
fb.txn do |fbt|
|
300
|
+
fact = fbt.insert
|
301
|
+
fact.thread_id = i
|
302
|
+
end
|
303
|
+
end
|
304
|
+
assert_equal(100, fb.size)
|
305
|
+
assert_equal(100, fb.query('(exists thread_id)').each.to_a.size)
|
306
|
+
end
|
307
|
+
|
308
|
+
def test_concurrent_transactions_with_rollbacks
|
309
|
+
fb = Factbase.new
|
310
|
+
Threads.new(100).assert do |i|
|
311
|
+
fb.txn do |fbt|
|
312
|
+
fact = fbt.insert
|
313
|
+
fact.thread_id = i
|
314
|
+
raise Factbase::Rollback
|
315
|
+
end
|
316
|
+
end
|
317
|
+
assert_equal(0, fb.size)
|
318
|
+
end
|
319
|
+
|
320
|
+
def test_concurrent_transactions_successful
|
321
|
+
fb = Factbase.new
|
322
|
+
Threads.new(100).assert do |i|
|
323
|
+
fb.txn do |fbt|
|
324
|
+
fact = fbt.insert
|
325
|
+
fact.thread_id = i
|
326
|
+
fact.value = i * 10
|
327
|
+
end
|
328
|
+
end
|
329
|
+
facts = fb.query('(exists thread_id)').each.to_a
|
330
|
+
assert_equal(100, facts.size)
|
331
|
+
facts.each do |fact|
|
332
|
+
assert_equal(fact.value, fact.thread_id * 10)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# @todo #98:1h I assumed that the test `test_concurrent_queries` would be passed.
|
337
|
+
# I see like this:
|
338
|
+
# ```
|
339
|
+
# [2024-08-22 17:40:19.224] ERROR -- Expected: [0, 1]
|
340
|
+
# Actual: [0, 0]: nil
|
341
|
+
# [2024-08-22 17:40:19.224] ERROR -- Expected: [0, 1]
|
342
|
+
# Actual: [0, 0]: nil
|
343
|
+
# test_concurrent_queries ERROR (0.00s)
|
344
|
+
# Minitest::UnexpectedError: RuntimeError: Only 0 out of 2 threads completed successfully
|
345
|
+
# /home/suban/.rbenv/versions/3.3.4/lib/ruby/gems/3.3.0/gems/threads-0.4.0/lib/threads.rb:73:in `assert'
|
346
|
+
# test/test_factbase.rb:329:in `test_concurrent_queries'
|
347
|
+
# ```
|
348
|
+
def test_concurrent_queries
|
349
|
+
skip('Does not work')
|
350
|
+
fb = Factbase.new
|
351
|
+
Threads.new(2).assert do |i|
|
352
|
+
fact = fb.insert
|
353
|
+
fact.thread_id = i
|
354
|
+
fact.value = i * 10
|
355
|
+
end
|
356
|
+
Threads.new(2).assert do
|
357
|
+
results = fb.query('(exists thread_id)').each.to_a
|
358
|
+
assert_equal(2, results.size)
|
359
|
+
|
360
|
+
thread_ids = results.map(&:thread_id)
|
361
|
+
assert_equal((0..1).to_a, thread_ids.sort)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def test_export_import_concurrent
|
366
|
+
fb = Factbase.new
|
367
|
+
Threads.new(100).assert do
|
368
|
+
fact = fb.insert
|
369
|
+
fact.value = 42
|
370
|
+
end
|
371
|
+
Threads.new(5).assert do
|
372
|
+
new_fb = Factbase.new
|
373
|
+
new_fb.import(fb.export)
|
374
|
+
assert_equal(fb.size, new_fb.size)
|
375
|
+
facts = fb.query('(eq value 42)').each.to_a
|
376
|
+
assert_equal(100, facts.size)
|
377
|
+
facts.each do |fact|
|
378
|
+
new_fact = new_fb.query("(eq value #{fact.value})").each.to_a.first
|
379
|
+
assert_equal(fact.value, new_fact.value)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def test_dup_concurrent
|
385
|
+
fb = Factbase.new
|
386
|
+
mutex = Mutex.new
|
387
|
+
Threads.new(100).assert do
|
388
|
+
fact = fb.insert
|
389
|
+
fact.foo = 42
|
390
|
+
end
|
391
|
+
fbs = []
|
392
|
+
Threads.new(100).assert do
|
393
|
+
mutex.synchronize do
|
394
|
+
fbs << fb.dup
|
395
|
+
end
|
396
|
+
end
|
397
|
+
assert_equal(100, fbs.size)
|
398
|
+
fbs.each do |factbase|
|
399
|
+
assert_equal(100, factbase.query('(eq foo 42)').each.to_a.size)
|
400
|
+
end
|
401
|
+
end
|
222
402
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factbase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: backtrace
|
@@ -137,6 +137,7 @@ files:
|
|
137
137
|
- ".0pdd.yml"
|
138
138
|
- ".gitattributes"
|
139
139
|
- ".github/workflows/actionlint.yml"
|
140
|
+
- ".github/workflows/benchmark.yml"
|
140
141
|
- ".github/workflows/codecov.yml"
|
141
142
|
- ".github/workflows/copyrights.yml"
|
142
143
|
- ".github/workflows/markdown-lint.yml"
|
@@ -155,6 +156,7 @@ files:
|
|
155
156
|
- LICENSE.txt
|
156
157
|
- README.md
|
157
158
|
- Rakefile
|
159
|
+
- benchmarks/simple.rb
|
158
160
|
- factbase.gemspec
|
159
161
|
- lib/factbase.rb
|
160
162
|
- lib/factbase/accum.rb
|
@@ -164,10 +166,12 @@ files:
|
|
164
166
|
- lib/factbase/looged.rb
|
165
167
|
- lib/factbase/pre.rb
|
166
168
|
- lib/factbase/query.rb
|
169
|
+
- lib/factbase/query_once.rb
|
167
170
|
- lib/factbase/rules.rb
|
168
171
|
- lib/factbase/syntax.rb
|
169
172
|
- lib/factbase/tee.rb
|
170
173
|
- lib/factbase/term.rb
|
174
|
+
- lib/factbase/term_once.rb
|
171
175
|
- lib/factbase/terms/aggregates.rb
|
172
176
|
- lib/factbase/terms/aliases.rb
|
173
177
|
- lib/factbase/terms/casting.rb
|
@@ -186,9 +190,12 @@ files:
|
|
186
190
|
- test/factbase/terms/test_aggregates.rb
|
187
191
|
- test/factbase/terms/test_aliases.rb
|
188
192
|
- test/factbase/terms/test_casting.rb
|
193
|
+
- test/factbase/terms/test_debug.rb
|
189
194
|
- test/factbase/terms/test_defn.rb
|
190
195
|
- test/factbase/terms/test_logical.rb
|
191
196
|
- test/factbase/terms/test_math.rb
|
197
|
+
- test/factbase/terms/test_meta.rb
|
198
|
+
- test/factbase/terms/test_ordering.rb
|
192
199
|
- test/factbase/terms/test_strings.rb
|
193
200
|
- test/factbase/terms/test_system.rb
|
194
201
|
- test/factbase/test_accum.rb
|
@@ -212,7 +219,7 @@ licenses:
|
|
212
219
|
- MIT
|
213
220
|
metadata:
|
214
221
|
rubygems_mfa_required: 'true'
|
215
|
-
post_install_message:
|
222
|
+
post_install_message:
|
216
223
|
rdoc_options:
|
217
224
|
- "--charset=UTF-8"
|
218
225
|
require_paths:
|
@@ -229,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
229
236
|
version: '0'
|
230
237
|
requirements: []
|
231
238
|
rubygems_version: 3.4.10
|
232
|
-
signing_key:
|
239
|
+
signing_key:
|
233
240
|
specification_version: 4
|
234
241
|
summary: Factbase
|
235
242
|
test_files: []
|