Skip to content

Commit 435ef6c

Browse files
committed
feat: tests
1 parent eab5556 commit 435ef6c

File tree

2 files changed

+240
-36
lines changed

2 files changed

+240
-36
lines changed

kmp/src/nativeTest/kotlin/com/scorbutics/rubyvm/RubyVMGCStressTest.kt

Lines changed: 159 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,202 @@ package com.scorbutics.rubyvm
33
import kotlin.test.Test
44
import kotlin.test.assertEquals
55
import kotlin.test.assertTrue
6+
import kotlinx.atomicfu.atomic
67
import kotlinx.cinterop.ExperimentalForeignApi
7-
import kotlinx.cinterop.alloc
8-
import kotlinx.cinterop.convert
9-
import kotlinx.cinterop.memScoped
10-
import kotlinx.cinterop.ptr
118
import platform.posix.*
129

1310
/**
14-
* Smoke tests for Ruby VM on Kotlin/Native.
11+
* Stress test for Ruby VM GC and signal handling on Kotlin/Native.
12+
*
13+
* This test verifies that the Ruby VM can handle:
14+
* 1. Rapid script execution with heavy Ruby object allocation
15+
* 2. Concurrent Ruby GC without crashes
16+
* 3. Signal masking preventing Ruby GC signals from crashing native threads
17+
*
18+
* Context: Ruby's GC uses signals (SIGPROF/SIGALRM) to pause threads during
19+
* marking phase. When these signals hit non-Ruby threads, they can cause segfaults.
20+
* This test ensures the signal masking in ruby_vm_execute_sync prevents this.
1521
*
1622
* Note: Ruby cannot be re-initialized after cleanup in the same process,
17-
* so all tests share a single interpreter lifecycle within one test method.
23+
* so all test phases share a single interpreter lifecycle within one test method.
1824
*/
1925
@OptIn(ExperimentalForeignApi::class)
20-
class RubyVMSmokeTest {
26+
class RubyVMGCStressTest {
2127

2228
private fun createListener(): LogListener {
2329
return object : LogListener {
2430
override fun onLogMessage(logMessage: LogMessage) {
2531
val prefix = if (logMessage.isError()) "ERROR" else "LOG"
26-
println(" [$prefix][${logMessage.source}] ${logMessage.message}")
32+
if (logMessage.isError() || logMessage.message.contains("ERROR") || logMessage.message.contains("WARN")) {
33+
println(" [$prefix][${logMessage.source}] ${logMessage.message}")
34+
}
2735
}
2836
}
2937
}
3038

3139
@Test
32-
fun testInterpreterLifecycle() {
33-
println("=== Test: Interpreter Lifecycle ===")
40+
fun testGCStress() {
41+
println("=== Ruby VM GC Stress Test (Native) ===")
3442

35-
// Phase 1: Bootstrap and create
36-
println(" Bootstrapping Ruby VM paths...")
3743
val paths = RubyVMPaths.getDefaultPaths()
38-
println(" rubyBaseDir: ${paths.rubyBaseDir}")
39-
println(" nativeLibsDir: ${paths.nativeLibsDir}")
40-
41-
println(" Creating interpreter...")
4244
val interpreter = RubyInterpreter.create(
4345
appPath = ".",
4446
rubyBaseDir = paths.rubyBaseDir,
4547
nativeLibsDir = paths.nativeLibsDir,
4648
listener = createListener()
4749
)
48-
println(" Interpreter created successfully")
4950

5051
try {
51-
// Phase 2: Execute a single script
52-
val latch = NativeCountDownLatch(1)
53-
var resultCode = -1
52+
// Phase 1: Rapid script execution with GC
53+
testRapidScriptExecutionWithGC(interpreter)
54+
55+
// Phase 2: Synchronous execution with GC
56+
testSynchronousExecutionWithGC(interpreter)
57+
58+
// Phase 3: Memory pressure with GC
59+
testMemoryPressureWithGC(interpreter)
60+
61+
} finally {
62+
println()
63+
println("Cleaning up interpreter...")
64+
interpreter.destroy()
65+
println("Cleanup complete")
66+
}
67+
}
68+
69+
private fun testRapidScriptExecutionWithGC(interpreter: RubyInterpreter) {
70+
println()
71+
println("--- Phase 1: Rapid Script Execution with GC ---")
72+
73+
val scriptCount = 50
74+
val completedCount = atomic(0)
75+
val failureCount = atomic(0)
76+
val latch = NativeCountDownLatch(scriptCount)
5477

55-
val script = RubyScript.fromContent("puts 'Hello from Ruby on Kotlin/Native!'")
56-
println(" Enqueuing script...")
57-
fflush(null)
78+
println("Starting stress test with $scriptCount scripts...")
79+
80+
repeat(scriptCount) { i ->
81+
val script = RubyScript.fromContent("""
82+
# Script $i - Do some work and trigger GC
83+
sum = 0
84+
100.times do |n|
85+
sum += n
86+
# Allocate some objects to trigger GC
87+
arr = Array.new(100) { |x| "string_#{x}_#{n}" }
88+
end
89+
90+
# Explicitly trigger Ruby GC on some scripts
91+
${if (i % 5 == 0) "GC.start" else "# No explicit GC"}
92+
93+
puts "Script $i completed: sum=#{sum}"
94+
""".trimIndent())
5895

5996
interpreter.enqueue(script) { exitCode ->
60-
println(" Script completed with exit code: $exitCode")
61-
resultCode = exitCode
97+
if (exitCode == 0) {
98+
completedCount.incrementAndGet()
99+
} else {
100+
failureCount.incrementAndGet()
101+
println("Script $i FAILED with exit code: $exitCode")
102+
}
62103
latch.countDown()
63104
script.close()
64105
}
65106

66-
println(" Enqueue returned, waiting for script (timeout: 30s)...")
67-
fflush(null)
68-
val completed = latch.await(30)
107+
// Small delay to stagger script submissions
108+
usleep(10_000u) // 10ms
109+
}
69110

70-
assertTrue(completed, "Script did not complete within 30 seconds")
71-
assertEquals(0, resultCode, "Script exited with non-zero code")
72-
println(" Single script execution PASSED")
111+
println("Waiting for scripts to complete...")
112+
val completed = latch.await(60)
73113

74-
} finally {
75-
// Phase 3: Destroy
76-
println(" Destroying interpreter...")
77-
interpreter.destroy()
78-
println(" Interpreter destroyed successfully")
114+
println()
115+
println("Phase 1 Results:")
116+
println(" Scripts completed: ${completedCount.value}/$scriptCount")
117+
println(" Scripts failed: ${failureCount.value}")
118+
println(" Timeout: ${!completed}")
119+
120+
assertTrue(completed, "Test timed out waiting for scripts to complete")
121+
assertEquals(0, failureCount.value, "Some scripts failed")
122+
assertEquals(scriptCount, completedCount.value, "Not all scripts completed")
123+
124+
println(" Phase 1 PASSED")
125+
}
126+
127+
private fun testSynchronousExecutionWithGC(interpreter: RubyInterpreter) {
128+
println()
129+
println("--- Phase 2: Synchronous Execution with GC ---")
130+
131+
val completedCount = atomic(0)
132+
val scriptCount = 20
133+
val latch = NativeCountDownLatch(scriptCount)
134+
135+
repeat(scriptCount) { i ->
136+
val script = RubyScript.fromContent("""
137+
puts "Sync script $i starting"
138+
arr = []
139+
1000.times { |n| arr << "item_#{n}" }
140+
${if (i % 3 == 0) "GC.start" else ""}
141+
puts "Sync script $i completed"
142+
""".trimIndent())
143+
144+
interpreter.enqueue(script) { exitCode ->
145+
println(" Script $i completed with exit code: $exitCode")
146+
completedCount.incrementAndGet()
147+
latch.countDown()
148+
script.close()
149+
}
150+
151+
usleep(50_000u) // 50ms
152+
}
153+
154+
println("Waiting for all callbacks to complete...")
155+
val completed = latch.await(60)
156+
assertTrue(completed, "Test timed out waiting for callbacks")
157+
158+
println()
159+
println(" Phase 2 PASSED (completed: ${completedCount.value}/$scriptCount)")
160+
}
161+
162+
private fun testMemoryPressureWithGC(interpreter: RubyInterpreter) {
163+
println()
164+
println("--- Phase 3: Memory Pressure + GC ---")
165+
166+
val scriptCount = 10
167+
val latch = NativeCountDownLatch(scriptCount)
168+
val errors = atomic(0)
169+
170+
println("Executing memory-intensive scripts...")
171+
172+
repeat(scriptCount) { i ->
173+
val script = RubyScript.fromContent("""
174+
# Allocate significant memory
175+
big_arrays = []
176+
10.times do
177+
arr = Array.new(10000) { |n| "string_data_#{n}_#{rand(1000)}" }
178+
big_arrays << arr
179+
GC.start if rand(3) == 0
180+
end
181+
182+
# Force full GC
183+
GC.start
184+
185+
puts "Memory test $i complete"
186+
""".trimIndent())
187+
188+
interpreter.enqueue(script) { exitCode ->
189+
if (exitCode != 0) {
190+
errors.incrementAndGet()
191+
}
192+
latch.countDown()
193+
script.close()
194+
}
195+
196+
usleep(100_000u) // 100ms
79197
}
198+
199+
assertTrue(latch.await(30), "Memory pressure test timed out")
200+
assertEquals(0, errors.value, "Scripts failed under memory pressure")
201+
202+
println(" Phase 3 PASSED")
80203
}
81204
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.scorbutics.rubyvm
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertTrue
6+
import kotlinx.cinterop.ExperimentalForeignApi
7+
import kotlinx.cinterop.alloc
8+
import kotlinx.cinterop.convert
9+
import kotlinx.cinterop.memScoped
10+
import kotlinx.cinterop.ptr
11+
import platform.posix.*
12+
13+
/**
14+
* Smoke tests for Ruby VM on Kotlin/Native.
15+
*
16+
* Note: Ruby cannot be re-initialized after cleanup in the same process,
17+
* so all tests share a single interpreter lifecycle within one test method.
18+
*/
19+
@OptIn(ExperimentalForeignApi::class)
20+
class RubyVMSmokeTest {
21+
22+
private fun createListener(): LogListener {
23+
return object : LogListener {
24+
override fun onLogMessage(logMessage: LogMessage) {
25+
val prefix = if (logMessage.isError()) "ERROR" else "LOG"
26+
println(" [$prefix][${logMessage.source}] ${logMessage.message}")
27+
}
28+
}
29+
}
30+
31+
@Test
32+
fun testInterpreterLifecycle() {
33+
println("=== Test: Interpreter Lifecycle ===")
34+
35+
// Phase 1: Bootstrap and create
36+
println(" Bootstrapping Ruby VM paths...")
37+
val paths = RubyVMPaths.getDefaultPaths()
38+
println(" rubyBaseDir: ${paths.rubyBaseDir}")
39+
println(" nativeLibsDir: ${paths.nativeLibsDir}")
40+
41+
println(" Creating interpreter...")
42+
val interpreter = RubyInterpreter.create(
43+
appPath = ".",
44+
rubyBaseDir = paths.rubyBaseDir,
45+
nativeLibsDir = paths.nativeLibsDir,
46+
listener = createListener()
47+
)
48+
println(" Interpreter created successfully")
49+
50+
try {
51+
// Phase 2: Execute a single script
52+
val latch = NativeCountDownLatch(1)
53+
var resultCode = -1
54+
55+
val script = RubyScript.fromContent("puts 'Hello from Ruby on Kotlin/Native!'")
56+
println(" Enqueuing script...")
57+
fflush(null)
58+
59+
interpreter.enqueue(script) { exitCode ->
60+
println(" Script completed with exit code: $exitCode")
61+
resultCode = exitCode
62+
latch.countDown()
63+
script.close()
64+
}
65+
66+
println(" Enqueue returned, waiting for script (timeout: 30s)...")
67+
fflush(null)
68+
val completed = latch.await(30)
69+
70+
assertTrue(completed, "Script did not complete within 30 seconds")
71+
assertEquals(0, resultCode, "Script exited with non-zero code")
72+
println(" Single script execution PASSED")
73+
74+
} finally {
75+
// Phase 3: Destroy
76+
println(" Destroying interpreter...")
77+
interpreter.destroy()
78+
println(" Interpreter destroyed successfully")
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)