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
2 changes: 1 addition & 1 deletion app_dart/lib/cocoon_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export 'src/request_handlers/get_green_commits.dart';
export 'src/request_handlers/get_release_branches.dart';
export 'src/request_handlers/get_repos.dart';
export 'src/request_handlers/get_status.dart';
export 'src/request_handlers/get_test_suppression.dart';
export 'src/request_handlers/get_suppressed_tests.dart';
export 'src/request_handlers/github/webhook_subscription.dart';
export 'src/request_handlers/github_rate_limit_status.dart';
export 'src/request_handlers/github_webhook.dart';
Expand Down
8 changes: 4 additions & 4 deletions app_dart/lib/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'src/request_handlers/get_tree_status_changes.dart';
import 'src/request_handlers/lookup_hash.dart';
import 'src/request_handlers/trigger_workflow.dart';
import 'src/request_handlers/update_discord_status.dart';
import 'src/request_handlers/update_test_suppression.dart';
import 'src/request_handlers/update_suppressed_test.dart';
import 'src/request_handlers/update_tree_status.dart';
import 'src/service/big_query.dart';
import 'src/service/build_status_service.dart';
Expand Down Expand Up @@ -144,7 +144,7 @@ Server createServer({
authenticationProvider: authProvider,
firestore: firestore,
),
'/api/update-test-suppression': UpdateTestSuppression(
'/api/update-suppressed-test': UpdateSuppressedTest(
authenticationProvider: authProvider,
firestore: firestore,
config: config,
Expand Down Expand Up @@ -230,10 +230,10 @@ Server createServer({
),
ttl: const Duration(seconds: 15),
),
'/api/public/get-test-suppression': CacheRequestHandler(
'/api/public/suppressed-tests': CacheRequestHandler(
cache: cache,
config: config,
delegate: GetTestSuppression(config: config, firestore: firestore),
delegate: GetSuppressedTests(config: config, firestore: firestore),
ttl: const Duration(seconds: 15),
),
'/api/public/update-discord-status': CacheRequestHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import '../request_handling/exceptions.dart';

/// Request handler to get a list of suppressed tests.
///
/// GET /api/public/get-test-suppression
/// GET /api/public/suppressed-tests
///
/// Parameters:
/// repo: (string in query) default: 'flutter/flutter'. Name of the repo.
Expand All @@ -24,12 +24,20 @@ import '../request_handling/exceptions.dart';
/// "name": "foo_test",
/// "repository": "flutter/flutter",
/// "issueLink": "...",
/// "createTimestamp": 123456789
/// "createTimestamp": 123456789,
/// "updates": [
/// {
/// "updateTimestamp": 123456789,
/// "note": "...",
/// "user": "..."
/// "action": "SUPPRESS" or "UNSUPPRESS"
/// }
/// ]
/// }
/// ]
@immutable
final class GetTestSuppression extends RequestHandler {
const GetTestSuppression({required super.config, required this.firestore});
final class GetSuppressedTests extends RequestHandler {
const GetSuppressedTests({required super.config, required this.firestore});

final FirestoreService firestore;

Expand Down Expand Up @@ -60,7 +68,9 @@ final class GetTestSuppression extends RequestHandler {
'updates': [
for (var update in test.updates)
{
...update,
'user': update['user'],
'action': update['action'],
'note': update['note'],
'updateTimestamp':
update['updateTimestamp'].millisecondsSinceEpoch,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:cocoon_server/logging.dart';
import 'package:github/github.dart';
import 'package:googleapis/firestore/v1.dart';
import 'package:meta/meta.dart';
Expand All @@ -23,8 +24,8 @@ import '../service/firestore.dart';
/// - `action`: `SUPPRESS` or `UNSUPPRESS`.
/// - `issueLink`: URL to the GitHub issue tracking the failure.
/// - `note`: Optional note describing the action.
final class UpdateTestSuppression extends ApiRequestHandler {
const UpdateTestSuppression({
final class UpdateSuppressedTest extends ApiRequestHandler {
const UpdateSuppressedTest({
required FirestoreService firestore,
required super.config,
required super.authenticationProvider,
Expand Down Expand Up @@ -133,6 +134,10 @@ final class UpdateTestSuppression extends ApiRequestHandler {
note: note,
);

log.info(
'Test suppression update: $action $testName in $repository by ${authContext!.email}',
);

return Response.emptyOk;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ void main() {

late FakeFirestoreService firestore;
late ApiRequestHandlerTester tester;
late GetTestSuppression handler;
late GetSuppressedTests handler;
late FakeConfig config;

final fakeNow = DateTime.now().toUtc();
Expand All @@ -34,14 +34,14 @@ void main() {
dynamicConfig: DynamicConfig(dynamicTestSuppression: true),
);

handler = GetTestSuppression(config: config, firestore: firestore);
handler = GetSuppressedTests(config: config, firestore: firestore);
});

test('throws MethodNotAllowed if feature flag is disabled', () async {
config = FakeConfig(
dynamicConfig: DynamicConfig(dynamicTestSuppression: false),
);
handler = GetTestSuppression(config: config, firestore: firestore);
handler = GetSuppressedTests(config: config, firestore: firestore);

expect(() => tester.get(handler), throwsA(isA<MethodNotAllowed>()));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart:convert';

import 'package:cocoon_server_test/test_logging.dart';
import 'package:cocoon_service/src/model/firestore/suppressed_test.dart';
import 'package:cocoon_service/src/request_handlers/update_test_suppression.dart';
import 'package:cocoon_service/src/request_handlers/update_suppressed_test.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/firestore.dart';
import 'package:cocoon_service/src/service/flags/dynamic_config.dart';
Expand All @@ -24,7 +24,7 @@ void main() {

late FakeFirestoreService firestore;
late ApiRequestHandlerTester tester;
late UpdateTestSuppression handler;
late UpdateSuppressedTest handler;
late FakeConfig config;
late FakeGithubServiceWithIssue githubService;

Expand All @@ -45,7 +45,7 @@ void main() {
dynamicConfig: dynamicConfig,
);

handler = UpdateTestSuppression(
handler = UpdateSuppressedTest(
config: config,
authenticationProvider: FakeDashboardAuthentication(),
firestore: firestore,
Expand Down
65 changes: 65 additions & 0 deletions dashboard/lib/service/appengine_cocoon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,71 @@ class AppEngineCocoonService implements CocoonService {
}
}

@override
Future<CocoonResponse<List<SuppressedTest>>> fetchSuppressedTests({
String? repo,
}) async {
final getSuppressedTestsUrl = apiEndpoint(
'/api/public/suppressed-tests',
queryParameters: {'repo': ?repo},
);

final response = await _client.get(getSuppressedTestsUrl);

if (response.statusCode != HttpStatus.ok) {
return CocoonResponse.error(
'/api/public/suppressed-tests returned ${response.statusCode}',
statusCode: response.statusCode,
);
}

try {
final jsonResponse = jsonDecode(response.body) as List<Object?>;
final suppressedTests = <SuppressedTest>[];
for (final jsonTest in jsonResponse.cast<Map<String, Object?>>()) {
suppressedTests.add(SuppressedTest.fromJson(jsonTest));
}
return CocoonResponse.data(suppressedTests);
} catch (error) {
return CocoonResponse.error(
error.toString(),
statusCode: response.statusCode,
);
}
}

@override
Future<CocoonResponse<void>> updateTestSuppression({
required String idToken,
required String repo,
required String testName,
required bool suppress,
required String issueLink,
String? note,
}) async {
final updateTestSuppressionUrl = apiEndpoint(
'/api/update-suppressed-test',
);
final response = await _client.post(
updateTestSuppressionUrl,
headers: {'X-Flutter-IdToken': idToken},
body: jsonEncode({
'repo': repo,
'testName': testName,
'suppress': suppress,
'issueLink': issueLink,
'note': ?note,
}),
);
if (response.statusCode == HttpStatus.ok) {
return const CocoonResponse.data(null);
}
return CocoonResponse.error(
'HTTP Code: ${response.statusCode}, ${response.body}',
statusCode: response.statusCode,
);
}

@override
Future<CocoonResponse<void>> updateTreeStatus({
required String idToken,
Expand Down
17 changes: 17 additions & 0 deletions dashboard/lib/service/cocoon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ abstract class CocoonService {
required String repo,
required String branch,
});

/// Get the current list of suppressed tests in a particular repo.
Future<CocoonResponse<List<SuppressedTest>>> fetchSuppressedTests({
String? repo,
});

/// Updates the suppression status of a test.
///
/// [suppress] true means "suppress" (block tree), false means "unsuppress" (include test).
Future<CocoonResponse<void>> updateTestSuppression({
required String idToken,
required String repo,
required String testName,
required bool suppress,
required String issueLink,
String? note,
});
}

/// Wrapper class for data this state serves.
Expand Down
56 changes: 56 additions & 0 deletions dashboard/lib/service/dev_cocoon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,62 @@ class DevelopmentCocoonService implements CocoonService {
);
}

final _suppressedTests = <String, List<SuppressedTest>>{};

@override
Future<CocoonResponse<List<SuppressedTest>>> fetchSuppressedTests({
String? repo,
}) async {
final effectiveRepo = repo ?? 'flutter/flutter';
// Initialize with a default suppressed test if the repo hasn't been accessed yet
if (!_suppressedTests.containsKey(effectiveRepo)) {
_suppressedTests[effectiveRepo] = [
SuppressedTest(
name: 'Linux_android 0',
repository: effectiveRepo,
issueLink: 'https://github.com/flutter/flutter/issues/123',
createTimestamp: now.millisecondsSinceEpoch,
),
];
}
return CocoonResponse.data(_suppressedTests[effectiveRepo]!);
}

@override
Future<CocoonResponse<void>> updateTestSuppression({
required String idToken,
required String repo,
required String testName,
required bool suppress,
required String issueLink,
String? note,
}) async {
final list = _suppressedTests.putIfAbsent(repo, () => []);
if (suppress) {
if (!list.any((t) => t.name == testName)) {
list.add(
SuppressedTest(
name: testName,
repository: repo,
issueLink: issueLink,
createTimestamp: DateTime.now().millisecondsSinceEpoch,
updates: [
SuppressionUpdate(
user: 'dev-user',
action: 'SUPPRESS',
updateTimestamp: DateTime.now().millisecondsSinceEpoch,
note: note,
),
],
),
);
}
} else {
list.removeWhere((t) => t.name == testName);
}
return const CocoonResponse.data(null);
}

@override
Future<CocoonResponse<void>> updateTreeStatus({
required String idToken,
Expand Down
Loading