Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions app_dart/lib/src/request_handlers/get_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:cocoon_common/core_extensions.dart';
import 'package:cocoon_common/rpc_model.dart' as rpc_model;
import 'package:github/github.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -40,17 +41,23 @@ final class GetStatus extends RequestHandler {
final branch =
request.uri.queryParameters[kBranchParam] ?? Config.defaultBranch(slug);
final commitNumber = config.commitNumber;
final lastCommitTimestamp =
lastCommitSha != null
? (await Commit.fromFirestoreBySha(
_firestore,
sha: lastCommitSha,
)).createTimestamp
: _now().millisecondsSinceEpoch;

final DateTime lastCommitCreated;
if (lastCommitSha != null) {
final commit = await Commit.fromFirestoreBySha(
_firestore,
sha: lastCommitSha,
);
lastCommitCreated = DateTime.fromMillisecondsSinceEpoch(
commit.createTimestamp,
);
} else {
lastCommitCreated = _now();
}

final commits = await _buildStatusService.retrieveCommitStatusFirestore(
limit: commitNumber,
timestamp: lastCommitTimestamp,
created: TimeRange.before(lastCommitCreated, exclusive: true),
branch: branch,
slug: slug,
);
Expand Down
7 changes: 4 additions & 3 deletions app_dart/lib/src/service/build_status_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:cocoon_common/core_extensions.dart';
import 'package:cocoon_common/task_status.dart';
import 'package:cocoon_server/logging.dart';
import 'package:github/github.dart';
Expand Down Expand Up @@ -88,14 +89,14 @@ interface class BuildStatusService {
/// The returned stream will be ordered by most recent commit first, then
/// the next newest, and so on.
Future<List<CommitTasksStatus>> retrieveCommitStatusFirestore({
required RepositorySlug slug,
required int limit,
int? timestamp,
TimeRange? created,
String? branch,
required RepositorySlug slug,
}) async {
final commits = await _firestore.queryRecentCommits(
limit: limit,
timestamp: timestamp,
created: created,
branch: branch,
slug: slug,
);
Expand Down
24 changes: 21 additions & 3 deletions app_dart/lib/src/service/firestore.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:cocoon_common/core_extensions.dart';
import 'package:cocoon_common/task_status.dart';
import 'package:cocoon_server/access_client_provider.dart';
import 'package:cocoon_server/google_auth_provider.dart';
Expand Down Expand Up @@ -67,23 +68,40 @@ mixin FirestoreQueries {
Transaction? transaction,
});

static Map<String, String> _filterByTimeRAnge(
String fieldName,
TimeRange range,
) {
return switch (range) {
IndefiniteTimeRange() => const {},
SpecificTimeRange(:final start, :final end, :final exclusive) => {
if (start != null)
'$fieldName ${exclusive ? '>' : '>='}':
'${start.millisecondsSinceEpoch}',
if (end != null)
'$fieldName ${exclusive ? '<' : '<='}':
'${end.millisecondsSinceEpoch}',
},
};
}

/// Queries for recent commits.
///
/// The [limit] argument specifies the maximum number of commits to retrieve.
///
/// The returned commits will be ordered by most recent [Commit.timestamp].
Future<List<Commit>> queryRecentCommits({
int limit = 100,
int? timestamp,
TimeRange? created,
String? branch,
required RepositorySlug slug,
}) async {
timestamp ??= DateTime.now().millisecondsSinceEpoch;
branch ??= Config.defaultBranch(slug);
created ??= TimeRange.indefinite;
final filterMap = <String, Object>{
'${Commit.fieldBranch} =': branch,
'${Commit.fieldRepositoryPath} =': slug.fullName,
'${Commit.fieldCreateTimestamp} <': timestamp,
..._filterByTimeRAnge(Commit.fieldCreateTimestamp, created),
};
final orderMap = <String, String>{
Commit.fieldCreateTimestamp: kQueryOrderDescending,
Expand Down
17 changes: 13 additions & 4 deletions app_dart/test/request_handlers/get_status_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ void main() {
expect(result, containsPair('Commits', isEmpty));
});

test('reports statuses without input commit key', () async {
test('reports statuses without input commit SHA', () async {
buildStatusService = FakeBuildStatusService(
commitTasksStatuses: [
CommitTasksStatus(generateFirestoreCommit(1, sha: commit1.sha), []),
Expand All @@ -79,11 +79,20 @@ void main() {
expect(result, containsPair('Commits', hasLength(2)));
});

test('reports statuses with input commit key', () async {
test('reports statuses with input commit SHA', () async {
buildStatusService = FakeBuildStatusService(
commitTasksStatuses: [
CommitTasksStatus(generateFirestoreCommit(1, sha: commit1.sha), []),
CommitTasksStatus(generateFirestoreCommit(2, sha: commit2.sha), []),
// Newer
CommitTasksStatus(
generateFirestoreCommit(2, sha: commit2.sha, createTimestamp: 2),
[],
),

// Older
CommitTasksStatus(
generateFirestoreCommit(1, sha: commit1.sha, createTimestamp: 1),
[],
),
],
);
handler = GetStatus(
Expand Down
24 changes: 14 additions & 10 deletions app_dart/test/src/service/fake_build_status_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:cocoon_common/core_extensions.dart';
import 'package:cocoon_service/src/service/build_status_provider/commit_tasks_status.dart';
import 'package:cocoon_service/src/service/build_status_service.dart';
import 'package:collection/collection.dart';
import 'package:github/github.dart';

// TODO(matanlurey): Remove this shim over Firestore and use FakeFirestore instead.
class FakeBuildStatusService implements BuildStatusService {
FakeBuildStatusService({this.cumulativeStatus, this.commitTasksStatuses});

Expand All @@ -27,19 +29,21 @@ class FakeBuildStatusService implements BuildStatusService {
@override
Future<List<CommitTasksStatus>> retrieveCommitStatusFirestore({
int limit = 100,
int? timestamp,
TimeRange? created,
String? branch,
required RepositorySlug slug,
}) async {
commitTasksStatuses!.sortBy((c) => c.commit.createTimestamp);
return commitTasksStatuses!
.where(
(status) =>
((timestamp == null)
? true
: status.commit.createTimestamp < timestamp) &&
status.commit.branch == branch,
)
.toList();
return commitTasksStatuses!.where((status) {
if (status.commit.branch != branch) {
return false;
}
if (created != null) {
return created.contains(
DateTime.fromMillisecondsSinceEpoch(status.commit.createTimestamp),
);
}
return true;
}).toList();
}
}
2 changes: 2 additions & 0 deletions packages/cocoon_common/lib/core_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import 'dart:convert';
import 'dart:typed_data';

export 'src/time_range.dart';

extension BytesStreamExtension on Stream<List<int>> {
/// Collects and returns all of the integer lists as list of bytes.
Future<Uint8List> collectBytes({bool copy = true}) async {
Expand Down
161 changes: 161 additions & 0 deletions packages/cocoon_common/lib/src/time_range.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:meta/meta.dart';

/// A definition of a time range, which can be specific or indefinite.
@immutable
sealed class TimeRange {
const TimeRange();

/// Matches any time.
static const TimeRange indefinite = IndefiniteTimeRange();

/// Creates a range matching time between [start] and [end].
///
/// If [exclusive] is set, the specific moments described in [start] and [end]
/// are excluded.
///
/// [end] must be after [start].
factory TimeRange.between(
DateTime start, //
DateTime end, {
bool exclusive,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this either:
(start, end) or [start, end]
and never (start, end] or [start, end)

}) = SpecificTimeRange.between;

/// Creates a range matching time before [end].
///
/// If [exclusive] is set, the specific moment described in [end] is excluded.
factory TimeRange.before(
DateTime end, { //
bool exclusive,
}) = SpecificTimeRange.before;

/// Creates a range matching time after [start].
///
/// If [exclusive] is set, the specific moment described in [start] is
/// excluded.
factory TimeRange.after(
DateTime start, { //
bool exclusive,
}) = SpecificTimeRange.after;

@override
@nonVirtual
bool operator ==(Object other) {
return other is TimeRange && start == other.start && end == other.end;
}

@override
@nonVirtual
int get hashCode => Object.hash(start, end);

/// A starting point in time.
///
/// If `null`, the time range is infinite in the past.
DateTime? get start;

/// An ending point in time.
///
/// If `null`, the time range is infinite in the future.
DateTime? get end;

/// Returns whether [time] is within `this` range.
bool contains(DateTime time);

@override
String toString() {
if (start == null && end == null) {
return 'TimeRange.indefinite';
}
if (start == null) {
return 'TimeRange.before($end)';
}
if (end == null) {
return 'TimeRange.after($start)';
}
return 'TimeRange.between($start, $end)';
}
}

/// No specific start or end time.
final class IndefiniteTimeRange extends TimeRange {
@literal
const IndefiniteTimeRange();

@override
Null get start => null;

@override
Null get end => null;

@override
bool contains(DateTime time) => true;
}

/// A specific time range, which includes at _least_ [start] or [end], or both.
final class SpecificTimeRange extends TimeRange {
/// Creates a time range with a specific [end] time.
///
/// If [exclusive] is set, the specific moment described in [end] is excluded.
SpecificTimeRange.before(
DateTime this.end, { //
this.exclusive = false,
}) : start = null;

/// Creates a time range with a specific [start] time.
///
/// If [exclusive] is set, the specific moment described in [start] is
/// excluded.
SpecificTimeRange.after(
DateTime this.start, { //
this.exclusive = false,
}) : end = null;

/// Creates a time range with a specific [start] and [end] time.
///
/// If [exclusive] is set, the specific moment described in [start] and [end]
/// are excluded.
///
/// [start] must be before [end].
SpecificTimeRange.between(
DateTime this.start, //
DateTime this.end, {
this.exclusive = false,
}) {
if (start!.isAfter(end!)) {
throw ArgumentError.value(
start,
'start',
'Start time must be before end time.',
);
}
}

/// Whether [start], or [end], if specified, should exclude that instant.
final bool exclusive;

/// Whether [start], or [end], if specified, should include that instant.
bool get inclusive => !exclusive;

@override
final DateTime? start;

@override
final DateTime? end;

@override
bool contains(DateTime time) {
if (start case final start? when start.isAfter(time)) {
return false;
}
if (end case final end? when end.isBefore(time)) {
return false;
}
if (exclusive) {
return time != start && time != end;
}
return true;
}
}
Loading