blob: a86dcbc0a05a3ba06df545a860eac198ce628a7d [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
import 'dart:core';
// import 'package:angular/angular.dart';
import 'filter_service.dart';
import 'package:firebase/firestore.dart' as firestore;
int reverse(key1, key2) => key2.compareTo(key1);
class IntRangeIterator implements Iterator<int> {
int current;
int end;
IntRangeIterator(int start, this.end) {
current = start - 1;
}
bool moveNext() {
current++;
if (current > end) current = null;
return current != null;
}
}
class IntRange with IterableMixin<int> implements Comparable {
IntRange(this.start, this.end);
final int start;
final int end;
bool operator ==(other) =>
other is IntRange && other.start == start && other.end == end;
int get hashCode => start + end * 40927 % 63703;
Iterator<int> get iterator => IntRangeIterator(start, end);
bool contains(Object i) => i as int >= start && i as int <= end;
int get length => end - start + 1;
int compareTo(dynamic other) {
IntRange o = other; // Throw TypeError if not an IntRange
return end == o.end ? start.compareTo(o.start) : end.compareTo(o.end);
}
String toString() => "IntRange($start, $end)";
}
class Commit implements Comparable {
final String hash;
final String author;
final String title;
final DateTime created;
final int index;
Commit(this.hash, this.author, this.title, this.created, this.index);
Commit.fromDocument(firestore.DocumentSnapshot document)
: hash = document.id,
author = document.get('author'),
title = document.get('title'),
created = document.get('created'),
index = document.get('index');
/// Sort in reverse index order.
int compareTo(other) => (other as Commit).index.compareTo(index);
String toString() => "$index $hash $author $created $title";
}
class ChangeGroup implements Comparable {
final IntRange range;
List<Commit> commits;
final Changes changes;
final Changes latestChanges;
Changes _filteredChanges;
Filter _filter = Filter.defaultFilter;
ChangeGroup(IntRange this.range, Map<int, Commit> allCommits,
Iterable<Change> changeList, Iterable<Change> liveChangeList)
: changes = Changes(changeList),
latestChanges = Changes(liveChangeList) {
commits = [for (int i in range) if (allCommits[i] != null) allCommits[i]]
..sort(reverse);
}
/// Sort in reverse chronological order.
int compareTo(other) => (other as ChangeGroup).range.compareTo(range);
void addChanges(Iterable<Change> newChanges) {
newChanges.forEach(changes.add);
computeFilteredChanges(_filter);
}
void addLatestChanges(Iterable<Change> newChanges) {
newChanges.forEach(latestChanges.add);
computeFilteredChanges(_filter);
}
bool show(Filter filter) =>
filter.showAllCommits || filteredChanges(filter).isNotEmpty;
Changes shownChanges(Filter filter) =>
filter.showLatestFailures ? latestChanges : changes;
Changes filteredChanges(Filter filter) =>
(filter == _filter) ? _filteredChanges : computeFilteredChanges(filter);
Changes computeFilteredChanges(filter) {
_filteredChanges = shownChanges(filter).filtered(filter);
_filter = filter;
return _filteredChanges;
}
}
/// A list of configurations affected by a result change.
/// The list is canonicalized, and configurations are grouped into
/// summaries of similar configurations.
class Configurations {
static final _instances = <String, Configurations>{};
List<String> configurations;
String text;
Map<String, List<String>> summaries;
Configurations._(List<dynamic> configurations)
: configurations = List<String>.from(configurations),
text = configurations.join(),
summaries = _summarize(configurations);
factory Configurations(List configs) {
configs.sort();
return _instances.putIfAbsent(
configs.join(' '), () => Configurations._(configs.toList()));
}
static Map<String, List<String>> _summarize(List<dynamic> configurations) {
final groups = <String, List<String>>{};
for (final String configuration in configurations) {
groups
.putIfAbsent(configuration.split('-').first, () => <String>[])
.add(configuration);
}
// Sorts the values as a side effect, while changing their keys.
return SplayTreeMap.fromIterable(groups.values,
key: (list) => (list..sort).length == 1
? list.first
: list.first.split('-').first + '...');
}
bool show(Filter filter) =>
filter.allGroups ||
configurations.any((configuration) =>
filter.configurationGroups.any(configuration.startsWith));
}
class Change {
Change._(
this.id,
this.name,
this.configurations,
this.result,
this.previousResult,
this.expected,
this.blamelistStartIndex,
this.blamelistEndIndex,
this.pinnedIndex,
this.trivialBlamelist)
: changesText = '$previousResult -> $result (expected $expected)',
configurationsText = configurations.text;
Change.fromDocument(firestore.DocumentSnapshot document)
: this._(
document.id,
document.get('name'),
Configurations(document.get('configurations')),
document.get('result'),
document.get('previous_result'),
document.get('expected'),
document.get('blamelist_start_index'),
document.get('blamelist_end_index'),
document.get('pinned_index'),
document.get('trivial_blamelist'));
final String id;
final String name;
final Configurations configurations;
final String result;
final String previousResult;
final String expected;
final int blamelistStartIndex;
final int blamelistEndIndex;
int pinnedIndex;
bool trivialBlamelist;
final String configurationsText;
final String changesText;
Change copy({List<String> newConfigurations}) => Change._(
id,
name,
newConfigurations == null
? configurations
: Configurations(newConfigurations),
result,
previousResult,
expected,
blamelistStartIndex,
blamelistEndIndex,
pinnedIndex,
trivialBlamelist);
String get resultStyle => result == expected ? 'success' : 'failure';
}
class ResultGroup with IterableMixin<Change> {
final String changesText;
final Map<String, Change> _map = SplayTreeMap();
ResultGroup(this.changesText);
get iterator => _map.values.iterator;
void operator []=(Object resultText, Object change) {
_map[resultText] = change;
}
bool show(Filter filter) =>
!filter.showLatestFailures || first.resultStyle == 'failure';
}
class ConfigGroup with IterableMixin<ResultGroup> {
final Configurations configurations;
final Map<String, ResultGroup> _map;
ConfigGroup(this.configurations, {Map<String, ResultGroup> map})
: _map = map ?? SplayTreeMap();
ResultGroup operator [](String resultText) =>
_map.putIfAbsent(resultText, () => ResultGroup(resultText));
get iterator => _map.values.iterator;
ConfigGroup filtered(Filter filter) {
if (!configurations.show(filter)) {
return ConfigGroup(configurations, map: {});
}
final newMap =
Map.fromEntries(_map.entries.where((e) => e.value.show(filter)));
return newMap.length == _map.length
? this
: ConfigGroup(configurations, map: newMap);
}
}
class Changes with IterableMixin<ConfigGroup> {
final Map<String, ConfigGroup> _map;
Changes(Iterable<Change> changes, {Map map}) : _map = map ?? SplayTreeMap() {
changes.forEach(add);
}
get iterator => _map.values.iterator;
ConfigGroup operator [](Configurations configuration) =>
_map.putIfAbsent(configuration.text, () => ConfigGroup(configuration));
void add(Change change) {
this[change.configurations][change.changesText][change.name] = change;
}
Changes filtered(Filter filter) {
final newMap = <String, ConfigGroup>{};
bool changed = false;
for (final key in _map.keys) {
final newValue = _map[key].filtered(filter);
changed = changed || newValue != _map[key];
if (!newValue.isEmpty) {
newMap[key] = newValue;
}
}
return changed ? Changes([], map: newMap) : this;
}
}