Skip to content

Commit 3a6eb4c

Browse files
committed
[red] Preserve GitHub import context and retries
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent 45a41d4 commit 3a6eb4c

3 files changed

Lines changed: 166 additions & 5 deletions

File tree

‎app/test/unit/github_repository_client_test.dart‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,5 +603,50 @@ void main() {
603603
throwsA(_githubFailure(GithubFailureKind.cancelled)),
604604
);
605605
});
606+
607+
test('aborts an in-flight blob response through the same token', () async {
608+
final Completer<void> requestStarted = Completer<void>();
609+
final GithubCancellation cancellation = GithubCancellation();
610+
final GithubRepositoryClient api = GithubRepositoryClient(
611+
httpClient: MockClient.streaming((request, bodyStream) async {
612+
expect(request, isA<http.AbortableRequest>());
613+
final http.AbortableRequest abortable =
614+
request as http.AbortableRequest;
615+
requestStarted.complete();
616+
await abortable.abortTrigger;
617+
throw http.RequestAbortedException(request.url);
618+
}),
619+
);
620+
621+
final Future<Uint8List> pending = api.fetchFile(
622+
_pinned(),
623+
_helloEntry(),
624+
cancellation: cancellation,
625+
);
626+
await requestStarted.future;
627+
cancellation.cancel();
628+
629+
await expectLater(
630+
pending,
631+
throwsA(_githubFailure(GithubFailureKind.cancelled)),
632+
);
633+
});
634+
635+
test('ignores malformed Retry-After metadata without leaking', () async {
636+
final GithubRepositoryClient api = GithubRepositoryClient(
637+
httpClient: MockClient(
638+
(http.Request request) async => http.Response(
639+
'{}',
640+
429,
641+
headers: <String, String>{'retry-after': 'not-an-http-date'},
642+
),
643+
),
644+
);
645+
646+
await expectLater(
647+
api.resolve(_locator(), ref: 'main'),
648+
throwsA(_githubFailure(GithubFailureKind.rateLimited)),
649+
);
650+
});
606651
});
607652
}

‎app/test/widget/files_view_test.dart‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,24 @@ void main() {
5353

5454
Finder action = find.byTooltip(l10nOf(tester).githubImportAction);
5555
expect(action, findsOneWidget);
56-
expect(tester.widget<IconButton>(action).onPressed, isNotNull);
56+
Finder actionButton = find.ancestor(
57+
of: action,
58+
matching: find.byType(IconButton),
59+
);
60+
expect(actionButton, findsOneWidget);
61+
expect(tester.widget<IconButton>(actionButton).onPressed, isNotNull);
5762

5863
fake.emitRunState(RunState.running);
5964
await tester.pump();
60-
action = find.byTooltip(l10nOf(tester).githubImportAction);
65+
expect(find.byTooltip(l10nOf(tester).githubImportAction), findsNothing);
66+
action = find.byTooltip(l10nOf(tester).githubImportRequiresReady);
6167
expect(action, findsOneWidget);
68+
actionButton = find.ancestor(
69+
of: action,
70+
matching: find.byType(IconButton),
71+
);
6272
expect(
63-
tester.widget<IconButton>(action).onPressed,
73+
tester.widget<IconButton>(actionButton).onPressed,
6474
isNull,
6575
reason: 'a running board cannot accept import PUTs',
6676
);

‎app/test/widget/github_import_view_test.dart‎

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ final class _FakeGithubApi implements GithubApi {
7575
'blink.py': 'print(1)\n',
7676
},
7777
this.resolveFailure,
78+
this.childListFailure,
7879
this.resolveGate,
7980
this.listGate,
81+
this.fetchFailuresRemaining = 0,
8082
}) : _bytesByRemotePath = <String, Uint8List>{
8183
for (final MapEntry<String, String> source
8284
in sourceByRemotePath.entries)
@@ -86,8 +88,10 @@ final class _FakeGithubApi implements GithubApi {
8688
final List<GithubEntry> entries;
8789
final Map<String, Uint8List> _bytesByRemotePath;
8890
final GithubFailure? resolveFailure;
91+
final GithubFailure? childListFailure;
8992
final Completer<void>? resolveGate;
9093
final Completer<void>? listGate;
94+
int fetchFailuresRemaining;
9195
final List<(RepositoryLocator, String)> resolveCalls =
9296
<(RepositoryLocator, String)>[];
9397
final List<String> listedRemotePaths = <String>[];
@@ -97,6 +101,7 @@ final class _FakeGithubApi implements GithubApi {
97101
Future<PinnedRepository> resolve(
98102
RepositoryLocator locator, {
99103
String ref = '',
104+
GithubCancellation? cancellation,
100105
}) async {
101106
resolveCalls.add((locator, ref));
102107
await resolveGate?.future;
@@ -115,9 +120,14 @@ final class _FakeGithubApi implements GithubApi {
115120
PinnedRepository repository, {
116121
required String treeSha,
117122
required String remotePath,
123+
GithubCancellation? cancellation,
118124
}) async {
119125
listedRemotePaths.add(remotePath);
120126
await listGate?.future;
127+
if (remotePath.isNotEmpty && childListFailure
128+
case final GithubFailure failure) {
129+
throw failure;
130+
}
121131
return GithubDirectory(
122132
treeSha: treeSha,
123133
remotePath: remotePath,
@@ -128,9 +138,14 @@ final class _FakeGithubApi implements GithubApi {
128138
@override
129139
Future<Uint8List> fetchFile(
130140
PinnedRepository repository,
131-
GithubEntry entry,
132-
) async {
141+
GithubEntry entry, {
142+
GithubCancellation? cancellation,
143+
}) async {
133144
fetchedRemotePaths.add(entry.remotePath);
145+
if (fetchFailuresRemaining > 0) {
146+
fetchFailuresRemaining -= 1;
147+
throw GithubFailure(GithubFailureKind.offline, path: entry.remotePath);
148+
}
134149
return Uint8List.fromList(_bytesByRemotePath[entry.remotePath]!);
135150
}
136151
}
@@ -566,5 +581,96 @@ void main() {
566581
findsOneWidget,
567582
);
568583
});
584+
585+
testWidgets('failed child navigation retains the parent selection', (
586+
WidgetTester tester,
587+
) async {
588+
final RecordingConnection connection = RecordingConnection(
589+
initial: ConnState.ready,
590+
);
591+
final _FakeGithubApi api = _FakeGithubApi(
592+
childListFailure: const GithubFailure(GithubFailureKind.server),
593+
);
594+
addTearDown(connection.dispose);
595+
596+
await _openImport(tester, connection, api);
597+
await _browse(tester);
598+
await tester.tap(find.byKey(_selectionKey('hello.py')));
599+
await tester.pump();
600+
await tester.tap(find.byKey(_entryKey('nested')));
601+
await tester.pumpAndSettle();
602+
603+
expect(
604+
tester
605+
.widget<CheckboxListTile>(find.byKey(_selectionKey('hello.py')))
606+
.value,
607+
isTrue,
608+
);
609+
expect(find.text('/'), findsOneWidget);
610+
});
611+
612+
testWidgets('a zero-write blob failure retries from the same review', (
613+
WidgetTester tester,
614+
) async {
615+
final RecordingConnection connection = RecordingConnection(
616+
initial: ConnState.ready,
617+
);
618+
final _FakeGithubApi api = _FakeGithubApi(
619+
entries: const <GithubEntry>[_helloEntry],
620+
fetchFailuresRemaining: 1,
621+
);
622+
addTearDown(connection.dispose);
623+
624+
await _openImport(tester, connection, api);
625+
await _browse(tester);
626+
await tester.tap(find.byKey(_selectionKey('hello.py')));
627+
await tester.pump();
628+
await tester.tap(find.byKey(kGithubReviewButtonKey));
629+
await tester.pumpAndSettle();
630+
await tester.tap(find.byKey(kGithubCommitButtonKey));
631+
await tester.pumpAndSettle();
632+
633+
expect(connection.putFileCalls, isEmpty);
634+
expect(find.text(l10nOf(tester).githubImportRetry), findsOneWidget);
635+
expect(find.byKey(kGithubCommitButtonKey), findsOneWidget);
636+
637+
await tester.tap(find.text(l10nOf(tester).githubImportRetry));
638+
await tester.pumpAndSettle();
639+
640+
expect(connection.putFileCalls, hasLength(1));
641+
expect(
642+
find.byKey(const ValueKey<String>('githubImportResult')),
643+
findsOneWidget,
644+
);
645+
});
646+
647+
testWidgets('review marks an existing regular-file target before consent', (
648+
WidgetTester tester,
649+
) async {
650+
final RecordingConnection connection = RecordingConnection(
651+
initial: ConnState.ready,
652+
);
653+
final _FakeGithubApi api = _FakeGithubApi(
654+
entries: const <GithubEntry>[_helloEntry],
655+
);
656+
addTearDown(connection.dispose);
657+
await connection.putFile(
658+
'/hello.py',
659+
Uint8List.fromList(utf8.encode('print(0)\n')),
660+
);
661+
662+
await _openImport(tester, connection, api);
663+
await _browse(tester);
664+
await tester.tap(find.byKey(_selectionKey('hello.py')));
665+
await tester.pump();
666+
await tester.tap(find.byKey(kGithubReviewButtonKey));
667+
await tester.pumpAndSettle();
668+
669+
expect(
670+
find.text(l10nOf(tester).githubImportWillOverwrite),
671+
findsOneWidget,
672+
);
673+
expect(find.textContaining('/hello.py'), findsOneWidget);
674+
});
569675
});
570676
}

0 commit comments

Comments
 (0)