Skip to content

Commit 70ef8c0

Browse files
authored
Extract an abstract base class for QueryResults (#181)
1 parent 970e70d commit 70ef8c0

File tree

7 files changed

+327
-162
lines changed

7 files changed

+327
-162
lines changed

current_results_ui/lib/filter.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class _FilterUIState extends State<FilterUI> {
5252

5353
@override
5454
Widget build(BuildContext context) {
55-
return Consumer<QueryResults>(
55+
return Consumer<QueryResultsBase>(
5656
builder: (context, results, child) {
5757
final filter = results.filter;
5858
return Column(

current_results_ui/lib/main.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ class AppProviders extends StatelessWidget {
8888
return MultiProvider(
8989
providers: [
9090
ChangeNotifierProvider<AuthService>(create: (_) => AuthService()),
91-
ChangeNotifierProvider<QueryResults>(create: (_) => QueryResults()),
91+
ChangeNotifierProvider<QueryResultsBase>(
92+
create: (_) => QueryResults(Filter('')),
93+
),
9294
],
9395
child: child,
9496
);
@@ -115,7 +117,8 @@ class _CurrentResultsScreenState extends State<CurrentResultsScreen>
115117
@override
116118
void initState() {
117119
super.initState();
118-
Provider.of<QueryResults>(context, listen: false).fetch(widget.filter);
120+
Provider.of<QueryResultsBase>(context, listen: false).filter =
121+
widget.filter;
119122
_tabController = TabController(
120123
initialIndex: widget.initialTabIndex,
121124
length: 3,
@@ -127,7 +130,8 @@ class _CurrentResultsScreenState extends State<CurrentResultsScreen>
127130
void didUpdateWidget(CurrentResultsScreen oldWidget) {
128131
super.didUpdateWidget(oldWidget);
129132
if (widget.filter != oldWidget.filter) {
130-
Provider.of<QueryResults>(context, listen: false).fetch(widget.filter);
133+
Provider.of<QueryResultsBase>(context, listen: false).filter =
134+
widget.filter;
131135
}
132136
_tabController.index = widget.initialTabIndex;
133137
}
@@ -229,7 +233,7 @@ class JsonLink extends StatelessWidget {
229233

230234
@override
231235
Widget build(BuildContext context) {
232-
return Consumer<QueryResults>(
236+
return Consumer<QueryResultsBase>(
233237
builder: (context, results, child) {
234238
return TextButton(
235239
child: const Text('JSON'),
@@ -250,8 +254,8 @@ class TextPopup extends StatelessWidget {
250254

251255
@override
252256
Widget build(BuildContext context) {
253-
return Consumer<QueryResults>(
254-
builder: (context, QueryResults results, child) {
257+
return Consumer<QueryResultsBase>(
258+
builder: (context, QueryResultsBase results, child) {
255259
return Tooltip(
256260
message: 'Results query as text',
257261
waitDuration: const Duration(milliseconds: 500),

current_results_ui/lib/query.dart

Lines changed: 146 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:collection';
67

7-
import 'package:flutter/material.dart';
8+
import 'package:flutter/foundation.dart';
9+
import 'package:flutter_current_results/results.dart';
810
import 'package:http/http.dart' as http;
911
import 'dart:convert';
1012

@@ -17,111 +19,171 @@ const String apiHost = 'current-results-qvyo5rktwa-uc.a.run.app';
1719
const int fetchLimit = 3000;
1820
const int maxFetchedResults = 100 * fetchLimit;
1921

20-
class QueryResults extends ChangeNotifier {
21-
Filter filter = Filter('');
22-
StreamSubscription<GetResultsResponse>? fetcher;
23-
List<String> names = [];
24-
Map<String, Counts> counts = {};
25-
Map<String, Map<ChangeInResult, List<Result>>> grouped = {};
22+
abstract class QueryResultsBase extends ChangeNotifier {
23+
Filter _filter;
24+
StreamSubscription<Iterable<(ChangeInResult, Result)>>? _streamFetcher;
25+
bool get isDone => _streamFetcher == null;
26+
final bool supportsEmptyQuery;
27+
28+
SplayTreeMap<String, Counts> counts = SplayTreeMap();
29+
SplayTreeMap<String, SplayTreeMap<ChangeInResult, List<Result>>> grouped =
30+
SplayTreeMap();
2631
TestCounts testCounts = TestCounts();
2732
Counts resultCounts = Counts();
2833
int fetchedResultsCount = 0;
29-
bool get noQuery => filter.terms.isEmpty;
30-
31-
QueryResults();
3234

33-
void fetch(Filter newFilter) {
34-
if (filter != newFilter) {
35-
filter = newFilter;
36-
fetchCurrentResults();
35+
QueryResultsBase(
36+
this._filter, {
37+
bool fetchInitialResults = false,
38+
this.supportsEmptyQuery = false,
39+
}) {
40+
if (fetchInitialResults) {
41+
_fetchResults();
3742
}
3843
}
3944

40-
@override
41-
void dispose() {
42-
fetcher?.cancel();
43-
super.dispose();
44-
}
45+
bool get hasQuery => _filter.terms.isNotEmpty;
46+
47+
List<String> get names => grouped.keys.toList();
48+
49+
Filter get filter => _filter;
4550

46-
GetResultsResponse resultsObject = GetResultsResponse.create();
51+
set filter(Filter newFilter) {
52+
if (_filter != newFilter) {
53+
_filter = newFilter;
54+
Future.microtask(fetch);
55+
}
56+
}
4757

48-
void fetchCurrentResults() async {
49-
fetcher?.cancel();
50-
fetcher = null;
51-
names = [];
52-
counts = {};
53-
grouped = {};
58+
void fetch() {
59+
_streamFetcher?.cancel();
60+
_streamFetcher = null;
61+
counts.clear();
62+
grouped.clear();
5463
testCounts = TestCounts();
5564
resultCounts = Counts();
5665
fetchedResultsCount = 0;
57-
if (noQuery) return;
58-
fetcher = fetchResults(filter).listen(onResults, onDone: onDone);
66+
notifyListeners();
67+
if (hasQuery || supportsEmptyQuery) {
68+
_fetchResults();
69+
}
5970
}
6071

61-
void onResults(GetResultsResponse response) {
62-
final results = response.results;
63-
fetchedResultsCount += results.length;
64-
if (fetchedResultsCount >= maxFetchedResults) {
65-
fetcher?.cancel();
66-
fetcher = null;
67-
}
68-
for (final result in results) {
69-
final change = ChangeInResult(result);
72+
void _fetchResults() {
73+
_streamFetcher = createResultsStream().listen(
74+
_processResults,
75+
onDone: () {
76+
_streamFetcher = null;
77+
notifyListeners();
78+
},
79+
);
80+
}
81+
82+
@visibleForOverriding
83+
Stream<Iterable<(ChangeInResult, Result)>> createResultsStream();
84+
85+
void _processResults(Iterable<(ChangeInResult, Result)> results) {
86+
for (final (change, result) in results) {
7087
grouped
71-
.putIfAbsent(result.name, () => <ChangeInResult, List<Result>>{})
72-
.putIfAbsent(change, () => <Result>[])
88+
.putIfAbsent(result.name, SplayTreeMap.new)
89+
.putIfAbsent(change, () => [])
7390
.add(result);
7491
counts.putIfAbsent(result.name, () => Counts()).addResult(change, result);
7592
testCounts.addResult(change, result);
7693
resultCounts.addResult(change, result);
7794
}
78-
names = grouped.keys.toList()..sort();
7995
notifyListeners();
8096
}
8197

82-
void onDone() {
83-
fetcher = null;
98+
@override
99+
void dispose() {
100+
_streamFetcher?.cancel();
101+
super.dispose();
84102
}
85103
}
86104

87-
Stream<GetResultsResponse> fetchResults(Filter filter) async* {
88-
final client = http.Client();
89-
var pageToken = '';
90-
do {
91-
final resultsQuery = Uri.https(apiHost, 'v1/results', {
92-
'filter': filter.terms.join(','),
93-
'pageSize': '$fetchLimit',
94-
'pageToken': pageToken,
95-
});
96-
final response = await client.get(resultsQuery);
97-
final results = GetResultsResponse.create()
98-
..mergeFromProto3Json(json.decode(response.body));
99-
yield results;
100-
pageToken = results.nextPageToken;
101-
} while (pageToken.isNotEmpty);
105+
class QueryResults extends QueryResultsBase {
106+
final http.Client _client;
107+
108+
QueryResults(super.filter, {http.Client? client})
109+
: _client = client ?? http.Client();
110+
111+
@override
112+
Stream<Iterable<(ChangeInResult, Result)>> createResultsStream() {
113+
return _streamPagedResults().transform(
114+
StreamTransformer.fromHandlers(
115+
handleData: (response, sink) {
116+
fetchedResultsCount += response.results.length;
117+
sink.add(
118+
response.results.map((result) => (ChangeInResult(result), result)),
119+
);
120+
if (fetchedResultsCount >= maxFetchedResults) {
121+
sink.close();
122+
}
123+
},
124+
),
125+
);
126+
}
127+
128+
Stream<GetResultsResponse> _streamPagedResults() async* {
129+
var pageToken = '';
130+
do {
131+
final resultsQuery = Uri.https(apiHost, 'v1/results', {
132+
'filter': filter.terms.join(','),
133+
'pageSize': '$fetchLimit',
134+
'pageToken': pageToken,
135+
});
136+
final response = await _client.get(resultsQuery);
137+
final results = GetResultsResponse.create()
138+
..mergeFromProto3Json(json.decode(response.body));
139+
yield results;
140+
pageToken = results.nextPageToken;
141+
} while (pageToken.isNotEmpty);
142+
}
102143
}
103144

104-
class ChangeInResult {
105-
final String result;
106-
final String expected;
145+
class ChangeInResult implements Comparable<ChangeInResult> {
146+
static final _cache = <String, ChangeInResult>{};
147+
148+
final bool matches;
107149
final bool flaky;
108150
final String text;
109151

110-
bool get matches => result == expected;
111-
112-
String get kind => flaky
113-
? 'flaky'
152+
ResultKind get kind => flaky
153+
? ResultKind.flaky
114154
: matches
115-
? 'pass'
116-
: 'fail';
155+
? ResultKind.pass
156+
: ResultKind.fail;
117157

118-
ChangeInResult(Result result)
119-
: this._(result.result, result.expected, result.flaky);
158+
factory ChangeInResult(Result result) {
159+
return ChangeInResult._create(
160+
result: result.result,
161+
expected: result.expected,
162+
isFlaky: result.flaky,
163+
);
164+
}
120165

121-
ChangeInResult._(this.result, this.expected, this.flaky)
122-
: text = flaky
123-
? "flaky (latest result $result expected $expected)"
124-
: "$result (expected $expected)";
166+
factory ChangeInResult._create({
167+
required String result,
168+
required String expected,
169+
required bool isFlaky,
170+
}) {
171+
final bool matches = result == expected;
172+
final String text;
173+
174+
if (isFlaky) {
175+
text = 'flaky (latest result $result expected $expected)';
176+
} else {
177+
text = matches ? result : '$result (expected $expected)';
178+
}
179+
180+
return _cache.putIfAbsent(
181+
text,
182+
() => ChangeInResult._(text, matches, isFlaky),
183+
);
184+
}
185+
186+
ChangeInResult._(this.text, this.matches, this.flaky);
125187

126188
@override
127189
String toString() => text;
@@ -132,6 +194,18 @@ class ChangeInResult {
132194

133195
@override
134196
int get hashCode => text.hashCode;
197+
198+
@override
199+
int compareTo(ChangeInResult other) {
200+
if (matches != other.matches) {
201+
return matches ? 1 : -1;
202+
}
203+
204+
if (flaky != other.flaky) {
205+
return flaky ? -1 : 1;
206+
}
207+
return text.compareTo(other.text);
208+
}
135209
}
136210

137211
String resultAsCommaSeparated(Result result) => [

0 commit comments

Comments
 (0)