factbase 0.4.0 → 0.5.0

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.0pdd.yml +1 -1
  3. data/.github/workflows/actionlint.yml +1 -1
  4. data/.github/workflows/benchmark.yml +64 -0
  5. data/.github/workflows/codecov.yml +4 -2
  6. data/.github/workflows/copyrights.yml +2 -2
  7. data/.github/workflows/markdown-lint.yml +1 -1
  8. data/.github/workflows/pdd.yml +1 -1
  9. data/.github/workflows/rake.yml +2 -2
  10. data/.github/workflows/xcop.yml +1 -1
  11. data/.github/workflows/yamllint.yml +1 -1
  12. data/.gitignore +1 -1
  13. data/.rubocop.yml +6 -1
  14. data/.rultor.yml +2 -2
  15. data/.simplecov +1 -1
  16. data/.yamllint.yml +9 -4
  17. data/Gemfile +9 -7
  18. data/Gemfile.lock +99 -78
  19. data/LICENSE.txt +1 -1
  20. data/README.md +33 -0
  21. data/Rakefile +6 -1
  22. data/benchmarks/simple.rb +96 -0
  23. data/factbase.gemspec +1 -1
  24. data/lib/factbase/accum.rb +2 -2
  25. data/lib/factbase/fact.rb +6 -4
  26. data/lib/factbase/flatten.rb +2 -2
  27. data/lib/factbase/inv.rb +2 -2
  28. data/lib/factbase/looged.rb +6 -5
  29. data/lib/factbase/pre.rb +2 -2
  30. data/lib/factbase/query.rb +16 -8
  31. data/lib/factbase/query_once.rb +71 -0
  32. data/lib/factbase/rules.rb +3 -3
  33. data/lib/factbase/syntax.rb +13 -8
  34. data/lib/factbase/tee.rb +2 -2
  35. data/lib/factbase/term.rb +33 -5
  36. data/lib/factbase/term_once.rb +84 -0
  37. data/lib/factbase/terms/aggregates.rb +4 -4
  38. data/lib/factbase/terms/aliases.rb +5 -5
  39. data/lib/factbase/terms/casting.rb +2 -2
  40. data/lib/factbase/terms/debug.rb +2 -2
  41. data/lib/factbase/terms/defn.rb +2 -2
  42. data/lib/factbase/terms/logical.rb +3 -3
  43. data/lib/factbase/terms/math.rb +2 -2
  44. data/lib/factbase/terms/meta.rb +2 -2
  45. data/lib/factbase/terms/ordering.rb +2 -2
  46. data/lib/factbase/terms/strings.rb +2 -2
  47. data/lib/factbase/terms/system.rb +2 -2
  48. data/lib/factbase/to_json.rb +2 -2
  49. data/lib/factbase/to_xml.rb +2 -2
  50. data/lib/factbase/to_yaml.rb +2 -2
  51. data/lib/factbase.rb +16 -6
  52. data/test/factbase/terms/test_aggregates.rb +6 -6
  53. data/test/factbase/terms/test_aliases.rb +6 -6
  54. data/test/factbase/terms/test_casting.rb +6 -6
  55. data/test/factbase/terms/test_debug.rb +53 -0
  56. data/test/factbase/terms/test_defn.rb +15 -15
  57. data/test/factbase/terms/test_logical.rb +15 -13
  58. data/test/factbase/terms/test_math.rb +42 -42
  59. data/test/factbase/terms/test_meta.rb +87 -0
  60. data/test/factbase/terms/test_ordering.rb +45 -0
  61. data/test/factbase/terms/test_strings.rb +8 -8
  62. data/test/factbase/terms/test_system.rb +6 -6
  63. data/test/factbase/test_accum.rb +9 -9
  64. data/test/factbase/test_fact.rb +22 -22
  65. data/test/factbase/test_flatten.rb +6 -6
  66. data/test/factbase/test_inv.rb +3 -3
  67. data/test/factbase/test_looged.rb +13 -13
  68. data/test/factbase/test_pre.rb +2 -2
  69. data/test/factbase/test_query.rb +17 -17
  70. data/test/factbase/test_rules.rb +8 -8
  71. data/test/factbase/test_syntax.rb +29 -9
  72. data/test/factbase/test_tee.rb +11 -11
  73. data/test/factbase/test_term.rb +18 -18
  74. data/test/factbase/test_to_json.rb +4 -4
  75. data/test/factbase/test_to_xml.rb +12 -14
  76. data/test/factbase/test_to_yaml.rb +3 -3
  77. data/test/test__helper.rb +2 -2
  78. data/test/test_factbase.rb +198 -18
  79. metadata +12 -5
@@ -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
- assert(yaml.include?("a: 256\n b: 42\n c: 10"), yaml)
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
@@ -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
- assert(42, f.bar.positive?)
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.count)
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
- assert(
98
- assert_raises do
107
+ assert_includes(
108
+ assert_raises(StandardError) do
99
109
  fb.import(File.binread(f.path))
100
- end.message.include?('cannot load a factbase')
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
- assert(fb.txn { true }.is_a?(FalseClass))
125
- assert(fb.txn(&:insert).is_a?(TrueClass))
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
- assert(!fb.txn { |fbt| fbt.query('(always)').each.to_a })
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
- assert(
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.include?('intentionally')
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
- assert(
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.include?('oops')
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
- assert(!modified)
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.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: 2024-08-12 00:00:00.000000000 Z
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: []