kbs 0.0.1 → 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +291 -362
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +865 -0
- data/docs/advanced/testing.md +827 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +1047 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1104 -0
- data/docs/architecture/blackboard.md +544 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/fact-rule-relationship.svg +65 -0
- data/docs/assets/images/fact-structure.svg +42 -0
- data/docs/assets/images/inference-cycle.svg +47 -0
- data/docs/assets/images/kb-components.svg +43 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/rule-structure.svg +44 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/index.md +223 -0
- data/docs/guides/blackboard-memory.md +589 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +385 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +914 -0
- data/docs/index.md +155 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +221 -0
- data/docs/what-is-a-fact.md +694 -0
- data/docs/what-is-a-knowledge-base.md +350 -0
- data/docs/what-is-a-rule.md +833 -0
- data/examples/.gitignore +1 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -6
- data/examples/concurrent_inference_demo_dsl.rb +362 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/iot_demo_using_dsl.rb +1 -1
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/rule_source_demo.rb +123 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp_dsl.txt +9392 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/decompiler.rb +204 -0
- data/lib/kbs/dsl/knowledge_base.rb +101 -2
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/dsl.rb +3 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +42 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +15 -13
- data/mkdocs.yml +181 -0
- metadata +74 -9
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
# Testing Rules
|
|
2
|
+
|
|
3
|
+
Comprehensive testing strategies for rule-based systems. This guide covers unit testing, integration testing, test fixtures, and coverage analysis for KBS applications.
|
|
4
|
+
|
|
5
|
+
## Testing Overview
|
|
6
|
+
|
|
7
|
+
Rule-based systems require testing at multiple levels:
|
|
8
|
+
|
|
9
|
+
1. **Unit Tests** - Test individual rules in isolation
|
|
10
|
+
2. **Integration Tests** - Test rule interactions
|
|
11
|
+
3. **Fact Fixtures** - Reusable test data
|
|
12
|
+
4. **Coverage** - Ensure all rules and conditions are tested
|
|
13
|
+
5. **Performance Tests** - Verify rule execution speed
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
### Test Framework
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# Gemfile
|
|
21
|
+
group :test do
|
|
22
|
+
gem 'minitest', '~> 5.0'
|
|
23
|
+
gem 'simplecov', require: false # Coverage
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Test Helper
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# test/test_helper.rb
|
|
31
|
+
require 'simplecov'
|
|
32
|
+
SimpleCov.start
|
|
33
|
+
|
|
34
|
+
require 'minitest/autorun'
|
|
35
|
+
require 'kbs'
|
|
36
|
+
|
|
37
|
+
class Minitest::Test
|
|
38
|
+
def assert_rule_fired(kb, rule_name)
|
|
39
|
+
# Check if rule action was executed
|
|
40
|
+
# Implementation depends on tracking mechanism
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def refute_rule_fired(kb, rule_name)
|
|
44
|
+
# Check that rule did not fire
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Unit Testing Rules
|
|
50
|
+
|
|
51
|
+
### Test Single Rule
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require 'test_helper'
|
|
55
|
+
|
|
56
|
+
class TestTemperatureRule < Minitest::Test
|
|
57
|
+
def test_fires_when_temperature_high
|
|
58
|
+
fired = false
|
|
59
|
+
|
|
60
|
+
kb = KBS.knowledge_base do
|
|
61
|
+
rule "high_temp_alert", priority: 100 do
|
|
62
|
+
on :sensor,
|
|
63
|
+
type: "temperature",
|
|
64
|
+
value: :temp?,
|
|
65
|
+
predicate: greater_than(30)
|
|
66
|
+
|
|
67
|
+
perform do |facts, bindings|
|
|
68
|
+
fired = true
|
|
69
|
+
fact :alert,
|
|
70
|
+
type: "high_temperature",
|
|
71
|
+
temperature: bindings[:temp?]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
fact :sensor, type: "temperature", value: 35
|
|
76
|
+
run
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
assert fired, "Rule should fire for high temperature"
|
|
80
|
+
|
|
81
|
+
alerts = kb.engine.facts.select { |f| f.type == :alert }
|
|
82
|
+
assert_equal 1, alerts.size
|
|
83
|
+
assert_equal 35, alerts.first[:temperature]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_does_not_fire_when_temperature_normal
|
|
87
|
+
fired = false
|
|
88
|
+
|
|
89
|
+
kb = KBS.knowledge_base do
|
|
90
|
+
rule "high_temp_alert", priority: 100 do
|
|
91
|
+
on :sensor,
|
|
92
|
+
type: "temperature",
|
|
93
|
+
value: :temp?,
|
|
94
|
+
predicate: greater_than(30)
|
|
95
|
+
|
|
96
|
+
perform do |facts, bindings|
|
|
97
|
+
fired = true
|
|
98
|
+
fact :alert,
|
|
99
|
+
type: "high_temperature",
|
|
100
|
+
temperature: bindings[:temp?]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
fact :sensor, type: "temperature", value: 25
|
|
105
|
+
run
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
refute fired, "Rule should not fire for normal temperature"
|
|
109
|
+
|
|
110
|
+
alerts = kb.engine.facts.select { |f| f.type == :alert }
|
|
111
|
+
assert_empty alerts
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_threshold_boundary
|
|
115
|
+
fired = false
|
|
116
|
+
|
|
117
|
+
kb = KBS.knowledge_base do
|
|
118
|
+
rule "high_temp_alert" do
|
|
119
|
+
on :sensor,
|
|
120
|
+
type: "temperature",
|
|
121
|
+
value: :temp?,
|
|
122
|
+
predicate: greater_than(30)
|
|
123
|
+
|
|
124
|
+
perform do |facts, bindings|
|
|
125
|
+
fired = true
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Test at exact threshold
|
|
130
|
+
fact :sensor, type: "temperature", value: 30
|
|
131
|
+
run
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
refute fired, "Rule should not fire at exact threshold (> not >=)"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Test Rule with Multiple Conditions
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
class TestMultiConditionRule < Minitest::Test
|
|
143
|
+
def test_fires_when_both_conditions_met
|
|
144
|
+
fired = false
|
|
145
|
+
|
|
146
|
+
kb = KBS.knowledge_base do
|
|
147
|
+
rule "high_temp_and_low_humidity" do
|
|
148
|
+
on :temperature,
|
|
149
|
+
location: :loc?,
|
|
150
|
+
value: :temp?,
|
|
151
|
+
predicate: greater_than(30)
|
|
152
|
+
|
|
153
|
+
on :humidity,
|
|
154
|
+
location: :loc?,
|
|
155
|
+
value: :hum?,
|
|
156
|
+
predicate: less_than(40)
|
|
157
|
+
|
|
158
|
+
perform do |facts, bindings|
|
|
159
|
+
fired = true
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
fact :temperature, location: "room1", value: 35
|
|
164
|
+
fact :humidity, location: "room1", value: 30
|
|
165
|
+
run
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
assert fired, "Rule should fire when both conditions met"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_does_not_fire_with_mismatched_locations
|
|
172
|
+
fired = false
|
|
173
|
+
|
|
174
|
+
kb = KBS.knowledge_base do
|
|
175
|
+
rule "high_temp_and_low_humidity" do
|
|
176
|
+
on :temperature,
|
|
177
|
+
location: :loc?,
|
|
178
|
+
value: :temp?,
|
|
179
|
+
predicate: greater_than(30)
|
|
180
|
+
|
|
181
|
+
on :humidity,
|
|
182
|
+
location: :loc?,
|
|
183
|
+
value: :hum?,
|
|
184
|
+
predicate: less_than(40)
|
|
185
|
+
|
|
186
|
+
perform do |facts, bindings|
|
|
187
|
+
fired = true
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
fact :temperature, location: "room1", value: 35
|
|
192
|
+
fact :humidity, location: "room2", value: 30
|
|
193
|
+
run
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
refute fired, "Rule should not fire with different locations"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_does_not_fire_when_only_temperature_high
|
|
200
|
+
fired = false
|
|
201
|
+
|
|
202
|
+
kb = KBS.knowledge_base do
|
|
203
|
+
rule "high_temp_and_low_humidity" do
|
|
204
|
+
on :temperature,
|
|
205
|
+
location: :loc?,
|
|
206
|
+
value: :temp?,
|
|
207
|
+
predicate: greater_than(30)
|
|
208
|
+
|
|
209
|
+
on :humidity,
|
|
210
|
+
location: :loc?,
|
|
211
|
+
value: :hum?,
|
|
212
|
+
predicate: less_than(40)
|
|
213
|
+
|
|
214
|
+
perform do |facts, bindings|
|
|
215
|
+
fired = true
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
fact :temperature, location: "room1", value: 35
|
|
220
|
+
# No humidity fact
|
|
221
|
+
run
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
refute fired, "Rule should not fire without humidity fact"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_does_not_fire_when_temperature_normal
|
|
228
|
+
fired = false
|
|
229
|
+
|
|
230
|
+
kb = KBS.knowledge_base do
|
|
231
|
+
rule "high_temp_and_low_humidity" do
|
|
232
|
+
on :temperature,
|
|
233
|
+
location: :loc?,
|
|
234
|
+
value: :temp?,
|
|
235
|
+
predicate: greater_than(30)
|
|
236
|
+
|
|
237
|
+
on :humidity,
|
|
238
|
+
location: :loc?,
|
|
239
|
+
value: :hum?,
|
|
240
|
+
predicate: less_than(40)
|
|
241
|
+
|
|
242
|
+
perform do |facts, bindings|
|
|
243
|
+
fired = true
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
fact :temperature, location: "room1", value: 25
|
|
248
|
+
fact :humidity, location: "room1", value: 30
|
|
249
|
+
run
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
refute fired, "Rule should not fire with normal temperature"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Test Negated Conditions
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
class TestNegationRule < Minitest::Test
|
|
261
|
+
def test_fires_when_error_not_acknowledged
|
|
262
|
+
fired = false
|
|
263
|
+
|
|
264
|
+
kb = KBS.knowledge_base do
|
|
265
|
+
rule "alert_if_no_acknowledgment" do
|
|
266
|
+
on :error, id: :id?
|
|
267
|
+
without :acknowledged, error_id: :id?
|
|
268
|
+
|
|
269
|
+
perform do |facts, bindings|
|
|
270
|
+
fired = true
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
fact :error, id: 1
|
|
275
|
+
run
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
assert fired, "Rule should fire when error not acknowledged"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_does_not_fire_when_error_acknowledged
|
|
282
|
+
fired = false
|
|
283
|
+
|
|
284
|
+
kb = KBS.knowledge_base do
|
|
285
|
+
rule "alert_if_no_acknowledgment" do
|
|
286
|
+
on :error, id: :id?
|
|
287
|
+
without :acknowledged, error_id: :id?
|
|
288
|
+
|
|
289
|
+
perform do |facts, bindings|
|
|
290
|
+
fired = true
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
fact :error, id: 1
|
|
295
|
+
fact :acknowledged, error_id: 1
|
|
296
|
+
run
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
refute fired, "Rule should not fire when error acknowledged"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Integration Testing
|
|
305
|
+
|
|
306
|
+
### Test Rule Interactions
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
class TestRuleInteractions < Minitest::Test
|
|
310
|
+
def test_cascading_rules
|
|
311
|
+
alerts = []
|
|
312
|
+
|
|
313
|
+
kb = KBS.knowledge_base do
|
|
314
|
+
# Rule 1: Detect high temperature
|
|
315
|
+
rule "detect_high_temp" do
|
|
316
|
+
on :sensor, value: :temp?, predicate: greater_than(30)
|
|
317
|
+
|
|
318
|
+
perform do |facts, bindings|
|
|
319
|
+
fact :temp_alert, severity: "high"
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Rule 2: Escalate to critical
|
|
324
|
+
rule "escalate_critical" do
|
|
325
|
+
on :temp_alert, severity: "high"
|
|
326
|
+
on :sensor, value: :temp?, predicate: greater_than(40)
|
|
327
|
+
|
|
328
|
+
perform do |facts, bindings|
|
|
329
|
+
fact :critical_alert, type: "temperature"
|
|
330
|
+
alerts << :critical
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Add high temperature
|
|
335
|
+
fact :sensor, value: 45
|
|
336
|
+
run
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Both rules should fire
|
|
340
|
+
assert kb.engine.facts.any? { |f| f.type == :temp_alert }
|
|
341
|
+
assert kb.engine.facts.any? { |f| f.type == :critical_alert }
|
|
342
|
+
assert_includes alerts, :critical
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def test_partial_cascade
|
|
346
|
+
alerts = []
|
|
347
|
+
|
|
348
|
+
kb = KBS.knowledge_base do
|
|
349
|
+
rule "detect_high_temp" do
|
|
350
|
+
on :sensor, value: :temp?, predicate: greater_than(30)
|
|
351
|
+
perform { fact :temp_alert, severity: "high" }
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
rule "escalate_critical" do
|
|
355
|
+
on :temp_alert, severity: "high"
|
|
356
|
+
on :sensor, value: :temp?, predicate: greater_than(40)
|
|
357
|
+
perform do |facts, bindings|
|
|
358
|
+
fact :critical_alert, type: "temperature"
|
|
359
|
+
alerts << :critical
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Add moderately high temperature
|
|
364
|
+
fact :sensor, value: 35
|
|
365
|
+
run
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Only first rule fires
|
|
369
|
+
assert kb.engine.facts.any? { |f| f.type == :temp_alert }
|
|
370
|
+
refute kb.engine.facts.any? { |f| f.type == :critical_alert }
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Test Rule Priority
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
class TestRulePriority < Minitest::Test
|
|
379
|
+
def test_executes_in_priority_order
|
|
380
|
+
execution_order = []
|
|
381
|
+
|
|
382
|
+
kb = KBS.knowledge_base do
|
|
383
|
+
# High priority rule
|
|
384
|
+
rule "high_priority", priority: 100 do
|
|
385
|
+
on :trigger, {}
|
|
386
|
+
perform { execution_order << :high }
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Low priority rule
|
|
390
|
+
rule "low_priority", priority: 10 do
|
|
391
|
+
on :trigger, {}
|
|
392
|
+
perform { execution_order << :low }
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
fact :trigger, {}
|
|
396
|
+
run
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
assert_equal [:high, :low], execution_order
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Test Fixtures
|
|
405
|
+
|
|
406
|
+
### Fact Fixtures
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
module FactFixtures
|
|
410
|
+
def sensor_facts(count: 10)
|
|
411
|
+
count.times.map do |i|
|
|
412
|
+
{ type: :sensor, attributes: { id: i, value: rand(20..40) } }
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def high_temp_scenario
|
|
417
|
+
[
|
|
418
|
+
{ type: :sensor, attributes: { location: "room1", value: 35 } },
|
|
419
|
+
{ type: :sensor, attributes: { location: "room2", value: 38 } },
|
|
420
|
+
{ type: :threshold, attributes: { value: 30 } }
|
|
421
|
+
]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def normal_scenario
|
|
425
|
+
[
|
|
426
|
+
{ type: :sensor, attributes: { location: "room1", value: 22 } },
|
|
427
|
+
{ type: :sensor, attributes: { location: "room2", value: 24 } },
|
|
428
|
+
{ type: :threshold, attributes: { value: 30 } }
|
|
429
|
+
]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def load_facts_into_kb(kb, facts)
|
|
433
|
+
facts.each do |fact_data|
|
|
434
|
+
kb.fact fact_data[:type], fact_data[:attributes]
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
class TestWithFixtures < Minitest::Test
|
|
440
|
+
include FactFixtures
|
|
441
|
+
|
|
442
|
+
def test_with_high_temp_scenario
|
|
443
|
+
kb = KBS.knowledge_base do
|
|
444
|
+
rule "check_threshold" do
|
|
445
|
+
on :sensor, value: :v?, predicate: greater_than(30)
|
|
446
|
+
perform { }
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
load_facts_into_kb(kb, high_temp_scenario)
|
|
451
|
+
kb.run
|
|
452
|
+
|
|
453
|
+
# Assertions...
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Rule Fixtures
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
module RuleFixtures
|
|
462
|
+
# Note: Since DSL rules are defined in blocks,
|
|
463
|
+
# we provide factory methods instead of rule objects
|
|
464
|
+
|
|
465
|
+
def add_temperature_monitoring_rules(kb)
|
|
466
|
+
kb.instance_eval do
|
|
467
|
+
rule "detect_high" do
|
|
468
|
+
on :sensor, value: :v?, predicate: greater_than(30)
|
|
469
|
+
perform { |facts, bindings| facts[0][:alerted] = true }
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
rule "detect_low" do
|
|
473
|
+
on :sensor, value: :v?, predicate: less_than(15)
|
|
474
|
+
perform { |facts, bindings| facts[0][:alerted] = true }
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Coverage Strategies
|
|
482
|
+
|
|
483
|
+
### Track Rule Firings
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
class CoverageTracker
|
|
487
|
+
def initialize(kb)
|
|
488
|
+
@kb = kb
|
|
489
|
+
@rule_firings = Hash.new(0)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def wrap_rules
|
|
493
|
+
@kb.engine.instance_variable_get(:@rules).each do |rule|
|
|
494
|
+
original_action = rule.action
|
|
495
|
+
|
|
496
|
+
rule.action = lambda do |facts, bindings|
|
|
497
|
+
@rule_firings[rule.name] += 1
|
|
498
|
+
original_action.call(facts, bindings)
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def report
|
|
504
|
+
puts "\n=== Coverage Report ==="
|
|
505
|
+
|
|
506
|
+
total_rules = @kb.engine.instance_variable_get(:@rules).size
|
|
507
|
+
fired_rules = @rule_firings.keys.size
|
|
508
|
+
coverage = (fired_rules.to_f / total_rules * 100).round(2)
|
|
509
|
+
|
|
510
|
+
puts "Rules: #{fired_rules}/#{total_rules} (#{coverage}%)"
|
|
511
|
+
|
|
512
|
+
puts "\nRule Firings:"
|
|
513
|
+
@rule_firings.each do |name, count|
|
|
514
|
+
puts " #{name}: #{count}"
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
untested = @kb.engine.instance_variable_get(:@rules).map(&:name) - @rule_firings.keys
|
|
518
|
+
if untested.any?
|
|
519
|
+
puts "\nUntested Rules:"
|
|
520
|
+
untested.each { |name| puts " - #{name}" }
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
attr_reader :rule_firings
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Usage
|
|
528
|
+
class TestWithCoverage < Minitest::Test
|
|
529
|
+
def test_coverage
|
|
530
|
+
kb = KBS.knowledge_base do
|
|
531
|
+
rule "rule1" do
|
|
532
|
+
on :fact, {}
|
|
533
|
+
perform { }
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
rule "rule2" do
|
|
537
|
+
on :other, {}
|
|
538
|
+
perform { }
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
tracker = CoverageTracker.new(kb)
|
|
543
|
+
tracker.wrap_rules
|
|
544
|
+
|
|
545
|
+
# Add facts and run
|
|
546
|
+
kb.fact :fact, {}
|
|
547
|
+
kb.run
|
|
548
|
+
|
|
549
|
+
tracker.report
|
|
550
|
+
|
|
551
|
+
# Assert all rules fired
|
|
552
|
+
# (or check specific coverage requirements)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Condition Coverage
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
def test_all_condition_paths
|
|
561
|
+
# Test path 1: All conditions pass
|
|
562
|
+
kb1 = KBS.knowledge_base do
|
|
563
|
+
rule "multi_path" do
|
|
564
|
+
on :a, {}
|
|
565
|
+
on :b, {}
|
|
566
|
+
without :c, {}
|
|
567
|
+
perform { }
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
fact :a, {}
|
|
571
|
+
fact :b, {}
|
|
572
|
+
# c absent
|
|
573
|
+
run
|
|
574
|
+
end
|
|
575
|
+
# Assert...
|
|
576
|
+
|
|
577
|
+
# Test path 2: Negation fails
|
|
578
|
+
kb2 = KBS.knowledge_base do
|
|
579
|
+
rule "multi_path" do
|
|
580
|
+
on :a, {}
|
|
581
|
+
on :b, {}
|
|
582
|
+
without :c, {}
|
|
583
|
+
perform { }
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
fact :a, {}
|
|
587
|
+
fact :b, {}
|
|
588
|
+
fact :c, {} # Blocks negation
|
|
589
|
+
run
|
|
590
|
+
end
|
|
591
|
+
# Assert...
|
|
592
|
+
|
|
593
|
+
# Test path 3: Positive condition missing
|
|
594
|
+
kb3 = KBS.knowledge_base do
|
|
595
|
+
rule "multi_path" do
|
|
596
|
+
on :a, {}
|
|
597
|
+
on :b, {}
|
|
598
|
+
without :c, {}
|
|
599
|
+
perform { }
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
fact :a, {}
|
|
603
|
+
# b missing
|
|
604
|
+
run
|
|
605
|
+
end
|
|
606
|
+
# Assert...
|
|
607
|
+
end
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Performance Testing
|
|
611
|
+
|
|
612
|
+
### Benchmark Rule Execution
|
|
613
|
+
|
|
614
|
+
```ruby
|
|
615
|
+
require 'benchmark'
|
|
616
|
+
|
|
617
|
+
class PerformanceTest < Minitest::Test
|
|
618
|
+
def test_rule_performance
|
|
619
|
+
time = Benchmark.measure do
|
|
620
|
+
kb = KBS.knowledge_base do
|
|
621
|
+
rule "perf_test" do
|
|
622
|
+
on :data, value: :v?
|
|
623
|
+
perform { }
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Add many facts
|
|
627
|
+
1000.times { |i| fact :data, value: i }
|
|
628
|
+
run
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
assert time.real < 1.0, "Engine should complete in under 1 second"
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def test_fact_addition_performance
|
|
636
|
+
kb = KBS.knowledge_base
|
|
637
|
+
|
|
638
|
+
time = Benchmark.measure do
|
|
639
|
+
10_000.times { |i| kb.fact :data, value: i }
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
rate = 10_000 / time.real
|
|
643
|
+
assert rate > 10_000, "Should add >10k facts/sec, got #{rate.round(2)}"
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
## Testing Blackboard Persistence
|
|
649
|
+
|
|
650
|
+
### Test with SQLite
|
|
651
|
+
|
|
652
|
+
```ruby
|
|
653
|
+
class TestBlackboardPersistence < Minitest::Test
|
|
654
|
+
def test_facts_persist_across_sessions
|
|
655
|
+
# Session 1: Add facts
|
|
656
|
+
engine1 = KBS::Blackboard::Engine.new(db_path: 'test.db')
|
|
657
|
+
kb1 = KBS.knowledge_base(engine: engine1) do
|
|
658
|
+
fact :sensor, id: 1, value: 25
|
|
659
|
+
end
|
|
660
|
+
kb1.close
|
|
661
|
+
|
|
662
|
+
# Session 2: Load facts
|
|
663
|
+
engine2 = KBS::Blackboard::Engine.new(db_path: 'test.db')
|
|
664
|
+
assert_equal 1, engine2.facts.size
|
|
665
|
+
assert_equal 25, engine2.facts.first[:value]
|
|
666
|
+
|
|
667
|
+
engine2.close
|
|
668
|
+
File.delete('test.db') if File.exist?('test.db')
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def test_audit_trail
|
|
672
|
+
engine = KBS::Blackboard::Engine.new(db_path: ':memory:')
|
|
673
|
+
|
|
674
|
+
fact = engine.add_fact(:data, value: 1)
|
|
675
|
+
engine.update_fact(fact.id, value: 2)
|
|
676
|
+
engine.delete_fact(fact.id)
|
|
677
|
+
|
|
678
|
+
history = engine.fact_history(fact.id)
|
|
679
|
+
|
|
680
|
+
assert_equal 3, history.size
|
|
681
|
+
assert_equal "add", history[0][:operation]
|
|
682
|
+
assert_equal "update", history[1][:operation]
|
|
683
|
+
assert_equal "delete", history[2][:operation]
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Testing Best Practices
|
|
689
|
+
|
|
690
|
+
### 1. Isolate Rules
|
|
691
|
+
|
|
692
|
+
```ruby
|
|
693
|
+
def test_single_rule_only
|
|
694
|
+
kb = KBS.knowledge_base do
|
|
695
|
+
# Add ONLY the rule being tested
|
|
696
|
+
rule "my_test_rule" do
|
|
697
|
+
on :trigger, {}
|
|
698
|
+
perform { }
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# No other rules to interfere
|
|
702
|
+
fact :trigger, {}
|
|
703
|
+
run
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### 2. Test Edge Cases
|
|
709
|
+
|
|
710
|
+
```ruby
|
|
711
|
+
def test_edge_cases
|
|
712
|
+
# Empty facts
|
|
713
|
+
kb = KBS.knowledge_base do
|
|
714
|
+
rule "check" do
|
|
715
|
+
on :sensor, value: :v?
|
|
716
|
+
perform { }
|
|
717
|
+
end
|
|
718
|
+
run
|
|
719
|
+
end
|
|
720
|
+
assert_empty kb.engine.facts.select { |f| f.type == :alert }
|
|
721
|
+
|
|
722
|
+
# Exact threshold
|
|
723
|
+
kb = KBS.knowledge_base do
|
|
724
|
+
rule "check" do
|
|
725
|
+
on :sensor, value: :v?, predicate: greater_than(30)
|
|
726
|
+
perform { }
|
|
727
|
+
end
|
|
728
|
+
fact :sensor, value: 30
|
|
729
|
+
run
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Just below threshold
|
|
733
|
+
kb = KBS.knowledge_base do
|
|
734
|
+
rule "check" do
|
|
735
|
+
on :sensor, value: :v?, predicate: greater_than(30)
|
|
736
|
+
perform { }
|
|
737
|
+
end
|
|
738
|
+
fact :sensor, value: 29.99
|
|
739
|
+
run
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Just above threshold
|
|
743
|
+
kb = KBS.knowledge_base do
|
|
744
|
+
rule "check" do
|
|
745
|
+
on :sensor, value: :v?, predicate: greater_than(30)
|
|
746
|
+
perform { }
|
|
747
|
+
end
|
|
748
|
+
fact :sensor, value: 30.01
|
|
749
|
+
run
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### 3. Test Side Effects
|
|
755
|
+
|
|
756
|
+
```ruby
|
|
757
|
+
def test_action_side_effects
|
|
758
|
+
added_facts = []
|
|
759
|
+
|
|
760
|
+
kb = KBS.knowledge_base do
|
|
761
|
+
rule "test" do
|
|
762
|
+
on :trigger, {}
|
|
763
|
+
perform do |facts, bindings|
|
|
764
|
+
new_fact = fact :result, value: 42
|
|
765
|
+
added_facts << new_fact
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
fact :trigger, {}
|
|
770
|
+
run
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
assert_equal 1, added_facts.size
|
|
774
|
+
assert_equal 42, added_facts.first[:value]
|
|
775
|
+
end
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### 4. Use Descriptive Test Names
|
|
779
|
+
|
|
780
|
+
```ruby
|
|
781
|
+
def test_high_temperature_alert_fires_when_sensor_exceeds_threshold
|
|
782
|
+
# Clear what this tests
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def test_alert_not_sent_twice_for_same_sensor
|
|
786
|
+
# Explains the scenario
|
|
787
|
+
end
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 5. Setup and Teardown
|
|
791
|
+
|
|
792
|
+
```ruby
|
|
793
|
+
class TestWithSetup < Minitest::Test
|
|
794
|
+
def setup
|
|
795
|
+
@test_db = "test_#{SecureRandom.hex(8)}.db"
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def teardown
|
|
799
|
+
File.delete(@test_db) if File.exist?(@test_db)
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
## Testing Checklist
|
|
805
|
+
|
|
806
|
+
- [ ] Test each rule fires with correct facts
|
|
807
|
+
- [ ] Test each rule doesn't fire without required facts
|
|
808
|
+
- [ ] Test boundary conditions
|
|
809
|
+
- [ ] Test negated conditions
|
|
810
|
+
- [ ] Test variable bindings
|
|
811
|
+
- [ ] Test rule priorities
|
|
812
|
+
- [ ] Test rule interactions
|
|
813
|
+
- [ ] Test action side effects
|
|
814
|
+
- [ ] Test persistence (if using blackboard)
|
|
815
|
+
- [ ] Measure performance
|
|
816
|
+
- [ ] Achieve high rule coverage
|
|
817
|
+
|
|
818
|
+
## Next Steps
|
|
819
|
+
|
|
820
|
+
- **[Debugging Guide](debugging.md)** - Debug failing tests
|
|
821
|
+
- **[Performance Guide](performance.md)** - Optimize slow tests
|
|
822
|
+
- **[Architecture](../architecture/index.md)** - Understand rule execution
|
|
823
|
+
- **[Examples](../examples/index.md)** - See tested examples
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
*Good tests make rule changes safe. Test each rule thoroughly.*
|