Restructure step 0 done: notice table test, flaky-link proxy, liveness on the engine clock, STARSYNC_PROFILE
- notice_table_test: one row per AppEvent variant through noticeFor. - FlakyProxy (core/testing.dart): pass, blackhole (half-open), delay, hold, cut after N bytes, reset; demo tests against today's server. - Liveness (stale/drop/ping) on the engine clock, so grace can be tested with a fake clock; Connection takes an injectable clock. - AppDirs with STARSYNC_PROFILE=<name>: separate data and logs, so two instances run on one machine. - DEVELOPMENT: scenario list per restructure step; test tags follow the new step order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eac45746ce
commit
f4ca02c3cf
23 files changed
+959
-48
No files matched your search
@@ -0,0 +1,134 @@
|
||||
// The flaky-link proxy itself, against a loopback echo server.
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:sync_relay_core/testing.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// A client socket through the proxy, collecting what comes back.
|
||||
class _Client {
|
||||
final Socket socket;
|
||||
final received = <int>[];
|
||||
final done = Completer<Object?>();
|
||||
_Client(this.socket) {
|
||||
socket.done.then((_) {}, onError: (_) {});
|
||||
socket.listen(received.addAll,
|
||||
onDone: () => done.isCompleted ? null : done.complete(null),
|
||||
onError: (Object e) => done.isCompleted ? null : done.complete(e));
|
||||
}
|
||||
|
||||
static Future<_Client> dial(FlakyProxy p) async => _Client(await Socket.connect('127.0.0.1', p.port));
|
||||
|
||||
void send(String s) => socket.add(s.codeUnits);
|
||||
String get text => String.fromCharCodes(received);
|
||||
}
|
||||
|
||||
Future<void> until(bool Function() ok, {Duration limit = const Duration(seconds: 3)}) async {
|
||||
final end = DateTime.now().add(limit);
|
||||
while (!ok()) {
|
||||
if (DateTime.now().isAfter(end)) fail('condition not met in $limit');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late ServerSocket echo;
|
||||
late FlakyProxy proxy;
|
||||
|
||||
setUp(() async {
|
||||
echo = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||
echo.listen((s) {
|
||||
s.done.then((_) {}, onError: (_) {});
|
||||
s.listen(s.add, onDone: () => s.close().then((_) {}, onError: (_) {}), onError: (_) {});
|
||||
});
|
||||
proxy = await FlakyProxy.start(echo.port);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await proxy.close();
|
||||
await echo.close();
|
||||
});
|
||||
|
||||
test('passes bytes both ways and counts them', () async {
|
||||
final c = await _Client.dial(proxy);
|
||||
c.send('hello');
|
||||
await until(() => c.text == 'hello');
|
||||
expect(proxy.links, hasLength(1));
|
||||
expect(proxy.last!.bytesUp, 5);
|
||||
expect(proxy.last!.bytesDown, 5);
|
||||
await c.socket.close();
|
||||
await proxy.last!.closed.timeout(const Duration(seconds: 3));
|
||||
});
|
||||
|
||||
test('blackhole drops everything both ways and keeps the socket open', () async {
|
||||
final c = await _Client.dial(proxy);
|
||||
c.send('a');
|
||||
await until(() => c.text == 'a');
|
||||
proxy.last!.blackhole();
|
||||
c.send('bcd');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
expect(c.text, 'a');
|
||||
expect(proxy.last!.dropped, 3);
|
||||
expect(c.done.isCompleted, isFalse);
|
||||
expect(proxy.last!.isClosed, isFalse);
|
||||
});
|
||||
|
||||
test('delay holds every chunk', () async {
|
||||
final c = await _Client.dial(proxy);
|
||||
await until(() => proxy.last != null);
|
||||
proxy.last!.delay = const Duration(milliseconds: 150);
|
||||
final sw = Stopwatch()..start();
|
||||
c.send('x');
|
||||
await until(() => c.text == 'x');
|
||||
// Up and down each wait 150 ms.
|
||||
expect(sw.elapsedMilliseconds, greaterThanOrEqualTo(290));
|
||||
});
|
||||
|
||||
test('holdNext delivers one chunk late and keeps order', () async {
|
||||
final c = await _Client.dial(proxy);
|
||||
await until(() => proxy.last != null);
|
||||
proxy.last!.holdNext(const Duration(milliseconds: 300), flow: Flow.up);
|
||||
final sw = Stopwatch()..start();
|
||||
c.send('1');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
c.send('2');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
expect(c.text, '', reason: 'both still held');
|
||||
await until(() => c.text == '12');
|
||||
expect(sw.elapsedMilliseconds, greaterThanOrEqualTo(290));
|
||||
});
|
||||
|
||||
test('cutAfter delivers N bytes then resets', () async {
|
||||
final c = await _Client.dial(proxy);
|
||||
await until(() => proxy.last != null);
|
||||
proxy.last!.cutAfter(3, flow: Flow.up);
|
||||
c.send('abcdef');
|
||||
await c.done.future.timeout(const Duration(seconds: 3));
|
||||
expect(proxy.last!.bytesUp, 3);
|
||||
expect(proxy.last!.isClosed, isTrue);
|
||||
});
|
||||
|
||||
test('reset ends the client at once', () async {
|
||||
final c = await _Client.dial(proxy);
|
||||
c.send('a');
|
||||
await until(() => c.text == 'a');
|
||||
proxy.last!.reset();
|
||||
await c.done.future.timeout(const Duration(seconds: 3));
|
||||
expect(proxy.last!.isClosed, isTrue);
|
||||
});
|
||||
|
||||
test('onConnect can steer a link before any byte goes through', () async {
|
||||
proxy.onConnect = (l) => l.reset();
|
||||
// The RST can beat the connect's own completion.
|
||||
try {
|
||||
final c = await _Client.dial(proxy);
|
||||
await c.done.future.timeout(const Duration(seconds: 3));
|
||||
expect(c.text, '');
|
||||
} on Object catch (e) {
|
||||
// dart:io throws the bare OSError here (ECONNRESET), on some
|
||||
// platforms a SocketException.
|
||||
expect(e, anyOf(isA<OSError>(), isA<SocketException>()));
|
||||
}
|
||||
expect(proxy.last!.isClosed, isTrue);
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// groups "sitting out", "membership + gate", "presence gate", "stale ready
|
||||
// rejoins" and the member rules of "swap" pin today's presence (out flag,
|
||||
// stale sweep, leave on drop); presence In/Away/Gone with grace replaces
|
||||
// them. The clock, correction, end-of-file and wire groups stay.
|
||||
// them ("liveness on the engine clock" then steps through the grace). The clock, correction, end-of-file and wire groups stay.
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
@@ -618,6 +618,35 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('liveness on the engine clock', () {
|
||||
test('stale and drop follow Liveness and the fake clock, not wall time', () {
|
||||
engine = SyncEngine(
|
||||
clock: clock,
|
||||
liveness: const Liveness(staleAfter: Duration(minutes: 5), dropAfter: Duration(minutes: 10)));
|
||||
join('a', 'alice');
|
||||
engine.applyReady('a', true);
|
||||
engine.feed.drain();
|
||||
t = t.add(const Duration(minutes: 5));
|
||||
engine.tick();
|
||||
expect(engine.members['a']!.inSet, isTrue, reason: 'not past staleAfter yet');
|
||||
t = t.add(const Duration(seconds: 1));
|
||||
engine.tick();
|
||||
expect(engine.members['a']!.inSet, isFalse);
|
||||
expect(engine.feed.drain().map((f) => f.action), ['stale']);
|
||||
t = t.add(const Duration(minutes: 4, seconds: 59));
|
||||
expect(engine.sweepDropped(), isEmpty, reason: 'exactly dropAfter is not past it');
|
||||
t = t.add(const Duration(seconds: 1));
|
||||
expect(engine.sweepDropped(), ['a']);
|
||||
expect(engine.feed.drain().map((f) => f.action), ['leave']);
|
||||
});
|
||||
|
||||
test('defaults are today\'s 15 s / 30 s', () {
|
||||
expect(engine.staleAfter, const Duration(seconds: 15));
|
||||
expect(engine.dropAfter, const Duration(seconds: 30));
|
||||
expect(engine.liveness.pingInterval, const Duration(seconds: 5));
|
||||
});
|
||||
});
|
||||
|
||||
group('end of file', () {
|
||||
setUp(() {
|
||||
engine.media = const HelloMedia(name: 'f', size: 1, duration: 20);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// restructure: rewrite in step 5 (D3: the watched rule moves from position coverage to time present)
|
||||
// restructure: rewrite in step 6 (D3: the watched rule moves from position coverage to time present)
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
||||
Reference in new issue
Block a user