Skip to content

Commit 67a28be

Browse files
authored
Merge pull request patr1ck-m#34 from patr1ck-m/improvemen/edge_case_tests
Add additional edge test cases and add tests for the counter object.
2 parents 70f88ef + e131032 commit 67a28be

5 files changed

Lines changed: 388 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,9 @@ Tests use the `test` module from Effekt's standard library and return exit code
280280
│ └── test/ # Test modules and data
281281
│ ├── aggregation_test.effekt
282282
│ ├── anomaly_detection_test.effekt
283-
│ ├── stream_input_test.effekt
283+
│ ├── counter_test.effekt
284284
│ ├── lib.effekt
285+
│ ├── stream_input_test.effekt
285286
│ ├── testdata.csv
286287
│ └── testdata_with_timestamp.csv
287288
├── plot_data.py # Visualization script for results

src/test.effekt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import test
55
import src/test/aggregation_test
66
import src/test/anomaly_detection_test
77
import src/test/stream_input_test
8+
import src/test/counter_test
89

910
def main() = {
1011
val testResults = [
1112
aggregation_test::testSuite(),
1213
anomaly_detection_test::testSuite(),
13-
stream_input_test::testSuite()
14+
stream_input_test::testSuite(),
15+
counter_test::testSuite()
1416
]
1517
val allPassed = testResults.all { res => res }
1618
val exitCode = if (allPassed) 0 else 1

src/test/aggregation_test.effekt

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,121 @@ def testSuite() = suite("aggregation") {
9090
}
9191
}
9292
}
93+
94+
// ==================
95+
// Edge Case Tests
96+
// ==================
97+
98+
test("aggregate single element using min") {
99+
val input = eventsFromList([42.0])
100+
val expectedOutput = eventsFromList([42.0])
101+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
102+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
103+
aggregation::aggregateMin()
104+
}
105+
}
106+
}
107+
108+
test("aggregate single element using max") {
109+
val input = eventsFromList([42.0])
110+
val expectedOutput = eventsFromList([42.0])
111+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
112+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
113+
aggregation::aggregateMax()
114+
}
115+
}
116+
}
117+
118+
test("aggregate single element using mean") {
119+
val input = eventsFromList([42.0])
120+
val expectedOutput = eventsFromList([42.0])
121+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
122+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
123+
aggregation::aggregateMean()
124+
}
125+
}
126+
}
127+
128+
test("aggregate single element using median") {
129+
val input = eventsFromList([42.0])
130+
val expectedOutput = eventsFromList([42.0])
131+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
132+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
133+
aggregation::aggregateMedian()
134+
}
135+
}
136+
}
137+
138+
test("aggregate with negative values using min") {
139+
val input = eventsFromList([-5.0, -3.0, -8.0, -1.0])
140+
val expectedOutput = eventsFromList([-5.0, -5.0, -8.0, -8.0])
141+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
142+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
143+
aggregation::aggregateMin()
144+
}
145+
}
146+
}
147+
148+
test("aggregate with negative values using max") {
149+
val input = eventsFromList([-5.0, -3.0, -8.0, -1.0])
150+
val expectedOutput = eventsFromList([-5.0, -3.0, -3.0, -1.0])
151+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
152+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
153+
aggregation::aggregateMax()
154+
}
155+
}
156+
}
157+
158+
test("aggregate with negative values using mean") {
159+
val input = eventsFromList([-5.0, -3.0, -7.0, -1.0])
160+
val expectedOutput = eventsFromList([-5.0, -4.0, -5.0, -4.0])
161+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
162+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
163+
aggregation::aggregateMean()
164+
}
165+
}
166+
}
167+
168+
test("aggregate with negative values using median") {
169+
val input = eventsFromList([-5.0, -3.0, -8.0, -1.0])
170+
val expectedOutput = eventsFromList([-5.0, -4.0, -5.0, -4.0])
171+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
172+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
173+
aggregation::aggregateMedian()
174+
}
175+
}
176+
}
177+
178+
test("windowed min with window size 1") {
179+
val input = eventsFromList([3.0, 1.0, 4.0, 1.0, 5.0])
180+
val expectedOutput = eventsFromList([3.0, 1.0, 4.0, 1.0, 5.0])
181+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
182+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
183+
aggregation::aggregateMinWindow(1)
184+
}
185+
}
186+
}
187+
188+
test("windowed max with window size 1") {
189+
val input = eventsFromList([3.0, 1.0, 4.0, 1.0, 5.0])
190+
val expectedOutput = eventsFromList([3.0, 1.0, 4.0, 1.0, 5.0])
191+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
192+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
193+
aggregation::aggregateMaxWindow(1)
194+
}
195+
}
196+
}
197+
198+
test("windowed mean with window larger than input") {
199+
val input = eventsFromList([2.0, 4.0, 6.0])
200+
// Window size 10, but only 3 elements, so it computes mean of all available
201+
val expectedOutput = eventsFromList([2.0, 3.0, 4.0])
202+
assertEmits[Event]( expectedOutput ) { (a, b) => sameEvent(a, b) } {
203+
assertReads[Event](input) { (a, b) => sameEvent(a, b) } {
204+
aggregation::aggregateMeanWindow(10)
205+
}
206+
}
207+
}
93208
}
94209

95210
def main() = {

src/test/anomaly_detection_test.effekt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,104 @@ def testSuite() = suite("anomaly_detection") {
107107
}
108108
}
109109
}
110+
111+
// ==================
112+
// Edge Case Tests
113+
// ==================
114+
115+
test("min-max detector single value in range") {
116+
val input = eventsFromList([2.5])
117+
val expected = [
118+
AnomalyEvaluation(Event(0, 2.5), false, 0.0)
119+
]
120+
assertAnomalyDetection(expected) {
121+
anomaly_detection::minMaxAnomalyDetector(0.0, 5.0) {
122+
input.foreach { e => do emit(e) }
123+
}
124+
}
125+
}
126+
127+
test("min-max detector single value out of range") {
128+
val input = eventsFromList([10.0])
129+
val expected = [
130+
AnomalyEvaluation(Event(0, 10.0), true, 5.0)
131+
]
132+
assertAnomalyDetection(expected) {
133+
anomaly_detection::minMaxAnomalyDetector(0.0, 5.0) {
134+
input.foreach { e => do emit(e) }
135+
}
136+
}
137+
}
138+
139+
test("min-max detector value exactly at boundary is not anomaly") {
140+
val input = eventsFromList([0.0, 5.0])
141+
val expected = [
142+
AnomalyEvaluation(Event(0, 0.0), false, 0.0),
143+
AnomalyEvaluation(Event(1, 5.0), false, 0.0)
144+
]
145+
assertAnomalyDetection(expected) {
146+
anomaly_detection::minMaxAnomalyDetector(0.0, 5.0) {
147+
input.foreach { e => do emit(e) }
148+
}
149+
}
150+
}
151+
152+
test("min-max detector with negative range") {
153+
val input = eventsFromList([-3.0, -7.0, 0.0])
154+
val expected = [
155+
AnomalyEvaluation(Event(0, -3.0), false, 0.0),
156+
AnomalyEvaluation(Event(1, -7.0), true, 2.0),
157+
AnomalyEvaluation(Event(2, 0.0), true, 1.0)
158+
]
159+
assertAnomalyDetection(expected) {
160+
anomaly_detection::minMaxAnomalyDetector(-5.0, -1.0) {
161+
input.foreach { e => do emit(e) }
162+
}
163+
}
164+
}
165+
166+
test("z-score detector with identical values has zero stddev") {
167+
val input = eventsFromList([5.0, 5.0, 5.0])
168+
// When stddev is 0, z-score should be 0 (no anomaly)
169+
val expected = [
170+
AnomalyEvaluation(Event(0, 5.0), false, 0.0),
171+
AnomalyEvaluation(Event(1, 5.0), false, 0.0),
172+
AnomalyEvaluation(Event(2, 5.0), false, 0.0)
173+
]
174+
assertAnomalyDetection(expected) {
175+
anomaly_detection::zScoreAnomalyDetector(1.0) {
176+
input.foreach { e => do emit(e) }
177+
}
178+
}
179+
}
180+
181+
test("mean threshold with single value") {
182+
val input = eventsFromList([5.0])
183+
// First value: mean = 5.0, deviation = 0.0
184+
val expected = [
185+
AnomalyEvaluation(Event(0, 5.0), false, 0.0)
186+
]
187+
assertAnomalyDetection(expected) {
188+
anomaly_detection::meanThresholdAnomalyDetector(1.0) {
189+
input.foreach { e => do emit(e) }
190+
}
191+
}
192+
}
193+
194+
test("mean threshold with zero threshold flags all deviations") {
195+
val input = eventsFromList([1.0, 2.0])
196+
// First: mean=1, dev=0 (not anomaly)
197+
// Second: mean=1.5, dev=0.5 (anomaly since threshold=0)
198+
val expected = [
199+
AnomalyEvaluation(Event(0, 1.0), false, 0.0),
200+
AnomalyEvaluation(Event(1, 2.0), true, 0.5)
201+
]
202+
assertAnomalyDetection(expected) {
203+
anomaly_detection::meanThresholdAnomalyDetector(0.0) {
204+
input.foreach { e => do emit(e) }
205+
}
206+
}
207+
}
110208
}
111209

112210
def main() = {

0 commit comments

Comments
 (0)