Skip to content

Commit ecf2ec5

Browse files
committed
uts: add nanoTime support to Clock and update usages for consistency
- moved uts infrastructure related files into `uts.infra` - updated skill - simplify code in constructor
1 parent ce4f2f2 commit ecf2ec5

22 files changed

Lines changed: 121 additions & 68 deletions

File tree

.claude/skills/uts-to-kotlin/SKILL.md

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,46 @@ allowed-tools: Bash, Read, Edit, Write
55

66
Translate the UTS pseudocode test spec at `$ARGUMENTS` into a runnable Kotlin test in the `uts` module.
77

8-
Reference: [Writing Derived Tests](https://github.com/ably/specification/blob/main/uts/docs/writing-derived-tests.md)
8+
Reference: [Writing Derived Tests](https://raw.githubusercontent.com/ably/specification/refs/heads/main/uts/docs/writing-derived-tests.md)
9+
10+
---
11+
12+
## Step 0 — Validate arguments
13+
14+
**If `$ARGUMENTS` is empty or blank**, stop immediately and tell the user:
15+
16+
```
17+
Usage: /uts-to-kotlin <path-to-spec-file>
18+
19+
Example:
20+
/uts-to-kotlin lib/src/spec/uts/test/realtime/unit/connection/connection_state_machine_test.md
21+
22+
Please re-run the command with the path to a UTS pseudocode spec file.
23+
```
24+
25+
Do not proceed to Step 1.
26+
27+
**If `$ARGUMENTS` is provided but does not end in `.md`**, stop and tell the user:
28+
29+
```
30+
Error: "<value>" does not look like a spec file path (expected a .md file).
31+
32+
Usage: /uts-to-kotlin <path-to-spec-file>
33+
```
34+
35+
Do not proceed to Step 1.
36+
37+
**If `$ARGUMENTS` ends in `.md` but the file does not exist** (check with `test -f "$ARGUMENTS"`), stop and tell the user:
38+
39+
```
40+
Error: file not found: "<value>"
41+
42+
Check the path and try again.
43+
```
44+
45+
Do not proceed to Step 1.
46+
47+
Only continue to Step 1 once the file is confirmed to exist.
948

1049
---
1150

@@ -37,19 +76,7 @@ Package: derived from the output path under `kotlin/`.
3776

3877
## Step 3 — Read infrastructure files
3978

40-
Read ALL of these before generating any code (you need exact method signatures):
41-
42-
```
43-
uts/src/test/kotlin/io/ably/lib/ClientFactories.kt
44-
uts/src/test/kotlin/io/ably/lib/test/mock/MockWebSocket.kt
45-
uts/src/test/kotlin/io/ably/lib/test/mock/MockHttpClient.kt
46-
uts/src/test/kotlin/io/ably/lib/test/mock/PendingRequest.kt
47-
uts/src/test/kotlin/io/ably/lib/test/mock/PendingConnection.kt
48-
uts/src/test/kotlin/io/ably/lib/test/mock/FakeClock.kt
49-
uts/src/test/kotlin/io/ably/lib/test/mock/MockEvent.kt
50-
```
51-
52-
---
79+
Read ALL of files in `uts/src/test/kotlin/io/ably/lib/uts/infra` before generating any code (you need exact method signatures).
5380

5481
## Step 4 — Generate the Kotlin test file
5582

@@ -332,7 +359,7 @@ Test fails
332359
@Test
333360
fun `RSA4c2 - callback error connecting disconnected`() = runTest {
334361
// DEVIATION: see deviations.md
335-
if (System.getenv("RUN_DEVIATIONS") != null) return@runTest
362+
if (System.getenv("RUN_DEVIATIONS") == null) return@runTest
336363

337364
// ... spec-correct setup and assertions ...
338365
}

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
import io.ably.lib.util.EventEmitter;
5151
import io.ably.lib.util.Listeners;
5252
import io.ably.lib.util.Log;
53-
import io.ably.lib.util.NamedTimer;
53+
import io.ably.lib.util.AblyTimer;
5454
import io.ably.lib.util.ReconnectionStrategy;
5555
import io.ably.lib.util.StringUtils;
5656
import io.ably.lib.util.SystemClock;
@@ -510,18 +510,18 @@ private void setFailed(ErrorInfo reason) {
510510
}
511511

512512
/* Timer for attach operation */
513-
private NamedTimer attachTimer;
513+
private AblyTimer attachTimer;
514514

515515
/* Timer for reattaching if attach failed */
516-
private NamedTimer reattachTimer;
516+
private AblyTimer reattachTimer;
517517

518518
/**
519519
* Cancel attach/reattach timers
520520
*/
521521
synchronized private void clearAttachTimers() {
522-
NamedTimer[] timers = new NamedTimer[]{attachTimer, reattachTimer};
522+
AblyTimer[] timers = new AblyTimer[]{attachTimer, reattachTimer};
523523
attachTimer = reattachTimer = null;
524-
for (NamedTimer t: timers) {
524+
for (AblyTimer t: timers) {
525525
if (t != null) {
526526
t.cancel();
527527
}
@@ -538,7 +538,7 @@ private void attachWithTimeout(final CompletionListener listener) throws AblyExc
538538
*/
539539
synchronized private void attachWithTimeout(final boolean forceReattach, final CompletionListener listener, ErrorInfo reattachmentReason) {
540540
checkChannelIsNotReleased();
541-
NamedTimer currentAttachTimer;
541+
AblyTimer currentAttachTimer;
542542
try {
543543
currentAttachTimer = clock.newTimer("attach-timer");
544544
} catch(Throwable t) {
@@ -572,7 +572,7 @@ public void onError(ErrorInfo reason) {
572572
return;
573573
}
574574

575-
final NamedTimer inProgressTimer = currentAttachTimer;
575+
final AblyTimer inProgressTimer = currentAttachTimer;
576576
attachTimer.schedule(
577577
new TimerTask() {
578578
@Override
@@ -602,7 +602,7 @@ private void checkChannelIsNotReleased() {
602602
* try to attach the channel
603603
*/
604604
synchronized private void reattachAfterTimeout() {
605-
NamedTimer currentReattachTimer;
605+
AblyTimer currentReattachTimer;
606606
try {
607607
currentReattachTimer = clock.newTimer("reattach-timer");
608608
} catch(Throwable t) {
@@ -614,7 +614,7 @@ synchronized private void reattachAfterTimeout() {
614614
this.retryAttempt++;
615615
int retryDelay = ReconnectionStrategy.getRetryTime(ably.options.channelRetryTimeout, retryAttempt);
616616

617-
final NamedTimer inProgressTimer = currentReattachTimer;
617+
final AblyTimer inProgressTimer = currentReattachTimer;
618618
reattachTimer.schedule(new TimerTask() {
619619
@Override
620620
public void run() {
@@ -641,7 +641,7 @@ public void run() {
641641
*/
642642
synchronized private void detachWithTimeout(final CompletionListener listener) {
643643
final ChannelState originalState = state;
644-
NamedTimer currentDetachTimer;
644+
AblyTimer currentDetachTimer;
645645
try {
646646
currentDetachTimer = released.get() ? null : clock.newTimer("detach-timer");
647647
} catch(Throwable t) {
@@ -677,7 +677,7 @@ public void onError(ErrorInfo reason) {
677677
return;
678678
}
679679

680-
final NamedTimer inProgressTimer = currentDetachTimer;
680+
final AblyTimer inProgressTimer = currentDetachTimer;
681681
attachTimer.schedule(new TimerTask() {
682682
@Override
683683
public void run() {

lib/src/main/java/io/ably/lib/rest/Auth.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ else if(!request.keyName.equals(keyName))
923923
if(request.timestamp == 0) {
924924
if(options.queryTime) {
925925
long oldNanoTimeDelta = nanoTimeDelta;
926-
long currentNanoTimeDelta = clock.currentTimeMillis() - System.nanoTime()/(1000*1000);
926+
long currentNanoTimeDelta = clock.currentTimeMillis() - clock.nanoTime()/(1000*1000);
927927

928928
if (timeDelta != Long.MAX_VALUE) {
929929
/* system time changed by more than 500ms since last time? */
@@ -1053,7 +1053,7 @@ public void onAuthError(ErrorInfo err) {
10531053
Auth(AblyBase ably, ClientOptions options) throws AblyException {
10541054
this.ably = ably;
10551055
this.clock = SystemClock.clockFrom(options);
1056-
this.nanoTimeDelta = clock.currentTimeMillis() - System.nanoTime()/(1000*1000);
1056+
this.nanoTimeDelta = clock.currentTimeMillis() - clock.nanoTime()/(1000*1000);
10571057
authOptions = options;
10581058
tokenParams = options.defaultTokenParams != null ?
10591059
options.defaultTokenParams : new TokenParams();

lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import io.ably.lib.util.Clock;
1818
import io.ably.lib.util.ClientOptionsUtils;
1919
import io.ably.lib.util.Log;
20-
import io.ably.lib.util.NamedTimer;
20+
import io.ably.lib.util.AblyTimer;
2121
import io.ably.lib.util.SystemClock;
2222
import io.ably.lib.util.TimerInstance;
2323

@@ -261,7 +261,7 @@ class WebSocketHandler implements WebSocketListener {
261261
* WsClient private members
262262
***************************/
263263

264-
private final NamedTimer timer = clock.newTimer("activity-timer");
264+
private final AblyTimer timer = clock.newTimer("activity-timer");
265265
private volatile TimerInstance activityTimerHandle = null;
266266
private volatile long lastActivityTime;
267267

lib/src/main/java/io/ably/lib/util/NamedTimer.java renamed to lib/src/main/java/io/ably/lib/util/AblyTimer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.util.TimerTask;
44

5-
public interface NamedTimer {
5+
public interface AblyTimer {
66
TimerInstance schedule(TimerTask task, long delayMs);
77
void cancel();
88
}

lib/src/main/java/io/ably/lib/util/Clock.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,23 @@ public interface Clock {
1919
long currentTimeMillis();
2020

2121
/**
22-
* Creates a new {@link NamedTimer} backed by this clock.
22+
* Returns the current wall-clock time in nanoseconds since the Unix epoch
23+
* (1 January 1970 00:00:00 UTC), analogous to {@link System#nanoTime()}.
24+
*
25+
* @return current time in nanoseconds
26+
*/
27+
long nanoTime();
28+
29+
/**
30+
* Creates a new {@link AblyTimer} backed by this clock.
2331
*
2432
* <p>The name is used for diagnostic and logging purposes (e.g. as the underlying
2533
* {@link java.util.Timer} thread name).
2634
*
2735
* @param name a human-readable label for the timer; must not be {@code null}
28-
* @return a new {@link NamedTimer} instance ready to schedule tasks
36+
* @return a new {@link AblyTimer} instance ready to schedule tasks
2937
*/
30-
NamedTimer newTimer(String name);
38+
AblyTimer newTimer(String name);
3139

3240
/**
3341
* Causes the current thread to wait until either another thread calls

lib/src/main/java/io/ably/lib/util/SystemClock.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ public long currentTimeMillis() {
1515
}
1616

1717
@Override
18-
public NamedTimer newTimer(String name) {
18+
public long nanoTime() {
19+
return System.nanoTime();
20+
}
21+
22+
@Override
23+
public AblyTimer newTimer(String name) {
1924
Timer jTimer = new Timer(name, true);
20-
return new NamedTimer() {
25+
return new AblyTimer() {
2126
@Override
2227
public TimerInstance schedule(TimerTask task, long delayMs) {
2328
jTimer.schedule(task, delayMs);

liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import io.ably.lib.realtime.ChannelState
1313
import io.ably.lib.types.AblyException
1414
import io.ably.lib.types.ProtocolMessage
1515
import io.ably.lib.types.PublishResult
16+
import io.ably.lib.util.Clock
1617
import io.ably.lib.util.Log
18+
import io.ably.lib.util.SystemClock
1719
import kotlinx.coroutines.*
1820
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
1921
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -361,3 +363,10 @@ internal class DefaultRealtimeObjects(internal val channelName: String, internal
361363
asyncScope.cancel(disposeReason) // cancel all ongoing callbacks
362364
}
363365
}
366+
367+
/**
368+
* Provides the default Clock instance for the DefaultRealtimeObjects.
369+
* This Clock is derived from the system clock, utilizing the client options
370+
* from the adapter configuration.
371+
*/
372+
internal val DefaultRealtimeObjects.clock get(): Clock = SystemClock.clockFrom(adapter.clientOptions)

liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import kotlinx.coroutines.runBlocking
2323
internal class DefaultLiveCounter private constructor(
2424
objectId: String,
2525
private val realtimeObjects: DefaultRealtimeObjects,
26-
) : LiveCounter, BaseRealtimeObject(objectId, ObjectType.Counter, SystemClock.clockFrom(realtimeObjects.adapter.clientOptions)) {
26+
) : LiveCounter, BaseRealtimeObject(objectId, ObjectType.Counter, realtimeObjects.clock) {
2727

2828
override val tag = "LiveCounter"
2929

liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal class DefaultLiveMap private constructor(
3030
objectId: String,
3131
private val realtimeObjects: DefaultRealtimeObjects,
3232
internal val semantics: ObjectsMapSemantics = ObjectsMapSemantics.LWW
33-
) : LiveMap, BaseRealtimeObject(objectId, ObjectType.Map, SystemClock.clockFrom(realtimeObjects.adapter.clientOptions)) {
33+
) : LiveMap, BaseRealtimeObject(objectId, ObjectType.Map, realtimeObjects.clock) {
3434

3535
override val tag = "LiveMap"
3636

0 commit comments

Comments
 (0)