-
Notifications
You must be signed in to change notification settings - Fork 1
Replace timeoutMs: Long with timeout: Duration across the timeout API
#9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| package io.github.anschnapp.mutflow | ||
|
|
||
| import java.time.Duration | ||
| import java.util.UUID | ||
| import java.util.concurrent.ConcurrentHashMap | ||
| import kotlin.random.Random | ||
|
|
@@ -58,7 +59,7 @@ object MutFlow { | |
| traps: List<String> = emptyList(), | ||
| includeTargets: List<String> = emptyList(), | ||
| excludeTargets: List<String> = emptyList(), | ||
| timeoutMs: Long = 60_000, | ||
| timeout: Duration = Duration.ofMinutes(1) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comma is missing after this argument, so the build fails. i guess this was coming in when you merged from master (so some kind of merge issue) i'm also not sure if this is the only one issue of this kind. |
||
| verificationMode: VerificationMode = VerificationMode.STRICT | ||
| ): SessionId { | ||
| val id = SessionId(UUID.randomUUID()) | ||
|
|
@@ -71,7 +72,7 @@ object MutFlow { | |
| traps = traps, | ||
| includeTargets = includeTargets, | ||
| excludeTargets = excludeTargets, | ||
| timeoutMs = timeoutMs, | ||
| timeout = timeout | ||
| verificationMode = verificationMode | ||
| ) | ||
| sessions[id] = session | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| package io.github.anschnapp.mutflow | ||
|
|
||
| import java.time.Duration | ||
| import kotlin.test.* | ||
|
|
||
| class SessionTimeoutTest { | ||
|
|
||
| @BeforeTest | ||
| fun setup() { | ||
| MutationRegistry.reset() | ||
| MutFlow.reset() | ||
| } | ||
|
|
||
| private fun simulateMutationPoint(): Int? = | ||
| MutationRegistry.check("point-a", 2, "Test.kt:1", ">", ">=,<,==") | ||
|
|
||
| // Helper that runs a baseline and returns the session id, leaving the session open. | ||
| private fun sessionWithBaseline(timeout: Duration): SessionId { | ||
| val sessionId = MutFlow.createSession( | ||
| selection = Selection.MostLikelyStable, | ||
| shuffle = Shuffle.PerChange, | ||
| maxRuns = 10, | ||
| timeout = timeout | ||
| ) | ||
| MutFlow.startRun(sessionId, 0) | ||
| MutFlow.underTest { simulateMutationPoint() } | ||
| MutFlow.endRun(sessionId) | ||
| return sessionId | ||
| } | ||
|
|
||
| @Test | ||
| fun `checkTimeout is a no-op when no session is active`() { | ||
| // Should never throw without an active session | ||
| MutationRegistry.checkTimeout() | ||
| } | ||
|
|
||
| @Test | ||
| fun `baseline run never times out even with a very short timeout configured`() { | ||
| // Baseline uses withSession(activeMutation = null), so no deadline is set — | ||
| // the timeout parameter only applies to mutation runs. | ||
| val sessionId = MutFlow.createSession( | ||
| selection = Selection.MostLikelyStable, | ||
| shuffle = Shuffle.PerChange, | ||
| maxRuns = 10, | ||
| timeout = Duration.ofMillis(1) | ||
| ) | ||
|
|
||
| MutFlow.startRun(sessionId, 0) | ||
| MutFlow.underTest { | ||
| simulateMutationPoint() | ||
| Thread.sleep(10) // sleep well past the 1 ms configured timeout | ||
| MutationRegistry.checkTimeout() // must not throw | ||
| } | ||
| MutFlow.endRun(sessionId) | ||
|
|
||
| MutFlow.closeSession(sessionId) | ||
| } | ||
|
|
||
| @Test | ||
| fun `mutation run throws MutationTimedOutException when deadline is exceeded`() { | ||
| val sessionId = sessionWithBaseline(Duration.ofMillis(1)) | ||
|
|
||
| val mutation = MutFlow.selectMutationForRun(sessionId, 1)!! | ||
| MutFlow.startRun(sessionId, 1, mutation) | ||
|
|
||
| assertFailsWith<MutationTimedOutException> { | ||
| MutFlow.underTest { | ||
| Thread.sleep(10) // exceed the 1 ms deadline | ||
| MutationRegistry.checkTimeout() | ||
| } | ||
| } | ||
|
|
||
| MutFlow.endRun(sessionId) | ||
| MutFlow.closeSession(sessionId) | ||
| } | ||
|
|
||
| @Test | ||
| fun `mutation run with generous timeout completes without timing out`() { | ||
| val sessionId = sessionWithBaseline(Duration.ofSeconds(30)) | ||
|
|
||
| val mutation = MutFlow.selectMutationForRun(sessionId, 1)!! | ||
| MutFlow.startRun(sessionId, 1, mutation) | ||
|
|
||
| // Multiple checkTimeout calls should be fine within the deadline | ||
| MutFlow.underTest { | ||
| repeat(5) { MutationRegistry.checkTimeout() } | ||
| } | ||
|
|
||
| MutFlow.endRun(sessionId) | ||
| MutFlow.closeSession(sessionId) | ||
| } | ||
|
|
||
| @Test | ||
| fun `timed-out mutation is recorded as TimedOut in summary`() { | ||
| val sessionId = sessionWithBaseline(Duration.ofMillis(1)) | ||
|
|
||
| val mutation = MutFlow.selectMutationForRun(sessionId, 1)!! | ||
| MutFlow.startRun(sessionId, 1, mutation) | ||
| val session = MutFlow.getSession(sessionId)!! | ||
|
|
||
| try { | ||
| MutFlow.underTest { | ||
| Thread.sleep(10) | ||
| MutationRegistry.checkTimeout() | ||
| } | ||
| } catch (_: MutationTimedOutException) { | ||
| // Simulates what MutFlowExtension.TestExecutionExceptionHandler does | ||
| session.markTestTimedOut() | ||
| } | ||
|
|
||
| session.recordMutationResult() | ||
| MutFlow.endRun(sessionId) | ||
|
|
||
| val summary = session.getSummary() | ||
| assertEquals(1, summary.timedOut) | ||
| assertEquals(0, summary.survived) | ||
| assertEquals(0, summary.killed) | ||
|
|
||
| MutFlow.closeSession(sessionId) | ||
| } | ||
|
|
||
| @Test | ||
| fun `timed-out mutation does not count as survived`() { | ||
| val sessionId = sessionWithBaseline(Duration.ofMillis(1)) | ||
|
|
||
| val mutation = MutFlow.selectMutationForRun(sessionId, 1)!! | ||
| MutFlow.startRun(sessionId, 1, mutation) | ||
| val session = MutFlow.getSession(sessionId)!! | ||
|
|
||
| try { | ||
| MutFlow.underTest { | ||
| Thread.sleep(10) | ||
| MutationRegistry.checkTimeout() | ||
| } | ||
| } catch (_: MutationTimedOutException) { | ||
| session.markTestTimedOut() | ||
| } | ||
|
|
||
| assertFalse(session.didMutationSurvive(), "A timed-out mutation must not be considered survived") | ||
|
|
||
| MutFlow.endRun(sessionId) | ||
| MutFlow.closeSession(sessionId) | ||
| } | ||
|
|
||
| @Test | ||
| fun `multiple checkTimeout calls before deadline do not throw`() { | ||
| val sessionId = sessionWithBaseline(Duration.ofSeconds(30)) | ||
|
|
||
| val mutation = MutFlow.selectMutationForRun(sessionId, 1)!! | ||
| MutFlow.startRun(sessionId, 1, mutation) | ||
|
|
||
| MutFlow.underTest { | ||
| // Simulates a loop body checked many times — all within the deadline | ||
| repeat(100) { MutationRegistry.checkTimeout() } | ||
| } | ||
|
|
||
| MutFlow.endRun(sessionId) | ||
| MutFlow.closeSession(sessionId) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old code used System.nanoTime() which is monotonic -- it only moves forward, regardless of what happens to the system clock.
Here you use Instant.now() which reads the wall clock and can jump backward or forward due to NTP sync, clock adjustments etc...
For a timeout that detects infinite loops in mutation runs:
I would say let's keep the Duration as the public API type (that's a good improvement), but internally still convert to a System.nanoTime()-based deadline for the actual expiry check.