337 lines
11 KiB
Dart
337 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:proto_socket/proto_socket.dart';
|
|
|
|
import 'package:alt_client/src/contracts/alt_api_result.dart';
|
|
import 'package:alt_client/src/generated/alt/v1/common.pb.dart';
|
|
import 'package:alt_client/src/generated/alt/v1/backtest.pb.dart';
|
|
import 'package:alt_client/src/integrations/socket/alt_socket_client.dart';
|
|
import 'package:alt_client/src/features/backtest/data/backtest_repository.dart';
|
|
|
|
class FakeWebSocket extends Stream<dynamic> implements WebSocket {
|
|
final StreamController<dynamic> _controller = StreamController<dynamic>();
|
|
final List<List<int>> sentBytes = [];
|
|
bool isClosed = false;
|
|
|
|
@override
|
|
void add(dynamic data) {
|
|
if (data is List<int>) {
|
|
sentBytes.add(data);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> close([int? code, String? reason]) async {
|
|
isClosed = true;
|
|
await _controller.close();
|
|
}
|
|
|
|
void feedFromServer(List<int> data) {
|
|
_controller.add(data);
|
|
}
|
|
|
|
@override
|
|
StreamSubscription<dynamic> listen(
|
|
void Function(dynamic event)? onData, {
|
|
Function? onError,
|
|
void Function()? onDone,
|
|
bool? cancelOnError,
|
|
}) {
|
|
return _controller.stream.listen(
|
|
onData,
|
|
onError: onError,
|
|
onDone: onDone,
|
|
cancelOnError: cancelOnError,
|
|
);
|
|
}
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
void main() {
|
|
group('BacktestRepository tests', () {
|
|
late FakeWebSocket fakeWs;
|
|
late AltSocketClient client;
|
|
late BacktestRepository repository;
|
|
|
|
setUp(() {
|
|
fakeWs = FakeWebSocket();
|
|
client = AltSocketClient(
|
|
fakeWs,
|
|
heartbeatIntervalTime: 10,
|
|
heartbeatWaitTime: 10,
|
|
);
|
|
repository = BacktestRepository(client);
|
|
});
|
|
|
|
tearDown(() async {
|
|
await client.close();
|
|
});
|
|
|
|
Future<PacketBase> roundTrip({
|
|
required Future<void> Function() trigger,
|
|
required void Function(PacketBase sentPacket) onSent,
|
|
required String responseTypeName,
|
|
required List<int> responseData,
|
|
}) async {
|
|
final sentCountBefore = fakeWs.sentBytes.length;
|
|
unawaited(trigger());
|
|
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(fakeWs.sentBytes.length, greaterThan(sentCountBefore));
|
|
final sentPacket = PacketBase.fromBuffer(fakeWs.sentBytes.last);
|
|
onSent(sentPacket);
|
|
|
|
final responsePacket = PacketBase()
|
|
..typeName = responseTypeName
|
|
..nonce = 200
|
|
..responseNonce = sentPacket.nonce
|
|
..data = responseData;
|
|
fakeWs.feedFromServer(responsePacket.writeToBuffer());
|
|
|
|
return sentPacket;
|
|
}
|
|
|
|
test('listBacktestRuns success flows', () async {
|
|
final request = ListBacktestRunsRequest()
|
|
..status = BacktestRunStatus.BACKTEST_RUN_STATUS_RUNNING;
|
|
final expectedResponse = ListBacktestRunsResponse()
|
|
..runs.add(
|
|
BacktestRun()
|
|
..id = 'run-1'
|
|
..spec = (BacktestRunSpec()..strategyId = 'strat-1'),
|
|
);
|
|
|
|
final future = repository.listBacktestRuns(
|
|
request,
|
|
timeout: const Duration(seconds: 1),
|
|
);
|
|
|
|
await roundTrip(
|
|
trigger: () => future,
|
|
onSent: (sentPacket) {
|
|
expect(
|
|
sentPacket.typeName,
|
|
equals(
|
|
ListBacktestRunsRequest.getDefault().info_.qualifiedMessageName,
|
|
),
|
|
);
|
|
final sentRequest = ListBacktestRunsRequest.fromBuffer(
|
|
sentPacket.data,
|
|
);
|
|
expect(
|
|
sentRequest.status,
|
|
equals(BacktestRunStatus.BACKTEST_RUN_STATUS_RUNNING),
|
|
);
|
|
},
|
|
responseTypeName:
|
|
ListBacktestRunsResponse.getDefault().info_.qualifiedMessageName,
|
|
responseData: expectedResponse.writeToBuffer(),
|
|
);
|
|
|
|
final result = await future;
|
|
expect(result, isA<AltApiSuccess<ListBacktestRunsResponse>>());
|
|
final val = (result as AltApiSuccess<ListBacktestRunsResponse>).value;
|
|
expect(val.runs, hasLength(1));
|
|
expect(val.runs.first.id, equals('run-1'));
|
|
});
|
|
|
|
test('getBacktestRunDetail success flows', () async {
|
|
final request = GetBacktestRunDetailRequest()..runId = 'run-1';
|
|
final expectedResponse = GetBacktestRunDetailResponse()
|
|
..run = (BacktestRun()
|
|
..id = 'run-1'
|
|
..spec = (BacktestRunSpec()..strategyId = 'strat-1'));
|
|
|
|
final future = repository.getBacktestRunDetail(
|
|
request,
|
|
timeout: const Duration(seconds: 1),
|
|
);
|
|
|
|
await roundTrip(
|
|
trigger: () => future,
|
|
onSent: (sentPacket) {
|
|
expect(
|
|
sentPacket.typeName,
|
|
equals(
|
|
GetBacktestRunDetailRequest.getDefault()
|
|
.info_
|
|
.qualifiedMessageName,
|
|
),
|
|
);
|
|
final sentRequest = GetBacktestRunDetailRequest.fromBuffer(
|
|
sentPacket.data,
|
|
);
|
|
expect(sentRequest.runId, equals('run-1'));
|
|
},
|
|
responseTypeName: GetBacktestRunDetailResponse.getDefault()
|
|
.info_
|
|
.qualifiedMessageName,
|
|
responseData: expectedResponse.writeToBuffer(),
|
|
);
|
|
|
|
final result = await future;
|
|
expect(result, isA<AltApiSuccess<GetBacktestRunDetailResponse>>());
|
|
final val = (result as AltApiSuccess<GetBacktestRunDetailResponse>).value;
|
|
expect(val.run.id, equals('run-1'));
|
|
});
|
|
|
|
test('getBacktestResult success flows', () async {
|
|
final request = GetBacktestResultRequest()..runId = 'run-1';
|
|
final expectedResponse = GetBacktestResultResponse()
|
|
..result = (BacktestResult()
|
|
..runId = 'run-1'
|
|
..summary = (BacktestSummaryMetrics()..tradeCount = 42));
|
|
|
|
final future = repository.getBacktestResult(
|
|
request,
|
|
timeout: const Duration(seconds: 1),
|
|
);
|
|
|
|
await roundTrip(
|
|
trigger: () => future,
|
|
onSent: (sentPacket) {
|
|
expect(
|
|
sentPacket.typeName,
|
|
equals(
|
|
GetBacktestResultRequest.getDefault().info_.qualifiedMessageName,
|
|
),
|
|
);
|
|
final sentRequest = GetBacktestResultRequest.fromBuffer(
|
|
sentPacket.data,
|
|
);
|
|
expect(sentRequest.runId, equals('run-1'));
|
|
},
|
|
responseTypeName:
|
|
GetBacktestResultResponse.getDefault().info_.qualifiedMessageName,
|
|
responseData: expectedResponse.writeToBuffer(),
|
|
);
|
|
|
|
final result = await future;
|
|
expect(result, isA<AltApiSuccess<GetBacktestResultResponse>>());
|
|
final val = (result as AltApiSuccess<GetBacktestResultResponse>).value;
|
|
expect(val.result.runId, equals('run-1'));
|
|
expect(val.result.summary.tradeCount, equals(42));
|
|
});
|
|
|
|
test('startBacktest success flows', () async {
|
|
final request = StartBacktestRequest()
|
|
..spec = (BacktestRunSpec()
|
|
..strategyId = 'strat-1'
|
|
..market = Market.MARKET_US);
|
|
final expectedResponse = StartBacktestResponse()
|
|
..run = (BacktestRun()..id = 'run-new');
|
|
|
|
final future = repository.startBacktest(
|
|
request,
|
|
timeout: const Duration(seconds: 1),
|
|
);
|
|
|
|
await roundTrip(
|
|
trigger: () => future,
|
|
onSent: (sentPacket) {
|
|
expect(
|
|
sentPacket.typeName,
|
|
equals(
|
|
StartBacktestRequest.getDefault().info_.qualifiedMessageName,
|
|
),
|
|
);
|
|
final sentRequest = StartBacktestRequest.fromBuffer(sentPacket.data);
|
|
expect(sentRequest.spec.strategyId, equals('strat-1'));
|
|
expect(sentRequest.spec.market, equals(Market.MARKET_US));
|
|
},
|
|
responseTypeName:
|
|
StartBacktestResponse.getDefault().info_.qualifiedMessageName,
|
|
responseData: expectedResponse.writeToBuffer(),
|
|
);
|
|
|
|
final result = await future;
|
|
expect(result, isA<AltApiSuccess<StartBacktestResponse>>());
|
|
final val = (result as AltApiSuccess<StartBacktestResponse>).value;
|
|
expect(val.run.id, equals('run-new'));
|
|
});
|
|
|
|
test('ErrorInfo maps correctly to AltApiFailure across calls', () async {
|
|
final request = StartBacktestRequest()
|
|
..spec = (BacktestRunSpec()..strategyId = 'invalid-strat');
|
|
final expectedResponse = StartBacktestResponse()
|
|
..error = ErrorInfo(
|
|
code: 'STRATEGY_NOT_FOUND',
|
|
message: 'no strategy with id invalid-strat',
|
|
);
|
|
|
|
final future = repository.startBacktest(
|
|
request,
|
|
timeout: const Duration(seconds: 1),
|
|
);
|
|
|
|
await roundTrip(
|
|
trigger: () => future,
|
|
onSent: (sentPacket) {},
|
|
responseTypeName:
|
|
StartBacktestResponse.getDefault().info_.qualifiedMessageName,
|
|
responseData: expectedResponse.writeToBuffer(),
|
|
);
|
|
|
|
final result = await future;
|
|
expect(result, isA<AltApiFailure<StartBacktestResponse>>());
|
|
final failure = result as AltApiFailure<StartBacktestResponse>;
|
|
expect(failure.error.code, equals('STRATEGY_NOT_FOUND'));
|
|
expect(
|
|
failure.error.message,
|
|
equals('no strategy with id invalid-strat'),
|
|
);
|
|
});
|
|
|
|
test('timeout maps correctly to AltTransportKind.timeout', () async {
|
|
final request = StartBacktestRequest();
|
|
final result = await repository.startBacktest(
|
|
request,
|
|
timeout: const Duration(milliseconds: 10),
|
|
);
|
|
|
|
expect(result, isA<AltTransportFailure<StartBacktestResponse>>());
|
|
final failure = result as AltTransportFailure<StartBacktestResponse>;
|
|
expect(failure.kind, equals(AltTransportKind.timeout));
|
|
});
|
|
|
|
test(
|
|
'type mismatch maps correctly to AltTransportKind.decodeFailure',
|
|
() async {
|
|
final request = StartBacktestRequest();
|
|
|
|
final future = repository.startBacktest(
|
|
request,
|
|
timeout: const Duration(seconds: 1),
|
|
);
|
|
|
|
await roundTrip(
|
|
trigger: () => future,
|
|
onSent: (sentPacket) {},
|
|
responseTypeName:
|
|
HelloResponse.getDefault().info_.qualifiedMessageName,
|
|
responseData: HelloResponse().writeToBuffer(),
|
|
);
|
|
|
|
final result = await future;
|
|
expect(result, isA<AltTransportFailure<StartBacktestResponse>>());
|
|
final failure = result as AltTransportFailure<StartBacktestResponse>;
|
|
expect(failure.kind, equals(AltTransportKind.decodeFailure));
|
|
expect(failure.detail, contains('Bad state'));
|
|
},
|
|
);
|
|
|
|
test('client not alive returns AltTransportKind.disconnected', () async {
|
|
await client.close();
|
|
|
|
final result = await repository.startBacktest(StartBacktestRequest());
|
|
expect(result, isA<AltTransportFailure<StartBacktestResponse>>());
|
|
final failure = result as AltTransportFailure<StartBacktestResponse>;
|
|
expect(failure.kind, equals(AltTransportKind.disconnected));
|
|
expect(failure.detail, contains('client is not alive'));
|
|
});
|
|
});
|
|
}
|