Skip to content

Commit 84c2f76

Browse files
committed
[red] Bound and attribute GitHub import failures
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent e7c69b2 commit 84c2f76

3 files changed

Lines changed: 199 additions & 2 deletions

File tree

‎app/test/unit/connection_manager_test.dart‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,52 @@ void main() {
231231
reason: 'reattaching the same device id is a new local session',
232232
);
233233
});
234+
235+
test(
236+
'local session stamp rotates once when an attached link reconnects',
237+
() async {
238+
final r = _rig();
239+
addTearDown(r.manager.dispose);
240+
final Connection facade = r.manager.connection;
241+
242+
await r.manager.connect(_hitA.id);
243+
r.board.emit(ConnState.ready);
244+
await pumpEventQueue();
245+
final Object establishedStamp = connectionSessionStampOf(facade);
246+
247+
r.board.emit(ConnState.running);
248+
r.board.emit(ConnState.ready);
249+
await pumpEventQueue();
250+
expect(
251+
identical(connectionSessionStampOf(facade), establishedStamp),
252+
isTrue,
253+
reason: 'ordinary ready/running transitions retain one link session',
254+
);
255+
256+
r.board.emit(ConnState.connecting);
257+
await pumpEventQueue();
258+
final Object successorStamp = connectionSessionStampOf(facade);
259+
expect(identical(successorStamp, establishedStamp), isFalse);
260+
261+
r.board.emit(ConnState.disconnected);
262+
r.board.emit(ConnState.connecting);
263+
r.board.emit(ConnState.ready);
264+
await pumpEventQueue();
265+
expect(
266+
identical(connectionSessionStampOf(facade), successorStamp),
267+
isTrue,
268+
reason: 'one departure rotates once for the complete reconnect',
269+
);
270+
271+
r.board.emit(ConnState.disconnected);
272+
await pumpEventQueue();
273+
expect(
274+
identical(connectionSessionStampOf(facade), successorStamp),
275+
isFalse,
276+
reason: 'a later established-session departure rotates again',
277+
);
278+
},
279+
);
234280
});
235281

236282
group('A-22 ConnectionManager — not-connected + failure', () {

‎app/test/unit/github_repository_client_test.dart‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ GithubEntry _helloEntry() => const GithubEntry(
3737
remotePath: 'examples/hello.py',
3838
kind: GithubEntryKind.regularFile,
3939
objectSha: _helloBlobSha,
40-
declaredSize: 18,
40+
declaredSize: 15,
4141
);
4242

4343
void _expectGitHubRequest(http.Request request) {
@@ -550,6 +550,55 @@ void main() {
550550
});
551551

552552
group('GithubRepositoryClient failures', () {
553+
test('enforces one absolute deadline against a trickling body', () async {
554+
final StreamController<List<int>> body = StreamController<List<int>>();
555+
final Completer<void> abortObserved = Completer<void>();
556+
Timer? trickle;
557+
final MockClient httpClient = MockClient.streaming((
558+
http.BaseRequest request,
559+
http.ByteStream requestBody,
560+
) async {
561+
expect(request, isA<http.AbortableRequest>());
562+
final http.AbortableRequest abortable =
563+
request as http.AbortableRequest;
564+
trickle = Timer.periodic(const Duration(milliseconds: 10), (_) {
565+
if (!body.isClosed) body.add(const <int>[0x20]);
566+
});
567+
unawaited(
568+
abortable.abortTrigger!.then((_) async {
569+
if (!abortObserved.isCompleted) abortObserved.complete();
570+
trickle?.cancel();
571+
if (!body.isClosed) await body.close();
572+
}),
573+
);
574+
return http.StreamedResponse(body.stream, HttpStatus.ok);
575+
});
576+
final GithubRepositoryClient api = GithubRepositoryClient(
577+
httpClient: httpClient,
578+
timeout: const Duration(milliseconds: 80),
579+
);
580+
final Stopwatch elapsed = Stopwatch()..start();
581+
582+
try {
583+
await expectLater(
584+
api
585+
.resolve(_locator(), ref: 'main')
586+
.timeout(const Duration(seconds: 1)),
587+
throwsA(_githubFailure(GithubFailureKind.offline)),
588+
);
589+
expect(elapsed.elapsed, lessThan(const Duration(milliseconds: 500)));
590+
await expectLater(
591+
abortObserved.future.timeout(const Duration(milliseconds: 200)),
592+
completes,
593+
);
594+
} finally {
595+
elapsed.stop();
596+
trickle?.cancel();
597+
if (!body.isClosed) await body.close();
598+
httpClient.close();
599+
}
600+
});
601+
553602
test(
554603
'cancels response bodies rejected by status or declared length',
555604
() async {

‎app/test/widget/github_import_view_test.dart‎

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
import 'dart:async';
1111
import 'dart:convert';
12-
import 'dart:typed_data';
1312

1413
import 'package:flutter/material.dart';
1514
import 'package:flutter_riverpod/flutter_riverpod.dart';
15+
import 'package:flutter/services.dart';
1616
import 'package:flutter_test/flutter_test.dart';
1717

1818
import 'package:pyble/files/files.dart';
@@ -245,6 +245,17 @@ Future<void> _browse(
245245
await tester.pumpAndSettle();
246246
}
247247

248+
Future<void> _activateButtonWithEnter(WidgetTester tester, Key key) async {
249+
final Finder button = find.byKey(key);
250+
final Finder label = find.descendant(of: button, matching: find.byType(Text));
251+
final FocusNode focusNode = Focus.of(tester.element(label));
252+
focusNode.requestFocus();
253+
await tester.pump();
254+
expect(focusNode.hasFocus, isTrue);
255+
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
256+
await tester.pumpAndSettle();
257+
}
258+
248259
void main() {
249260
group('A-33 GitHub import — adaptive connected Files workflow', () {
250261
testWidgets(
@@ -331,6 +342,34 @@ void main() {
331342
expect(find.byKey(kGithubRefFieldKey), findsOneWidget);
332343
});
333344

345+
testWidgets('hardware Enter activates Browse and Review', (
346+
WidgetTester tester,
347+
) async {
348+
final RecordingConnection connection = RecordingConnection(
349+
initial: ConnState.ready,
350+
);
351+
final _FakeGithubApi api = _FakeGithubApi(
352+
entries: const <GithubEntry>[_helloEntry],
353+
);
354+
addTearDown(connection.dispose);
355+
356+
await _openImport(tester, connection, api);
357+
await tester.enterText(
358+
find.byKey(kGithubRepositoryFieldKey),
359+
'https://github.com/PyBLE-dev/examples',
360+
);
361+
await _activateButtonWithEnter(tester, kGithubBrowseButtonKey);
362+
363+
expect(api.resolveCalls, hasLength(1));
364+
expect(api.listedRemotePaths, <String>['']);
365+
await tester.tap(find.byKey(_selectionKey('hello.py')));
366+
await tester.pump();
367+
await _activateButtonWithEnter(tester, kGithubReviewButtonKey);
368+
369+
expect(find.text(l10nOf(tester).githubImportReviewTitle), findsOneWidget);
370+
expect(connection.putFileCalls, isEmpty);
371+
});
372+
334373
testWidgets(
335374
'an existing target needs a separate localized confirmation; Cancel writes nothing',
336375
(WidgetTester tester) async {
@@ -450,6 +489,20 @@ void main() {
450489
hasFocusAction: true,
451490
),
452491
);
492+
Finder failureLiveRegions() => find.descendant(
493+
of: failure,
494+
matching: find.byWidgetPredicate(
495+
(Widget widget) =>
496+
widget is Semantics && widget.properties.liveRegion == true,
497+
),
498+
);
499+
expect(failureLiveRegions(), findsOneWidget);
500+
final int failureNodeId = tester.getSemantics(failureLiveRegions()).id;
501+
tester.platformDispatcher.textScaleFactorTestValue = 1.1;
502+
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
503+
await tester.pump();
504+
expect(failureLiveRegions(), findsOneWidget);
505+
expect(tester.getSemantics(failureLiveRegions()).id, failureNodeId);
453506
expect(find.text(l10nOf(tester).githubImportRetry), findsNothing);
454507
expect(
455508
tester
@@ -571,10 +624,23 @@ void main() {
571624
const ValueKey<String>('githubImportResult'),
572625
);
573626
expect(result, findsOneWidget);
627+
Finder resultLiveRegions() => find.byWidgetPredicate(
628+
(Widget widget) =>
629+
widget is Semantics &&
630+
widget.key == const ValueKey<String>('githubImportResult') &&
631+
widget.properties.liveRegion == true,
632+
);
633+
expect(resultLiveRegions(), findsOneWidget);
574634
expect(
575635
tester.getSemantics(result),
576636
matchesSemantics(isLiveRegion: true),
577637
);
638+
final int resultNodeId = tester.getSemantics(resultLiveRegions()).id;
639+
tester.platformDispatcher.textScaleFactorTestValue = 1.1;
640+
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
641+
await tester.pumpAndSettle();
642+
expect(resultLiveRegions(), findsOneWidget);
643+
expect(tester.getSemantics(resultLiveRegions()).id, resultNodeId);
578644
},
579645
);
580646

@@ -712,6 +778,42 @@ void main() {
712778
);
713779
});
714780

781+
testWidgets('a size failure identifies its safe repository path', (
782+
WidgetTester tester,
783+
) async {
784+
const GithubEntry oversized = GithubEntry(
785+
name: 'oversized.py',
786+
remotePath: 'examples/oversized.py',
787+
kind: GithubEntryKind.regularFile,
788+
objectSha: _helloBlobSha,
789+
declaredSize: 21,
790+
);
791+
final RecordingConnection connection = RecordingConnection(
792+
initial: ConnState.ready,
793+
);
794+
final _FakeGithubApi api = _FakeGithubApi(
795+
entries: const <GithubEntry>[oversized],
796+
fetchFailuresRemaining: 1,
797+
fetchFailure: const GithubFailure(
798+
GithubFailureKind.fileTooLarge,
799+
path: 'examples/oversized.py',
800+
),
801+
);
802+
addTearDown(connection.dispose);
803+
804+
await _openImport(tester, connection, api);
805+
await _browse(tester);
806+
await tester.tap(find.byKey(_selectionKey('examples/oversized.py')));
807+
await tester.pump();
808+
await tester.tap(find.byKey(kGithubReviewButtonKey));
809+
await tester.pumpAndSettle();
810+
await tester.tap(find.byKey(kGithubCommitButtonKey));
811+
await tester.pumpAndSettle();
812+
813+
expect(connection.putFileCalls, isEmpty);
814+
expect(find.textContaining('examples/oversized.py'), findsOneWidget);
815+
});
816+
715817
testWidgets('a rate-limited blob retry waits without writing', (
716818
WidgetTester tester,
717819
) async {

0 commit comments

Comments
 (0)