33// BSD-style license that can be found in the LICENSE file.
44
55import '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' ;
810import 'package:http/http.dart' as http;
911import 'dart:convert' ;
1012
@@ -17,111 +19,171 @@ const String apiHost = 'current-results-qvyo5rktwa-uc.a.run.app';
1719const int fetchLimit = 3000 ;
1820const 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
137211String resultAsCommaSeparated (Result result) => [
0 commit comments