diff --git a/analysis_options.yaml b/analysis_options.yaml index c2c9d51e3f..d809773723 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,6 +12,7 @@ analyzer: avoid_dynamic_calls: ignore comment_references: ignore deprecated_member_use_from_same_package: ignore + duplicate_ignore: ignore lines_longer_than_80_chars: ignore only_throw_errors: ignore exclude: diff --git a/app_dart/bin/gae_server.dart b/app_dart/bin/gae_server.dart index 28575c5ab7..ac93aac6de 100644 --- a/app_dart/bin/gae_server.dart +++ b/app_dart/bin/gae_server.dart @@ -9,7 +9,8 @@ import 'package:cocoon_server/google_auth_provider.dart'; import 'package:cocoon_server/secret_manager.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/server.dart'; -import 'package:cocoon_service/src/request_handling/dashboard_authentication.dart'; +import 'package:cocoon_service/src/foundation/appengine_utils.dart'; +import 'package:cocoon_service/src/foundation/providers.dart'; import 'package:cocoon_service/src/service/big_query.dart'; import 'package:cocoon_service/src/service/build_status_service.dart'; import 'package:cocoon_service/src/service/commit_service.dart'; @@ -24,6 +25,8 @@ Future main() async { await withAppEngineServices(() async { useLoggingPackageAdaptor(); + Providers.contextProvider = () => AppEngineClientContext(context); + // This is bad, and I should feel bad, but I won't because the logging system // is inherently bad. We're allocating the logger (or getting back one) and // then turning it off - there is no way to "filter". Luckily; the library diff --git a/app_dart/lib/cocoon_service.dart b/app_dart/lib/cocoon_service.dart index 245ddd83c4..27214df51c 100644 --- a/app_dart/lib/cocoon_service.dart +++ b/app_dart/lib/cocoon_service.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/foundation/context.dart'; export 'src/foundation/utils.dart'; export 'src/request_handlers/check_flaky_builders.dart'; export 'src/request_handlers/create_branch.dart'; @@ -32,6 +33,8 @@ export 'src/request_handlers/update_existing_flaky_issues.dart'; export 'src/request_handlers/vacuum_github_commits.dart'; export 'src/request_handling/authentication.dart'; export 'src/request_handling/cache_request_handler.dart'; +export 'src/request_handling/checkrun_authentication.dart'; +export 'src/request_handling/dashboard_authentication.dart'; export 'src/request_handling/pubsub.dart'; export 'src/request_handling/pubsub_authentication.dart'; export 'src/request_handling/request_handler.dart'; diff --git a/app_dart/lib/server.dart b/app_dart/lib/server.dart index c7fb0212c1..6457eee212 100644 --- a/app_dart/lib/server.dart +++ b/app_dart/lib/server.dart @@ -9,6 +9,7 @@ import 'cocoon_service.dart'; import 'src/request_handlers/get_engine_artifacts_ready.dart'; import 'src/request_handlers/get_presubmit_checks.dart'; import 'src/request_handlers/get_presubmit_guard.dart'; +import 'src/request_handlers/get_presubmit_guard_summaries.dart'; import 'src/request_handlers/get_tree_status_changes.dart'; import 'src/request_handlers/github_webhook_replay.dart'; import 'src/request_handlers/lookup_hash.dart'; @@ -190,6 +191,11 @@ Server createServer({ authenticationProvider: authProvider, firestore: firestore, ), + '/api/get-presubmit-guard-summaries': GetPresubmitGuardSummaries( + config: config, + authenticationProvider: authProvider, + firestore: firestore, + ), '/api/get-presubmit-checks': GetPresubmitChecks( config: config, authenticationProvider: authProvider, diff --git a/app_dart/lib/src/foundation/appengine_utils.dart b/app_dart/lib/src/foundation/appengine_utils.dart new file mode 100644 index 0000000000..5c0370c15a --- /dev/null +++ b/app_dart/lib/src/foundation/appengine_utils.dart @@ -0,0 +1,15 @@ +// Copyright 2026 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:appengine/appengine.dart' as gae; + +import 'context.dart'; + +class AppEngineClientContext implements ClientContext { + final gae.ClientContext _context; + AppEngineClientContext(this._context); + + @override + bool get isDevelopmentEnvironment => _context.isDevelopmentEnvironment; +} diff --git a/app_dart/lib/src/foundation/context.dart b/app_dart/lib/src/foundation/context.dart new file mode 100644 index 0000000000..83d080d131 --- /dev/null +++ b/app_dart/lib/src/foundation/context.dart @@ -0,0 +1,12 @@ +// Copyright 2026 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. + +/// A context for the current request. +/// +/// This abstracts the underlying App Engine context to avoid direct dependencies +/// on package:appengine in core logic. +abstract class ClientContext { + /// Whether the application is running in the development environment. + bool get isDevelopmentEnvironment; +} diff --git a/app_dart/lib/src/foundation/providers.dart b/app_dart/lib/src/foundation/providers.dart index 3c2831899c..6f69d6799f 100644 --- a/app_dart/lib/src/foundation/providers.dart +++ b/app_dart/lib/src/foundation/providers.dart @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:appengine/appengine.dart' as gae; import 'package:http/http.dart' as http; +import 'context.dart'; import 'typedefs.dart'; /// Class that holds static default providers. @@ -18,17 +18,16 @@ class Providers { /// * [HttpClientProvider], which defines this interface. static http.Client freshHttpClient() => http.Client(); - /// Default [gae.Logging] provider. + /// Initializes the [ClientContext] provider. /// - /// See also: - /// - /// * [LoggingProvider], which defines this interface. - static gae.Logging serviceScopeLogger() => gae.loggingService; + /// This must be called before [serviceScopeContext] is used. + static ClientContextProvider contextProvider = () => + throw UnimplementedError('ClientContext provider not initialized'); - /// Default [gae.ClientContext] provider. + /// Default [ClientContext] provider. /// /// See also: /// /// * [ClientContextProvider], which defines this interface. - static gae.ClientContext serviceScopeContext() => gae.context; + static ClientContext serviceScopeContext() => contextProvider(); } diff --git a/app_dart/lib/src/foundation/typedefs.dart b/app_dart/lib/src/foundation/typedefs.dart index fe0ddc51dd..b027fbf83e 100644 --- a/app_dart/lib/src/foundation/typedefs.dart +++ b/app_dart/lib/src/foundation/typedefs.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:appengine/appengine.dart'; import 'package:http/http.dart' as http; +import 'context.dart'; + /// Signature for a function that returns an App Engine [ClientContext]. /// /// This is used in [AuthenticationProvider] to provide the client context diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart index dbe1b76ebe..f52e7501fa 100644 --- a/app_dart/lib/src/request_handlers/get_presubmit_guard.dart +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard.dart @@ -63,18 +63,18 @@ final class GetPresubmitGuard extends ApiRequestHandler { // Consolidate metadata from the first record. final first = guards.first; - final GuardStatus guardStatus; - if (guards.any((g) => g.failedBuilds > 0)) { - guardStatus = GuardStatus.failed; - } else if (guards.every( - (g) => g.failedBuilds == 0 && g.remainingBuilds == 0, - )) { - guardStatus = GuardStatus.succeeded; - } else if (guards.every((g) => g.remainingBuilds == g.builds.length)) { - guardStatus = GuardStatus.waitingForBackfill; - } else { - guardStatus = GuardStatus.inProgress; - } + final totalFailed = guards.fold(0, (sum, g) => sum + g.failedBuilds); + final totalRemaining = guards.fold( + 0, + (sum, g) => sum + g.remainingBuilds, + ); + final totalBuilds = guards.fold(0, (sum, g) => sum + g.builds.length); + + final guardStatus = GuardStatus.calculate( + failedBuilds: totalFailed, + remainingBuilds: totalRemaining, + totalBuilds: totalBuilds, + ); final response = rpc_model.PresubmitGuardResponse( prNum: first.pullRequestId, diff --git a/app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart b/app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart new file mode 100644 index 0000000000..22b70cd444 --- /dev/null +++ b/app_dart/lib/src/request_handlers/get_presubmit_guard_summaries.dart @@ -0,0 +1,112 @@ +// Copyright 2026 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 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart' as rpc_model; +import 'package:github/github.dart'; +import 'package:meta/meta.dart'; + +import '../../cocoon_service.dart'; +import '../model/firestore/presubmit_guard.dart'; +import '../request_handling/api_request_handler.dart'; +import '../service/firestore/unified_check_run.dart'; + +/// Request handler for retrieving all presubmit guards for a specific pull request. +/// +/// GET: /api/get-presubmit-guard-summaries +/// +/// Parameters: +/// repo: (string in query) required. The repository name (e.g., 'flutter'). +/// pr: (int in query) required. The pull request number. +/// owner: (string in query) optional. The repository owner (e.g., 'flutter'). +@immutable +final class GetPresubmitGuardSummaries extends ApiRequestHandler { + /// Defines the [GetPresubmitGuardSummaries] handler. + const GetPresubmitGuardSummaries({ + required super.config, + required super.authenticationProvider, + required FirestoreService firestore, + }) : _firestore = firestore; + + final FirestoreService _firestore; + + /// The name of the query parameter for the repository name (e.g. 'flutter'). + static const String kRepoParam = 'repo'; + + /// The name of the query parameter for the pull request number. + static const String kPRParam = 'pr'; + + /// The name of the query parameter for the repository owner (e.g. 'flutter'). + static const String kOwnerParam = 'owner'; + + @override + Future get(Request request) async { + checkRequiredQueryParameters(request, [kRepoParam, kPRParam]); + + final repo = request.uri.queryParameters[kRepoParam]!; + final prNumber = int.parse(request.uri.queryParameters[kPRParam]!); + final owner = request.uri.queryParameters[kOwnerParam] ?? 'flutter'; + + final slug = RepositorySlug(owner, repo); + final guards = await UnifiedCheckRun.getPresubmitGuardsForPullRequest( + firestoreService: _firestore, + slug: slug, + pullRequestId: prNumber, + ); + + if (guards.isEmpty) { + return Response.json({ + 'error': 'No guards found for PR $prNumber in $slug', + }, statusCode: HttpStatus.notFound); + } + + // Group guards by commitSha + final groupedGuards = >{}; + for (final guard in guards) { + groupedGuards.putIfAbsent(guard.commitSha, () => []).add(guard); + } + + final responseGuards = []; + for (final entry in groupedGuards.entries) { + final sha = entry.key; + final shaGuards = entry.value; + + final totalFailed = shaGuards.fold( + 0, + (int sum, PresubmitGuard g) => sum + g.failedBuilds, + ); + final totalRemaining = shaGuards.fold( + 0, + (int sum, PresubmitGuard g) => sum + g.remainingBuilds, + ); + final totalBuilds = shaGuards.fold( + 0, + (int sum, PresubmitGuard g) => sum + g.builds.length, + ); + final earliestCreationTime = shaGuards.fold( + // assuming creation time is always in the past :) + DateTime.now().millisecondsSinceEpoch, + (int curr, PresubmitGuard g) => min(g.creationTime, curr), + ); + + responseGuards.add( + rpc_model.PresubmitGuardSummary( + commitSha: sha, + creationTime: earliestCreationTime, + guardStatus: GuardStatus.calculate( + failedBuilds: totalFailed, + remainingBuilds: totalRemaining, + totalBuilds: totalBuilds, + ), + ), + ); + } + + return Response.json(responseGuards); + } +} diff --git a/app_dart/lib/src/request_handling/authentication.dart b/app_dart/lib/src/request_handling/authentication.dart index 3a970f68cc..218ec07a5d 100644 --- a/app_dart/lib/src/request_handling/authentication.dart +++ b/app_dart/lib/src/request_handling/authentication.dart @@ -5,9 +5,9 @@ import 'dart:async'; import 'dart:io'; -import 'package:appengine/appengine.dart'; import 'package:meta/meta.dart'; +import '../foundation/context.dart'; import 'exceptions.dart'; @immutable diff --git a/app_dart/lib/src/request_handling/checkrun_authentication.dart b/app_dart/lib/src/request_handling/checkrun_authentication.dart index 730abd538f..10350d1e17 100644 --- a/app_dart/lib/src/request_handling/checkrun_authentication.dart +++ b/app_dart/lib/src/request_handling/checkrun_authentication.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:appengine/appengine.dart'; import 'package:cocoon_server/logging.dart'; import 'package:github/github.dart'; import 'package:meta/meta.dart'; @@ -15,7 +14,6 @@ import '../foundation/providers.dart'; import '../foundation/typedefs.dart'; import '../model/google/token_info.dart'; import '../service/firebase_jwt_validator.dart'; -import 'dashboard_authentication.dart'; import 'exceptions.dart'; /// Class capable of authenticating [HttpRequest]s from the Checkrun page. diff --git a/app_dart/lib/src/request_handling/dashboard_authentication.dart b/app_dart/lib/src/request_handling/dashboard_authentication.dart index f492e289fc..62139ec424 100644 --- a/app_dart/lib/src/request_handling/dashboard_authentication.dart +++ b/app_dart/lib/src/request_handling/dashboard_authentication.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:appengine/appengine.dart'; import 'package:cocoon_server/logging.dart'; import 'package:meta/meta.dart'; diff --git a/app_dart/lib/src/request_handling/pubsub_authentication.dart b/app_dart/lib/src/request_handling/pubsub_authentication.dart index fc7e7ac89f..eb6a290daa 100644 --- a/app_dart/lib/src/request_handling/pubsub_authentication.dart +++ b/app_dart/lib/src/request_handling/pubsub_authentication.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:appengine/appengine.dart'; import 'package:cocoon_server/logging.dart'; import 'package:googleapis/oauth2/v2.dart'; import 'package:meta/meta.dart'; diff --git a/app_dart/lib/src/request_handling/swarming_authentication.dart b/app_dart/lib/src/request_handling/swarming_authentication.dart index 57d1324e55..f2a1b02284 100644 --- a/app_dart/lib/src/request_handling/swarming_authentication.dart +++ b/app_dart/lib/src/request_handling/swarming_authentication.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:appengine/appengine.dart'; import 'package:cocoon_server/logging.dart'; import 'package:meta/meta.dart'; diff --git a/app_dart/lib/src/service/firestore/unified_check_run.dart b/app_dart/lib/src/service/firestore/unified_check_run.dart index 771e722b37..6b33b9a8d4 100644 --- a/app_dart/lib/src/service/firestore/unified_check_run.dart +++ b/app_dart/lib/src/service/firestore/unified_check_run.dart @@ -243,6 +243,19 @@ final class UnifiedCheckRun { ); } + /// Queries for [PresubmitGuard] records by [slug] and [pullRequestId]. + static Future> getPresubmitGuardsForPullRequest({ + required FirestoreService firestoreService, + required RepositorySlug slug, + required int pullRequestId, + }) async { + return await _queryPresubmitGuards( + firestoreService: firestoreService, + slug: slug, + pullRequestId: pullRequestId, + ); + } + static Future> _queryPresubmitGuards({ required FirestoreService firestoreService, Transaction? transaction, diff --git a/app_dart/pubspec.yaml b/app_dart/pubspec.yaml index 30f06bf6ab..9acaaa86df 100644 --- a/app_dart/pubspec.yaml +++ b/app_dart/pubspec.yaml @@ -57,6 +57,8 @@ dev_dependencies: build_runner: ^2.4.15 cocoon_common_test: path: ../packages/cocoon_common_test + cocoon_integration_test: + path: ../packages/cocoon_integration_test cocoon_server_test: path: ../packages/cocoon_server_test dart_flutter_team_lints: 3.5.2 diff --git a/app_dart/test/foundation/utils_test.dart b/app_dart/test/foundation/utils_test.dart index c4bd9ef5c2..49a69b2b6a 100644 --- a/app_dart/test/foundation/utils_test.dart +++ b/app_dart/test/foundation/utils_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:cocoon_common/cocoon_common.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/foundation/utils.dart'; @@ -18,9 +19,6 @@ import 'package:http/testing.dart'; import 'package:retry/retry.dart'; import 'package:test/test.dart'; -import '../src/bigquery/fake_tabledata_resource.dart'; -import '../src/utilities/entity_generators.dart'; - const String branchRegExp = ''' master flutter-1.1-candidate.1 diff --git a/app_dart/test/model/ci_yaml/ci_yaml_test.dart b/app_dart/test/model/ci_yaml/ci_yaml_test.dart index 32780417bb..bffd545a6b 100644 --- a/app_dart/test/model/ci_yaml/ci_yaml_test.dart +++ b/app_dart/test/model/ci_yaml/ci_yaml_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/protos.dart' as pb; import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart'; @@ -10,9 +11,6 @@ import 'package:cocoon_service/src/service/config.dart'; import 'package:cocoon_service/src/service/flags/ci_yaml_flags.dart'; import 'package:test/test.dart'; -import '../../src/model/ci_yaml_matcher.dart'; -import '../../src/service/fake_scheduler.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/ci_yaml/target_test.dart b/app_dart/test/model/ci_yaml/target_test.dart index 487ccf45aa..68dc82bbc3 100644 --- a/app_dart/test/model/ci_yaml/target_test.dart +++ b/app_dart/test/model/ci_yaml/target_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/proto/internal/scheduler.pbenum.dart'; import 'package:cocoon_service/src/model/proto/protos.dart' as pb; @@ -10,9 +11,6 @@ import 'package:cocoon_service/src/service/scheduler/policy.dart'; import 'package:github/github.dart' as github; import 'package:test/test.dart'; -import '../../src/model/ci_yaml_matcher.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/ci_staging_test.dart b/app_dart/test/model/firestore/ci_staging_test.dart index 11c4169b66..60f926f9ee 100644 --- a/app_dart/test/model/firestore/ci_staging_test.dart +++ b/app_dart/test/model/firestore/ci_staging_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/common/presubmit_guard_conclusion.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; @@ -11,8 +12,6 @@ import 'package:github/github.dart'; import 'package:googleapis/firestore/v1.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/commit_test.dart b/app_dart/test/model/firestore/commit_test.dart index c2b05ff1af..91985ccc42 100644 --- a/app_dart/test/model/firestore/commit_test.dart +++ b/app_dart/test/model/firestore/commit_test.dart @@ -2,13 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/commit.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/github_build_status_test.dart b/app_dart/test/model/firestore/github_build_status_test.dart index 2a6bf267d5..db4b962921 100644 --- a/app_dart/test/model/firestore/github_build_status_test.dart +++ b/app_dart/test/model/firestore/github_build_status_test.dart @@ -2,13 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/github_build_status.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/github_gold_status_test.dart b/app_dart/test/model/firestore/github_gold_status_test.dart index 48c3fb3863..3c75fb8fc8 100644 --- a/app_dart/test/model/firestore/github_gold_status_test.dart +++ b/app_dart/test/model/firestore/github_gold_status_test.dart @@ -2,13 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/github_gold_status.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/pr_check_runs_test.dart b/app_dart/test/model/firestore/pr_check_runs_test.dart index 308ca9f679..766f101de9 100644 --- a/app_dart/test/model/firestore/pr_check_runs_test.dart +++ b/app_dart/test/model/firestore/pr_check_runs_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/common/firestore_extensions.dart'; import 'package:cocoon_service/src/model/firestore/pr_check_runs.dart'; @@ -11,9 +12,6 @@ import 'package:github/github.dart'; import 'package:googleapis/firestore/v1.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/presubmit_check_test.dart b/app_dart/test/model/firestore/presubmit_check_test.dart index 8548f209a4..db89b824a4 100644 --- a/app_dart/test/model/firestore/presubmit_check_test.dart +++ b/app_dart/test/model/firestore/presubmit_check_test.dart @@ -4,14 +4,13 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/presubmit_check.dart'; import 'package:fixnum/fixnum.dart'; import 'package:googleapis/firestore/v1.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/firestore/task_test.dart b/app_dart/test/model/firestore/task_test.dart index f4588e7a32..da99b08350 100644 --- a/app_dart/test/model/firestore/task_test.dart +++ b/app_dart/test/model/firestore/task_test.dart @@ -6,13 +6,11 @@ import 'dart:convert'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/task.dart'; import 'package:test/test.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/model/ref_test.dart b/app_dart/test/model/ref_test.dart index f735749c7e..fb4695f76d 100644 --- a/app_dart/test/model/ref_test.dart +++ b/app_dart/test/model/ref_test.dart @@ -3,14 +3,13 @@ // found in the LICENSE file. import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; import 'package:cocoon_service/src/model/task_ref.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../src/model/ref_matcher.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/check_flaky_builders_test.dart b/app_dart/test/request_handlers/check_flaky_builders_test.dart index f1b701d6ef..401bc8431d 100644 --- a/app_dart/test/request_handlers/check_flaky_builders_test.dart +++ b/app_dart/test/request_handlers/check_flaky_builders_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:io' as io; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/ci_yaml.dart'; @@ -20,11 +21,7 @@ import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/utilities/mocks.dart'; import 'check_flaky_builders_test_data.dart'; const String kThreshold = '0.02'; diff --git a/app_dart/test/request_handlers/create_branch_test.dart b/app_dart/test/request_handlers/create_branch_test.dart index 9c04411e0c..acb73e77ad 100644 --- a/app_dart/test/request_handlers/create_branch_test.dart +++ b/app_dart/test/request_handlers/create_branch_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handlers/create_branch.dart'; import 'package:cocoon_service/src/request_handling/request_handler.dart'; @@ -9,11 +10,7 @@ import 'package:cocoon_service/src/service/branch_service.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/dart_internal_subscription_test.dart b/app_dart/test/request_handlers/dart_internal_subscription_test.dart index 78334d66c3..0765e48972 100644 --- a/app_dart/test/request_handlers/dart_internal_subscription_test.dart +++ b/app_dart/test/request_handlers/dart_internal_subscription_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/task.dart' as fs; @@ -13,13 +14,7 @@ import 'package:cocoon_service/src/model/luci/pubsub_message.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/subscription_tester.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/file_flaky_issue_and_pr_test.dart b/app_dart/test/request_handlers/file_flaky_issue_and_pr_test.dart index 7f2f558e17..171cbf65ba 100644 --- a/app_dart/test/request_handlers/file_flaky_issue_and_pr_test.dart +++ b/app_dart/test/request_handlers/file_flaky_issue_and_pr_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/ci_yaml.dart'; @@ -19,11 +20,7 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/utilities/mocks.dart'; import 'file_flaky_issue_and_pr_test_data.dart'; const String kThreshold = '0.02'; diff --git a/app_dart/test/request_handlers/flaky_handler_utiles_test.dart b/app_dart/test/request_handlers/flaky_handler_utiles_test.dart index f8b6228f1a..5532cc9b33 100644 --- a/app_dart/test/request_handlers/flaky_handler_utiles_test.dart +++ b/app_dart/test/request_handlers/flaky_handler_utiles_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/protos.dart' as pb; @@ -12,8 +13,6 @@ import 'package:github/github.dart' hide Team; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/flush_cache_test.dart b/app_dart/test/request_handlers/flush_cache_test.dart index ac6e463a2d..2ad8ab4c97 100644 --- a/app_dart/test/request_handlers/flush_cache_test.dart +++ b/app_dart/test/request_handlers/flush_cache_test.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handlers/flush_cache.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; @@ -11,10 +12,7 @@ import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:cocoon_service/src/service/config.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_build_status_badge_test.dart b/app_dart/test/request_handlers/get_build_status_badge_test.dart index a9547f7d24..55dffbe7fb 100644 --- a/app_dart/test/request_handlers/get_build_status_badge_test.dart +++ b/app_dart/test/request_handlers/get_build_status_badge_test.dart @@ -3,14 +3,12 @@ // found in the LICENSE file. import 'package:cocoon_common/rpc_model.dart' as rpc_model; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handlers/get_build_status_badge.dart'; import 'package:cocoon_service/src/service/build_status_service.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/service/fake_firestore_service.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_build_status_test.dart b/app_dart/test/request_handlers/get_build_status_test.dart index 815e4d9c1e..a3c48d0dc8 100644 --- a/app_dart/test/request_handlers/get_build_status_test.dart +++ b/app_dart/test/request_handlers/get_build_status_test.dart @@ -5,15 +5,13 @@ import 'dart:convert'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/service/build_status_service.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_engine_artifacts_ready_test.dart b/app_dart/test/request_handlers/get_engine_artifacts_ready_test.dart index 9d2484c506..8e53b1549e 100644 --- a/app_dart/test/request_handlers/get_engine_artifacts_ready_test.dart +++ b/app_dart/test/request_handlers/get_engine_artifacts_ready_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/common/firestore_extensions.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; @@ -14,9 +15,7 @@ import 'package:cocoon_service/src/service/config.dart'; import 'package:googleapis/firestore/v1.dart' as g; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_green_commits_test.dart b/app_dart/test/request_handlers/get_green_commits_test.dart index 9838230b93..a27a374785 100644 --- a/app_dart/test/request_handlers/get_green_commits_test.dart +++ b/app_dart/test/request_handlers/get_green_commits_test.dart @@ -5,16 +5,13 @@ import 'dart:convert'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/service/build_status_provider/commit_tasks_status.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_build_status_service.dart'; -import '../src/utilities/entity_generators.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_presubmit_checks_test.dart b/app_dart/test/request_handlers/get_presubmit_checks_test.dart index ffc39bdeb5..fd6cc77f9c 100644 --- a/app_dart/test/request_handlers/get_presubmit_checks_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_checks_test.dart @@ -7,17 +7,14 @@ import 'dart:io'; import 'package:cocoon_common/rpc_model.dart'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/presubmit_check.dart' as fs; import 'package:cocoon_service/src/request_handlers/get_presubmit_checks.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { group('GetPresubmitChecks', () { diff --git a/app_dart/test/request_handlers/get_presubmit_guard_summaries_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_summaries_test.dart new file mode 100644 index 0000000000..bc0fa002de --- /dev/null +++ b/app_dart/test/request_handlers/get_presubmit_guard_summaries_test.dart @@ -0,0 +1,129 @@ +// Copyright 2026 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 'dart:convert'; +import 'dart:io'; + +import 'package:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart' as rpc_model; +import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; +import 'package:cocoon_server_test/test_logging.dart'; +import 'package:cocoon_service/src/request_handlers/get_presubmit_guard_summaries.dart'; +import 'package:cocoon_service/src/request_handling/exceptions.dart'; +import 'package:github/github.dart'; +import 'package:test/test.dart'; + +import '../src/request_handling/request_handler_tester.dart'; + +void main() { + useTestLoggerPerTest(); + + late RequestHandlerTester tester; + late GetPresubmitGuardSummaries handler; + late FakeFirestoreService firestore; + + Future?> getResponse() async { + final response = await tester.get(handler); + if (response.statusCode != HttpStatus.ok) { + return null; + } + final responseBody = await utf8.decoder + .bind(response.body as Stream>) + .transform(json.decoder) + .single; + return (responseBody as List) + .map( + (e) => rpc_model.PresubmitGuardSummary.fromJson( + e as Map, + ), + ) + .toList(); + } + + setUp(() { + firestore = FakeFirestoreService(); + tester = RequestHandlerTester(); + handler = GetPresubmitGuardSummaries( + config: FakeConfig(), + authenticationProvider: FakeDashboardAuthentication(), + firestore: firestore, + ); + }); + + test('missing parameters', () async { + tester.request = FakeHttpRequest(); + expect(tester.get(handler), throwsA(isA())); + }); + + test('no guards found', () async { + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuardSummaries.kRepoParam: 'flutter', + GetPresubmitGuardSummaries.kPRParam: '123', + }, + ); + + final response = await tester.get(handler); + expect(response.statusCode, HttpStatus.notFound); + }); + + test('returns multiple guards grouped by commitSha', () async { + final slug = RepositorySlug('flutter', 'flutter'); + const prNumber = 123; + + // SHA1: Two stages, both succeeded. + final guard1a = generatePresubmitGuard( + slug: slug, + pullRequestId: prNumber, + commitSha: 'sha1', + checkRun: generateCheckRun(1), + creationTime: 100, + builds: {'test1': TaskStatus.succeeded}, + remainingBuilds: 0, + ); + + final guard1b = generatePresubmitGuard( + slug: slug, + pullRequestId: prNumber, + commitSha: 'sha1', + checkRun: generateCheckRun(2), + creationTime: 110, + builds: {'test2': TaskStatus.succeeded}, + remainingBuilds: 0, + ); + + // SHA2: One stage, failed. + final guard2 = generatePresubmitGuard( + slug: slug, + pullRequestId: prNumber, + commitSha: 'sha2', + checkRun: generateCheckRun(3), + creationTime: 200, + builds: {'test3': TaskStatus.failed}, + failedBuilds: 1, + remainingBuilds: 0, + ); + + firestore.putDocuments([guard1a, guard1b, guard2]); + + tester.request = FakeHttpRequest( + queryParametersValue: { + GetPresubmitGuardSummaries.kRepoParam: 'flutter', + GetPresubmitGuardSummaries.kPRParam: prNumber.toString(), + }, + ); + + final result = (await getResponse())!; + expect(result.length, 2); + + final item1 = result.firstWhere((g) => g.commitSha == 'sha1'); + expect(item1.creationTime, 100); // earliest + expect(item1.guardStatus, GuardStatus.succeeded); + + final item2 = result.firstWhere((g) => g.commitSha == 'sha2'); + expect(item2.creationTime, 200); + expect(item2.guardStatus, GuardStatus.failed); + }); +} diff --git a/app_dart/test/request_handlers/get_presubmit_guard_test.dart b/app_dart/test/request_handlers/get_presubmit_guard_test.dart index 65e74e3e2f..b57f740fb5 100644 --- a/app_dart/test/request_handlers/get_presubmit_guard_test.dart +++ b/app_dart/test/request_handlers/get_presubmit_guard_test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:cocoon_common/guard_status.dart'; import 'package:cocoon_common/src/rpc_model/presubmit_guard.dart'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/request_handlers/get_presubmit_guard.dart'; @@ -15,12 +16,7 @@ import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; void main() { useTestLoggerPerTest(); @@ -82,9 +78,8 @@ void main() { commitSha: sha, stage: CiStage.fusionTests, builds: {'test1': TaskStatus.succeeded}, + remainingBuilds: 0, ); - guard1.failedBuilds = 0; - guard1.remainingBuilds = 0; final guard2 = generatePresubmitGuard( slug: slug, @@ -92,8 +87,6 @@ void main() { stage: CiStage.fusionEngineBuild, builds: {'engine1': TaskStatus.inProgress}, ); - guard2.failedBuilds = 0; - guard2.remainingBuilds = 1; firestore.putDocuments([guard1, guard2]); @@ -128,9 +121,9 @@ void main() { slug: slug, commitSha: sha, builds: {'test1': TaskStatus.failed}, + failedBuilds: 1, + remainingBuilds: 0, ); - guard.failedBuilds = 1; - guard.remainingBuilds = 0; firestore.putDocuments([guard]); @@ -155,9 +148,8 @@ void main() { slug: slug, commitSha: sha, builds: {'test1': TaskStatus.succeeded}, + remainingBuilds: 0, ); - guard.failedBuilds = 0; - guard.remainingBuilds = 0; firestore.putDocuments([guard]); @@ -182,8 +174,6 @@ void main() { commitSha: sha, builds: {'test1': TaskStatus.waitingForBackfill}, ); - guard.failedBuilds = 0; - guard.remainingBuilds = 1; firestore.putDocuments([guard]); diff --git a/app_dart/test/request_handlers/get_status_test.dart b/app_dart/test/request_handlers/get_status_test.dart index 28dafc6b86..ddaae72a4c 100644 --- a/app_dart/test/request_handlers/get_status_test.dart +++ b/app_dart/test/request_handlers/get_status_test.dart @@ -5,17 +5,13 @@ import 'dart:convert'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handlers/get_status.dart'; import 'package:cocoon_service/src/service/build_status_provider/commit_tasks_status.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_build_status_service.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_suppressed_tests_test.dart b/app_dart/test/request_handlers/get_suppressed_tests_test.dart index 79f0ee13a8..d10cc6a2aa 100644 --- a/app_dart/test/request_handlers/get_suppressed_tests_test.dart +++ b/app_dart/test/request_handlers/get_suppressed_tests_test.dart @@ -4,17 +4,14 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/suppressed_test.dart'; - import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; - import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/get_tree_status_test.dart b/app_dart/test/request_handlers/get_tree_status_test.dart index dd1a164d52..25570e9d09 100644 --- a/app_dart/test/request_handlers/get_tree_status_test.dart +++ b/app_dart/test/request_handlers/get_tree_status_test.dart @@ -4,6 +4,7 @@ import 'package:cocoon_common/core_extensions.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/tree_status_change.dart'; import 'package:cocoon_service/src/request_handlers/get_tree_status_changes.dart'; @@ -11,10 +12,7 @@ import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/github/webhook_subscription_test.dart b/app_dart/test/request_handlers/github/webhook_subscription_test.dart index bd11cbfb81..d5fb711fd0 100644 --- a/app_dart/test/request_handlers/github/webhook_subscription_test.dart +++ b/app_dart/test/request_handlers/github/webhook_subscription_test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/core_extensions.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; @@ -29,17 +30,7 @@ import 'package:googleapis/bigquery/v2.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_http.dart'; import '../../src/request_handling/subscription_tester.dart'; -import '../../src/service/fake_build_bucket_client.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/service/fake_github_service.dart'; -import '../../src/service/fake_scheduler.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.dart'; -import '../../src/utilities/webhook_generators.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/github_webhook_replay_test.dart b/app_dart/test/request_handlers/github_webhook_replay_test.dart index 0e22636f14..7c603637a1 100644 --- a/app_dart/test/request_handlers/github_webhook_replay_test.dart +++ b/app_dart/test/request_handlers/github_webhook_replay_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/github_webhook_message.dart'; @@ -9,11 +10,7 @@ import 'package:cocoon_service/src/request_handlers/github_webhook_replay.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_pubsub.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/github_webhook_test.dart b/app_dart/test/request_handlers/github_webhook_test.dart index 70744b1d72..add8bf63d1 100644 --- a/app_dart/test/request_handlers/github_webhook_test.dart +++ b/app_dart/test/request_handlers/github_webhook_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/protos.dart'; @@ -14,11 +15,7 @@ import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:crypto/crypto.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/request_handling/fake_pubsub.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/lookup_hash_test.dart b/app_dart/test/request_handlers/lookup_hash_test.dart index d450202802..737d3a4c92 100644 --- a/app_dart/test/request_handlers/lookup_hash_test.dart +++ b/app_dart/test/request_handlers/lookup_hash_test.dart @@ -5,15 +5,13 @@ import 'dart:convert'; import 'package:cocoon_common/rpc_model.dart' show ContentHashLookup; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/content_aware_hash_builds.dart'; import 'package:cocoon_service/src/request_handlers/lookup_hash.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_content_aware_hash_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/merge_queue_hooks_test.dart b/app_dart/test/request_handlers/merge_queue_hooks_test.dart index 127289b91a..bfdbae8a20 100644 --- a/app_dart/test/request_handlers/merge_queue_hooks_test.dart +++ b/app_dart/test/request_handlers/merge_queue_hooks_test.dart @@ -5,16 +5,14 @@ import 'dart:convert'; import 'package:cocoon_common/rpc_model.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/github_webhook_message.dart'; import 'package:cocoon_service/src/request_handlers/merge_queue_hooks.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart b/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart index 27757a3141..fcd3e5fd42 100644 --- a/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart +++ b/app_dart/test/request_handlers/postsubmit_luci_subscription_test.dart @@ -5,6 +5,7 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -14,17 +15,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/subscription_tester.dart'; -import '../src/service/fake_ci_yaml_fetcher.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_luci_build_service.dart'; -import '../src/service/fake_scheduler.dart'; -import '../src/utilities/build_bucket_messages.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart b/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart index 300bbaef7e..3432b2de45 100644 --- a/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart +++ b/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart @@ -4,6 +4,7 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -19,16 +20,7 @@ import 'package:github/github.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; import '../src/request_handling/subscription_tester.dart'; -import '../src/service/fake_ci_yaml_fetcher.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_luci_build_service.dart'; -import '../src/service/fake_scheduler.dart'; -import '../src/utilities/build_bucket_messages.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/push_build_status_to_github_test.dart b/app_dart/test/request_handlers/push_build_status_to_github_test.dart index cf0660508f..02f94672b4 100644 --- a/app_dart/test/request_handlers/push_build_status_to_github_test.dart +++ b/app_dart/test/request_handlers/push_build_status_to_github_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/github_build_status.dart'; @@ -14,15 +15,7 @@ import 'package:github/github.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/bigquery/fake_tabledata_resource.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_build_status_service.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_github_service.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/push_gold_status_to_github_test.dart b/app_dart/test/request_handlers/push_gold_status_to_github_test.dart index 0a13e6e4c9..097fc64071 100644 --- a/app_dart/test/request_handlers/push_gold_status_to_github_test.dart +++ b/app_dart/test/request_handlers/push_gold_status_to_github_test.dart @@ -6,12 +6,13 @@ import 'dart:async'; import 'dart:io'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; -import 'package:cocoon_service/src/model/firestore/github_gold_status.dart'; import 'package:cocoon_service/src/model/firestore/github_gold_status.dart' as fs; +import 'package:cocoon_service/src/model/firestore/github_gold_status.dart'; import 'package:cocoon_service/src/request_handlers/push_gold_status_to_github.dart'; import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:github/github.dart'; @@ -21,13 +22,7 @@ import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_graphql_client.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/rerun_prod_task_test.dart b/app_dart/test/request_handlers/rerun_prod_task_test.dart index 09ace3d7c2..fc518d34ec 100644 --- a/app_dart/test/request_handlers/rerun_prod_task_test.dart +++ b/app_dart/test/request_handlers/rerun_prod_task_test.dart @@ -4,6 +4,7 @@ import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -13,14 +14,7 @@ import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_ci_yaml_fetcher.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_scheduler.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/reset_try_task_test.dart b/app_dart/test/request_handlers/reset_try_task_test.dart index b48d281be1..e1f6cb0346 100644 --- a/app_dart/test/request_handlers/reset_try_task_test.dart +++ b/app_dart/test/request_handlers/reset_try_task_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handlers/reset_try_task.dart'; @@ -11,15 +12,7 @@ import 'package:github/github.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_github_service.dart'; -import '../src/service/fake_scheduler.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/scheduler/backfill_grid_test.dart b/app_dart/test/request_handlers/scheduler/backfill_grid_test.dart index 57e011011c..1178d2b48e 100644 --- a/app_dart/test/request_handlers/scheduler/backfill_grid_test.dart +++ b/app_dart/test/request_handlers/scheduler/backfill_grid_test.dart @@ -3,13 +3,12 @@ // found in the LICENSE file. import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/protos.dart'; import 'package:cocoon_service/src/request_handlers/scheduler/backfill_grid.dart'; import 'package:test/test.dart'; -import '../../src/model/ci_yaml_matcher.dart'; -import '../../src/utilities/entity_generators.dart'; import 'backfill_matcher.dart'; void main() { diff --git a/app_dart/test/request_handlers/scheduler/backfill_matcher.dart b/app_dart/test/request_handlers/scheduler/backfill_matcher.dart index 67e52afc8e..081e15069a 100644 --- a/app_dart/test/request_handlers/scheduler/backfill_matcher.dart +++ b/app_dart/test/request_handlers/scheduler/backfill_matcher.dart @@ -2,13 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; import 'package:cocoon_service/src/model/task_ref.dart'; import 'package:cocoon_service/src/request_handlers/scheduler/backfill_grid.dart'; import 'package:test/expect.dart'; -import '../../src/model/ci_yaml_matcher.dart'; - /// Returns a matcher that asserts the state of [BackfillGrid.eligibleTasks]. Matcher hasGridTargetsMatching( Iterable<(TargetMatcher, List)> targets, diff --git a/app_dart/test/request_handlers/scheduler/backfill_strategy_test.dart b/app_dart/test/request_handlers/scheduler/backfill_strategy_test.dart index 564ef9ff49..af57ed859f 100644 --- a/app_dart/test/request_handlers/scheduler/backfill_strategy_test.dart +++ b/app_dart/test/request_handlers/scheduler/backfill_strategy_test.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/task_ref.dart'; import 'package:cocoon_service/src/request_handlers/scheduler/backfill_grid.dart'; @@ -13,7 +14,6 @@ import 'package:cocoon_service/src/service/luci_build_service.dart'; import 'package:test/fake.dart'; import 'package:test/test.dart'; -import '../../src/utilities/entity_generators.dart'; import 'backfill_matcher.dart'; void main() { diff --git a/app_dart/test/request_handlers/scheduler/batch_backfiller_test.dart b/app_dart/test/request_handlers/scheduler/batch_backfiller_test.dart index 0e9a9f2808..a44dd5b74f 100644 --- a/app_dart/test/request_handlers/scheduler/batch_backfiller_test.dart +++ b/app_dart/test/request_handlers/scheduler/batch_backfiller_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; import 'package:cocoon_service/src/request_handlers/scheduler/backfill_grid.dart'; @@ -16,11 +17,7 @@ import 'package:collection/collection.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; import '../../src/request_handling/request_handler_tester.dart'; -import '../../src/service/fake_ci_yaml_fetcher.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/scheduler/scheduler_request_subscription_test.dart b/app_dart/test/request_handlers/scheduler/scheduler_request_subscription_test.dart index b259f08539..8c79f703a9 100644 --- a/app_dart/test/request_handlers/scheduler/scheduler_request_subscription_test.dart +++ b/app_dart/test/request_handlers/scheduler/scheduler_request_subscription_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/luci/pubsub_message.dart'; @@ -15,12 +16,7 @@ import 'package:mockito/mockito.dart'; import 'package:retry/retry.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_dashboard_authentication.dart'; -import '../../src/request_handling/fake_http.dart'; import '../../src/request_handling/subscription_tester.dart'; -import '../../src/service/fake_github_service.dart'; -import '../../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart b/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart index d475582753..52c7bc17e3 100644 --- a/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart +++ b/app_dart/test/request_handlers/scheduler/vacuum_stale_tasks_test.dart @@ -5,6 +5,7 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/rpc_model.dart' as rpc; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/task.dart' as fs; @@ -12,11 +13,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; import '../../src/request_handling/request_handler_tester.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/update_discord_status_test.dart b/app_dart/test/request_handlers/update_discord_status_test.dart index 18e3f0af7e..19ef72a8de 100644 --- a/app_dart/test/request_handlers/update_discord_status_test.dart +++ b/app_dart/test/request_handlers/update_discord_status_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/build_status_snapshot.dart'; import 'package:cocoon_service/src/request_handlers/update_discord_status.dart'; @@ -14,13 +15,7 @@ import 'package:cocoon_service/src/service/discord_service.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/model/firestore_matcher.dart'; import '../src/request_handling/request_handler_tester.dart'; -import '../src/service/fake_firestore_service.dart' - show FakeFirestoreService, existsInStorage; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/update_existing_flaky_issues_test.dart b/app_dart/test/request_handlers/update_existing_flaky_issues_test.dart index f3368d68a7..2a53c0646c 100644 --- a/app_dart/test/request_handlers/update_existing_flaky_issues_test.dart +++ b/app_dart/test/request_handlers/update_existing_flaky_issues_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -15,12 +16,7 @@ import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/service/fake_ci_yaml_fetcher.dart'; -import '../src/utilities/mocks.dart'; import 'update_existing_flaky_issues_test_data.dart'; const String kThreshold = '0.02'; diff --git a/app_dart/test/request_handlers/update_suppressed_test_test.dart b/app_dart/test/request_handlers/update_suppressed_test_test.dart index 78cf2717f1..feae6971a8 100644 --- a/app_dart/test/request_handlers/update_suppressed_test_test.dart +++ b/app_dart/test/request_handlers/update_suppressed_test_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; 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_suppressed_test.dart'; @@ -13,11 +14,7 @@ import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_github_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/update_tree_status_test.dart b/app_dart/test/request_handlers/update_tree_status_test.dart index 23890eee91..4e03261d61 100644 --- a/app_dart/test/request_handlers/update_tree_status_test.dart +++ b/app_dart/test/request_handlers/update_tree_status_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/tree_status_change.dart'; import 'package:cocoon_service/src/request_handlers/update_tree_status.dart'; @@ -11,10 +12,7 @@ import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:cocoon_service/src/service/config.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_firestore_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handlers/vacuum_github_commits_test.dart b/app_dart/test/request_handlers/vacuum_github_commits_test.dart index cc81f5b898..5b6cdae2c3 100644 --- a/app_dart/test/request_handlers/vacuum_github_commits_test.dart +++ b/app_dart/test/request_handlers/vacuum_github_commits_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/commit.dart' as fs; @@ -13,13 +14,7 @@ import 'package:googleapis/bigquery/v2.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/api_request_handler_tester.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_github_service.dart'; -import '../src/service/fake_scheduler.dart'; -import '../src/utilities/mocks.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/api_request_handler_test.dart b/app_dart/test/request_handling/api_request_handler_test.dart index bb84668026..da367846b2 100644 --- a/app_dart/test/request_handling/api_request_handler_test.dart +++ b/app_dart/test/request_handling/api_request_handler_test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:cocoon_common/core_extensions.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handling/api_request_handler.dart'; @@ -16,9 +17,6 @@ import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:gcloud/service_scope.dart' as ss; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/cache_request_handler_test.dart b/app_dart/test/request_handling/cache_request_handler_test.dart index d0e24198d9..146bb5a4b0 100644 --- a/app_dart/test/request_handling/cache_request_handler_test.dart +++ b/app_dart/test/request_handling/cache_request_handler_test.dart @@ -5,15 +5,13 @@ import 'dart:convert'; import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handling/cache_request_handler.dart'; import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/request_handling/fake_request_handler.dart'; import '../src/request_handling/request_handler_tester.dart'; void main() { diff --git a/app_dart/test/request_handling/checkrun_authentication_test.dart b/app_dart/test/request_handling/checkrun_authentication_test.dart index 601521384a..7ba78a78aa 100644 --- a/app_dart/test/request_handling/checkrun_authentication_test.dart +++ b/app_dart/test/request_handling/checkrun_authentication_test.dart @@ -5,12 +5,12 @@ import 'dart:convert'; import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/google/firebase_jwt_claim.dart'; import 'package:cocoon_service/src/model/google/token_info.dart'; -import 'package:cocoon_service/src/request_handling/checkrun_authentication.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:cocoon_service/src/service/github_service.dart'; import 'package:github/github.dart'; @@ -18,12 +18,6 @@ import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; -import '../src/service/fake_firebase_jwt_validator.dart'; -import '../src/utilities/mocks.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/dashboard_authentication_test.dart b/app_dart/test/request_handling/dashboard_authentication_test.dart index 122ea5f116..83fae7fb50 100644 --- a/app_dart/test/request_handling/dashboard_authentication_test.dart +++ b/app_dart/test/request_handling/dashboard_authentication_test.dart @@ -2,20 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/account.dart'; import 'package:cocoon_service/src/model/google/token_info.dart'; -import 'package:cocoon_service/src/request_handling/dashboard_authentication.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:test/test.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; -// import '../src/service/fake_firebase_jwt_validator.dart'; -import '../src/service/fake_firebase_jwt_validator.dart'; -import '../src/service/fake_firestore_service.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/pubsub_authentication_test.dart b/app_dart/test/request_handling/pubsub_authentication_test.dart index 1bdb501463..0d8a6da2a1 100644 --- a/app_dart/test/request_handling/pubsub_authentication_test.dart +++ b/app_dart/test/request_handling/pubsub_authentication_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:cocoon_service/src/request_handling/pubsub_authentication.dart'; @@ -12,10 +13,6 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/request_handler_test.dart b/app_dart/test/request_handling/request_handler_test.dart index 646b01c441..711b7515db 100644 --- a/app_dart/test/request_handling/request_handler_test.dart +++ b/app_dart/test/request_handling/request_handler_test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:cocoon_common/core_extensions.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; @@ -16,8 +17,6 @@ import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:gcloud/service_scope.dart' as ss; import 'package:test/test.dart'; -import '../src/fake_config.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/static_file_handler_test.dart b/app_dart/test/request_handling/static_file_handler_test.dart index 69b54aafd1..b209d689df 100644 --- a/app_dart/test/request_handling/static_file_handler_test.dart +++ b/app_dart/test/request_handling/static_file_handler_test.dart @@ -4,6 +4,7 @@ import 'dart:convert' show utf8; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; @@ -11,7 +12,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; import '../src/request_handling/request_handler_tester.dart'; void main() { diff --git a/app_dart/test/request_handling/subscription_handler_test.dart b/app_dart/test/request_handling/subscription_handler_test.dart index 7ab8e986bd..e0f47a2585 100644 --- a/app_dart/test/request_handling/subscription_handler_test.dart +++ b/app_dart/test/request_handling/subscription_handler_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/luci/pubsub_message.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; @@ -15,9 +16,6 @@ import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:gcloud/service_scope.dart' as ss; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/request_handling/swarming_authentication_test.dart b/app_dart/test/request_handling/swarming_authentication_test.dart index 2cc29d6c6b..8556725acf 100644 --- a/app_dart/test/request_handling/swarming_authentication_test.dart +++ b/app_dart/test/request_handling/swarming_authentication_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:cocoon_service/src/request_handling/swarming_authentication.dart'; @@ -12,10 +13,6 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_dashboard_authentication.dart'; -import '../src/request_handling/fake_http.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/server_test.dart b/app_dart/test/server_test.dart index 768eb52729..28845258ba 100644 --- a/app_dart/test/server_test.dart +++ b/app_dart/test/server_test.dart @@ -2,23 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/server.dart'; import 'package:cocoon_service/src/service/commit_service.dart'; import 'package:test/test.dart'; -import 'src/fake_config.dart'; -import 'src/request_handling/fake_dashboard_authentication.dart'; -import 'src/service/fake_build_bucket_client.dart'; -import 'src/service/fake_build_status_service.dart'; -import 'src/service/fake_ci_yaml_fetcher.dart'; -import 'src/service/fake_content_aware_hash_service.dart'; -import 'src/service/fake_firestore_service.dart'; -import 'src/service/fake_gerrit_service.dart'; -import 'src/service/fake_luci_build_service.dart'; -import 'src/service/fake_scheduler.dart'; -import 'src/utilities/mocks.dart'; - void main() { test('verify server can be created', () { final firestore = FakeFirestoreService(); diff --git a/app_dart/test/service/bigquery_test.dart b/app_dart/test/service/bigquery_test.dart index e61c2e1516..633277dd4d 100644 --- a/app_dart/test/service/bigquery_test.dart +++ b/app_dart/test/service/bigquery_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/big_query.dart'; @@ -11,8 +12,6 @@ import 'package:googleapis/bigquery/v2.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/utilities/mocks.dart'; - const String semanticsIntegrationTestResponse = ''' { "jobComplete" : true, diff --git a/app_dart/test/service/branch_service_test.dart b/app_dart/test/service/branch_service_test.dart index 716aad0676..9d5cb9c02e 100644 --- a/app_dart/test/service/branch_service_test.dart +++ b/app_dart/test/service/branch_service_test.dart @@ -4,6 +4,7 @@ import 'package:cocoon_common/rpc_model.dart' as rpc_model; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; @@ -18,11 +19,6 @@ import 'package:mockito/mockito.dart'; import 'package:retry/retry.dart'; import 'package:test/test.dart'; -import '../src/service/fake_gerrit_service.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/matchers.dart'; -import '../src/utilities/mocks.mocks.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/build_bucket_client_test.dart b/app_dart/test/service/build_bucket_client_test.dart index 9a751c55d4..b81c77acf3 100644 --- a/app_dart/test/service/build_bucket_client_test.dart +++ b/app_dart/test/service/build_bucket_client_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/build_bucket_client.dart'; import 'package:fixnum/fixnum.dart'; @@ -16,8 +17,6 @@ import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/utilities/mocks.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/build_status_service_test.dart b/app_dart/test/service/build_status_service_test.dart index 769c6daf9b..fccaa0d933 100644 --- a/app_dart/test/service/build_status_service_test.dart +++ b/app_dart/test/service/build_status_service_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/tree_status_change.dart'; import 'package:cocoon_service/src/service/build_status_service.dart'; @@ -10,10 +11,6 @@ import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/cache_service_test.dart b/app_dart/test/service/cache_service_test.dart index 19f1672d08..41c8cc1c01 100644 --- a/app_dart/test/service/cache_service_test.dart +++ b/app_dart/test/service/cache_service_test.dart @@ -5,14 +5,13 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:mockito/mockito.dart'; import 'package:neat_cache/neat_cache.dart'; import 'package:test/test.dart'; -import '../src/utilities/mocks.dart'; - void main() { useTestLoggerPerTest(); @@ -294,23 +293,3 @@ void main() { }); }); } - -class FakeEntry extends Entry { - Uint8List value = Uint8List.fromList('abc123'.codeUnits); - - @override - Future get([ - Future Function()? create, - Duration? ttl, - ]) async => value; - - @override - Future purge({int retries = 0}) => throw UnimplementedError(); - - @override - Future set(Uint8List? value, [Duration? ttl]) async { - value = value; - - return value; - } -} diff --git a/app_dart/test/service/commit_service_test.dart b/app_dart/test/service/commit_service_test.dart index b90e21c122..4c1a058e91 100644 --- a/app_dart/test/service/commit_service_test.dart +++ b/app_dart/test/service/commit_service_test.dart @@ -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_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/commit.dart' as fs; @@ -10,12 +11,6 @@ import 'package:github/github.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.mocks.dart'; -import '../src/utilities/webhook_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/commit_tasks_status_test.dart b/app_dart/test/service/commit_tasks_status_test.dart index 5108e45a88..80f152b372 100644 --- a/app_dart/test/service/commit_tasks_status_test.dart +++ b/app_dart/test/service/commit_tasks_status_test.dart @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/build_status_provider/commit_tasks_status.dart'; import 'package:test/test.dart'; -import '../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/content_aware_hash_service_test.dart b/app_dart/test/service/content_aware_hash_service_test.dart index aabfd016cb..4c6187fd6f 100644 --- a/app_dart/test/service/content_aware_hash_service_test.dart +++ b/app_dart/test/service/content_aware_hash_service_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; @@ -17,9 +18,6 @@ import 'package:retry/retry.dart'; import 'package:test/test.dart'; import '../model/github/workflow_job_data.dart'; -import '../src/fake_config.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_github_service.dart'; void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/firestore/unified_check_run_test.dart b/app_dart/test/service/firestore/unified_check_run_test.dart index 960793646d..7db68690d1 100644 --- a/app_dart/test/service/firestore/unified_check_run_test.dart +++ b/app_dart/test/service/firestore/unified_check_run_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/common/presubmit_check_state.dart'; import 'package:cocoon_service/src/model/common/presubmit_guard_conclusion.dart'; @@ -16,10 +17,6 @@ import 'package:github/github.dart'; import 'package:googleapis/firestore/v1.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/firestore_test.dart b/app_dart/test/service/firestore_test.dart index 923e2b3c77..160e4bb3eb 100644 --- a/app_dart/test/service/firestore_test.dart +++ b/app_dart/test/service/firestore_test.dart @@ -2,15 +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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/firestore.dart'; import 'package:cocoon_service/src/service/firestore/commit_and_tasks.dart'; import 'package:googleapis/firestore/v1.dart'; import 'package:test/test.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/gerrit_service_test.dart b/app_dart/test/service/gerrit_service_test.dart index dbe206f63b..9302e0b275 100644 --- a/app_dart/test/service/gerrit_service_test.dart +++ b/app_dart/test/service/gerrit_service_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:cocoon_service/src/service/config.dart'; @@ -14,10 +15,6 @@ import 'package:http/http.dart'; import 'package:http/testing.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/service/fake_auth_client.dart'; -import '../src/utilities/matchers.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/github_checks_service_test.dart b/app_dart/test/service/github_checks_service_test.dart index 734108f4dc..f0511621c2 100644 --- a/app_dart/test/service/github_checks_service_test.dart +++ b/app_dart/test/service/github_checks_service_test.dart @@ -5,20 +5,17 @@ import 'dart:convert'; import 'package:buildbucket/buildbucket_pb.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; import 'package:cocoon_service/src/service/github_checks_service.dart'; import 'package:cocoon_service/src/service/luci_build_service/user_data.dart'; import 'package:fixnum/fixnum.dart'; - import 'package:github/github.dart' as github; import 'package:github/github.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/fake_config.dart'; -import '../src/utilities/mocks.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/github_service_test.dart b/app_dart/test/service/github_service_test.dart index c01df81694..3cdd788f78 100644 --- a/app_dart/test/service/github_service_test.dart +++ b/app_dart/test/service/github_service_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/github_service.dart'; @@ -13,8 +14,6 @@ import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../src/utilities/mocks.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/luci_build_service/cancel_builds_test.dart b/app_dart/test/service/luci_build_service/cancel_builds_test.dart index 77bc2bcba1..c792fcdb31 100644 --- a/app_dart/test/service/luci_build_service/cancel_builds_test.dart +++ b/app_dart/test/service/luci_build_service/cancel_builds_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:cocoon_service/src/service/luci_build_service.dart'; @@ -10,13 +11,6 @@ import 'package:fixnum/fixnum.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; - /// Tests [LuciBuildService] public API related to fetching prod-bot builds. /// /// Specifically: diff --git a/app_dart/test/service/luci_build_service/check_rerun_builder_test.dart b/app_dart/test/service/luci_build_service/check_rerun_builder_test.dart index c0b358288c..a1daa8bc2a 100644 --- a/app_dart/test/service/luci_build_service/check_rerun_builder_test.dart +++ b/app_dart/test/service/luci_build_service/check_rerun_builder_test.dart @@ -4,18 +4,12 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/firestore/task.dart' as fs; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; - /// Tests [LuciBuildService] public API related to rerunning TOT test failures. /// /// Specifically: diff --git a/app_dart/test/service/luci_build_service/find_or_get_builds_test.dart b/app_dart/test/service/luci_build_service/find_or_get_builds_test.dart index 681bd8b81b..b2416518b3 100644 --- a/app_dart/test/service/luci_build_service/find_or_get_builds_test.dart +++ b/app_dart/test/service/luci_build_service/find_or_get_builds_test.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:cocoon_service/src/service/luci_build_service.dart'; @@ -13,13 +14,6 @@ import 'package:github/github.dart' as gh; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; - /// Tests [LuciBuildService] public API related to fetching [bbv2.Build]s. /// /// Specifically: diff --git a/app_dart/test/service/luci_build_service/rerun_dart_internal_test.dart b/app_dart/test/service/luci_build_service/rerun_dart_internal_test.dart index e7ce64aa0f..6171c23f82 100644 --- a/app_dart/test/service/luci_build_service/rerun_dart_internal_test.dart +++ b/app_dart/test/service/luci_build_service/rerun_dart_internal_test.dart @@ -5,6 +5,7 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -14,13 +15,6 @@ import 'package:fixnum/fixnum.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; - /// Tests [LuciBuildService] public API related to `dart-internal` builds. /// /// Specifically: diff --git a/app_dart/test/service/luci_build_service/schedule_merge_group_builds_test.dart b/app_dart/test/service/luci_build_service/schedule_merge_group_builds_test.dart index f7540fef5a..9403ee0624 100644 --- a/app_dart/test/service/luci_build_service/schedule_merge_group_builds_test.dart +++ b/app_dart/test/service/luci_build_service/schedule_merge_group_builds_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; import 'package:cocoon_service/src/service/cache_service.dart'; @@ -14,13 +15,6 @@ import 'package:mockito/mockito.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; - /// Tests [LuciBuildService] public API related to scheduling for a merge group. /// /// Specifically: diff --git a/app_dart/test/service/luci_build_service/schedule_prod_builds_test.dart b/app_dart/test/service/luci_build_service/schedule_prod_builds_test.dart index dff48ca435..8a3cc366bc 100644 --- a/app_dart/test/service/luci_build_service/schedule_prod_builds_test.dart +++ b/app_dart/test/service/luci_build_service/schedule_prod_builds_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/task.dart' as fs; import 'package:cocoon_service/src/model/github/checks.dart'; @@ -19,14 +20,6 @@ import 'package:fixnum/fixnum.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; -import '../../src/utilities/webhook_generators.dart'; - /// Tests [LuciBuildService] public API related to fetching prod-bot builds. /// /// Specifically: diff --git a/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart b/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart index 8665f4a87c..e09bcfd2f6 100644 --- a/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart +++ b/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart @@ -4,6 +4,7 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; @@ -21,14 +22,6 @@ import 'package:github/github.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/model/ci_yaml_matcher.dart'; -import '../../src/request_handling/fake_pubsub.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_gerrit_service.dart'; -import '../../src/utilities/entity_generators.dart'; -import '../../src/utilities/mocks.mocks.dart'; - /// Tests [LuciBuildService] public API related to fetching try-bot builds. /// /// Specifically: diff --git a/app_dart/test/service/presubmit_guard_query_test.dart b/app_dart/test/service/presubmit_guard_query_test.dart index 884d601d1a..33900d3b7d 100644 --- a/app_dart/test/service/presubmit_guard_query_test.dart +++ b/app_dart/test/service/presubmit_guard_query_test.dart @@ -2,15 +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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/service/firestore/unified_check_run.dart'; import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/scheduler/ci_yaml_fetcher_test.dart b/app_dart/test/service/scheduler/ci_yaml_fetcher_test.dart index f6eab7aa20..6ec741067d 100644 --- a/app_dart/test/service/scheduler/ci_yaml_fetcher_test.dart +++ b/app_dart/test/service/scheduler/ci_yaml_fetcher_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/ci_yaml.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -17,10 +18,6 @@ import 'package:path/path.dart' as p; import 'package:retry/retry.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/scheduler/files_changed_optimization_test.dart b/app_dart/test/service/scheduler/files_changed_optimization_test.dart index 4006284b86..f917f047b8 100644 --- a/app_dart/test/service/scheduler/files_changed_optimization_test.dart +++ b/app_dart/test/service/scheduler/files_changed_optimization_test.dart @@ -4,6 +4,7 @@ import 'package:cocoon_common/cocoon_common.dart'; import 'package:cocoon_common_test/cocoon_common_test.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/protos.dart' as pb; @@ -15,11 +16,6 @@ import 'package:cocoon_service/src/service/scheduler/files_changed_optimization. import 'package:github/github.dart'; import 'package:test/test.dart'; -import '../../src/fake_config.dart'; -import '../../src/service/fake_ci_yaml_fetcher.dart'; -import '../../src/service/fake_get_files_changed.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/scheduler/hash_workflow_test.dart b/app_dart/test/service/scheduler/hash_workflow_test.dart index 7bee9f98ec..61be590630 100644 --- a/app_dart/test/service/scheduler/hash_workflow_test.dart +++ b/app_dart/test/service/scheduler/hash_workflow_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart' hide Response; @@ -18,12 +19,6 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import '../../model/github/workflow_job_data.dart'; -import '../../src/fake_config.dart'; -import '../../src/service/fake_ci_yaml_fetcher.dart'; -import '../../src/service/fake_firestore_service.dart'; -import '../../src/service/fake_get_files_changed.dart'; -import '../../src/service/fake_github_service.dart'; -import '../../src/utilities/mocks.mocks.dart'; import '../content_aware_hash_service_test.dart' show goodAnnotation; import 'ci_yaml_strings.dart'; import 'create_check_run.dart'; diff --git a/app_dart/test/service/scheduler/policy_test.dart b/app_dart/test/service/scheduler/policy_test.dart index 7b705965df..32740ddcc8 100644 --- a/app_dart/test/service/scheduler/policy_test.dart +++ b/app_dart/test/service/scheduler/policy_test.dart @@ -3,14 +3,13 @@ // found in the LICENSE file. import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/firestore/task.dart'; import 'package:cocoon_service/src/service/luci_build_service.dart'; import 'package:cocoon_service/src/service/scheduler/policy.dart'; import 'package:test/test.dart'; -import '../../src/utilities/entity_generators.dart'; - void main() { useTestLoggerPerTest(); diff --git a/app_dart/test/service/scheduler_test.dart b/app_dart/test/service/scheduler_test.dart index f0bdd30270..837ad9a8b6 100644 --- a/app_dart/test/service/scheduler_test.dart +++ b/app_dart/test/service/scheduler_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; @@ -37,19 +38,6 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import '../model/github/checks_test_data.dart'; -import '../src/fake_config.dart'; -import '../src/request_handling/fake_pubsub.dart'; -import '../src/service/fake_build_bucket_client.dart'; -import '../src/service/fake_ci_yaml_fetcher.dart'; -import '../src/service/fake_content_aware_hash_service.dart'; -import '../src/service/fake_firestore_service.dart'; -import '../src/service/fake_gerrit_service.dart'; -import '../src/service/fake_get_files_changed.dart'; -import '../src/service/fake_github_service.dart'; -import '../src/service/fake_luci_build_service.dart'; -import '../src/utilities/entity_generators.dart'; -import '../src/utilities/mocks.dart'; -import '../src/utilities/webhook_generators.dart'; import 'scheduler/ci_yaml_strings.dart'; import 'scheduler/create_check_run.dart'; diff --git a/app_dart/test/src/request_handling/api_request_handler_tester.dart b/app_dart/test/src/request_handling/api_request_handler_tester.dart index d0c968d19e..02f0832ea0 100644 --- a/app_dart/test/src/request_handling/api_request_handler_tester.dart +++ b/app_dart/test/src/request_handling/api_request_handler_tester.dart @@ -5,12 +5,12 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_service/src/request_handling/api_request_handler.dart'; import 'package:cocoon_service/src/request_handling/request_handler.dart'; import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:meta/meta.dart'; -import 'fake_dashboard_authentication.dart'; import 'request_handler_tester.dart'; class ApiRequestHandlerTester extends RequestHandlerTester { diff --git a/app_dart/test/src/request_handling/request_handler_tester.dart b/app_dart/test/src/request_handling/request_handler_tester.dart index e881f8e2b2..b3ba131274 100644 --- a/app_dart/test/src/request_handling/request_handler_tester.dart +++ b/app_dart/test/src/request_handling/request_handler_tester.dart @@ -4,12 +4,11 @@ import 'dart:async'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_service/src/request_handling/request_handler.dart'; import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:meta/meta.dart'; -import 'fake_http.dart'; - class RequestHandlerTester { RequestHandlerTester({FakeHttpRequest? request}) : request = request ?? FakeHttpRequest(); diff --git a/app_dart/test/src/request_handling/subscription_tester.dart b/app_dart/test/src/request_handling/subscription_tester.dart index f1a91d6793..87b20235e8 100644 --- a/app_dart/test/src/request_handling/subscription_tester.dart +++ b/app_dart/test/src/request_handling/subscription_tester.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_service/src/model/luci/pubsub_message.dart'; import 'package:cocoon_service/src/request_handling/api_request_handler.dart'; import 'package:cocoon_service/src/request_handling/request_handler.dart'; @@ -11,7 +12,6 @@ import 'package:cocoon_service/src/request_handling/response.dart'; import 'package:cocoon_service/src/request_handling/subscription_handler.dart'; import 'package:meta/meta.dart'; -import 'fake_dashboard_authentication.dart'; import 'request_handler_tester.dart'; class SubscriptionTester extends RequestHandlerTester { diff --git a/app_dart/tool/local_server.dart b/app_dart/tool/local_server.dart index 49d00cf952..04a4c270de 100644 --- a/app_dart/tool/local_server.dart +++ b/app_dart/tool/local_server.dart @@ -5,12 +5,12 @@ import 'dart:io'; import 'package:appengine/appengine.dart'; +import 'package:cocoon_integration_test/testing.dart'; import 'package:cocoon_server/google_auth_provider.dart'; import 'package:cocoon_server_test/fake_secret_manager.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/server.dart'; import 'package:cocoon_service/src/foundation/providers.dart'; -import 'package:cocoon_service/src/request_handling/dashboard_authentication.dart'; import 'package:cocoon_service/src/service/big_query.dart'; import 'package:cocoon_service/src/service/build_status_service.dart'; import 'package:cocoon_service/src/service/commit_service.dart'; @@ -19,9 +19,6 @@ import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; import 'package:cocoon_service/src/service/get_files_changed.dart'; import 'package:cocoon_service/src/service/scheduler/ci_yaml_fetcher.dart'; -import '../test/src/service/fake_content_aware_hash_service.dart'; -import '../test/src/service/fake_firestore_service.dart'; - Future main() async { final cache = CacheService(inMemory: false); final config = Config( diff --git a/conductor/tracks/get_presubmit_checks_20260205/index.md b/conductor/archive/get_presubmit_check_20260205/index.md similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/index.md rename to conductor/archive/get_presubmit_check_20260205/index.md diff --git a/conductor/tracks/get_presubmit_checks_20260205/metadata.json b/conductor/archive/get_presubmit_check_20260205/metadata.json similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/metadata.json rename to conductor/archive/get_presubmit_check_20260205/metadata.json diff --git a/conductor/tracks/get_presubmit_checks_20260205/plan.md b/conductor/archive/get_presubmit_check_20260205/plan.md similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/plan.md rename to conductor/archive/get_presubmit_check_20260205/plan.md diff --git a/conductor/tracks/get_presubmit_checks_20260205/spec.md b/conductor/archive/get_presubmit_check_20260205/spec.md similarity index 100% rename from conductor/tracks/get_presubmit_checks_20260205/spec.md rename to conductor/archive/get_presubmit_check_20260205/spec.md diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/index.md b/conductor/archive/get_presubmit_guard_summaries_20260211/index.md new file mode 100644 index 0000000000..02fa3b9634 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/index.md @@ -0,0 +1,5 @@ +# Track get_presubmit_guards_20260211 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/metadata.json b/conductor/archive/get_presubmit_guard_summaries_20260211/metadata.json new file mode 100644 index 0000000000..7502f3f68c --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "get_presubmit_guards_20260211", + "type": "feature", + "status": "new", + "created_at": "2026-02-11T12:00:00Z", + "updated_at": "2026-02-11T12:00:00Z", + "description": "Implement getPresubmitGuardsForPullRequest request handler" +} diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/plan.md b/conductor/archive/get_presubmit_guard_summaries_20260211/plan.md new file mode 100644 index 0000000000..bc447f41e5 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/plan.md @@ -0,0 +1,46 @@ +# Implementation Plan: Implement `getPresubmitGuardsForPullRequest` Request Handler + +This plan outlines the steps to refactor `GuardStatus` logic and implement the `getPresubmitGuardsForPullRequest` request handler in `app_dart`. + +## Phase 1: Refactor GuardStatus Logic [checkpoint: 9cb5948] +- [x] Task: Create failing tests for `GuardStatus.calculate` in `packages/cocoon_common`. +- [x] Task: Implement `GuardStatus.calculate` in `packages/cocoon_common/lib/guard_status.dart`. +- [x] Task: Update `GetPresubmitGuard` in `app_dart` to use the new `GuardStatus.calculate`. +- [x] Task: Verify existing tests for `GetPresubmitGuard` pass. +- [x] Task: Conductor - User Manual Verification 'Refactor GuardStatus Logic' (Protocol in workflow.md) + +## Phase 2: Implement `GetPresubmitGuards` Handler [checkpoint: ab5947b] +- [x] Task: Create failing tests for `GetPresubmitGuards` in `app_dart/test/request_handlers/`. +- [x] Task: Implement `GetPresubmitGuards` class in `app_dart/lib/src/request_handlers/get_presubmit_guards.dart`. +- [x] Task: Register the new handler in `app_dart/bin/server.dart`. +- [x] Task: Create RPC model `PresubmitGuardsResponse` in `packages/cocoon_common`. +- [x] Task: Update `GetPresubmitGuards` to use the new RPC model. +- [x] Task: Verify all tests pass and coverage is > 95%. +- [x] Task: Conductor - User Manual Verification 'Implement GetPresubmitGuards Handler' (Protocol in workflow.md) + +## Phase 3: Group Guards by Commit SHA [checkpoint: c1e36b1] +- [x] Task: Update `PresubmitGuardItem` in `packages/cocoon_common` to remove `checkRunId`. +- [x] Task: Update `GetPresubmitGuards` in `app_dart` to group guards by `commitSha` and aggregate status. +- [x] Task: Update `GetPresubmitGuards` tests to verify grouping logic. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Group Guards by Commit SHA' (Protocol in workflow.md) + +## Phase 4: Refactor to PresubmitGuardSummary [checkpoint: 722a108] +- [x] Task: Rename `PresubmitGuardItem` to `PresubmitGuardSummary` and remove `PresubmitGuardsResponse` wrapper in `packages/cocoon_common`. +- [x] Task: Update `GetPresubmitGuards` in `app_dart` to return `List`. +- [x] Task: Update `GetPresubmitGuards` tests for the new response format. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Refactor to PresubmitGuardSummary' (Protocol in workflow.md) + +## Phase 5: Rename Handler to GetPresubmitGuardSummaries [checkpoint: 0223684] +- [x] Task: Rename `GetPresubmitGuards` class and file to `GetPresubmitGuardSummaries`. +- [x] Task: Update registration in `app_dart/lib/server.dart`. +- [x] Task: Update tests for the renamed handler. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Rename Handler to GetPresubmitGuardSummaries' (Protocol in workflow.md) + +## Phase 6: Update API URL +- [x] Task: Update API URL in `app_dart/lib/server.dart` to `/api/get-presubmit-guard-summaries`. +- [x] Task: Update documentation in `GetPresubmitGuardSummaries` handler. +- [x] Task: Verify all tests pass. +- [x] Task: Conductor - User Manual Verification 'Update API URL' (Protocol in workflow.md) diff --git a/conductor/archive/get_presubmit_guard_summaries_20260211/spec.md b/conductor/archive/get_presubmit_guard_summaries_20260211/spec.md new file mode 100644 index 0000000000..07ce7a9032 --- /dev/null +++ b/conductor/archive/get_presubmit_guard_summaries_20260211/spec.md @@ -0,0 +1,34 @@ +# Specification: Implement `getPresubmitGuardSummaries` Request Handler + +## Overview +This track involves implementing a new request handler in the `app_dart` service to retrieve a summary of presubmit guards for a specific pull request, grouped by commit SHA. It also includes refactoring the `GuardStatus` calculation logic and creating a shared RPC model. + +## Functional Requirements +- **Refactoring:** + - Moved `GuardStatus` calculation logic to `packages/cocoon_common/lib/guard_status.dart` as a static `calculate` method. + - Updated existing `GetPresubmitGuard` handler to use the centralized logic. +- **RPC Model:** + - Created `PresubmitGuardSummary` model in `packages/cocoon_common` to represent the aggregated status of guards for a single commit SHA. +- **Endpoint:** Created a new GET endpoint: `/api/get-presubmit-guard-summaries`. +- **Input Parameters:** + - `repo`: The GitHub repository name (required). + - `pr`: The pull request number (required). + - `owner`: The GitHub repository owner (optional, defaults to "flutter"). +- **Service Integration:** Uses `UnifiedCheckRun.getPresubmitGuardsForPullRequest` to fetch all guards for the PR. +- **Grouping Logic:** Guards are grouped by `commit_sha`. For each group, the status is aggregated using `GuardStatus.calculate` based on the sum of failed, remaining, and total builds across all stages for that SHA. +- **Response Data:** A JSON array of `PresubmitGuardSummary` objects: + - `commit_sha`: The SHA of the commit. + - `creation_time`: The latest creation timestamp among all guards for that SHA. + - `guard_status`: The aggregated status (`New`, `In Progress`, `Failed`, `Succeeded`). +- **Error Handling:** + - Returns **400 Bad Request** if `repo` or `pr` are missing. + - Returns **404 Not Found** if no guards are found for the PR. + +## Acceptance Criteria +- [x] `GuardStatus.calculate` implemented and tested. +- [x] `GetPresubmitGuard` refactored to use `GuardStatus.calculate`. +- [x] `PresubmitGuardSummary` RPC model created and exported. +- [x] `GetPresubmitGuardSummaries` handler implemented with grouping and aggregation logic. +- [x] Endpoint registered at `/api/get-presubmit-guard-summaries`. +- [x] Unit tests cover all requirements and grouping logic. +- [x] Code coverage > 95%. diff --git a/conductor/product-guidelines.md b/conductor/product-guidelines.md index 5bf6e82eb0..64ec2f4833 100644 --- a/conductor/product-guidelines.md +++ b/conductor/product-guidelines.md @@ -9,6 +9,15 @@ * **Layered Complexity:** While the primary view should be dense and informative, use visual cues (like color-coding and iconography) to provide immediate high-level status (Red/Green/Yellow). Detailed logs and historical data should be easily accessible but secondary to the main grid. * **Performance First:** Given the volume of data in Flutter's CI, the UI must remain responsive. Lazy loading, efficient data fetching, and minimal re-renders are critical for a smooth monitoring experience. +## Visual Identity & UX +* **Design System:** Standard Material Design 3. The dashboard will strictly follow the latest Material Design specifications to ensure a modern, consistent, and recognizable Flutter user experience. +* **Layout:** Prioritize clarity and logical flow. Use Material components (Scaffolds, Cards, Buttons, etc.) to structure information predictably. + +## Accessibility (a11y) +* **Compliance:** Strict adherence to WCAG 2.1 Level AA standards. +* **Implementation:** All UI elements must have appropriate semantic labels, maintain high contrast ratios, and provide full support for screen readers. +* **Focus Management:** Ensure a logical focus order for keyboard navigation across all interactive elements. + ## Reliability & Error Handling * **Resilience through Automation:** The system must automatically handle transient failures (e.g., GitHub API timeouts or LUCI build-bucket flakes) using robust retry logic to maintain high data fidelity. * **Graceful Degradation:** The dashboard must remain functional even if partial data is missing. If a specific service (like BigQuery or a specific LUCI project) is down, the UI should clearly indicate the limitation while still serving available information. diff --git a/conductor/product.md b/conductor/product.md index 93b76087b9..d217d08744 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -15,11 +15,15 @@ Cocoon is the CI coordination and orchestration system for the Flutter project. * **Reliable Integration:** Seamlessly coordinate between GitHub webhooks, LUCI build bots, and Google Cloud services. ## Key Features -* **CI Orchestration:** Automates the scheduling and tracking of LUCI builds for the Flutter framework and engine. +* **Offline Integration Testing:** A dedicated testing environment that simulates all backend services (GitHub, Firestore, BigQuery, LUCI) with functional fakes, enabling deterministic, offline verification of frontend and backend logic. * **Tree Status Dashboard:** A Flutter-based web application that provides a visual overview of build health across various commits and branches. -* **Presubmit Check Details:** APIs to retrieve detailed attempt history and status for specific presubmit checks, aiding in debugging and visibility. +* **Presubmit Check Details:** Backend APIs to retrieve detailed attempt history and status for specific presubmit checks, aiding in debugging and visibility. +* **Presubmit Guard Summaries:** Backend APIs to retrieve summaries of all presubmit checks (Presubmit Guards) of the provided pull request to the dashboard. +* **Presubmit Guard Details:** Backend APIs to retrieve detailed information about a specific presubmit check (Presubmit Guard) to the dashboard. * **Merge Queue Visibility:** APIs for querying and inspecting recent GitHub Merge Queue webhook events to diagnose integration issues. -* **Presubmit Visibility:** Backend APIs to provide real-time status of presubmit checks (Presubmit Guard) to the dashboard for active pull requests. * **Auto-submit Bot:** Handles automated pull request management, including label-based merges, reverts, and validation checks. * **GitHub Integration:** Robust handling of GitHub webhooks to sync commits, manage check runs, and report build statuses back to PRs. * **Unified Data Store:** Leverages Cloud Firestore and BigQuery for tracking build history, metrics, and CI health trends. + +## Visual Aesthetic & UX +The dashboard adheres to **Material Design** principles, providing a standard, familiar, and polished Flutter user experience. The interface focuses on clarity and accessibility, ensuring that critical build information is easy to find and interpret. \ No newline at end of file diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 866f7383bc..d8aa022e20 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -26,7 +26,9 @@ * `app_dart/`: Main CI orchestrator and backend API. * `auto_submit/`: Automated PR management bot. * `dashboard/`: Flutter Web frontend. - * `packages/`: Shared libraries and internal SDKs (`cocoon_common`, `buildbucket-dart`). + * `packages/`: Shared libraries and internal SDKs (`cocoon_common`, `buildbucket-dart`, `cocoon_integration_test`). +* **Testing Infrastructure:** + * **Integration Testing:** `packages/cocoon_integration_test` provides an offline environment with functional fakes for all GCP and external services, allowing full-stack integration testing without live connections. * **Code Generation:** * `build_runner` for `json_serializable` (DTOs/Database models). * `package:protobuf` for internal configuration (`.ci.yaml`) and BuildBucket interactions. \ No newline at end of file diff --git a/conductor/tracks.md b/conductor/tracks.md index 39602a5806..8e99eb01fb 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,6 +1,15 @@ # Tracks Registry +<<<<<<< 176990-guards-for-pr +======= --- -- [x] **Track: Implement get presubmit check api to show check details on dashboard** -*Link: [./tracks/get_presubmit_checks_20260205/](./tracks/get_presubmit_checks_20260205/)* +- [~] **Track: Cocoon Integration Test Environment** +*Link: [./tracks/cocoon_integration_test_20260206/](./tracks/cocoon_integration_test_20260206/)* + +--- + +- [~] **Track: Build a Merge Queue Dashboard** +*Link: [./tracks/merge_queue_dashboard_20260205/](./tracks/merge_queue_dashboard_20260205/)* + +>>>>>>> main diff --git a/conductor/tracks/cocoon_integration_test_20260206/plan.md b/conductor/tracks/cocoon_integration_test_20260206/plan.md new file mode 100644 index 0000000000..491b41efcc --- /dev/null +++ b/conductor/tracks/cocoon_integration_test_20260206/plan.md @@ -0,0 +1,25 @@ +# Implementation Plan: Cocoon Integration Test + +## Phase 1: Package Setup & Fake Consolidation +- [x] Task: Create `packages/cocoon_integration_test` structure. + - [x] Create directory and `pubspec.yaml`. + - [x] Add dependencies: `cocoon_service` (app_dart), `cocoon_server_test`, `test`, `http`, etc. +- [x] Task: Consolidate Fakes into `packages/cocoon_integration_test`. + - [x] Identify `FakeConfig`, `FakeDashboardAuthentication`, `FakeBuildBucketClient`, `FakeLuciBuildService`, `FakeGerritService`, `FakeScheduler`, `FakeCiYamlFetcher`, `FakeBuildStatusService`, `FakeContentAwareHashService`. + - [x] Move these from `app_dart/test/src/` (or wherever they are) to `packages/cocoon_integration_test/lib/src/fakes/`. + - [x] Export them from `packages/cocoon_integration_test/lib/testing.dart`. + - [x] Update `app_dart` to depend on `cocoon_integration_test` for these fakes (refactor existing tests). + +## Phase 2: Implementation of Integration Server +- [x] Task: Implement `IntegrationServer` class. + - [x] Create `packages/cocoon_integration_test/lib/src/server.dart`. + - [x] Implement `startServer({Config? config, ...})` which sets up the `CocoonService` (from `app_dart`) with the Fakes. + - [x] Ensure `CacheService` is `inMemory: true`. +- [x] Task: Implement Test Helpers. + - [x] Create helper methods to seed the Fakes (e.g., `populateFirestore`, `setConfigs`). + +## Phase 3: Verification +- [x] Task: Write a "Smoke Test". + - [x] Create `packages/cocoon_integration_test/test/server_test.dart`. + - [x] Verify the server starts and responds to a simple health check or API call. +- [ ] Task: Conductor - User Manual Verification (Protocol in workflow.md) diff --git a/conductor/tracks/cocoon_integration_test_20260206/spec.md b/conductor/tracks/cocoon_integration_test_20260206/spec.md new file mode 100644 index 0000000000..7176174956 --- /dev/null +++ b/conductor/tracks/cocoon_integration_test_20260206/spec.md @@ -0,0 +1,33 @@ +# Specification: Cocoon Integration Test + +## Goal +Create a new package `packages/cocoon_integration_test` to facilitate offline integration testing of the Cocoon backend and frontend. + +## Background +Currently, testing the Cocoon server (`app_dart`) involves unit tests or live integration tests. There is a need for a "fake server" environment that can run offline, simulating all external dependencies (GitHub, Gerrit, BuildBucket, etc.) with in-memory fakes. This will allow for robust, deterministic integration testing of the dashboard and other clients against a running (fake) server. + +## Requirements +1. **New Package:** `packages/cocoon_integration_test`. +2. **Fake Server Factory:** Provide a method to start the `app_dart` server with injected fakes. +3. **Fakes over Mocks:** Prefer using Fakes (functional implementations) over Mocks (record/replay) where possible. +4. **Components to Fake:** + - `Config` (`FakeConfig` with `webhookKeyValue`) + - `CacheService` (In-memory) + - `Authentication` (`FakeDashboardAuthentication` for both standard and swarming) + - `BranchService` + - `BuildBucketClient` + - `LuciBuildService` + - `GithubChecksService` + - `CommitService` + - `GerritService` + - `Scheduler` + - `CiYamlFetcher` + - `BuildStatusService` + - `ContentAwareHashService` +5. **External Dependencies:** The package should depend on `app_dart` (likely via path or git dependency if strict package separation isn't enforced yet, or by moving shared types to `cocoon_common`). *Note: For this iteration, we will assume `app_dart` is accessible.* + +## Deliverables +- `packages/cocoon_integration_test/pubspec.yaml` +- `packages/cocoon_integration_test/lib/cocoon_integration_test.dart` (Entry point) +- `packages/cocoon_integration_test/lib/src/server.dart` (Server setup logic) +- Basic test to verify the fake server starts. diff --git a/conductor/tracks/merge_queue_dashboard_20260205/index.md b/conductor/tracks/merge_queue_dashboard_20260205/index.md new file mode 100644 index 0000000000..d8c1a82ca8 --- /dev/null +++ b/conductor/tracks/merge_queue_dashboard_20260205/index.md @@ -0,0 +1,5 @@ +# Track merge_queue_dashboard_20260205 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/tracks/merge_queue_dashboard_20260205/metadata.json b/conductor/tracks/merge_queue_dashboard_20260205/metadata.json new file mode 100644 index 0000000000..8009d6d0be --- /dev/null +++ b/conductor/tracks/merge_queue_dashboard_20260205/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "merge_queue_dashboard_20260205", + "type": "feature", + "status": "new", + "created_at": "2026-02-05T10:00:00Z", + "updated_at": "2026-02-05T10:00:00Z", + "description": "Build a Merge Queue Dashboard for rendering the @app_dart/lib/src/request_handlers/merge_queue_hooks.dart and providing an ability to resend merge queue events using the `/api/github-webhook-replay` API." +} diff --git a/conductor/tracks/merge_queue_dashboard_20260205/plan.md b/conductor/tracks/merge_queue_dashboard_20260205/plan.md new file mode 100644 index 0000000000..718178263b --- /dev/null +++ b/conductor/tracks/merge_queue_dashboard_20260205/plan.md @@ -0,0 +1,51 @@ +# Implementation Plan: Merge Queue Dashboard + +## Phase 1: Service Layer Integration +- [x] Task: Write Tests for `fetchMergeQueueHooks` and `replayGitHubWebhook` in `CocoonService`. + - [x] Add tests to `dashboard/test/service/appengine_cocoon_test.dart` (or create a new test file). + - [x] Define expected behavior for successful fetches (List of `MergeGroupHook`) and successful replays (POST request). + - [x] Define expected behavior for access denied (403) and other API errors. +- [x] Task: Update `CocoonService` interface and implement in `AppEngineCocoonService`. + - [x] Add `fetchMergeQueueHooks` to `CocoonService` in `dashboard/lib/service/cocoon.dart`. + - [x] Add `replayGitHubWebhook` to `CocoonService` in `dashboard/lib/service/cocoon.dart`. + - [x] Implement `fetchMergeQueueHooks` (GET `/api/merge_queue_hooks`) in `AppEngineCocoonService`. + - [x] Implement `replayGitHubWebhook` (POST `/api/github-webhook-replay?id=...`) in `AppEngineCocoonService`. + - [x] Ensure the `idToken` is included in the headers for both requests. +- [x] Task: Conductor - User Manual Verification 'Phase 1: Service Layer Integration' (Protocol in workflow.md) +[checkpoint: ea18514] + +## Phase 2: State Management +- [ ] Task: Write Tests for `MergeQueueState`. + - [ ] Create `dashboard/test/state/merge_queue_test.dart`. + - [ ] Test initial state, successful data fetching, and handling of 403 Forbidden errors. + - [ ] Test searching/filtering logic by commit hash, ref, and message. + - [ ] Test auto-refresh timer logic and debounce logic for the "Resend" action. +- [ ] Task: Implement `MergeQueueState`. + - [ ] Create `dashboard/lib/state/merge_queue.dart` extending `ChangeNotifier`. + - [ ] Integrate `CocoonService` and `FirebaseAuthService`. + - [ ] Implement the `fetch` logic with state variables for loading, error, and filtered data. + - [ ] Implement the 3-second debounce logic for the `replay` action. + - [ ] Implement auto-refresh logic based on a user-selectable interval. +- [ ] Task: Conductor - User Manual Verification 'Phase 2: State Management' (Protocol in workflow.md) + +## Phase 3: Dashboard View & UI +- [ ] Task: Write Widget and Golden Tests for `MergeQueueDashboard`. + - [ ] Create `dashboard/test/views/merge_queue_dashboard_test.dart`. + - [ ] Verify that the dashboard displays an error message when access is denied. + - [ ] Verify that the search bar correctly triggers filtering. + - [ ] Verify that the "Resend" button is disabled during the debounce period. + - [ ] **Golden Test:** Create a golden image test to ensure the UI renders correctly (data table, search bar, buttons) across different screen sizes. +- [ ] Task: Implement `MergeQueueDashboard` view. + - [ ] Create `dashboard/lib/views/merge_queue_dashboard_page.dart`. + - [ ] Implement the UI layout using a `Scaffold` with a header for Search and Auto-Refresh controls. + - [ ] Implement the data table to display Merge Queue event details. + - [ ] Connect the UI to `MergeQueueState` and provide visual feedback for resend actions. +- [ ] Task: Conductor - User Manual Verification 'Phase 3: Dashboard View & UI' (Protocol in workflow.md) + +## Phase 4: Navigation & Integration +- [ ] Task: Write Tests for navigation and routing. + - [ ] Update `dashboard/test/main_test.dart` or similar to verify the new route. +- [ ] Task: Register the new route and add navigation. + - [ ] Update `dashboard/lib/main.dart` to include the `/merge-queue` route in `onGenerateRoute`. + - [ ] Update `dashboard/lib/logic/links.dart` to add the "Merge Queue" link to the navigation drawer. +- [ ] Task: Conductor - User Manual Verification 'Phase 4: Navigation & Integration' (Protocol in workflow.md) diff --git a/conductor/tracks/merge_queue_dashboard_20260205/spec.md b/conductor/tracks/merge_queue_dashboard_20260205/spec.md new file mode 100644 index 0000000000..1c7c42d219 --- /dev/null +++ b/conductor/tracks/merge_queue_dashboard_20260205/spec.md @@ -0,0 +1,47 @@ +# Specification: Merge Queue Dashboard + +## Overview +Create a new standalone dashboard page in the Flutter-based Cocoon dashboard to visualize and manage GitHub Merge Queue events. This tool will allow Flutter EngProd and Release Engineers to monitor merge queue activity, search for specific events, and manually trigger event replays to diagnose and fix integration issues. + +## Functional Requirements +- **Standalone Page:** A new route (e.g., `/merge-queue`) in the dashboard application. +- **Data Table:** A scrollable list of merge queue events with the following columns: + - Document ID (Firestore ID) + - Date/Time + - Event/Action (e.g., `dequeued`, `merged`) + - Base Ref + - Commit Message + - Git Hash (Head Commit ID) +- **Search/Filter:** A search bar to filter the displayed list by: + - Git Hash + - Base Ref + - Commit Message +- **Individual Resend Action:** A "Resend" button on each row that calls the `/api/github-webhook-replay` API. + - **Debouncing:** Disable the "Resend" button for 3 seconds after a click to prevent accidental double-deliveries. +- **Interactive Feedback:** + - Visual indicator on the row showing the status of the replay attempt (Success/Failure) after the action is triggered. +- **Access Control & Error Handling:** + - If the fetch from `/api/merge_queue_hooks` fails (e.g., 403 Forbidden for non-Google users), display a clear error message on the screen (e.g., "Access Denied: You must be logged in with a @google.com account"). +- **Auto-Refresh:** + - A dropdown menu to select an auto-refresh interval (e.g., Off, 30s, 1m, 5m). + - When enabled, the dashboard periodically calls `/api/merge_queue_hooks` to update the event list. +- **Data Source:** Integration with the existing `MergeQueueHooks` API endpoint. + +## Non-Functional Requirements +- **Consistency:** Follow Material Design 3 principles as defined in the project's tech stack and product guidelines. +- **Performance:** Efficient rendering of the scrollable list using `ListView.builder` or a Data Table. +- **Responsive Design:** Ensure the table layout is usable on desktop and larger tablet screens. + +## Acceptance Criteria +- [ ] Users can navigate to the Merge Queue Dashboard via the navigation menu. +- [ ] The list displays real-time data from the `MergeQueueHooks` endpoint. +- [ ] Users see a clear error message if they lack permission to view the data. +- [ ] Users can search for a specific commit hash and see matching rows. +- [ ] Clicking "Resend" triggers the replay API and disables the button for 3 seconds. +- [ ] The UI visually confirms when a replay request succeeds or fails on the corresponding row. +- [ ] Selecting an auto-refresh interval correctly updates the data periodically. + +## Out of Scope +- Bulk resending of events. +- Advanced date-range filtering. +- Editing event data. diff --git a/dashboard/README.md b/dashboard/README.md index b64dfcab3a..680a2411ce 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -13,10 +13,10 @@ It is possible to run a simulation of the UI locally with fake data: ```sh # Launches Chrome -flutter run -d chrome --web-port=8080 +flutter run -d chrome --web-port=8080 --web-define=description=Dashboard --web-define=projectName=Dashboard # Starts a web server, bring your own browser instance -flutter run -d web-server --web-port=8080 +flutter run -d web-server --web-port=8080 --web-define=description=Dashboard --web-define=projectName=Dashboard ``` NOTE: Must run on port 8080[^8080] for authentication to work. diff --git a/dashboard/conductor/code_styleguides/dart.md b/dashboard/conductor/code_styleguides/dart.md new file mode 100644 index 0000000000..5af023b8b1 --- /dev/null +++ b/dashboard/conductor/code_styleguides/dart.md @@ -0,0 +1,238 @@ +# Dart Code Style Guide + +This guide summarizes key recommendations from the official Effective Dart documentation, covering style, documentation, language usage, and API design principles. Adhering to these guidelines promotes consistent, readable, and maintainable Dart code. + +## 1. Style + +### 1.1. Identifiers + +- **DO** name types, extensions, and enum types using `UpperCamelCase`. +- **DO** name packages, directories, and source files using `lowercase_with_underscores`. +- **DO** name import prefixes using `lowercase_with_underscores`. +- **DO** name other identifiers (class members, top-level definitions, variables, parameters) using `lowerCamelCase`. +- **PREFER** using `lowerCamelCase` for constant names. +- **DO** capitalize acronyms and abbreviations longer than two letters like words (e.g., `Http`, `Nasa`, `Uri`). Two-letter acronyms (e.g., `ID`, `TV`, `UI`) should remain capitalized. +- **PREFER** using wildcards (`_`) for unused callback parameters in anonymous and local functions. +- **DON'T** use a leading underscore for identifiers that aren't private. +- **DON'T** use prefix letters (e.g., `kDefaultTimeout`). +- **DON'T** explicitly name libraries using the `library` directive. + +### 1.2. Ordering + +- **DO** place `dart:` imports before other imports. +- **DO** place `package:` imports before relative imports. +- **DO** specify exports in a separate section after all imports. +- **DO** sort sections alphabetically. + +### 1.3. Formatting + +- **DO** format your code using `dart format`. +- **CONSIDER** changing your code to make it more formatter-friendly (e.g., shortening long identifiers, simplifying nested expressions). +- **PREFER** lines 80 characters or fewer. +- **DO** use curly braces for all flow control statements (`if`, `for`, `while`, `do`, `try`, `catch`, `finally`). + +## 2. Documentation + +### 2.1. Comments + +- **DO** format comments like sentences (capitalize the first word, end with a period). +- **DON'T** use block comments (`/* ... */`) for documentation; use `//` for regular comments. + +### 2.2. Doc Comments + +- **DO** use `///` doc comments to document members and types. +- **PREFER** writing doc comments for public APIs. +- **CONSIDER** writing a library-level doc comment. +- **CONSIDER** writing doc comments for private APIs. +- **DO** start doc comments with a single-sentence summary. +- **DO** separate the first sentence of a doc comment into its own paragraph. +- **AVOID** redundancy with the surrounding context (e.g., don't repeat the class name in its doc comment). +- **PREFER** starting comments of a function or method with third-person verbs if its main purpose is a side effect (e.g., "Connects to..."). +- **PREFER** starting a non-boolean variable or property comment with a noun phrase (e.g., "The current day..."). +- **PREFER** starting a boolean variable or property comment with "Whether" followed by a noun or gerund phrase (e.g., "Whether the modal is..."). +- **PREFER** a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose. +- **DON'T** write documentation for both the getter and setter of a property. +- **PREFER** starting library or type comments with noun phrases. +- **CONSIDER** including code samples in doc comments using triple backticks. +- **DO** use square brackets (`[]`) in doc comments to refer to in-scope identifiers (e.g., `[StateError]`, `[anotherMethod()]`, `[Duration.inDays]`, `[Point.new]`). +- **DO** use prose to explain parameters, return values, and exceptions. +- **DO** put doc comments before metadata annotations. + +### 2.3. Markdown + +- **AVOID** using markdown excessively. +- **AVOID** using HTML for formatting. +- **PREFER** backtick fences (```) for code blocks. + +### 2.4. Writing + +- **PREFER** brevity. +- **AVOID** abbreviations and acronyms unless they are obvious. +- **PREFER** using "this" instead of "the" to refer to a member's instance. + +## 3. Usage + +### 3.1. Libraries + +- **DO** use strings in `part of` directives. +- **DON'T** import libraries that are inside the `src` directory of another package. +- **DON'T** allow an import path to reach into or out of `lib`. +- **PREFER** relative import paths when not crossing the `lib` boundary. + +### 3.2. Null Safety + +- **DON'T** explicitly initialize variables to `null`. +- **DON'T** use an explicit default value of `null`. +- **DON'T** use `true` or `false` in equality operations (e.g., `if (nonNullableBool == true)`). +- **AVOID** `late` variables if you need to check whether they are initialized; prefer nullable types. +- **CONSIDER** type promotion or null-check patterns for using nullable types. + +### 3.3. Strings + +- **DO** use adjacent strings to concatenate string literals. +- **PREFER** using interpolation (`$variable`, `${expression}`) to compose strings and values. +- **AVOID** using curly braces in interpolation when not needed (e.g., `'$name'` instead of `'${name}'`). + +### 3.4. Collections + +- **DO** use collection literals (`[]`, `{}`, `{}`) when possible. +- **DON'T** use `.length` to check if a collection is empty; use `.isEmpty` or `.isNotEmpty`. +- **AVOID** using `Iterable.forEach()` with a function literal; prefer `for-in` loops. +- **DON'T** use `List.from()` unless you intend to change the type of the result; prefer `.toList()`. +- **DO** use `whereType()` to filter a collection by type. +- **AVOID** using `cast()` when a nearby operation (like `List.from()` or `map()`) will do. + +### 3.5. Functions + +- **DO** use a function declaration to bind a function to a name. +- **DON'T** create a lambda when a tear-off will do (e.g., `list.forEach(print)` instead of `list.forEach((e) => print(e))`). + +### 3.6. Variables + +- **DO** follow a consistent rule for `var` and `final` on local variables (either `final` for non-reassigned and `var` for reassigned, or `var` for all locals). +- **AVOID** storing what you can calculate (e.g., don't store `area` if you have `radius`). + +### 3.7. Members + +- **DON'T** wrap a field in a getter and setter unnecessarily. +- **PREFER** using a `final` field to make a read-only property. +- **CONSIDER** using `=>` for simple members (getters, setters, single-expression methods). +- **DON'T** use `this.` except to redirect to a named constructor or to avoid shadowing. +- **DO** initialize fields at their declaration when possible. + +### 3.8. Constructors + +- **DO** use initializing formals (`this.field`) when possible. +- **DON'T** use `late` when a constructor initializer list will do. +- **DO** use `;` instead of `{}` for empty constructor bodies. +- **DON'T** use `new`. +- **DON'T** use `const` redundantly in constant contexts. + +### 3.9. Error Handling + +- **AVOID** `catch` clauses without `on` clauses. +- **DON'T** discard errors from `catch` clauses without `on` clauses. +- **DO** throw objects that implement `Error` only for programmatic errors. +- **DON'T** explicitly catch `Error` or types that implement it. +- **DO** use `rethrow` to rethrow a caught exception to preserve the original stack trace. + +### 3.10. Asynchrony + +- **PREFER** `async`/`await` over using raw `Future`s. +- **DON'T** use `async` when it has no useful effect. +- **CONSIDER** using higher-order methods to transform a stream. +- **AVOID** using `Completer` directly. + +## 4. API Design + +### 4.1. Names + +- **DO** use terms consistently. +- **AVOID** abbreviations unless more common than the unabbreviated term. +- **PREFER** putting the most descriptive noun last (e.g., `pageCount`). +- **CONSIDER** making the code read like a sentence when using the API. +- **PREFER** a noun phrase for a non-boolean property or variable. +- **PREFER** a non-imperative verb phrase for a boolean property or variable (e.g., `isEnabled`, `canClose`). +- **CONSIDER** omitting the verb for a named boolean parameter (e.g., `growable: true`). +- **PREFER** the "positive" name for a boolean property or variable (e.g., `isConnected` over `isDisconnected`). +- **PREFER** an imperative verb phrase for a function or method whose main purpose is a side effect (e.g., `list.add()`, `window.refresh()`). +- **PREFER** a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose (e.g., `list.elementAt(3)`). +- **CONSIDER** an imperative verb phrase for a function or method if you want to draw attention to the work it performs (e.g., `database.downloadData()`). +- **AVOID** starting a method name with `get`. +- **PREFER** naming a method `to___()` if it copies the object's state to a new object (e.g., `toList()`). +- **PREFER** naming a method `as___()` if it returns a different representation backed by the original object (e.g., `asMap()`). +- **AVOID** describing the parameters in the function's or method's name. +- **DO** follow existing mnemonic conventions when naming type parameters (e.g., `E` for elements, `K`, `V` for map keys/values, `T`, `S`, `U` for general types). + +### 4.2. Libraries + +- **PREFER** making declarations private (`_`). +- **CONSIDER** declaring multiple classes in the same library if they logically belong together. + +### 4.3. Classes and Mixins + +- **AVOID** defining a one-member abstract class when a simple function (`typedef`) will do. +- **AVOID** defining a class that contains only static members; prefer top-level functions/variables or a library. +- **AVOID** extending a class that isn't intended to be subclassed. +- **DO** use class modifiers (e.g., `final`, `interface`, `sealed`) to control if your class can be extended. +- **AVOID** implementing a class that isn't intended to be an interface. +- **DO** use class modifiers to control if your class can be an interface. +- **PREFER** defining a pure mixin or pure class to a `mixin class`. + +### 4.4. Constructors + +- **CONSIDER** making your constructor `const` if the class supports it (all fields are `final` and initialized in the constructor). + +### 4.5. Members + +- **PREFER** making fields and top-level variables `final`. +- **DO** use getters for operations that conceptually access properties (no arguments, returns a result, no user-visible side effects, idempotent). +- **DO** use setters for operations that conceptually change properties (single argument, no result, changes state, idempotent). +- **DON'T** define a setter without a corresponding getter. +- **AVOID** using runtime type tests to fake overloading. +- **AVOID** public `late final` fields without initializers. +- **AVOID** returning nullable `Future`, `Stream`, and collection types; prefer empty containers or non-nullable futures of nullable types. +- **AVOID** returning `this` from methods just to enable a fluent interface; prefer method cascades. + +### 4.6. Types + +- **DO** type annotate variables without initializers. +- **DO** type annotate fields and top-level variables if the type isn't obvious. +- **DON'T** redundantly type annotate initialized local variables. +- **DO** annotate return types on function declarations. +- **DO** annotate parameter types on function declarations. +- **DON'T** annotate inferred parameter types on function expressions. +- **DON'T** type annotate initializing formals. +- **DO** write type arguments on generic invocations that aren't inferred. +- **DON'T** write type arguments on generic invocations that are inferred. +- **AVOID** writing incomplete generic types. +- **DO** annotate with `dynamic` instead of letting inference fail silently. +- **PREFER** signatures in function type annotations. +- **DON'T** specify a return type for a setter. +- **DON'T** use the legacy `typedef` syntax. +- **PREFER** inline function types over `typedef`s. +- **PREFER** using function type syntax for parameters. +- **AVOID** using `dynamic` unless you want to disable static checking. +- **DO** use `Future` as the return type of asynchronous members that do not produce values. +- **AVOID** using `FutureOr` as a return type. + +### 4.7. Parameters + +- **AVOID** positional boolean parameters. +- **AVOID** optional positional parameters if the user may want to omit earlier parameters. +- **AVOID** mandatory parameters that accept a special "no argument" value. +- **DO** use inclusive start and exclusive end parameters to accept a range. + +### 4.8. Equality + +- **DO** override `hashCode` if you override `==`. +- **DO** make your `==` operator obey the mathematical rules of equality (reflexive, symmetric, transitive, consistent). +- **AVOID** defining custom equality for mutable classes. +- **DON'T** make the parameter to `==` nullable. + +_Sources:_ + +- [Effective Dart: Style](https://dart.dev/effective-dart/style) +- [Effective Dart: Documentation](https://dart.dev/effective-dart/documentation) +- [Effective Dart: Usage](https://dart.dev/effective-dart/usage) +- [Effective Dart: Design](https://dart.dev/effective-dart/design) diff --git a/dashboard/conductor/index.md b/dashboard/conductor/index.md new file mode 100644 index 0000000000..ce6eea166f --- /dev/null +++ b/dashboard/conductor/index.md @@ -0,0 +1,14 @@ +# Project Context + +## Definition +- [Product Definition](./product.md) +- [Product Guidelines](./product-guidelines.md) +- [Tech Stack](./tech-stack.md) + +## Workflow +- [Workflow](./workflow.md) +- [Code Style Guides](./code_styleguides/) + +## Management +- [Tracks Registry](./tracks.md) +- [Tracks Directory](./tracks/) diff --git a/dashboard/conductor/product-guidelines.md b/dashboard/conductor/product-guidelines.md new file mode 100644 index 0000000000..d944717c53 --- /dev/null +++ b/dashboard/conductor/product-guidelines.md @@ -0,0 +1,19 @@ +# Product Guidelines - Flutter Dashboard + +## Communication & Prose +* **Style:** Technical and precise. Documentation and user-facing text should prioritize accuracy, clarity, and brevity to ensure information is conveyed efficiently to a technical audience. +* **Tone:** Professional and objective. Focus on facts and actionable information. + +## Visual Identity & UX +* **Design System:** Standard Material Design 3. The dashboard will strictly follow the latest Material Design specifications to ensure a modern, consistent, and recognizable Flutter user experience. +* **Layout:** Prioritize clarity and logical flow. Use Material components (Scaffolds, Cards, Buttons, etc.) to structure information predictably. + +## Accessibility (a11y) +* **Compliance:** Strict adherence to WCAG 2.1 Level AA standards. +* **Implementation:** All UI elements must have appropriate semantic labels, maintain high contrast ratios, and provide full support for screen readers. +* **Focus Management:** Ensure a logical focus order for keyboard navigation across all interactive elements. + +## Error Handling & Feedback +* **Clarity:** Errors must be explicit and actionable. Avoid vague error messages. +* **Guidance:** Provide users with clear instructions on how to resolve the issue (e.g., "Authentication failed, please sign in again"). +* **Visibility:** Use appropriate UI patterns (dialogs for critical errors, snackbars for transient info) to ensure feedback is noticed without being unnecessarily disruptive. diff --git a/dashboard/conductor/product.md b/dashboard/conductor/product.md new file mode 100644 index 0000000000..7abe2370cf --- /dev/null +++ b/dashboard/conductor/product.md @@ -0,0 +1,20 @@ +# Product Definition - Flutter Dashboard + +## Initial Concept +The Flutter dashboard. + +## Target Audience +The primary users of the Flutter Dashboard are **Flutter framework developers and contributors**. The tool is designed to provide them with the necessary visibility into the health and stability of the Flutter project. + +## Core Goals +The main objective of the dashboard is **monitoring the health and status of the Flutter build tree**. It serves as a central hub for observing the continuous integration process and ensuring that changes to the framework do not introduce regressions. + +## Key Features +* **Commit Status Grid:** A comprehensive grid view displaying the status of various tasks across different commits, allowing for quick identification of regressions or flaky tests. +* **PreSubmit Monitoring:** A detailed view for specific Pull Requests or commit SHAs, allowing maintainers to inspect individual CI check statuses, view execution logs with attempt history, and re-run failed tasks. +* **Real-time Updates:** The dashboard provides real-time updates of build statuses, ensuring that developers have the most current information regarding the tree health. +* **Task Summary Popovers:** Clicking on a task in the grid opens a summary popover, providing immediate access to task details and direct links to full logs for deeper investigation. +* **Actionable UI:** Integration with backend services to allow authorized users to trigger re-runs of failed tasks directly from the dashboard interface. + +## Visual Aesthetic & UX +The dashboard adheres to **Material Design** principles, providing a standard, familiar, and polished Flutter user experience. The interface focuses on clarity and accessibility, ensuring that critical build information is easy to find and interpret. diff --git a/dashboard/conductor/setup_state.json b/dashboard/conductor/setup_state.json new file mode 100644 index 0000000000..e23b6a6288 --- /dev/null +++ b/dashboard/conductor/setup_state.json @@ -0,0 +1 @@ +{"last_successful_step": "3.3_initial_track_generated"} \ No newline at end of file diff --git a/dashboard/conductor/tech-stack.md b/dashboard/conductor/tech-stack.md new file mode 100644 index 0000000000..7a3010afef --- /dev/null +++ b/dashboard/conductor/tech-stack.md @@ -0,0 +1,18 @@ +# Technology Stack - Flutter Dashboard + +## Frontend +- **Framework:** Flutter (Mobile/Web) +- **Language:** Dart +- **State Management:** Provider +- **UI Components:** Material Design 3 + +## Services & Integration +- **Backend/Authentication:** Firebase (Core, Auth) +- **Identity Provider:** Google Sign-In +- **Monitoring:** Firebase Crashlytics +- **Networking:** http + +## Testing & Quality +- **Unit & Widget Testing:** flutter_test +- **Mocking:** mockito +- **Linting:** dart_flutter_team_lints diff --git a/dashboard/conductor/tracks.md b/dashboard/conductor/tracks.md new file mode 100644 index 0000000000..081b348f9a --- /dev/null +++ b/dashboard/conductor/tracks.md @@ -0,0 +1,13 @@ +# Project Tracks + +This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder. + +--- + +- [ ] **Track: Implement a pull request dashboard view to monitor the status of pending PRs and their associated CI checks.** + *Link: [./tracks/pr_dashboard_20260209/](./tracks/pr_dashboard_20260209/)* + +--- + +- [x] **Track: Implement pr_dashboard_page.dart view in Flutter Dashboard (renamed to PreSubmitView)** + *Link: [./tracks/presubmit_view_20260209/](./tracks/presubmit_view_20260209/)* diff --git a/dashboard/conductor/tracks/presubmit_view_20260209/code.html b/dashboard/conductor/tracks/presubmit_view_20260209/code.html new file mode 100644 index 0000000000..899c204ee8 --- /dev/null +++ b/dashboard/conductor/tracks/presubmit_view_20260209/code.html @@ -0,0 +1,372 @@ + + + + + + + + + + PR Test Dashboard View + + + + + + + + + +
+
+

PR #1234: Feature Implementation

+ Pending +
+ +
+
+ +
+
+
+
+
+
+

+ flutter-dashboard / Linux linux_android_aot_engine +

+

+ Started 7m 52s ago +

+
+
+
+
+
+
+
+ + + +
+
+ Build History +
+
+
+ Execution Log +
+ + Raw + output +
+
+
+
+ 12:04:12 + [INFO] Starting task linux_android_aot_engine... +
+
+ 12:04:13 + [INFO] Downloading dependencies... +
+
+ 12:04:45 + [SUCCESS] Dependencies installed. +
+
+ 12:05:01 + [INFO] Running build script ./scripts/build.sh +
+
+ 12:08:22 + [INFO] Build completed successfully. +
+
+ 12:08:23 + [INFO] Running tests... +
+
+ 12:08:24 + [INFO] Test suite started: Unit Tests +
+
+ 12:08:25 + [INFO] Executing test case 1/452... +
+
+ 12:08:26 + [INFO] Executing test case 2/452... +
+
+ 12:08:28 + [INFO] Executing test case 3/452... +
+
+ 12:08:35 + [INFO] Executing test case 10/452... +
+
+ 12:09:12 + [INFO] Executing test case 50/452... +
+
+ 12:10:45 + [INFO] Executing test case 200/452... +
+
+ 12:11:30 + [INFO] Executing test case 350/452... +
+
+ 12:12:04 + [SUCCESS] All tests passed (452/452) +
+
+ 12:12:05 + [INFO] Cleaning up workspace... +
+
+ 12:12:06 + [INFO] Uploading artifacts... +
+
+ 12:12:08 + [INFO] Task finished successfully. +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/dashboard/conductor/tracks/presubmit_view_20260209/index.md b/dashboard/conductor/tracks/presubmit_view_20260209/index.md new file mode 100644 index 0000000000..1efa3575dc --- /dev/null +++ b/dashboard/conductor/tracks/presubmit_view_20260209/index.md @@ -0,0 +1,5 @@ +# Track presubmit_view_20260209 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/dashboard/conductor/tracks/presubmit_view_20260209/metadata.json b/dashboard/conductor/tracks/presubmit_view_20260209/metadata.json new file mode 100644 index 0000000000..cd35ae7661 --- /dev/null +++ b/dashboard/conductor/tracks/presubmit_view_20260209/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "presubmit_view_20260209", + "type": "feature", + "status": "new", + "created_at": "2026-02-09T19:30:00Z", + "updated_at": "2026-02-09T19:30:00Z", + "description": "Implement pr_dashboard_page.dart view in Flutter Dashboard (renamed to PreSubmitView)" +} diff --git a/dashboard/conductor/tracks/presubmit_view_20260209/plan.md b/dashboard/conductor/tracks/presubmit_view_20260209/plan.md new file mode 100644 index 0000000000..45fc722870 --- /dev/null +++ b/dashboard/conductor/tracks/presubmit_view_20260209/plan.md @@ -0,0 +1,37 @@ +# Implementation Plan - Pull Request Detailed View + +## Phase 1: Infrastructure & Data Model [checkpoint: 085b744] +- [x] Task: Define the `PresubmitGuardResponse` and `PresubmitCheckResponse` models. (3c968d9) + - [x] Write Tests: Create unit tests for the new models, ensuring correct JSON deserialization based on the Cocoon API structure. (3c968d9) + - [x] Implement: Reuse model classes from `cocoon_common`. (3c968d9) +- [x] Task: Integrate the new endpoints into `CocoonService`. (3c968d9) + - [x] Write Tests: Mock the `/api/get-presubmit-guard` and `/api/get-presubmit-checks` endpoints and verify the service correctly fetches and parses the data. (3c968d9) + - [x] Implement: Add `fetchPresubmitGuard` and `fetchPresubmitCheckDetails` methods to `CocoonService` and its implementations. (3c968d9) +- [x] Task: Conductor - User Manual Verification 'Phase 1: Infrastructure & Data Model' (Protocol in workflow.md) (085b744) + +## Phase 2: UI Implementation - Sidebar & Header [checkpoint: a498231] +- [x] Task: Create the `PreSubmitView` page scaffold and routing. (a498231) + - [x] Write Tests: Verify that the application correctly routes to the new view using query parameters (`?repo=...&sha=...` and `?repo=...&pr=...`). (a498231) + - [x] Implement: Add the new route and create the basic `PreSubmitView` widget. (a498231) +- [x] Task: Implement the Header and Metadata components. (a498231) + - [x] Write Tests: Create widget tests to ensure PR number, title, and author are displayed correctly (mocked or from API). (a498231) + - [x] Implement: Build the header following the layout in `code.html`. (a498231) +- [x] Task: Implement the Checks Sidebar UI. (a498231) + - [x] Write Tests: Verify that checks are correctly grouped by stage and show the correct status icons. (a498231) + - [x] Implement: Build the scrollable sidebar with grouping and "Re-run" buttons (mocked actions). (a498231) +- [x] Task: Conductor - User Manual Verification 'Phase 2: UI Implementation - Sidebar & Header' (Protocol in workflow.md) (a498231) + +## Phase 3: UI Implementation - Log Viewer & Integration [checkpoint: a498231] +- [x] Task: Implement the Log Viewer Pane with Attempt Tabs. (a498231) + - [x] Write Tests: Verify that clicking a check in the sidebar updates the log pane and that attempt tabs correctly switch between log summaries. (a498231) + - [x] Implement: Build the tabbed log viewer and the log content area. (a498231) +- [x] Task: Integrate real data for the `sha` route. (a498231) + - [x] Write Tests: Verify that the `sha` route correctly triggers API calls and populates the UI with live data. (a498231) + - [x] Implement: Connect the `sha` route logic to the `CocoonService`. (a498231) +- [x] Task: Implement the mocked `pr` route. (a498231) + - [x] Write Tests: Verify that the `pr` route displays the placeholder data from the layout. (a498231) + - [x] Implement: Add the fallback/mocked data logic for the `pr` query parameter. (a498231) +- [x] Task: Final Accessibility & Dark Mode Pass. (a498231) + - [x] Write Tests: Run accessibility audits and verify high contrast/screen reader support for the new view. (a498231) + - [x] Implement: Refine styling for perfect WCAG 2.1 Level AA compliance and ensure seamless Dark Mode switching. (a498231) +- [x] Task: Conductor - User Manual Verification 'Phase 3: UI Implementation - Log Viewer & Integration' (Protocol in workflow.md) (a498231) diff --git a/dashboard/conductor/tracks/presubmit_view_20260209/screen.png b/dashboard/conductor/tracks/presubmit_view_20260209/screen.png new file mode 100644 index 0000000000..1bf1497f16 Binary files /dev/null and b/dashboard/conductor/tracks/presubmit_view_20260209/screen.png differ diff --git a/dashboard/conductor/tracks/presubmit_view_20260209/spec.md b/dashboard/conductor/tracks/presubmit_view_20260209/spec.md new file mode 100644 index 0000000000..4f9b36afc5 --- /dev/null +++ b/dashboard/conductor/tracks/presubmit_view_20260209/spec.md @@ -0,0 +1,52 @@ +# Specification - Pull Request Detailed View (PreSubmitView) + +## Overview +Implement a detailed monitoring view for a specific Pull Request (PR) in the Flutter Dashboard, based on the provided layout (`code.html` and `screen.png`). This view allows maintainers to inspect CI check statuses and view execution logs for a specific commit SHA or a mocked Pull Request. + +## Functional Requirements +- **Deep Link Navigation:** The view must be accessible via query parameters: + - `?repo=&sha=`: **Functional Route**. + - `?repo=&pr=`: **Mocked Route**. Displays placeholder data based on the provided layout. +- **Data Integration (Checks Sidebar):** + - For the functional `sha` route, the sidebar MUST be populated by calling the Cocoon API: + `GET /api/get-presubmit-guard?slug=flutter/&sha=` + - **API Response Handling:** The UI must parse the `PresubmitGuardResponse` and map it to the sidebar: + - `stages`: Map each stage to a sidebar section (e.g., Engine, Framework). + - `builds` (within stages): Map each build entry to an individual check item with its name and status. + - `author` and `prNum`: Use these to update the header metadata. + - `guardStatus`: Reflect the overall status in the header. + - **`checkRunId`**: Store this ID, as it is required to fetch specific check details/logs. +- **Checks Sidebar UI:** + - List CI checks grouped by section (from API stages). + - Show status icons (Success, Error, Pending) mapped from API build statuses. + - **Re-run Actions (Mocked):** Buttons provide visual feedback when clicked but do not perform backend actions. +- **Log Viewer Pane:** + - Display the "Execution Log" for the selected check in the sidebar. + - For the functional `sha` route, fetch the check details using the Cocoon API: + `GET /api/get-presubmit-checks?check_run_id=&build_name=` + - **Handling Multiple Attempts (Tabs):** + - If the API returns multiple `PresubmitCheckResponse` objects for a build, display them as tabs in the log viewer (as shown in the layout). + - **Tab Naming:** Use the `attemptNumber` prefixed with a hash as the tab label (e.g., `#1`, `#2`). + - Selecting a tab displays the `summary` or log content from that specific attempt. + - Include a link to view details on the external LUCI UI. + - For the mocked `pr` route: Use placeholder log content and tabs. +- **Dark Mode Support:** Implement the layout's dark mode theme, consistent with the project's visual identity. + +## Non-Functional Requirements +- **Material Design 3:** Adhere to the project's tech stack and visual guidelines while implementing the specific layout provided. +- **Responsiveness:** Correctly handle sidebar and log pane on different screen sizes. +- **Accessibility:** Adhere to WCAG 2.1 Level AA standards for new UI components. + +## Acceptance Criteria +- [ ] Navigating to `?repo=flutter&sha=` correctly calls the `/api/get-presubmit-guard` endpoint and renders the sidebar. +- [ ] Navigating to `?repo=flutter&pr=` displays the mocked dashboard layout. +- [ ] Selecting a check in the sidebar correctly calls the `/api/get-presubmit-checks` endpoint (for functional SHA routes). +- [ ] Multiple attempts for a check are displayed as clickable tabs labeled by their attempt number. +- [ ] The UI supports both Light and Dark modes. + +## Out of Scope +- Main PR list/dashboard page. +- Functional backend integration for re-running tasks. +- Real-time log streaming. +- Navigation/Browsing between different Pull Requests (beyond basic URL entry). +- Log download functionality. diff --git a/dashboard/conductor/workflow.md b/dashboard/conductor/workflow.md new file mode 100644 index 0000000000..6f9cfd8fcd --- /dev/null +++ b/dashboard/conductor/workflow.md @@ -0,0 +1,333 @@ +# Project Workflow + +## Guiding Principles + +1. **The Plan is the Source of Truth:** All work must be tracked in `plan.md` +2. **The Tech Stack is Deliberate:** Changes to the tech stack must be documented in `tech-stack.md` *before* implementation +3. **Test-Driven Development:** Write unit tests before implementing functionality +4. **High Code Coverage:** Aim for >80% code coverage for all modules +5. **User Experience First:** Every decision should prioritize user experience +6. **Non-Interactive & CI-Aware:** Prefer non-interactive commands. Use `CI=true` for watch-mode tools (tests, linters) to ensure single execution. + +## Task Workflow + +All tasks follow a strict lifecycle: + +### Standard Task Workflow + +1. **Select Task:** Choose the next available task from `plan.md` in sequential order + +2. **Mark In Progress:** Before beginning work, edit `plan.md` and change the task from `[ ]` to `[~]` + +3. **Write Failing Tests (Red Phase):** + - Create a new test file for the feature or bug fix. + - Write one or more unit tests that clearly define the expected behavior and acceptance criteria for the task. + - **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests. + +4. **Implement to Pass Tests (Green Phase):** + - Write the minimum amount of application code necessary to make the failing tests pass. + - Run the test suite again and confirm that all tests now pass. This is the "Green" phase. + +5. **Refactor (Optional but Recommended):** + - With the safety of passing tests, refactor the implementation code and the test code to improve clarity, remove duplication, and enhance performance without changing the external behavior. + - Rerun tests to ensure they still pass after refactoring. + +6. **Verify Coverage:** Run coverage reports using the project's chosen tools. For example, in a Python project, this might look like: + ```bash + pytest --cov=app --cov-report=html + ``` + Target: >80% coverage for new code. The specific tools and commands will vary by language and framework. + +7. **Document Deviations:** If implementation differs from tech stack: + - **STOP** implementation + - Update `tech-stack.md` with new design + - Add dated note explaining the change + - Resume implementation + +8. **Commit Code Changes:** + - Stage all code changes related to the task. + - Propose a clear, concise commit message e.g, `feat(ui): Create basic HTML structure for calculator`. + - Perform the commit. + +9. **Attach Task Summary with Git Notes:** + - **Step 9.1: Get Commit Hash:** Obtain the hash of the *just-completed commit* (`git log -1 --format="%H"`). + - **Step 9.2: Draft Note Content:** Create a detailed summary for the completed task. This should include the task name, a summary of changes, a list of all created/modified files, and the core "why" for the change. + - **Step 9.3: Attach Note:** Use the `git notes` command to attach the summary to the commit. + ```bash + # The note content from the previous step is passed via the -m flag. + git notes add -m "" + ``` + +10. **Get and Record Task Commit SHA:** + - **Step 10.1: Update Plan:** Read `plan.md`, find the line for the completed task, update its status from `[~]` to `[x]`, and append the first 7 characters of the *just-completed commit's* commit hash. + - **Step 10.2: Write Plan:** Write the updated content back to `plan.md`. + +11. **Commit Plan Update:** + - **Action:** Stage the modified `plan.md` file. + - **Action:** Commit this change with a descriptive message (e.g., `conductor(plan): Mark task 'Create user model' as complete`). + +### Phase Completion Verification and Checkpointing Protocol + +**Trigger:** This protocol is executed immediately after a task is completed that also concludes a phase in `plan.md`. + +1. **Announce Protocol Start:** Inform the user that the phase is complete and the verification and checkpointing protocol has begun. + +2. **Ensure Test Coverage for Phase Changes:** + - **Step 2.1: Determine Phase Scope:** To identify the files changed in this phase, you must first find the starting point. Read `plan.md` to find the Git commit SHA of the *previous* phase's checkpoint. If no previous checkpoint exists, the scope is all changes since the first commit. + - **Step 2.2: List Changed Files:** Execute `git diff --name-only HEAD` to get a precise list of all files modified during this phase. + - **Step 2.3: Verify and Create Tests:** For each file in the list: + - **CRITICAL:** First, check its extension. Exclude non-code files (e.g., `.json`, `.md`, `.yaml`). + - For each remaining code file, verify a corresponding test file exists. + - If a test file is missing, you **must** create one. Before writing the test, **first, analyze other test files in the repository to determine the correct naming convention and testing style.** The new tests **must** validate the functionality described in this phase's tasks (`plan.md`). + +3. **Execute Automated Tests with Proactive Debugging:** + - Before execution, you **must** announce the exact shell command you will use to run the tests. + - **Example Announcement:** "I will now run the automated test suite to verify the phase. **Command:** `CI=true npm test`" + - Execute the announced command. + - If tests fail, you **must** inform the user and begin debugging. You may attempt to propose a fix a **maximum of two times**. If the tests still fail after your second proposed fix, you **must stop**, report the persistent failure, and ask the user for guidance. + +4. **Propose a Detailed, Actionable Manual Verification Plan:** + - **CRITICAL:** To generate the plan, first analyze `product.md`, `product-guidelines.md`, and `plan.md` to determine the user-facing goals of the completed phase. + - You **must** generate a step-by-step plan that walks the user through the verification process, including any necessary commands and specific, expected outcomes. + - The plan you present to the user **must** follow this format: + + **For a Frontend Change:** + ``` + The automated tests have passed. For manual verification, please follow these steps: + + **Manual Verification Steps:** + 1. **Start the development server with the command:** `npm run dev` + 2. **Open your browser to:** `http://localhost:3000` + 3. **Confirm that you see:** The new user profile page, with the user's name and email displayed correctly. + ``` + + **For a Backend Change:** + ``` + The automated tests have passed. For manual verification, please follow these steps: + + **Manual Verification Steps:** + 1. **Ensure the server is running.** + 2. **Execute the following command in your terminal:** `curl -X POST http://localhost:8080/api/v1/users -d '{"name": "test"}'` + 3. **Confirm that you receive:** A JSON response with a status of `201 Created`. + ``` + +5. **Await Explicit User Feedback:** + - After presenting the detailed plan, ask the user for confirmation: "**Does this meet your expectations? Please confirm with yes or provide feedback on what needs to be changed.**" + - **PAUSE** and await the user's response. Do not proceed without an explicit yes or confirmation. + +6. **Create Checkpoint Commit:** + - Stage all changes. If no changes occurred in this step, proceed with an empty commit. + - Perform the commit with a clear and concise message (e.g., `conductor(checkpoint): Checkpoint end of Phase X`). + +7. **Attach Auditable Verification Report using Git Notes:** + - **Step 7.1: Draft Note Content:** Create a detailed verification report including the automated test command, the manual verification steps, and the user's confirmation. + - **Step 7.2: Attach Note:** Use the `git notes` command and the full commit hash from the previous step to attach the full report to the checkpoint commit. + +8. **Get and Record Phase Checkpoint SHA:** + - **Step 8.1: Get Commit Hash:** Obtain the hash of the *just-created checkpoint commit* (`git log -1 --format="%H"`). + - **Step 8.2: Update Plan:** Read `plan.md`, find the heading for the completed phase, and append the first 7 characters of the commit hash in the format `[checkpoint: ]`. + - **Step 8.3: Write Plan:** Write the updated content back to `plan.md`. + +9. **Commit Plan Update:** + - **Action:** Stage the modified `plan.md` file. + - **Action:** Commit this change with a descriptive message following the format `conductor(plan): Mark phase '' as complete`. + +10. **Announce Completion:** Inform the user that the phase is complete and the checkpoint has been created, with the detailed verification report attached as a git note. + +### Quality Gates + +Before marking any task complete, verify: + +- [ ] All tests pass +- [ ] Code coverage meets requirements (>80%) +- [ ] Code follows project's code style guidelines (as defined in `code_styleguides/`) +- [ ] All public functions/methods are documented (e.g., docstrings, JSDoc, GoDoc) +- [ ] Type safety is enforced (e.g., type hints, TypeScript types, Go types) +- [ ] No linting or static analysis errors (using the project's configured tools) +- [ ] Works correctly on mobile (if applicable) +- [ ] Documentation updated if needed +- [ ] No security vulnerabilities introduced + +## Development Commands + +**AI AGENT INSTRUCTION: This section should be adapted to the project's specific language, framework, and build tools.** + +### Setup +```bash +# Example: Commands to set up the development environment (e.g., install dependencies, configure database) +# e.g., for a Node.js project: npm install +# e.g., for a Go project: go mod tidy +``` + +### Daily Development +```bash +# Example: Commands for common daily tasks (e.g., start dev server, run tests, lint, format) +# e.g., for a Node.js project: npm run dev, npm test, npm run lint +# e.g., for a Go project: go run main.go, go test ./..., go fmt ./... +``` + +### Before Committing +```bash +# Example: Commands to run all pre-commit checks (e.g., format, lint, type check, run tests) +# e.g., for a Node.js project: npm run check +# e.g., for a Go project: make check (if a Makefile exists) +``` + +## Testing Requirements + +### Unit Testing +- Every module must have corresponding tests. +- Use appropriate test setup/teardown mechanisms (e.g., fixtures, beforeEach/afterEach). +- Mock external dependencies. +- Test both success and failure cases. + +### Integration Testing +- Test complete user flows +- Verify database transactions +- Test authentication and authorization +- Check form submissions + +### Mobile Testing +- Test on actual iPhone when possible +- Use Safari developer tools +- Test touch interactions +- Verify responsive layouts +- Check performance on 3G/4G + +## Code Review Process + +### Self-Review Checklist +Before requesting review: + +1. **Functionality** + - Feature works as specified + - Edge cases handled + - Error messages are user-friendly + +2. **Code Quality** + - Follows style guide + - DRY principle applied + - Clear variable/function names + - Appropriate comments + +3. **Testing** + - Unit tests comprehensive + - Integration tests pass + - Coverage adequate (>80%) + +4. **Security** + - No hardcoded secrets + - Input validation present + - SQL injection prevented + - XSS protection in place + +5. **Performance** + - Database queries optimized + - Images optimized + - Caching implemented where needed + +6. **Mobile Experience** + - Touch targets adequate (44x44px) + - Text readable without zooming + - Performance acceptable on mobile + - Interactions feel native + +## Commit Guidelines + +### Message Format +``` +(): + +[optional body] + +[optional footer] +``` + +### Types +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation only +- `style`: Formatting, missing semicolons, etc. +- `refactor`: Code change that neither fixes a bug nor adds a feature +- `test`: Adding missing tests +- `chore`: Maintenance tasks + +### Examples +```bash +git commit -m "feat(auth): Add remember me functionality" +git commit -m "fix(posts): Correct excerpt generation for short posts" +git commit -m "test(comments): Add tests for emoji reaction limits" +git commit -m "style(mobile): Improve button touch targets" +``` + +## Definition of Done + +A task is complete when: + +1. All code implemented to specification +2. Unit tests written and passing +3. Code coverage meets project requirements +4. Documentation complete (if applicable) +5. Code passes all configured linting and static analysis checks +6. Works beautifully on mobile (if applicable) +7. Implementation notes added to `plan.md` +8. Changes committed with proper message +9. Git note with task summary attached to the commit + +## Emergency Procedures + +### Critical Bug in Production +1. Create hotfix branch from main +2. Write failing test for bug +3. Implement minimal fix +4. Test thoroughly including mobile +5. Deploy immediately +6. Document in plan.md + +### Data Loss +1. Stop all write operations +2. Restore from latest backup +3. Verify data integrity +4. Document incident +5. Update backup procedures + +### Security Breach +1. Rotate all secrets immediately +2. Review access logs +3. Patch vulnerability +4. Notify affected users (if any) +5. Document and update security procedures + +## Deployment Workflow + +### Pre-Deployment Checklist +- [ ] All tests passing +- [ ] Coverage >80% +- [ ] No linting errors +- [ ] Mobile testing complete +- [ ] Environment variables configured +- [ ] Database migrations ready +- [ ] Backup created + +### Deployment Steps +1. Merge feature branch to main +2. Tag release with version +3. Push to deployment service +4. Run database migrations +5. Verify deployment +6. Test critical paths +7. Monitor for errors + +### Post-Deployment +1. Monitor analytics +2. Check error logs +3. Gather user feedback +4. Plan next iteration + +## Continuous Improvement + +- Review workflow weekly +- Update based on pain points +- Document lessons learned +- Optimize for user happiness +- Keep things simple and maintainable diff --git a/dashboard/lib/logic/links.dart b/dashboard/lib/logic/links.dart index 2b905c0b62..5a0bf3a315 100644 --- a/dashboard/lib/logic/links.dart +++ b/dashboard/lib/logic/links.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../views/build_dashboard_page.dart'; +import '../views/presubmit_view.dart'; import '../views/tree_status_page.dart'; /// List of links that are shown in the [DashboardNavigationDrawer]. @@ -22,6 +23,14 @@ List createCocoonLinks(BuildContext context) { ); }, ), + CocoonLink( + name: 'PreSubmit', + route: PreSubmitView.routeName, + icon: const Icon(Icons.playlist_add_check), + action: () async { + await Navigator.pushReplacementNamed(context, PreSubmitView.routeName); + }, + ), CocoonLink( name: 'Manual Tree Status', route: BuildDashboardPage.routeName, diff --git a/dashboard/lib/main.dart b/dashboard/lib/main.dart index a1f0f40852..7631259aed 100644 --- a/dashboard/lib/main.dart +++ b/dashboard/lib/main.dart @@ -15,6 +15,7 @@ import 'service/cocoon.dart'; import 'service/firebase_auth.dart'; import 'state/build.dart'; import 'views/build_dashboard_page.dart'; +import 'views/presubmit_view.dart'; import 'views/tree_status_page.dart'; import 'widgets/now.dart'; import 'widgets/state_provider.dart'; @@ -134,6 +135,15 @@ class MyApp extends StatelessWidget { ); }, ); + case PreSubmitView.routeSegment: + return MaterialPageRoute( + settings: settings, + builder: (_) { + return PreSubmitView( + queryParameters: uriData.queryParameters, + ); + }, + ); } return null; }, diff --git a/dashboard/lib/service/appengine_cocoon.dart b/dashboard/lib/service/appengine_cocoon.dart index bd5c017c20..46e73393e0 100644 --- a/dashboard/lib/service/appengine_cocoon.dart +++ b/dashboard/lib/service/appengine_cocoon.dart @@ -288,6 +288,83 @@ class AppEngineCocoonService implements CocoonService { ); } + @override + Future> fetchPresubmitGuard({ + required String repo, + required String sha, + }) async { + final queryParameters = { + 'slug': 'flutter/$repo', + 'sha': sha, + }; + final getGuardUrl = apiEndpoint( + '/api/get-presubmit-guard', + queryParameters: queryParameters, + ); + + final response = await _client.get(getGuardUrl); + + if (response.statusCode != HttpStatus.ok) { + return CocoonResponse.error( + '/api/get-presubmit-guard returned ${response.statusCode}', + statusCode: response.statusCode, + ); + } + + try { + return CocoonResponse.data( + PresubmitGuardResponse.fromJson( + jsonDecode(response.body) as Map, + ), + ); + } catch (error) { + return CocoonResponse.error( + error.toString(), + statusCode: response.statusCode, + ); + } + } + + @override + Future>> + fetchPresubmitCheckDetails({ + required int checkRunId, + required String buildName, + }) async { + final queryParameters = { + 'check_run_id': checkRunId.toString(), + 'build_name': buildName, + }; + final getChecksUrl = apiEndpoint( + '/api/get-presubmit-checks', + queryParameters: queryParameters, + ); + + final response = await _client.get(getChecksUrl); + + if (response.statusCode != HttpStatus.ok) { + return CocoonResponse.error( + '/api/get-presubmit-checks returned ${response.statusCode}', + statusCode: response.statusCode, + ); + } + + try { + final jsonResponse = jsonDecode(response.body) as List; + return CocoonResponse.data( + jsonResponse + .cast>() + .map(PresubmitCheckResponse.fromJson) + .toList(), + ); + } catch (error) { + return CocoonResponse.error( + error.toString(), + statusCode: response.statusCode, + ); + } + } + @override Future> updateTreeStatus({ required String idToken, @@ -395,6 +472,68 @@ class AppEngineCocoonService implements CocoonService { ); } + @override + Future>> fetchMergeQueueHooks({ + required String idToken, + }) async { + final getMergeQueueHooksUrl = apiEndpoint('/api/merge_queue_hooks'); + + final response = await _client.get( + getMergeQueueHooksUrl, + headers: {'X-Flutter-IdToken': idToken}, + ); + + if (response.statusCode != HttpStatus.ok) { + return CocoonResponse.error( + '/api/merge_queue_hooks returned ${response.statusCode}', + statusCode: response.statusCode, + ); + } + + try { + final jsonResponse = jsonDecode(response.body) as Map; + final hooks = MergeGroupHooks.fromJson(jsonResponse); + return CocoonResponse.data(hooks.hooks); + } catch (error) { + return CocoonResponse.error( + error.toString(), + statusCode: response.statusCode, + ); + } + } + + @override + Future> replayGitHubWebhook({ + required String idToken, + required String id, + }) async { + if (idToken.isEmpty) { + return const CocoonResponse.error( + 'Sign in to replay events', + statusCode: 401, + ); + } + + final replayUrl = apiEndpoint( + '/api/github-webhook-replay', + queryParameters: {'id': id}, + ); + + final response = await _client.post( + replayUrl, + headers: {'X-Flutter-IdToken': idToken}, + ); + + if (response.statusCode == HttpStatus.ok) { + return const CocoonResponse.data(null); + } + + return CocoonResponse.error( + 'HTTP Code: ${response.statusCode}, ${response.body}', + statusCode: response.statusCode, + ); + } + /// Construct the API endpoint based on the priority of using a local endpoint /// before falling back to the production endpoint. /// diff --git a/dashboard/lib/service/cocoon.dart b/dashboard/lib/service/cocoon.dart index 6c02b834f5..9605bb6b74 100644 --- a/dashboard/lib/service/cocoon.dart +++ b/dashboard/lib/service/cocoon.dart @@ -106,6 +106,30 @@ abstract class CocoonService { String? issueLink, String? note, }); + + /// Get the current list of merge queue hooks. + Future>> fetchMergeQueueHooks({ + required String idToken, + }); + + /// Replay a GitHub webhook. + Future> replayGitHubWebhook({ + required String idToken, + required String id, + }); + + /// Gets the presubmit guard status for a given [repo] and commit [sha]. + Future> fetchPresubmitGuard({ + required String repo, + required String sha, + }); + + /// Gets the details for a specific presubmit check. + Future>> + fetchPresubmitCheckDetails({ + required int checkRunId, + required String buildName, + }); } /// Wrapper class for data this state serves. diff --git a/dashboard/lib/service/dev_cocoon.dart b/dashboard/lib/service/dev_cocoon.dart index 93454ceeb0..08c5e478df 100644 --- a/dashboard/lib/service/dev_cocoon.dart +++ b/dashboard/lib/service/dev_cocoon.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:cocoon_common/guard_status.dart'; import 'package:cocoon_common/rpc_model.dart'; import 'package:cocoon_common/task_status.dart'; @@ -176,6 +177,72 @@ class DevelopmentCocoonService implements CocoonService { return const CocoonResponse.data(null); } + @override + Future> fetchPresubmitGuard({ + required String repo, + required String sha, + }) async { + // Extract a number from the SHA if it's a mock SHA to provide varied data + final num = sha.contains('_') ? sha.split('_')[2] : '1'; + final prNum = int.tryParse(num) ?? 123; + + return CocoonResponse.data( + PresubmitGuardResponse( + prNum: prNum, + checkRunId: 456, + author: _authors[prNum % _authors.length], + guardStatus: GuardStatus.inProgress, + stages: [ + PresubmitGuardStage( + name: 'Engine', + createdAt: now.millisecondsSinceEpoch, + builds: { + 'Mac mac_host_engine $num': TaskStatus.failed, + 'Mac mac_ios_engine $num': TaskStatus.waitingForBackfill, + 'Linux linux_android_aot_engine $num': TaskStatus.succeeded, + }, + ), + PresubmitGuardStage( + name: 'Framework', + createdAt: now.millisecondsSinceEpoch, + builds: { + 'Linux framework_tests $num': TaskStatus.inProgress, + 'Mac framework_tests $num': TaskStatus.cancelled, + 'Linux android framework_tests $num': TaskStatus.skipped, + 'Windows framework_tests $num': TaskStatus.infraFailure, + }, + ), + ], + ), + ); + } + + @override + Future>> + fetchPresubmitCheckDetails({ + required int checkRunId, + required String buildName, + }) async { + return CocoonResponse.data([ + PresubmitCheckResponse( + attemptNumber: 1, + buildName: buildName, + creationTime: now.millisecondsSinceEpoch - 10000, + status: 'Succeeded', + summary: + '[INFO] Starting task $buildName...\n[SUCCESS] Dependencies installed.\n[INFO] Running build script...\n[SUCCESS] All tests passed (452/452)', + ), + PresubmitCheckResponse( + attemptNumber: 2, + buildName: buildName, + creationTime: now.millisecondsSinceEpoch, + status: 'Failed', + summary: + '[INFO] Starting task $buildName...\n[ERROR] Test failed: Unit Tests', + ), + ]); + } + @override Future> updateTreeStatus({ required String idToken, @@ -275,6 +342,21 @@ class DevelopmentCocoonService implements CocoonService { ); } + @override + Future>> fetchMergeQueueHooks({ + required String idToken, + }) async { + return const CocoonResponse>.data([]); + } + + @override + Future> replayGitHubWebhook({ + required String idToken, + required String id, + }) async { + return const CocoonResponse.data(null); + } + static const int _commitGap = 2 * 60 * 1000; // 2 minutes between commits List _createFakeCommitStatuses( diff --git a/dashboard/lib/views/presubmit_view.dart b/dashboard/lib/views/presubmit_view.dart new file mode 100644 index 0000000000..6395c4cfc5 --- /dev/null +++ b/dashboard/lib/views/presubmit_view.dart @@ -0,0 +1,703 @@ +// 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 'dart:async'; + +import 'package:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart'; +import 'package:cocoon_common/task_status.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../dashboard_navigation_drawer.dart'; +import '../state/build.dart'; +import '../widgets/app_bar.dart'; +import '../widgets/sha_selector.dart'; +import '../widgets/task_box.dart'; + +/// A detailed monitoring view for a specific Pull Request (PR) or commit SHA. +/// +/// This view displays CI check statuses and execution logs. +final class PreSubmitView extends StatefulWidget { + const PreSubmitView({super.key, this.queryParameters}); + + static const String routeSegment = 'presubmit'; + static const String routeName = '/$routeSegment'; + + final Map? queryParameters; + + @override + State createState() => _PreSubmitViewState(); +} + +class _PreSubmitViewState extends State { + late String repo; + String? sha; + String? pr; + PresubmitGuardResponse? _guardResponse; + bool _isLoading = false; + String? _selectedCheck; + + @override + void initState() { + super.initState(); + final params = widget.queryParameters ?? {}; + repo = params['repo'] ?? 'flutter'; + sha = params['sha']; + pr = params['pr']; + + if (pr == '123' || (sha != null && sha!.startsWith('mock_sha_'))) { + // Use a default mock SHA for the PR route if none selected + sha ??= 'mock_sha_1_long_hash_value'; + pr ??= '123'; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (sha != null && _guardResponse == null && !_isLoading) { + unawaited(_fetchGuardStatus()); + } + } + + Future _fetchGuardStatus() async { + setState(() { + _isLoading = true; + _selectedCheck = null; + }); + final buildState = Provider.of(context, listen: false); + + // For mock SHAs, we don't need a real API call if we're in Development mode, + // but PreSubmitView currently unifies everything through cocoonService. + // If cocoonService is DevelopmentCocoonService, it will return mock data. + + final response = await buildState.cocoonService.fetchPresubmitGuard( + repo: repo, + sha: sha!, + ); + if (mounted) { + setState(() { + _guardResponse = response.data; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final buildState = Provider.of(context); + + final availableShas = pr != null + ? [ + 'mock_sha_1_long_hash_value', + 'mock_sha_2_long_hash_value', + 'mock_sha_3_long_hash_value', + ] + : buildState.statuses.map((s) => s.commit.sha).toList(); + + // Ensure current sha is in the list + if (sha != null && !availableShas.contains(sha)) { + availableShas.insert(0, sha!); + } + + final title = _guardResponse != null + ? 'PR #${_guardResponse!.prNum}: [${_guardResponse!.author}]' + : (pr != null + ? 'PR #$pr: Feature Implementation' + : 'PreSubmit: $repo @ $sha'); + + final statusText = + _guardResponse?.guardStatus.value ?? + (pr != null ? 'Pending' : 'Loading...'); + final statusColor = _getStatusColor(statusText, isDark); + + return Scaffold( + appBar: CocoonAppBar( + title: Row( + children: [ + Flexible( + child: SelectionArea( + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: statusColor.withValues(alpha: 0.2)), + ), + child: SelectionArea( + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + actions: [ + SizedBox( + width: 250, + child: ShaSelector( + availableShas: availableShas, + selectedSha: sha, + onShaSelected: (newSha) { + setState(() { + sha = newSha; + _guardResponse = null; + }); + unawaited(_fetchGuardStatus()); + }, + ), + ), + const SizedBox(width: 8), + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.refresh, size: 18), + label: const Text('Re-run failed'), + style: TextButton.styleFrom( + foregroundColor: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(width: 8), + ], + ), + drawer: const DashboardNavigationDrawer(), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + const Divider(height: 1, thickness: 1), + Expanded( + child: SelectionArea( + child: Row( + children: [ + if (_guardResponse != null) + _ChecksSidebar( + guardResponse: _guardResponse!, + selectedCheck: _selectedCheck, + onCheckSelected: (name) { + setState(() { + _selectedCheck = name; + }); + }, + ), + const VerticalDivider(width: 1, thickness: 1), + Expanded( + child: _selectedCheck == null + ? const Center( + child: Text('Select a check to view logs'), + ) + : _LogViewerPane( + repo: repo, + checkRunId: _guardResponse!.checkRunId, + buildName: _selectedCheck!, + isMocked: sha!.startsWith('mock_sha_'), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Color _getStatusColor(String status, bool isDark) { + switch (status) { + case 'Succeeded': + return const Color(0xFF2DA44E); + case 'Failed': + return const Color(0xFFF85149); + case 'In Progress': + case 'Pending': + return const Color(0xFFD29922); + default: + return isDark ? Colors.grey[400]! : Colors.grey[600]!; + } + } +} + +class _LogViewerPane extends StatefulWidget { + const _LogViewerPane({ + required this.repo, + required this.checkRunId, + required this.buildName, + this.isMocked = false, + }); + + final String repo; + final int checkRunId; + final String buildName; + final bool isMocked; + + @override + State<_LogViewerPane> createState() => _LogViewerPaneState(); +} + +class _LogViewerPaneState extends State<_LogViewerPane> { + List? _checks; + bool _isLoading = false; + int _selectedAttemptIndex = 0; + + @override + void initState() { + super.initState(); + _fetchCheckDetails(); + } + + @override + void didUpdateWidget(_LogViewerPane oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.buildName != widget.buildName || + oldWidget.checkRunId != widget.checkRunId) { + _selectedAttemptIndex = 0; + _fetchCheckDetails(); + } + } + + Future _fetchCheckDetails() async { + if (widget.isMocked) { + setState(() { + _checks = [ + PresubmitCheckResponse( + attemptNumber: 1, + buildName: widget.buildName, + creationTime: 0, + status: 'Succeeded', + summary: + '[INFO] Starting task ${widget.buildName}...\n[SUCCESS] Dependencies installed.\n[INFO] Running build script...\n[SUCCESS] All tests passed (452/452)', + ), + PresubmitCheckResponse( + attemptNumber: 2, + buildName: widget.buildName, + creationTime: 0, + status: 'Failed', + summary: + '[INFO] Starting task ${widget.buildName}...\n[ERROR] Test failed: Unit Tests', + ), + ]; + }); + return; + } + + setState(() { + _isLoading = true; + }); + final buildState = Provider.of(context, listen: false); + final response = await buildState.cocoonService.fetchPresubmitCheckDetails( + checkRunId: widget.checkRunId, + buildName: widget.buildName, + ); + if (mounted) { + setState(() { + _checks = response.data; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final borderColor = isDark + ? const Color(0xFF333333) + : const Color(0xFFD1D5DB); + + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (_checks == null || _checks!.isEmpty) { + return const Center(child: Text('No details found for this check')); + } + + final selectedCheck = + _checks![_selectedAttemptIndex < _checks!.length + ? _selectedAttemptIndex + : 0]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${widget.repo} / ${widget.buildName}', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'Status: ${selectedCheck.status}', + style: TextStyle( + fontSize: 14, + color: isDark + ? const Color(0xFF8B949E) + : const Color(0xFF6B7280), + ), + ), + ], + ), + ), + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + border: Border(bottom: BorderSide(color: borderColor)), + ), + child: Row( + children: [ + ..._checks!.asMap().entries.map((entry) { + final index = entry.key; + final check = entry.value; + final isSelected = _selectedAttemptIndex == index; + return InkWell( + onTap: () => setState(() => _selectedAttemptIndex = index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isSelected + ? const Color(0xFF3B82F6) + : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + '#${check.attemptNumber}', + style: TextStyle( + fontSize: 13, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? (isDark ? Colors.white : Colors.black) + : (isDark + ? const Color(0xFF8B949E) + : const Color(0xFF6B7280)), + ), + ), + ), + ); + }), + const Spacer(), + const Text( + 'BUILD HISTORY', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.grey, + letterSpacing: 1.0, + ), + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Row( + children: [ + Text( + 'Execution Log', + style: TextStyle(fontWeight: FontWeight.w600), + ), + Spacer(), + Text( + 'Raw output', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(6), + ), + width: double.infinity, + child: SingleChildScrollView( + child: Text( + selectedCheck.summary ?? 'No log summary available', + style: const TextStyle(fontFamily: 'monospace', fontSize: 13), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: InkWell( + onTap: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.open_in_new, + size: 18, + color: isDark + ? const Color(0xFF58A6FF) + : const Color(0xFF0969DA), + ), + const SizedBox(width: 8), + Text( + 'View more details on LUCI UI', + style: TextStyle( + color: isDark + ? const Color(0xFF58A6FF) + : const Color(0xFF0969DA), + fontSize: 14, + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _ChecksSidebar extends StatefulWidget { + const _ChecksSidebar({ + required this.guardResponse, + this.selectedCheck, + required this.onCheckSelected, + }); + + final PresubmitGuardResponse guardResponse; + final String? selectedCheck; + final ValueChanged onCheckSelected; + + @override + State<_ChecksSidebar> createState() => _ChecksSidebarState(); +} + +class _ChecksSidebarState extends State<_ChecksSidebar> { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Container( + width: 350, + color: theme.scaffoldBackgroundColor, + child: Column( + children: [ + Expanded( + child: Scrollbar( + controller: _scrollController, + child: ListView.builder( + controller: _scrollController, + itemCount: widget.guardResponse.stages.length, + itemBuilder: (context, stageIndex) { + final stage = widget.guardResponse.stages[stageIndex]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + color: theme.scaffoldBackgroundColor, + child: Text( + stage.name.toUpperCase(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: isDark + ? const Color(0xFF8B949E) + : const Color(0xFF6B7280), + letterSpacing: 1.2, + ), + ), + ), + ...stage.builds.entries.map((entry) { + final isSelected = widget.selectedCheck == entry.key; + return _CheckItem( + name: entry.key, + status: entry.value, + isSelected: isSelected, + onTap: () => widget.onCheckSelected(entry.key), + ); + }), + ], + ); + }, + ), + ), + ), + ], + ), + ); + } +} + +class _CheckItem extends StatelessWidget { + const _CheckItem({ + required this.name, + required this.status, + required this.isSelected, + required this.onTap, + }); + + final String name; + final TaskStatus status; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return InkWell( + onTap: onTap, + + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + + decoration: BoxDecoration( + color: isSelected + ? (isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.05)) + : Colors.transparent, + + border: Border( + left: BorderSide( + color: isSelected ? const Color(0xFF3B82F6) : Colors.transparent, + + width: 2, + ), + ), + ), + + child: Row( + children: [ + _getStatusIcon(status), + const SizedBox(width: 12), + Expanded( + child: Text( + name, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected && !isDark ? const Color(0xFF1F2937) : null, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (status == TaskStatus.failed || + status == TaskStatus.infraFailure) + TextButton( + onPressed: () {}, + child: Text( + 'Re-run', + style: TextStyle( + fontSize: 12, + color: isDark + ? const Color(0xFF58A6FF) + : const Color(0xFF0969DA), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _getStatusIcon(TaskStatus status) { + switch (status) { + case TaskStatus.succeeded: + return Icon( + Icons.check_circle_outline, + color: TaskBox.statusColor[status], + size: 18, + ); + case TaskStatus.failed: + return Icon( + Icons.error_outline, + color: TaskBox.statusColor[status], + size: 18, + ); + case TaskStatus.infraFailure: + return Icon( + Icons.error_outline, + color: TaskBox.statusColor[status], + size: 18, + ); + case TaskStatus.waitingForBackfill: + return Icon( + Icons.not_started_outlined, + color: TaskBox.statusColor[status], + size: 18, + ); + case TaskStatus.skipped: + return Icon( + Icons.do_not_disturb_on_outlined, + color: TaskBox.statusColor[status], + size: 18, + ); + case TaskStatus.cancelled: + return Icon( + Icons.block_outlined, + color: TaskBox.statusColor[status], + size: 18, + ); + case TaskStatus.inProgress: + return SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + TaskBox.statusColor[status] ?? const Color(0xFFD29922), + ), + ), + ); + } + } +} diff --git a/dashboard/lib/widgets/sha_selector.dart b/dashboard/lib/widgets/sha_selector.dart new file mode 100644 index 0000000000..5bd794be21 --- /dev/null +++ b/dashboard/lib/widgets/sha_selector.dart @@ -0,0 +1,65 @@ +// 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:flutter/material.dart'; + +/// A dropdown widget for selecting a commit SHA. +class ShaSelector extends StatelessWidget { + const ShaSelector({ + super.key, + required this.availableShas, + this.selectedSha, + required this.onShaSelected, + }); + + final List availableShas; + final String? selectedSha; + final ValueChanged onShaSelected; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final borderColor = isDark + ? const Color(0xFF333333) + : const Color(0xFFD1D5DB); + + return Container( + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(6), + color: theme.scaffoldBackgroundColor, + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + isDense: true, + value: selectedSha, + icon: const Icon(Icons.expand_more, size: 16), + style: TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + onChanged: (value) { + if (value != null) { + onShaSelected(value); + } + }, + items: availableShas.map((sha) { + return DropdownMenuItem( + value: sha, + child: Text( + sha.length > 20 ? '${sha.substring(0, 20)}...' : sha, + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + ), + ), + ); + } +} diff --git a/dashboard/merge_queue_dashboard_modal.png b/dashboard/merge_queue_dashboard_modal.png new file mode 100644 index 0000000000..35db25af90 Binary files /dev/null and b/dashboard/merge_queue_dashboard_modal.png differ diff --git a/dashboard/pubspec.yaml b/dashboard/pubspec.yaml index e857016531..416cc78869 100644 --- a/dashboard/pubspec.yaml +++ b/dashboard/pubspec.yaml @@ -17,13 +17,15 @@ dependencies: cached_network_image_platform_interface: ^4.1.1 cocoon_common: path: ../packages/cocoon_common + cocoon_service: ^0.0.0 collection: any # Match Flutter SDK firebase_auth: ^6.0.0 - firebase_core: 4.4.0 # Rolled by dependabot + firebase_core: ^4.4.0 # Rolled by dependabot firebase_crashlytics: 5.0.6 # Rolled by dependabot flutter: sdk: flutter flutter_app_icons: 0.1.1 # Rolled by dependabot + github: ^9.25.0 google_sign_in: ^7.0.0 google_sign_in_platform_interface: ^3.0.0 google_sign_in_web: ^1.0.0 @@ -39,6 +41,8 @@ dependencies: dev_dependencies: build_runner: ^2.4.15 # Rolled by dependabot + cocoon_integration_test: + path: ../packages/cocoon_integration_test dart_flutter_team_lints: 3.5.2 flutter_test: sdk: flutter diff --git a/dashboard/test/integration/update_test_suppression_test.dart b/dashboard/test/integration/update_test_suppression_test.dart new file mode 100644 index 0000000000..7db29fab1d --- /dev/null +++ b/dashboard/test/integration/update_test_suppression_test.dart @@ -0,0 +1,77 @@ +// Copyright 2026 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:cocoon_integration_test/cocoon_integration_test.dart'; +import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; +import 'package:flutter_dashboard/service/appengine_cocoon.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:github/github.dart'; + +void main() { + group('Integration: Update Test Suppression', () { + late IntegrationServer server; + late IntegrationHttpClient client; + late AppEngineCocoonService service; + + setUp(() async { + final githubService = FakeGithubService(); + githubService.getIssueMock = (slug, {issueNumber}) { + if (issueNumber == 123) { + return Future.value(Issue(state: 'open', id: 123, number: 123)); + } + return null; + }; + + server = IntegrationServer( + config: FakeConfig( + webhookKeyValue: 'fake-secret', + dynamicConfig: DynamicConfig(dynamicTestSuppression: true), + githubService: githubService, + ), + ); + + client = IntegrationHttpClient(server); + service = AppEngineCocoonService(client: client); + }); + + test('suppress and unsuppress a test', () async { + const repo = 'flutter/flutter'; + const testName = 'linux_android'; + const idToken = 'fake-token'; + + // 1. Verify initially empty + var suppressedTests = await service.fetchSuppressedTests(repo: repo); + expect(suppressedTests.data, isEmpty); + + // 2. Suppress the test + final suppressResponse = await service.updateTestSuppression( + idToken: idToken, + repo: repo, + testName: testName, + suppress: true, + issueLink: 'https://github.com/flutter/flutter/issues/123', + note: 'Flaky', + ); + expect(suppressResponse.error, isNull); + + // 3. Verify it is suppressed + suppressedTests = await service.fetchSuppressedTests(repo: repo); + expect(suppressedTests.data, hasLength(1)); + expect(suppressedTests.data!.first.name, testName); + + // 4. Unsuppress the test + final unsuppressResponse = await service.updateTestSuppression( + idToken: idToken, + repo: repo, + testName: testName, + suppress: false, + ); + expect(unsuppressResponse.error, isNull); + + // 5. Verify it is gone + suppressedTests = await service.fetchSuppressedTests(repo: repo); + expect(suppressedTests.data, isEmpty); + }); + }); +} diff --git a/dashboard/test/logic/presubmit_guard_test.dart b/dashboard/test/logic/presubmit_guard_test.dart new file mode 100644 index 0000000000..4b4d810fbd --- /dev/null +++ b/dashboard/test/logic/presubmit_guard_test.dart @@ -0,0 +1,59 @@ +// 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:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart'; +import 'package:cocoon_common/task_status.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PresubmitGuardResponse', () { + test('fromJson creates a valid object', () { + final json = { + 'pr_num': 123, + 'check_run_id': 456, + 'author': 'dash', + 'guard_status': 'In Progress', + 'stages': [ + { + 'name': 'fusion', + 'created_at': 123456789, + 'builds': {'test1': 'Succeeded', 'test2': 'In Progress'}, + }, + ], + }; + + final response = PresubmitGuardResponse.fromJson(json); + + expect(response.prNum, 123); + expect(response.checkRunId, 456); + expect(response.author, 'dash'); + expect(response.guardStatus, GuardStatus.inProgress); + expect(response.stages.length, 1); + expect(response.stages[0].name, 'fusion'); + expect(response.stages[0].builds['test1'], TaskStatus.succeeded); + expect(response.stages[0].builds['test2'], TaskStatus.inProgress); + }); + }); + + group('PresubmitCheckResponse', () { + test('fromJson creates a valid object', () { + final json = { + 'attempt_number': 1, + 'build_name': 'Linux Device Doctor', + 'creation_time': 1620134239000, + 'status': 'Succeeded', + 'summary': 'Check passed', + }; + + final response = PresubmitCheckResponse.fromJson(json); + + expect(response.attemptNumber, 1); + expect(response.buildName, 'Linux Device Doctor'); + expect(response.creationTime, 1620134239000); + expect(response.status, 'Succeeded'); + expect(response.summary, 'Check passed'); + }); + }); +} diff --git a/dashboard/test/service/appengine_cocoon_test.dart b/dashboard/test/service/appengine_cocoon_test.dart index 287b239920..4c0d5a7f1b 100644 --- a/dashboard/test/service/appengine_cocoon_test.dart +++ b/dashboard/test/service/appengine_cocoon_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:cocoon_common/rpc_model.dart'; import 'package:cocoon_common/task_status.dart'; import 'package:flutter/foundation.dart' show kIsWeb; @@ -537,4 +539,90 @@ void main() { } }); }); + + group('AppEngine CocoonService fetchMergeQueueHooks', () { + late AppEngineCocoonService service; + + test('should return list of MergeGroupHook', () async { + final expectedHooks = MergeGroupHooks( + hooks: [ + MergeGroupHook( + id: '1', + timestamp: 123, + action: 'dequeued', + headRef: 'refs/heads/main', + headCommitId: 'sha', + headCommitMessage: 'msg', + ), + ], + ); + + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response(jsonEncode(expectedHooks.toJson()), 200); + }), + ); + + final response = await service.fetchMergeQueueHooks(idToken: 'token'); + expect(response.error, isNull); + expect(response.data!.length, 1); + expect(response.data!.first.id, '1'); + }); + + test('should return error on failure', () async { + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response('Internal Server Error', 500); + }), + ); + + final response = await service.fetchMergeQueueHooks(idToken: 'token'); + expect(response.error, isNotNull); + }); + }); + + group('AppEngine CocoonService replayGitHubWebhook', () { + late AppEngineCocoonService service; + + test('should return true if request succeeds', () async { + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response('', 200); + }), + ); + + final response = await service.replayGitHubWebhook( + idToken: 'token', + id: '1', + ); + expect(response.error, isNull); + }); + + test('should set error in response if ID token is null', () async { + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response('', 200); + }), + ); + final response = await service.replayGitHubWebhook(idToken: '', id: '1'); + expect( + response.error, + allOf([isNotNull, contains('Sign in to replay events')]), + ); + }); + + test('should return error on failure', () async { + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response('Internal Server Error', 500); + }), + ); + + final response = await service.replayGitHubWebhook( + idToken: 'token', + id: '1', + ); + expect(response.error, isNotNull); + }); + }); } diff --git a/dashboard/test/service/dev_cocoon_test.dart b/dashboard/test/service/dev_cocoon_test.dart new file mode 100644 index 0000000000..232e652bed --- /dev/null +++ b/dashboard/test/service/dev_cocoon_test.dart @@ -0,0 +1,30 @@ +// 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:flutter_dashboard/service/dev_cocoon.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DevelopmentCocoonService', () { + late DevelopmentCocoonService service; + + setUp(() { + service = DevelopmentCocoonService(DateTime.now()); + }); + + test('fetchMergeQueueHooks returns empty list', () async { + final response = await service.fetchMergeQueueHooks(idToken: 'token'); + expect(response.error, isNull); + expect(response.data, isEmpty); + }); + + test('replayGitHubWebhook returns success', () async { + final response = await service.replayGitHubWebhook( + idToken: 'token', + id: '1', + ); + expect(response.error, isNull); + }); + }); +} diff --git a/dashboard/test/service/presubmit_service_test.dart b/dashboard/test/service/presubmit_service_test.dart new file mode 100644 index 0000000000..365dbae793 --- /dev/null +++ b/dashboard/test/service/presubmit_service_test.dart @@ -0,0 +1,91 @@ +// 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 'dart:convert'; + +import 'package:flutter_dashboard/service/appengine_cocoon.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' show Request, Response; +import 'package:http/testing.dart'; + +void main() { + group('AppEngine CocoonService fetchPresubmitGuard', () { + late AppEngineCocoonService service; + + test('should return expected PresubmitGuardResponse', () async { + final guardData = { + 'pr_num': 123, + 'check_run_id': 456, + 'author': 'dash', + 'guard_status': 'In Progress', + 'stages': [ + { + 'name': 'fusion', + 'created_at': 123456789, + 'builds': {'test1': 'Succeeded'}, + }, + ], + }; + + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response(jsonEncode(guardData), 200); + }), + ); + + final response = await service.fetchPresubmitGuard( + repo: 'flutter', + sha: 'abc', + ); + + expect(response.error, isNull); + expect(response.data!.prNum, 123); + expect(response.data!.stages[0].name, 'fusion'); + }); + + test('should have error if given non-200 response', () async { + service = AppEngineCocoonService( + client: MockClient((Request request) async => Response('', 404)), + ); + + final response = await service.fetchPresubmitGuard( + repo: 'flutter', + sha: 'abc', + ); + expect(response.error, isNotNull); + expect(response.statusCode, 404); + }); + }); + + group('AppEngine CocoonService fetchPresubmitCheckDetails', () { + late AppEngineCocoonService service; + + test('should return expected List', () async { + final checkData = [ + { + 'attempt_number': 1, + 'build_name': 'test1', + 'creation_time': 123456789, + 'status': 'Succeeded', + 'summary': 'Passed', + }, + ]; + + service = AppEngineCocoonService( + client: MockClient((Request request) async { + return Response(jsonEncode(checkData), 200); + }), + ); + + final response = await service.fetchPresubmitCheckDetails( + checkRunId: 456, + buildName: 'test1', + ); + + expect(response.error, isNull); + expect(response.data!.length, 1); + expect(response.data!.first.buildName, 'test1'); + }); + }); +} diff --git a/dashboard/test/utils/mocks.mocks.dart b/dashboard/test/utils/mocks.mocks.dart index 9c8b71f822..3080f9fe81 100644 --- a/dashboard/test/utils/mocks.mocks.dart +++ b/dashboard/test/utils/mocks.mocks.dart @@ -547,6 +547,94 @@ class MockCocoonService extends _i1.Mock implements _i3.CocoonService { ), ) as _i8.Future<_i3.CocoonResponse>); + + @override + _i8.Future<_i3.CocoonResponse>> + fetchMergeQueueHooks({required String? idToken}) => + (super.noSuchMethod( + Invocation.method(#fetchMergeQueueHooks, [], {#idToken: idToken}), + returnValue: + _i8.Future<_i3.CocoonResponse>>.value( + _FakeCocoonResponse_2>( + this, + Invocation.method(#fetchMergeQueueHooks, [], { + #idToken: idToken, + }), + ), + ), + ) + as _i8.Future<_i3.CocoonResponse>>); + + @override + _i8.Future<_i3.CocoonResponse> replayGitHubWebhook({ + required String? idToken, + required String? id, + }) => + (super.noSuchMethod( + Invocation.method(#replayGitHubWebhook, [], { + #idToken: idToken, + #id: id, + }), + returnValue: _i8.Future<_i3.CocoonResponse>.value( + _FakeCocoonResponse_2( + this, + Invocation.method(#replayGitHubWebhook, [], { + #idToken: idToken, + #id: id, + }), + ), + ), + ) + as _i8.Future<_i3.CocoonResponse>); + + @override + _i8.Future<_i3.CocoonResponse<_i12.PresubmitGuardResponse>> + fetchPresubmitGuard({required String? repo, required String? sha}) => + (super.noSuchMethod( + Invocation.method(#fetchPresubmitGuard, [], { + #repo: repo, + #sha: sha, + }), + returnValue: + _i8.Future< + _i3.CocoonResponse<_i12.PresubmitGuardResponse> + >.value( + _FakeCocoonResponse_2<_i12.PresubmitGuardResponse>( + this, + Invocation.method(#fetchPresubmitGuard, [], { + #repo: repo, + #sha: sha, + }), + ), + ), + ) + as _i8.Future<_i3.CocoonResponse<_i12.PresubmitGuardResponse>>); + + @override + _i8.Future<_i3.CocoonResponse>> + fetchPresubmitCheckDetails({ + required int? checkRunId, + required String? buildName, + }) => + (super.noSuchMethod( + Invocation.method(#fetchPresubmitCheckDetails, [], { + #checkRunId: checkRunId, + #buildName: buildName, + }), + returnValue: + _i8.Future< + _i3.CocoonResponse> + >.value( + _FakeCocoonResponse_2>( + this, + Invocation.method(#fetchPresubmitCheckDetails, [], { + #checkRunId: checkRunId, + #buildName: buildName, + }), + ), + ), + ) + as _i8.Future<_i3.CocoonResponse>>); } /// A class which mocks [BuildState]. diff --git a/dashboard/test/views/presubmit_view_test.dart b/dashboard/test/views/presubmit_view_test.dart new file mode 100644 index 0000000000..fe6d55f036 --- /dev/null +++ b/dashboard/test/views/presubmit_view_test.dart @@ -0,0 +1,316 @@ +// 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:cocoon_common/guard_status.dart'; +import 'package:cocoon_common/rpc_model.dart'; +import 'package:cocoon_common/task_status.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_app_icons/flutter_app_icons_platform_interface.dart'; +import 'package:flutter_dashboard/service/cocoon.dart'; +import 'package:flutter_dashboard/state/build.dart'; +import 'package:flutter_dashboard/views/presubmit_view.dart'; +import 'package:flutter_dashboard/widgets/sha_selector.dart'; +import 'package:flutter_dashboard/widgets/state_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../utils/fake_flutter_app_icons.dart'; +import '../utils/mocks.dart'; + +void main() { + late MockCocoonService mockCocoonService; + late MockFirebaseAuthService mockAuthService; + late BuildState buildState; + + setUp(() { + mockCocoonService = MockCocoonService(); + mockAuthService = MockFirebaseAuthService(); + + FlutterAppIconsPlatform.instance = FakeFlutterAppIcons(); + + when(mockAuthService.user).thenReturn(null); + when(mockAuthService.isAuthenticated).thenReturn(false); + + when( + mockCocoonService.fetchFlutterBranches(), + ).thenAnswer((_) async => const CocoonResponse.data([])); + when( + mockCocoonService.fetchRepos(), + ).thenAnswer((_) async => const CocoonResponse.data([])); + when( + mockCocoonService.fetchCommitStatuses( + branch: anyNamed('branch'), + repo: anyNamed('repo'), + ), + ).thenAnswer((_) async => const CocoonResponse.data([])); + when( + mockCocoonService.fetchTreeBuildStatus( + branch: anyNamed('branch'), + repo: anyNamed('repo'), + ), + ).thenAnswer( + (_) async => CocoonResponse.data( + BuildStatusResponse(buildStatus: BuildStatus.success, failingTasks: []), + ), + ); + when( + mockCocoonService.fetchSuppressedTests(repo: anyNamed('repo')), + ).thenAnswer((_) async => const CocoonResponse.data([])); + + when( + mockCocoonService.fetchPresubmitGuard( + repo: anyNamed('repo'), + sha: anyNamed('sha'), + ), + ).thenAnswer( + (_) async => const CocoonResponse.error('Not found', statusCode: 404), + ); + + buildState = BuildState( + cocoonService: mockCocoonService, + authService: mockAuthService, + ); + }); + + Widget createPreSubmitView(Map queryParameters) { + return Material( + child: StateProvider( + buildState: buildState, + signInService: mockAuthService, + child: MaterialApp( + home: PreSubmitView(queryParameters: queryParameters), + ), + ), + ); + } + + testWidgets( + 'PreSubmitView displays correct title and status with repo and sha', + (WidgetTester tester) async { + tester.view.physicalSize = const Size(2000, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const guardResponse = PresubmitGuardResponse( + prNum: 123, + checkRunId: 456, + author: 'dash', + stages: [ + PresubmitGuardStage( + name: 'Engine', + createdAt: 0, + builds: {'Mac mac_host_engine': TaskStatus.succeeded}, + ), + ], + guardStatus: GuardStatus.succeeded, + ); + + when( + mockCocoonService.fetchPresubmitGuard(repo: 'flutter', sha: 'abc'), + ).thenAnswer((_) async => const CocoonResponse.data(guardResponse)); + + await tester.pumpWidget( + createPreSubmitView({'repo': 'flutter', 'sha': 'abc'}), + ); + await tester.pump(); + await tester.pump(); + + expect(find.textContaining('PR #123'), findsOneWidget); + expect(find.textContaining('[dash]'), findsOneWidget); + expect(find.text('Succeeded'), findsAtLeastNWidgets(1)); + expect(find.text('ENGINE'), findsOneWidget); + expect(find.textContaining('mac_host_engine'), findsOneWidget); + }, + ); + + testWidgets('PreSubmitView displays mocked data and switches tabs', ( + WidgetTester tester, + ) async { + tester.view.physicalSize = const Size(2000, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const mockSha = 'mock_sha_1_long_hash_value'; + const guardResponse = PresubmitGuardResponse( + prNum: 123, + checkRunId: 456, + author: 'dash', + stages: [ + PresubmitGuardStage( + name: 'Engine', + createdAt: 0, + builds: {'Mac mac_host_engine 1': TaskStatus.failed}, + ), + ], + guardStatus: GuardStatus.inProgress, + ); + + when( + mockCocoonService.fetchPresubmitGuard(repo: 'flutter', sha: mockSha), + ).thenAnswer((_) async => const CocoonResponse.data(guardResponse)); + + await tester.pumpWidget( + createPreSubmitView({'repo': 'flutter', 'pr': '123'}), + ); + await tester.pump(); + await tester.pump(); + + expect(find.textContaining('PR #123'), findsOneWidget); + + // Select a check + // The check name in mock data is 'Mac mac_host_engine 1' (suffix is from mock_sha_1) + final checkName = 'mac_host_engine 1'; + + // Stub the details fetch for the mock check + when( + mockCocoonService.fetchPresubmitCheckDetails( + checkRunId: anyNamed('checkRunId'), + buildName: argThat(contains('mac_host_engine'), named: 'buildName'), + ), + ).thenAnswer( + (_) async => CocoonResponse.data([ + PresubmitCheckResponse( + attemptNumber: 1, + buildName: checkName, + creationTime: 0, + status: 'Succeeded', + summary: 'All tests passed (452/452)', + ), + PresubmitCheckResponse( + attemptNumber: 2, + buildName: checkName, + creationTime: 0, + status: 'Failed', + summary: 'Test failed: Unit Tests', + ), + ]), + ); + + await tester.tap(find.textContaining(checkName).first); + await tester.pump(); + await tester.pump(); + + // Verify log for attempt #1 + expect(find.textContaining('All tests passed (452/452)'), findsOneWidget); + expect(find.textContaining('Status: Succeeded'), findsOneWidget); + + // Switch to attempt #2 + await tester.tap(find.text('#2')); + await tester.pump(); + + expect(find.textContaining('Test failed: Unit Tests'), findsOneWidget); + expect(find.textContaining('Status: Failed'), findsOneWidget); + }); + + testWidgets('PreSubmitView SHA dropdown switches mock SHAs', ( + WidgetTester tester, + ) async { + tester.view.physicalSize = const Size(2000, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + createPreSubmitView({'repo': 'flutter', 'pr': '123'}), + ); + await tester.pump(); + await tester.pump(); + + // Find ShaSelector widget + expect(find.byType(ShaSelector), findsOneWidget); + + // Tap the dropdown to open it + await tester.tap(find.byType(ShaSelector)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + // Select the second item in the dropdown menu (mock_sha_2...) + await tester.tap(find.textContaining('mock_sha_2').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(find.byType(ShaSelector), findsOneWidget); + expect(find.textContaining('mock_sha_2'), findsOneWidget); + expect(find.text('Re-run failed'), findsOneWidget); + }); + + testWidgets('PreSubmitView functional sha route fetches check details', ( + WidgetTester tester, + ) async { + tester.view.physicalSize = const Size(2000, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + const guardResponse = PresubmitGuardResponse( + prNum: 123, + checkRunId: 456, + author: 'dash', + stages: [ + PresubmitGuardStage( + name: 'Engine', + createdAt: 0, + builds: {'Mac mac_host_engine': TaskStatus.succeeded}, + ), + ], + guardStatus: GuardStatus.succeeded, + ); + + when( + mockCocoonService.fetchPresubmitGuard(repo: 'flutter', sha: 'abc'), + ).thenAnswer((_) async => const CocoonResponse.data(guardResponse)); + + when( + mockCocoonService.fetchPresubmitCheckDetails( + checkRunId: 456, + buildName: 'Mac mac_host_engine', + ), + ).thenAnswer( + (_) async => CocoonResponse.data([ + PresubmitCheckResponse( + attemptNumber: 1, + buildName: 'Mac mac_host_engine', + creationTime: 0, + status: 'Succeeded', + summary: 'Live log content', + ), + ]), + ); + + await tester.pumpWidget( + createPreSubmitView({'repo': 'flutter', 'sha': 'abc'}), + ); + await tester.pump(); + await tester.pump(); + + await tester.tap(find.textContaining('mac_host_engine')); + await tester.pump(); + await tester.pump(); + + expect(find.text('Live log content'), findsOneWidget); + }); + + testWidgets('PreSubmitView meets accessibility guidelines', ( + WidgetTester tester, + ) async { + final handle = tester.ensureSemantics(); + tester.view.physicalSize = const Size(2000, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + createPreSubmitView({'repo': 'flutter', 'pr': '123'}), + ); + await tester.pump(); + await tester.pump(); + + // Verify text contrast + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); +} diff --git a/packages/cocoon_common/lib/guard_status.dart b/packages/cocoon_common/lib/guard_status.dart index 5f4c453330..af3998c5e5 100644 --- a/packages/cocoon_common/lib/guard_status.dart +++ b/packages/cocoon_common/lib/guard_status.dart @@ -26,4 +26,21 @@ enum GuardStatus { /// Returns the JSON representation of `this`. Object? toJson() => value; + + /// Calculates the [GuardStatus] based on build counts. + static GuardStatus calculate({ + required int failedBuilds, + required int remainingBuilds, + required int totalBuilds, + }) { + if (failedBuilds > 0) { + return GuardStatus.failed; + } else if (failedBuilds == 0 && remainingBuilds == 0) { + return GuardStatus.succeeded; + } else if (remainingBuilds == totalBuilds) { + return GuardStatus.waitingForBackfill; + } else { + return GuardStatus.inProgress; + } + } } diff --git a/packages/cocoon_common/lib/rpc_model.dart b/packages/cocoon_common/lib/rpc_model.dart index 9de70abc0a..99bcad1e99 100644 --- a/packages/cocoon_common/lib/rpc_model.dart +++ b/packages/cocoon_common/lib/rpc_model.dart @@ -38,6 +38,7 @@ export 'src/rpc_model/presubmit_check_response.dart' show PresubmitCheckResponse; export 'src/rpc_model/presubmit_guard.dart' show PresubmitGuardResponse, PresubmitGuardStage; +export 'src/rpc_model/presubmit_guard_summary.dart' show PresubmitGuardSummary; export 'src/rpc_model/suppressed_test.dart' show SuppressedTest, SuppressionUpdate; export 'src/rpc_model/task.dart' show Task; diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.dart new file mode 100644 index 0000000000..1b6a54fc59 --- /dev/null +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.dart @@ -0,0 +1,37 @@ +// Copyright 2026 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:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import '../../guard_status.dart'; + +part 'presubmit_guard_summary.g.dart'; + +/// Represents a summary of presubmit guard for a specific commit. +@immutable +@JsonSerializable(fieldRename: FieldRename.snake) +final class PresubmitGuardSummary { + const PresubmitGuardSummary({ + required this.commitSha, + required this.creationTime, + required this.guardStatus, + }); + + /// The commit SHA. + final String commitSha; + + /// The creation timestamp in microseconds since epoch. + final int creationTime; + + /// The status of the guard. + final GuardStatus guardStatus; + + /// Creates a [PresubmitGuardSummary] from a JSON map. + factory PresubmitGuardSummary.fromJson(Map json) => + _$PresubmitGuardSummaryFromJson(json); + + /// Converts this [PresubmitGuardSummary] to a JSON map. + Map toJson() => _$PresubmitGuardSummaryToJson(this); +} diff --git a/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.g.dart b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.g.dart new file mode 100644 index 0000000000..d4acbe96d7 --- /dev/null +++ b/packages/cocoon_common/lib/src/rpc_model/presubmit_guard_summary.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'presubmit_guard_summary.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PresubmitGuardSummary _$PresubmitGuardSummaryFromJson( + Map json, +) => PresubmitGuardSummary( + commitSha: json['commit_sha'] as String, + creationTime: (json['creation_time'] as num).toInt(), + guardStatus: $enumDecode(_$GuardStatusEnumMap, json['guard_status']), +); + +Map _$PresubmitGuardSummaryToJson( + PresubmitGuardSummary instance, +) => { + 'commit_sha': instance.commitSha, + 'creation_time': instance.creationTime, + 'guard_status': instance.guardStatus, +}; + +const _$GuardStatusEnumMap = { + GuardStatus.waitingForBackfill: 'New', + GuardStatus.inProgress: 'In Progress', + GuardStatus.failed: 'Failed', + GuardStatus.succeeded: 'Succeeded', +}; diff --git a/packages/cocoon_common/lib/task_status.dart b/packages/cocoon_common/lib/task_status.dart index 993f5f2dba..36ff18a5d7 100644 --- a/packages/cocoon_common/lib/task_status.dart +++ b/packages/cocoon_common/lib/task_status.dart @@ -30,8 +30,7 @@ enum TaskStatus { /// The task was skipped instead of being executed. skipped('Skipped'); - const TaskStatus(this._schemaValue); - final String _schemaValue; + const TaskStatus(this.value); /// Returns the status represented by the provided [value]. /// @@ -50,7 +49,7 @@ enum TaskStatus { /// The canonical string value representing `this`. /// /// This is the inverse of [TaskStatus.from] or [TaskStatus.tryFrom]. - String get value => _schemaValue; + final String value; /// Whether the status represents a completed task reaching a terminal state. bool get isComplete => _complete.contains(this); @@ -87,8 +86,8 @@ enum TaskStatus { bool get isBuildCompleted => isBuildSuccessed || isBuildFailed; /// Returns the JSON representation of `this`. - Object? toJson() => _schemaValue; + Object? toJson() => value; @override - String toString() => _schemaValue; + String toString() => value; } diff --git a/packages/cocoon_common/test/guard_status_test.dart b/packages/cocoon_common/test/guard_status_test.dart new file mode 100644 index 0000000000..b7d10e7a9b --- /dev/null +++ b/packages/cocoon_common/test/guard_status_test.dart @@ -0,0 +1,54 @@ +// Copyright 2026 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:cocoon_common/guard_status.dart'; +import 'package:test/test.dart'; + +void main() { + group('GuardStatus.calculate', () { + test('returns failed if there are failed builds', () { + expect( + GuardStatus.calculate( + failedBuilds: 1, + remainingBuilds: 0, + totalBuilds: 10, + ), + GuardStatus.failed, + ); + }); + + test('returns succeeded if no failures and no remaining builds', () { + expect( + GuardStatus.calculate( + failedBuilds: 0, + remainingBuilds: 0, + totalBuilds: 10, + ), + GuardStatus.succeeded, + ); + }); + + test('returns waitingForBackfill if all builds are remaining', () { + expect( + GuardStatus.calculate( + failedBuilds: 0, + remainingBuilds: 10, + totalBuilds: 10, + ), + GuardStatus.waitingForBackfill, + ); + }); + + test('returns inProgress if some builds are done and no failures yet', () { + expect( + GuardStatus.calculate( + failedBuilds: 0, + remainingBuilds: 5, + totalBuilds: 10, + ), + GuardStatus.inProgress, + ); + }); + }); +} diff --git a/packages/cocoon_integration_test/.gitignore b/packages/cocoon_integration_test/.gitignore new file mode 100644 index 0000000000..3cceda5578 --- /dev/null +++ b/packages/cocoon_integration_test/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/cocoon_integration_test/CHANGELOG.md b/packages/cocoon_integration_test/CHANGELOG.md new file mode 100644 index 0000000000..effe43c82c --- /dev/null +++ b/packages/cocoon_integration_test/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/cocoon_integration_test/README.md b/packages/cocoon_integration_test/README.md new file mode 100644 index 0000000000..8831761b89 --- /dev/null +++ b/packages/cocoon_integration_test/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/cocoon_integration_test/analysis_options.yaml b/packages/cocoon_integration_test/analysis_options.yaml new file mode 100644 index 0000000000..6ea6759dd7 --- /dev/null +++ b/packages/cocoon_integration_test/analysis_options.yaml @@ -0,0 +1,8 @@ +include: ../../analysis_options.yaml + +analyzer: + errors: + duplicate_ignore: ignore + implementation_imports: ignore + invalid_use_of_internal_member: ignore + invalid_use_of_visible_for_testing_member: ignore \ No newline at end of file diff --git a/packages/cocoon_integration_test/build.yaml b/packages/cocoon_integration_test/build.yaml new file mode 100644 index 0000000000..4b55d7f6f7 --- /dev/null +++ b/packages/cocoon_integration_test/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + mockito:mockBuilder: + generate_for: + - lib/src/utilities/mocks.dart diff --git a/packages/cocoon_integration_test/lib/cocoon_integration_test.dart b/packages/cocoon_integration_test/lib/cocoon_integration_test.dart new file mode 100644 index 0000000000..347125dbf6 --- /dev/null +++ b/packages/cocoon_integration_test/lib/cocoon_integration_test.dart @@ -0,0 +1,6 @@ +// Copyright 2026 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. + +export 'src/server.dart'; +export 'testing.dart'; diff --git a/app_dart/test/src/delegate_matcher.dart b/packages/cocoon_integration_test/lib/src/delegate_matcher.dart similarity index 100% rename from app_dart/test/src/delegate_matcher.dart rename to packages/cocoon_integration_test/lib/src/delegate_matcher.dart diff --git a/app_dart/test/src/service/fake_auth_client.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_auth_client.dart similarity index 100% rename from app_dart/test/src/service/fake_auth_client.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_auth_client.dart diff --git a/packages/cocoon_integration_test/lib/src/fakes/fake_big_query_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_big_query_service.dart new file mode 100644 index 0000000000..eed8f419fd --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_big_query_service.dart @@ -0,0 +1,13 @@ +// Copyright 2020 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:cocoon_server_test/mocks.dart'; +import 'package:cocoon_service/src/service/big_query.dart'; + +import 'fake_tabledata_resource.dart'; + +class FakeBigQueryService extends BigQueryService { + FakeBigQueryService() + : super.forTesting(FakeTabledataResource(), MockJobsResource()); +} diff --git a/app_dart/test/src/service/fake_build_bucket_client.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_build_bucket_client.dart similarity index 100% rename from app_dart/test/src/service/fake_build_bucket_client.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_build_bucket_client.dart diff --git a/app_dart/test/src/service/fake_build_status_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_build_status_service.dart similarity index 100% rename from app_dart/test/src/service/fake_build_status_service.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_build_status_service.dart diff --git a/packages/cocoon_integration_test/lib/src/fakes/fake_cache_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_cache_service.dart new file mode 100644 index 0000000000..d8c8f23e36 --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_cache_service.dart @@ -0,0 +1,55 @@ +// Copyright 2026 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 'dart:typed_data'; + +import 'package:cocoon_service/src/service/cache_service.dart'; + +/// A [CacheService] that doesn't actually cache anything. +class FakeCacheService extends CacheService { + FakeCacheService() : super(inMemory: true); + + @override + Future getOrCreate( + String subcacheName, + String key, { + required Future Function()? createFn, + Duration ttl = const Duration(minutes: 1), + }) async { + return createFn?.call(); + } + + @override + Future getOrCreateWithLocking( + String subcacheName, + String key, { + required Future Function()? createFn, + Duration ttl = const Duration(minutes: 1), + }) async { + return createFn?.call(); + } + + @override + Future set( + String subcacheName, + String key, + Uint8List? value, { + Duration ttl = const Duration(minutes: 1), + }) async { + return value; + } + + @override + Future setWithLocking( + String subcacheName, + String key, + Uint8List? value, { + Duration ttl = const Duration(minutes: 1), + }) async { + return value; + } + + @override + Future purge(String subcacheName, String key) async {} +} diff --git a/app_dart/test/src/service/fake_ci_yaml_fetcher.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_ci_yaml_fetcher.dart similarity index 100% rename from app_dart/test/src/service/fake_ci_yaml_fetcher.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_ci_yaml_fetcher.dart diff --git a/app_dart/test/src/fake_config.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_config.dart similarity index 99% rename from app_dart/test/src/fake_config.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_config.dart index cb6fa73efe..c3dca0682f 100644 --- a/app_dart/test/src/fake_config.dart +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_config.dart @@ -11,7 +11,7 @@ import 'package:cocoon_service/src/service/luci_build_service/cipd_version.dart' import 'package:github/github.dart' as gh; import 'package:graphql/client.dart'; -import 'service/fake_github_service.dart'; +import 'fake_github_service.dart'; // ignore: must_be_immutable // TODO(matanlurey): Make this *not* a mess. See https://github.com/flutter/flutter/issues/164646. diff --git a/app_dart/test/src/service/fake_content_aware_hash_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_content_aware_hash_service.dart similarity index 100% rename from app_dart/test/src/service/fake_content_aware_hash_service.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_content_aware_hash_service.dart diff --git a/app_dart/test/src/request_handling/fake_dashboard_authentication.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_dashboard_authentication.dart similarity index 55% rename from app_dart/test/src/request_handling/fake_dashboard_authentication.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_dashboard_authentication.dart index 501bab52b5..38d8721273 100644 --- a/app_dart/test/src/request_handling/fake_dashboard_authentication.dart +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_dashboard_authentication.dart @@ -4,9 +4,7 @@ import 'dart:io'; -import 'package:appengine/appengine.dart'; -import 'package:cocoon_service/src/request_handling/authentication.dart'; -import 'package:cocoon_service/src/request_handling/dashboard_authentication.dart'; +import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; // ignore: must_be_immutable @@ -42,50 +40,8 @@ class FakeAuthenticatedContext implements AuthenticatedContext { } class FakeClientContext implements ClientContext { - FakeClientContext({ - this.isDevelopmentEnvironment = true, - this.isProductionEnvironment = false, - FakeAppEngineContext? applicationContext, - }) : applicationContext = applicationContext ?? FakeAppEngineContext(); - - @override - FakeAppEngineContext applicationContext; + FakeClientContext({this.isDevelopmentEnvironment = true}); @override bool isDevelopmentEnvironment; - - @override - bool isProductionEnvironment; - - @override - late Services services; - - @override - String? traceId; -} - -class FakeAppEngineContext implements AppEngineContext { - @override - String applicationID = 'flutter-dashboard'; - - @override - late String fullQualifiedApplicationId; - - @override - late String instance; - - @override - String? instanceId; - - @override - late bool isDevelopmentEnvironment; - - @override - late String module; - - @override - String partition = '[default]'; - - @override - late String version; } diff --git a/packages/cocoon_integration_test/lib/src/fakes/fake_entry.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_entry.dart new file mode 100644 index 0000000000..600d06f63f --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_entry.dart @@ -0,0 +1,28 @@ +// 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 'dart:async'; +import 'dart:typed_data'; + +import 'package:neat_cache/neat_cache.dart'; + +class FakeEntry extends Entry { + Uint8List value = Uint8List.fromList('abc123'.codeUnits); + + @override + Future get([ + Future Function()? create, + Duration? ttl, + ]) async => value; + + @override + Future purge({int retries = 0}) => throw UnimplementedError(); + + @override + Future set(Uint8List? value, [Duration? ttl]) async { + value = value; + + return value; + } +} diff --git a/app_dart/test/src/service/fake_firebase_jwt_validator.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_firebase_jwt_validator.dart similarity index 100% rename from app_dart/test/src/service/fake_firebase_jwt_validator.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_firebase_jwt_validator.dart diff --git a/app_dart/test/src/service/fake_firestore_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_firestore_service.dart similarity index 100% rename from app_dart/test/src/service/fake_firestore_service.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_firestore_service.dart diff --git a/app_dart/test/src/service/fake_gerrit_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_gerrit_service.dart similarity index 100% rename from app_dart/test/src/service/fake_gerrit_service.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_gerrit_service.dart diff --git a/app_dart/test/src/service/fake_get_files_changed.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_get_files_changed.dart similarity index 100% rename from app_dart/test/src/service/fake_get_files_changed.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_get_files_changed.dart diff --git a/app_dart/test/src/service/fake_github_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_github_service.dart similarity index 97% rename from app_dart/test/src/service/fake_github_service.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_github_service.dart index cc4128d2bd..bc76297603 100644 --- a/app_dart/test/src/service/fake_github_service.dart +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_github_service.dart @@ -143,8 +143,13 @@ class FakeGithubService implements GithubService { return []; } + Future? Function(RepositorySlug, {int? issueNumber})? getIssueMock; + @override Future? getIssue(RepositorySlug slug, {int? issueNumber}) { + if (getIssueMock != null) { + return getIssueMock!(slug, issueNumber: issueNumber); + } return null; } diff --git a/app_dart/test/src/service/fake_graphql_client.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_graphql_client.dart similarity index 100% rename from app_dart/test/src/service/fake_graphql_client.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_graphql_client.dart diff --git a/app_dart/test/src/request_handling/fake_http.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_http.dart similarity index 100% rename from app_dart/test/src/request_handling/fake_http.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_http.dart diff --git a/app_dart/test/src/service/fake_luci_build_service.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_luci_build_service.dart similarity index 96% rename from app_dart/test/src/service/fake_luci_build_service.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_luci_build_service.dart index 50b87046dc..9c6e628ae6 100644 --- a/app_dart/test/src/service/fake_luci_build_service.dart +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_luci_build_service.dart @@ -9,10 +9,10 @@ import 'package:cocoon_service/src/service/cache_service.dart'; import 'package:cocoon_service/src/service/gerrit_service.dart'; import 'package:cocoon_service/src/service/luci_build_service.dart'; -import '../request_handling/fake_pubsub.dart'; import '../utilities/mocks.dart'; import 'fake_build_bucket_client.dart'; import 'fake_gerrit_service.dart'; +import 'fake_pubsub.dart'; /// Fake [LuciBuildService] for use in tests. class FakeLuciBuildService extends LuciBuildService { diff --git a/app_dart/test/src/request_handling/fake_pubsub.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_pubsub.dart similarity index 89% rename from app_dart/test/src/request_handling/fake_pubsub.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_pubsub.dart index 1810a74193..8d44afd32a 100644 --- a/app_dart/test/src/request_handling/fake_pubsub.dart +++ b/packages/cocoon_integration_test/lib/src/fakes/fake_pubsub.dart @@ -11,7 +11,7 @@ class FakePubSub extends PubSub { int exceptionRepetition = 1; @override - Future> publish(String topicName, dynamic json) async { + Future> publish(String topic, dynamic json) async { if (exceptionFlag && exceptionRepetition > 0) { exceptionRepetition--; throw DetailedApiRequestError(500, 'test api error'); diff --git a/app_dart/test/src/request_handling/fake_request_handler.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_request_handler.dart similarity index 100% rename from app_dart/test/src/request_handling/fake_request_handler.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_request_handler.dart diff --git a/app_dart/test/src/service/fake_scheduler.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_scheduler.dart similarity index 100% rename from app_dart/test/src/service/fake_scheduler.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_scheduler.dart diff --git a/app_dart/test/src/bigquery/fake_tabledata_resource.dart b/packages/cocoon_integration_test/lib/src/fakes/fake_tabledata_resource.dart similarity index 100% rename from app_dart/test/src/bigquery/fake_tabledata_resource.dart rename to packages/cocoon_integration_test/lib/src/fakes/fake_tabledata_resource.dart diff --git a/packages/cocoon_integration_test/lib/src/integration_http_client.dart b/packages/cocoon_integration_test/lib/src/integration_http_client.dart new file mode 100644 index 0000000000..cf0373f31c --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/integration_http_client.dart @@ -0,0 +1,49 @@ +// Copyright 2026 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 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../testing.dart'; +import 'server.dart'; + +class IntegrationHttpClient extends http.BaseClient { + IntegrationHttpClient(this.server); + + final IntegrationServer server; + + @override + Future send(http.BaseRequest request) async { + final bodyBytes = await request.finalize().toBytes(); + final body = utf8.decode(bodyBytes); + + final fakeResponse = FakeHttpResponse(); + final fakeRequest = FakeHttpRequest( + method: request.method, + body: body, + path: request.url.path, + queryParametersValue: request.url.queryParameters, + response: fakeResponse, + ); + + request.headers.forEach((key, value) { + fakeRequest.headers.add(key, value); + }); + + await server.server(fakeRequest); + + final responseHeaders = {}; + fakeResponse.headers.forEach((name, values) { + responseHeaders[name] = values.join(','); + }); + + return http.StreamedResponse( + Stream.value(utf8.encode(fakeResponse.body)), + fakeResponse.statusCode, + contentLength: fakeResponse.body.length, + headers: responseHeaders, + ); + } +} diff --git a/app_dart/test/src/model/_build_status_snapshot.dart b/packages/cocoon_integration_test/lib/src/model/_build_status_snapshot.dart similarity index 100% rename from app_dart/test/src/model/_build_status_snapshot.dart rename to packages/cocoon_integration_test/lib/src/model/_build_status_snapshot.dart diff --git a/app_dart/test/src/model/_ci_staging.dart b/packages/cocoon_integration_test/lib/src/model/_ci_staging.dart similarity index 100% rename from app_dart/test/src/model/_ci_staging.dart rename to packages/cocoon_integration_test/lib/src/model/_ci_staging.dart diff --git a/app_dart/test/src/model/_commit.dart b/packages/cocoon_integration_test/lib/src/model/_commit.dart similarity index 100% rename from app_dart/test/src/model/_commit.dart rename to packages/cocoon_integration_test/lib/src/model/_commit.dart diff --git a/app_dart/test/src/model/_content_aware_hash_builds.dart b/packages/cocoon_integration_test/lib/src/model/_content_aware_hash_builds.dart similarity index 100% rename from app_dart/test/src/model/_content_aware_hash_builds.dart rename to packages/cocoon_integration_test/lib/src/model/_content_aware_hash_builds.dart diff --git a/app_dart/test/src/model/_github_build_status.dart b/packages/cocoon_integration_test/lib/src/model/_github_build_status.dart similarity index 100% rename from app_dart/test/src/model/_github_build_status.dart rename to packages/cocoon_integration_test/lib/src/model/_github_build_status.dart diff --git a/app_dart/test/src/model/_github_gold_status.dart b/packages/cocoon_integration_test/lib/src/model/_github_gold_status.dart similarity index 100% rename from app_dart/test/src/model/_github_gold_status.dart rename to packages/cocoon_integration_test/lib/src/model/_github_gold_status.dart diff --git a/app_dart/test/src/model/_github_webhook_message.dart b/packages/cocoon_integration_test/lib/src/model/_github_webhook_message.dart similarity index 100% rename from app_dart/test/src/model/_github_webhook_message.dart rename to packages/cocoon_integration_test/lib/src/model/_github_webhook_message.dart diff --git a/app_dart/test/src/model/_pr_check_run.dart b/packages/cocoon_integration_test/lib/src/model/_pr_check_run.dart similarity index 100% rename from app_dart/test/src/model/_pr_check_run.dart rename to packages/cocoon_integration_test/lib/src/model/_pr_check_run.dart diff --git a/app_dart/test/src/model/_suppressed_test_.dart b/packages/cocoon_integration_test/lib/src/model/_suppressed_test_.dart similarity index 100% rename from app_dart/test/src/model/_suppressed_test_.dart rename to packages/cocoon_integration_test/lib/src/model/_suppressed_test_.dart diff --git a/app_dart/test/src/model/_task.dart b/packages/cocoon_integration_test/lib/src/model/_task.dart similarity index 100% rename from app_dart/test/src/model/_task.dart rename to packages/cocoon_integration_test/lib/src/model/_task.dart diff --git a/app_dart/test/src/model/_tree_status_change.dart b/packages/cocoon_integration_test/lib/src/model/_tree_status_change.dart similarity index 100% rename from app_dart/test/src/model/_tree_status_change.dart rename to packages/cocoon_integration_test/lib/src/model/_tree_status_change.dart diff --git a/app_dart/test/src/model/check_run_matcher.dart b/packages/cocoon_integration_test/lib/src/model/check_run_matcher.dart similarity index 95% rename from app_dart/test/src/model/check_run_matcher.dart rename to packages/cocoon_integration_test/lib/src/model/check_run_matcher.dart index 9a88796ad2..2a65ec9be9 100644 --- a/app_dart/test/src/model/check_run_matcher.dart +++ b/packages/cocoon_integration_test/lib/src/model/check_run_matcher.dart @@ -5,7 +5,7 @@ import 'package:github/github.dart'; import 'package:test/test.dart'; -const isTarget = CheckRunMatcher._(TypeMatcher()); +const isCheckRun = CheckRunMatcher._(TypeMatcher()); final class CheckRunMatcher extends Matcher { const CheckRunMatcher._(this._delegate); diff --git a/app_dart/test/src/model/ci_yaml_matcher.dart b/packages/cocoon_integration_test/lib/src/model/ci_yaml_matcher.dart similarity index 100% rename from app_dart/test/src/model/ci_yaml_matcher.dart rename to packages/cocoon_integration_test/lib/src/model/ci_yaml_matcher.dart diff --git a/app_dart/test/src/model/firestore_matcher.dart b/packages/cocoon_integration_test/lib/src/model/firestore_matcher.dart similarity index 92% rename from app_dart/test/src/model/firestore_matcher.dart rename to packages/cocoon_integration_test/lib/src/model/firestore_matcher.dart index e9034609ad..bdca4a4a3d 100644 --- a/app_dart/test/src/model/firestore_matcher.dart +++ b/packages/cocoon_integration_test/lib/src/model/firestore_matcher.dart @@ -117,23 +117,30 @@ abstract final class ModelMatcher> extends Matcher { @override @nonVirtual - Description describeMismatch(Object? item, Description description, _, _) { + Description describeMismatch( + Object? item, + Description mismatchDescription, + _, + _, + ) { // Not a document and not the wrapped type. if (item is! g.Document) { - return description.add('not a Document'); + return mismatchDescription.add('not a Document'); } // Not a saved document (does not have a name). if (item.name == null) { - return description.add('not a saved Document (missing "name")'); + return mismatchDescription.add('not a saved Document (missing "name")'); } // Not a document of the expected type. if (!_isPathTo(item.name!, metadata)) { final collection = p.posix.basename(p.posix.dirname(item.name!)); - return description.add('not a $T, belongs to collection "$collection"'); + return mismatchDescription.add( + 'not a $T, belongs to collection "$collection"', + ); } - return _delegate.describeMismatch(item, description, {}, false); + return _delegate.describeMismatch(item, mismatchDescription, {}, false); } } diff --git a/app_dart/test/src/model/ref_matcher.dart b/packages/cocoon_integration_test/lib/src/model/ref_matcher.dart similarity index 100% rename from app_dart/test/src/model/ref_matcher.dart rename to packages/cocoon_integration_test/lib/src/model/ref_matcher.dart diff --git a/packages/cocoon_integration_test/lib/src/server.dart b/packages/cocoon_integration_test/lib/src/server.dart new file mode 100644 index 0000000000..db03e8fa8d --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/server.dart @@ -0,0 +1,92 @@ +// Copyright 2026 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:cocoon_service/cocoon_service.dart'; +import 'package:cocoon_service/server.dart'; +import 'package:cocoon_service/src/service/commit_service.dart'; + +import '../testing.dart'; + +class IntegrationServer { + IntegrationServer({ + FakeConfig? config, + FakeFirestoreService? firestore, + FakeBigQueryService? bigQuery, + FakeDashboardAuthentication? authProvider, + FakeDashboardAuthentication? swarmingAuthProvider, + FakeGerritService? gerritService, + FakeBuildBucketClient? buildBucketClient, + FakeLuciBuildService? luciBuildService, + FakeScheduler? scheduler, + FakeCiYamlFetcher? ciYamlFetcher, + FakeBuildStatusService? buildStatusService, + FakeContentAwareHashService? contentAwareHashService, + CacheService? cache, + }) { + this.config = config ?? FakeConfig(webhookKeyValue: 'fake-secret'); + this.firestore = firestore ?? FakeFirestoreService(); + this.bigQuery = bigQuery ?? FakeBigQueryService(); + this.authProvider = authProvider ?? FakeDashboardAuthentication(); + this.swarmingAuthProvider = + swarmingAuthProvider ?? FakeDashboardAuthentication(); + this.gerritService = gerritService ?? FakeGerritService(); + this.buildBucketClient = buildBucketClient ?? FakeBuildBucketClient(); + this.luciBuildService = + luciBuildService ?? + FakeLuciBuildService(config: this.config, firestore: this.firestore); + this.scheduler = + scheduler ?? + FakeScheduler( + config: this.config, + firestore: this.firestore, + bigQuery: this.bigQuery, + ); + this.ciYamlFetcher = ciYamlFetcher ?? FakeCiYamlFetcher(); + this.buildStatusService = buildStatusService ?? FakeBuildStatusService(); + this.contentAwareHashService = + contentAwareHashService ?? + FakeContentAwareHashService(config: this.config); + this.cache = cache ?? FakeCacheService(); + + server = createServer( + config: this.config, + firestore: this.firestore, + bigQuery: this.bigQuery, + cache: this.cache, + authProvider: this.authProvider, + swarmingAuthProvider: this.swarmingAuthProvider, + branchService: BranchService( + config: this.config, + gerritService: this.gerritService, + ), + buildBucketClient: this.buildBucketClient, + luciBuildService: this.luciBuildService, + githubChecksService: GithubChecksService(this.config), + commitService: CommitService( + config: this.config, + firestore: this.firestore, + ), + gerritService: this.gerritService, + scheduler: this.scheduler, + ciYamlFetcher: this.ciYamlFetcher, + buildStatusService: this.buildStatusService, + contentAwareHashService: this.contentAwareHashService, + ); + } + + late final Server server; + late final FakeConfig config; + late final FakeFirestoreService firestore; + late final FakeBigQueryService bigQuery; + late final FakeDashboardAuthentication authProvider; + late final FakeDashboardAuthentication swarmingAuthProvider; + late final FakeGerritService gerritService; + late final FakeBuildBucketClient buildBucketClient; + late final FakeLuciBuildService luciBuildService; + late final FakeScheduler scheduler; + late final FakeCiYamlFetcher ciYamlFetcher; + late final FakeBuildStatusService buildStatusService; + late final FakeContentAwareHashService contentAwareHashService; + late final CacheService cache; +} diff --git a/packages/cocoon_integration_test/lib/src/utilities/build_bucket_messages.dart b/packages/cocoon_integration_test/lib/src/utilities/build_bucket_messages.dart new file mode 100644 index 0000000000..aa24202304 --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/utilities/build_bucket_messages.dart @@ -0,0 +1,720 @@ +// Copyright 2024 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 'dart:convert'; + +import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:cocoon_service/src/model/luci/pubsub_message.dart'; +import 'package:cocoon_service/src/service/luci_build_service/user_data.dart'; +import 'package:fixnum/fixnum.dart'; + +PushMessage createPushMessage( + Int64 id, { + required BuildBucketUserData userData, + String? project = 'flutter', + String? bucket = 'try', + String? builder = 'Windows Engine Drone', + int number = 259942, + bbv2.Status? status = bbv2.Status.SCHEDULED, + bool? addBuildSet = true, + List extraTags = const [], +}) { + final pubSubCallBack = createPubSubCallBack( + id, + project: project, + bucket: bucket, + builder: builder, + number: number, + status: status, + userData: userData, + extraTags: extraTags, + ); + + final pubSubCallBackMap = + pubSubCallBack.toProto3Json() as Map; + + final pubSubCallBackString = jsonEncode(pubSubCallBackMap); + + return PushMessage(data: pubSubCallBackString); +} + +bbv2.PubSubCallBack createPubSubCallBack( + Int64 id, { + required BuildBucketUserData userData, + String? project = 'flutter', + String? bucket = 'try', + String? builder = 'Windows Engine Drone', + int number = 259942, + bbv2.Status? status = bbv2.Status.SCHEDULED, + List extraTags = const [], +}) { + // this contains BuildsV2PubSub and UserData (List). + final buildsPubSub = createBuild( + id, + project: project, + bucket: bucket, + builder: builder, + number: number, + status: status, + extraTags: extraTags, + ); + return bbv2.PubSubCallBack( + buildPubsub: buildsPubSub, + userData: userData.toBytes(), + ); +} + +bbv2.BuildsV2PubSub createBuild( + Int64 id, { + String? project = 'flutter', + String? bucket = 'try', + String? builder = 'Windows Engine Drone', + int number = 259942, + bbv2.Status? status = bbv2.Status.SCHEDULED, + List extraTags = const [], +}) { + final build = bbv2.BuildsV2PubSub().createEmptyInstance(); + build.mergeFromProto3Json( + jsonDecode( + createBuildString( + id, + project: project, + bucket: bucket, + builder: builder, + number: number, + status: status, + ), + ) + as Map, + ); + if (extraTags.isNotEmpty) { + build.build.tags.addAll(extraTags); + } + return build; +} + +String createBuildString( + Int64 id, { + String? project = 'flutter', + String? bucket = 'try', + String? builder = 'Windows Engine Drone', + int number = 259942, + bbv2.Status? status = bbv2.Status.SCHEDULED, +}) { + return ''' + { + "build": { + "id": "$id", + "builder": { + "project": "${project ?? 'flutter'}", + "bucket": "${bucket ?? 'try'}", + "builder": "${builder ?? 'Linux web_long_running_tests_1_5'}" + }, + "number": $number, + "createdBy": "user:flutter-dashboard@appspot.gserviceaccount.com", + "createTime": "2024-06-03T15:48:25.490485466Z", + "startTime": "2024-06-03T15:48:35.560843535Z", + "endTime": "2024-06-03T16:05:18.072809938Z", + "updateTime": "2024-06-03T16:05:18.072809938Z", + "status": "${status?.name ?? 'SUCCESS'}", + "input": { + "experiments": [ + "luci.buildbucket.agent.cipd_installation", + "luci.buildbucket.agent.start_build", + "luci.buildbucket.backend_alt", + "luci.buildbucket.backend_go", + "luci.buildbucket.bbagent_getbuild", + "luci.buildbucket.bq_exporter_go", + "luci.buildbucket.parent_tracking", + "luci.buildbucket.use_bbagent", + "luci.recipes.use_python3" + ] + }, + "output": { + "logs": [ + { + "name": "stdout", + "viewUrl": "https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8746138145094216865/+/u/stdout", + "url": "logdog://logs.chromium.org/flutter/buildbucket/cr-buildbucket/8746138145094216865/+/u/stdout" + }, + { + "name": "stderr", + "viewUrl": "https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8746138145094216865/+/u/stderr", + "url": "logdog://logs.chromium.org/flutter/buildbucket/cr-buildbucket/8746138145094216865/+/u/stderr" + } + ], + "status": "SUCCESS" + }, + "infra": { + "buildbucket": { + "requestedProperties": { + "bringup": false, + "cores": 8, + "dependencies": [ + { + "dependency": "curl", + "version": "version:7.64.0" + }, + { + "dependency": "android_sdk", + "version": "version:34v3" + }, + { + "dependency": "chrome_and_driver", + "version": "version:119.0.6045.9" + }, + { + "dependency": "goldctl", + "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd" + } + ], + "device_type": "none", + "exe_cipd_version": "refs/heads/main", + "git_branch": "master", + "git_ref": "refs/pull/149147/head", + "git_url": "https://github.com/flutter/flutter", + "os": "Ubuntu", + "presubmit_max_attempts": 2, + "recipe": "flutter/flutter_drone", + "shard": "web_long_running_tests", + "subshard": "1_5", + "tags": [ + "framework", + "hostonly", + "shard", + "linux" + ] + }, + "requestedDimensions": [ + { + "key": "os", + "value": "Ubuntu" + }, + { + "key": "device_type", + "value": "none" + }, + { + "key": "cores", + "value": "8" + } + ], + "hostname": "cr-buildbucket.appspot.com", + "experimentReasons": { + "luci.best_effort_platform": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.agent.cipd_installation": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.agent.start_build": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.backend_alt": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.backend_go": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.bbagent_getbuild": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.bq_exporter_go": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.canary_software": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.parent_tracking": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.buildbucket.use_bbagent": "EXPERIMENT_REASON_BUILDER_CONFIG", + "luci.buildbucket.use_bbagent_race": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.non_production": "EXPERIMENT_REASON_GLOBAL_DEFAULT", + "luci.recipes.use_python3": "EXPERIMENT_REASON_BUILDER_CONFIG" + }, + "agent": { + "input": { + "data": { + "bbagent_utility_packages": { + "cipd": { + "server": "chrome-infra-packages.appspot.com", + "specs": [ + { + "package": "infra/tools/luci/cas/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + } + ] + }, + "onPath": [ + "bbagent_utility_packages", + "bbagent_utility_packages/bin" + ] + }, + "cipd_bin_packages": { + "cipd": { + "server": "chrome-infra-packages.appspot.com", + "specs": [ + { + "package": "infra/3pp/tools/git/\${platform}", + "version": "version:2@2.42.0.chromium.11" + }, + { + "package": "infra/tools/git/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/luci/git-credential-luci/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/luci/docker-credential-luci/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/luci/vpython3/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/luci/lucicfg/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/luci-auth/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/bb/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/cloudtail/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/prpc/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577" + }, + { + "package": "infra/tools/rdb/\${platform}", + "version": "git_revision:069157ce739832ec1a0a3fe11b2e37e632de50e9" + }, + { + "package": "infra/tools/luci/led/\${platform}", + "version": "git_revision:037d7079cf3faced3842e597c9dfcc7475b2ddca" + } + ] + }, + "onPath": [ + "cipd_bin_packages", + "cipd_bin_packages/bin" + ] + }, + "cipd_bin_packages/cpython3": { + "cipd": { + "server": "chrome-infra-packages.appspot.com", + "specs": [ + { + "package": "infra/3pp/tools/cpython3/\${platform}", + "version": "version:2@3.8.10.chromium.34" + } + ] + }, + "onPath": [ + "cipd_bin_packages/cpython3", + "cipd_bin_packages/cpython3/bin" + ] + }, + "kitchen-checkout": { + "cipd": { + "server": "chrome-infra-packages.appspot.com", + "specs": [ + { + "package": "flutter/recipe_bundles/flutter.googlesource.com/recipes", + "version": "refs/heads/main" + } + ] + } + } + }, + "cipdSource": { + "cipd": { + "cipd": { + "server": "chrome-infra-packages.appspot.com", + "specs": [ + { + "package": "infra/tools/cipd/\${platform}", + "version": "git_revision:215bc891d3d06dd49b11109abc9319a38aa66f0a" + } + ] + }, + "onPath": [ + "cipd", + "cipd/bin" + ] + } + } + }, + "output": { + "resolvedData": { + "": { + "cipd": { + "specs": [ + { + "package": "infra/tools/luci/bbagent/linux-amd64", + "version": "6tIw2DHVEcYg5xt0ETPIUzriHt6IfBcQp8ji-SvXcH8C" + } + ] + } + }, + "bbagent_utility_packages": { + "cipd": { + "specs": [ + { + "package": "infra/tools/luci/cas/linux-amd64", + "version": "pGapntrQBvOpG_fzREhX19L2a7rLGwyZEB4VqXDfBzgC" + } + ] + } + }, + "cipd_bin_packages": { + "cipd": { + "specs": [ + { + "package": "infra/3pp/tools/git/linux-amd64", + "version": "L93GSopVoB8RNkV6raxVg1eXHQmZdcYTrXgZoSTLWFEC" + }, + { + "package": "infra/tools/git/linux-amd64", + "version": "Xqu_HXC1MH-P79yv4Su2jDHjdlBlV_so9HF3ax8_YSsC" + }, + { + "package": "infra/tools/luci/git-credential-luci/linux-amd64", + "version": "f6M4HQ7vio2mfNQBCsVxBBugrsqd3p46Wvgb77mPakwC" + }, + { + "package": "infra/tools/luci/docker-credential-luci/linux-amd64", + "version": "kAqKupATGDUHCyutoNNS4eO4BQoAW9flmdKLzAs5gX4C" + }, + { + "package": "infra/tools/luci/vpython3/linux-amd64", + "version": "oe3aQL5rg2k6c6SGSFhCImGfhnl2zDPAFhdaCpruw_AC" + }, + { + "package": "infra/tools/luci/lucicfg/linux-amd64", + "version": "fLMtVWN-baUP3k_0YQt9dvY1_nBaHbju4g605ds38NEC" + }, + { + "package": "infra/tools/luci-auth/linux-amd64", + "version": "n4LZf93sXiBgdmdasB5L90v-usbwR4bRfrTU1TRNWzUC" + }, + { + "package": "infra/tools/bb/linux-amd64", + "version": "z6fapR2_VkZiA0KVeKLa1IhNpE49LLMR0Wj8-GbKMNQC" + }, + { + "package": "infra/tools/cloudtail/linux-amd64", + "version": "nhss9Uy2MR1OjQpgxCPE-p7kSOObNvgRnm9sBCIciQMC" + }, + { + "package": "infra/tools/prpc/linux-amd64", + "version": "vo1eow0ro1pQ5h-LjJ17Ra2EwTnt3C3U7rsSDwTrDAcC" + }, + { + "package": "infra/tools/rdb/linux-amd64", + "version": "zujGNFHlaTKRZBiMD_ypyXTeX8ypJfRDTkI3Rhx2AtEC" + }, + { + "package": "infra/tools/luci/led/linux-amd64", + "version": "9MscsPmNzDEXsue8XFb0B8fENKbCp9tkjo4EGsSV0IEC" + } + ] + } + }, + "cipd_bin_packages/cpython3": { + "cipd": { + "specs": [ + { + "package": "infra/3pp/tools/cpython3/linux-amd64", + "version": "3p-c2NpZEJWyv4KiHJopTR1ScaHxDKBRMfXlzpre-IwC" + } + ] + } + }, + "kitchen-checkout": { + "cipd": { + "specs": [ + { + "package": "flutter/recipe_bundles/flutter.googlesource.com/recipes", + "version": "PUKnbYmIYQbnhME2pu4sISywm3vPYbxQjRL3CnRv0HMC" + } + ] + } + } + }, + "status": "SUCCESS", + "agentPlatform": "linux-amd64", + "totalDuration": "4s" + }, + "source": { + "cipd": { + "package": "infra/tools/luci/bbagent/\${platform}", + "version": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577", + "server": "chrome-infra-packages.appspot.com" + } + }, + "purposes": { + "bbagent_utility_packages": "PURPOSE_BBAGENT_UTILITY", + "kitchen-checkout": "PURPOSE_EXE_PAYLOAD" + }, + "cipdClientCache": { + "name": "cipd_client_f970720a374db65e431d9836bdd8bc091f12dd8c12a454b5a67f5b163006301e", + "path": "cipd_client" + }, + "cipdPackagesCache": { + "name": "cipd_cache_9237a0836331b01a61cb7a5ed59c6f7b1fa85bf7f14cc136be4a6237cbd59011", + "path": "cipd_cache" + } + }, + "knownPublicGerritHosts": [ + "android.googlesource.com", + "aomedia.googlesource.com", + "boringssl.googlesource.com", + "chromium.googlesource.com", + "dart.googlesource.com", + "dawn.googlesource.com", + "fuchsia.googlesource.com", + "gn.googlesource.com", + "go.googlesource.com", + "llvm.googlesource.com", + "pdfium.googlesource.com", + "quiche.googlesource.com", + "skia.googlesource.com", + "swiftshader.googlesource.com", + "webrtc.googlesource.com" + ], + "buildNumber": true + }, + "logdog": { + "hostname": "logs.chromium.org", + "project": "flutter", + "prefix": "buildbucket/cr-buildbucket/8746138145094216865" + }, + "resultdb": { + "hostname": "results.api.cr.dev" + }, + "bbagent": { + "payloadPath": "kitchen-checkout", + "cacheDir": "cache" + }, + "backend": { + "config": { + "agent_binary_cipd_filename": "bbagent\${EXECUTABLE_SUFFIX}", + "agent_binary_cipd_pkg": "infra/tools/luci/bbagent/\${platform}", + "agent_binary_cipd_server": "chrome-infra-packages.appspot.com", + "agent_binary_cipd_vers": "git_revision:2aba496613f92a5b06d577f82b5d028225d3d577", + "priority": 30, + "service_account": "flutter-try-builder@chops-service-accounts.iam.gserviceaccount.com" + }, + "task": { + "id": { + "target": "swarming://chromium-swarm", + "id": "69f79b2ca0f23910" + }, + "link": "https://chromium-swarm.appspot.com/task?id=69f79b2ca0f23910&o=true&w=true", + "status": "SUCCESS", + "details": { + "bot_dimensions": { + "bot_config": [ + "bot_config.py" + ], + "caches": [ + "cipd_cache_9237a0836331b01a61cb7a5ed59c6f7b1fa85bf7f14cc136be4a6237cbd59011", + "cipd_client_f970720a374db65e431d9836bdd8bc091f12dd8c12a454b5a67f5b163006301e", + "engine_main_builder", + "engine_main_git", + "flutter_main_android_sdk_version_34v3_legacy", + "flutter_main_chrome_and_driver_version_119_0_6045_9_legacy", + "flutter_main_open_jdk_version_17_legacy", + "gradle", + "packages_main_android_sdk_version_33v6_legacy", + "packages_main_chrome_and_driver_version_114_0_legacy", + "packages_main_open_jdk_version_11_legacy" + ], + "cipd_platform": [ + "linux-amd64" + ], + "cores": [ + "8" + ], + "cpu": [ + "x86", + "x86-64", + "x86-64-Broadwell_GCE", + "x86-64-avx2" + ], + "device_os": [ + "none" + ], + "device_type": [ + "none" + ], + "display_attached": [ + "0" + ], + "gce": [ + "1" + ], + "gcp": [ + "flutter-machines-prod" + ], + "gpu": [ + "none" + ], + "id": [ + "flutter-try-flutterprj-ubuntu-us-central1-b-2-s2lu" + ], + "image": [ + "dart-focal-24052600-54a1aca43d9" + ], + "inside_docker": [ + "0" + ], + "kernel": [ + "5.15.0-1060-gcp" + ], + "kvm": [ + "1" + ], + "locale": [ + "en_US.UTF-8" + ], + "machine_type": [ + "n1-standard-8" + ], + "os": [ + "Linux", + "Ubuntu", + "Ubuntu-20", + "Ubuntu-20.04", + "Ubuntu-20.04.6" + ], + "pool": [ + "luci.flutter.try" + ], + "puppet_env": [ + "production" + ], + "python": [ + "3", + "3.8", + "3.8.10+chromium.23" + ], + "server_version": [ + "7672-d26f562" + ], + "ssd": [ + "1" + ], + "zone": [ + "us", + "us-central", + "us-central1", + "us-central1-b" + ] + } + }, + "updateId": "1717430716453135104" + }, + "caches": [ + { + "name": "pub_cache", + "path": ".pub-cache" + }, + { + "name": "flutter_main_android_sdk_version_34v3_legacy", + "path": "android" + }, + { + "name": "flutter_main_android_sdk_version_34v3", + "path": "android_sdk" + }, + { + "name": "flutter_main_builder", + "path": "builder" + }, + { + "name": "flutter_main_chrome_and_driver_version_119_0_6045_9_legacy", + "path": "chrome" + }, + { + "name": "flutter_main_chrome_and_driver_version_119_0_6045_9", + "path": "chrome_and_driver" + }, + { + "name": "flutter_main_curl_version_7_64_0", + "path": "curl" + }, + { + "name": "flutter_main_git", + "path": "git" + }, + { + "name": "flutter_main_goldctl_git_revision_720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd", + "path": "goldctl" + }, + { + "name": "gradle", + "path": "gradle" + }, + { + "name": "vpython", + "path": "vpython", + "envVar": "VPYTHON_VIRTUALENV_ROOT" + } + ], + "taskDimensions": [ + { + "key": "cores", + "value": "8" + }, + { + "key": "device_type", + "value": "none" + }, + { + "key": "os", + "value": "Ubuntu" + }, + { + "key": "pool", + "value": "luci.flutter.try" + } + ], + "hostname": "chromium-swarm.appspot.com" + } + }, + "tags": [ + { + "key": "buildset", + "value": "pr/git/149147" + }, + { + "key": "buildset", + "value": "sha/git/5bbe0ce383e7ed77c68911a20358bbdc2f4c69dd" + }, + { + "key": "cipd_version", + "value": "refs/heads/main" + }, + { + "key": "github_checkrun", + "value": "25743375958" + }, + { + "key": "github_link", + "value": "https://github.com/flutter/flutter/pull/149147" + }, + { + "key": "user_agent", + "value": "flutter-cocoon" + } + ], + "exe": { + "cipdPackage": "flutter/recipe_bundles/flutter.googlesource.com/recipes", + "cipdVersion": "refs/heads/main", + "cmd": [ + "luciexe" + ] + }, + "schedulingTimeout": "21600s", + "executionTimeout": "3600s", + "gracePeriod": "30s" + }, + "buildLargeFields": "eJzMnH2QFOWdx9ND2Jl9diDQEcx17nINIWfS1rzu7OzslpejklTqqu4uSVXq6v4yXf3yzEyzPd2TfllYQ5IFdFlmw4vAIqTALGdF1sRXkFJLqFP/UOpOyz0tFd85RQ/PQ6nzTvBKNNXvM9O9yy7l0z3+IwPT/fv+vt2f7/N0z9N90yUA/g+AVaBLgZxQh/i1xIqyqGsaVDL2/2lekSUIloOEwkJ6ncyq+GJiUT6bBStADw+HBQ7S2kgd4l3ElyXjm0djIMnDOpR4KHECVPHDsfztMdBH9YKvg/gwVFRBlvBlxFL7j4P96WIhnQXXAOBuNmLsjdMVEQxSJfBn3mZLiaSzWW9h2Nhj80ZLiB5G4hVZ4GmVHwJ/S/0QrGr5wleJ5VxVkWuQZiSe5hVhGCpglbf7lcQ1zu5zuYF0Nl3MFvrSA4CmbgTf877WT/RVBI1W4LBgdZDPMn2FfLlYhoXyQH4gn+d62VI5W+Y5rsDnGa7IsqVejgfXtqjpJuIVWeQ5TQQrQMLaY102/tr2HiwBcVYRpIpex2Pkl8D7GEiucY6PwkL8FYx6CQMDICFIqsZIHMRTxPV1RV4HOU11jmBKYWGqrsh8xvmWmuFhmdFFDRzAQKIuMlpZVmr4bzBiG8bJksYIElRSQo2pwL/mZW4IKoOZTIVT0oKc4URZ51M1RhmCWl1kOJipyHJFhBkesgIj5XJr1SqT7ysOFgdgvr80wA2Ue/l8iSsy2XKul833ZYt5Ll/IQtjf359nYa7cB4t8IceyhWIpyxZ7IQOLbG+2AP4eXLvGOi1pKFUECWYEVRYZDfJ4jsqAFOhSoTIMFfybxKqqptXVwYzzDesf0ky9rtZlLc3JNbASAMNhVmEkrooniK4aoxoW46C7IteYljP7m2BlXYGqztYEja4xG2hG02Ctrql49/IvWf+tNYhQdVatMgpvbJej+8AqsNj6/DVi5XrI0qIsVWhFlyRBqtAaVDXVOKK6CmmOUfEYiYE+8GWNqah4Kn896CG6ywpTg+tlZQgAIlGVVU2WxBEQJ6zdGn8QBUnfADIgbjSjKyK+hljtNF8RtKrOGt1m2iAGq0BizZCgcVUo4Suor4LloAfWdMMqusJBU8pqa58KLBspoMCymqnropjJFQZyhf5MFTK8sZlxxtKaMrJOZs3NloA4J8osCxXzFMXBYk5WoOcUuRZ8AwDLbImpQYN8ThSgpKUdcUtBTFaNQ/KPrC5pOvgH34FX1zNKTZAqeJ7KgrR74Jt6N7EW9FrK/GrLkf8WWAY3QJoT6jztELyc+IrZodGVmqkxggRWgK/odVFmeLrOcENMBapmQ/8ElrjIGScK/kPqB+B7roQSUXTwyqWhxNdlQdLUdFnnqqrApASprDApYzvzK2mTnrRBzGbsu+AGMAiSFdlLEpwivt3HsjDLwd5SL+yHfH8/VywN5HJMPtvbV2JZnsuXC1xxgOcb2ENdoEeFml6nWV0QeTyZePnfLh7tWjaz/9M7YoTzaersm3fGyCT13xgAii7RdtLXHedEuaKmHfvSslIx/8Y9g8xdszo3BLUMp6SaP5b6C8VcbylX6MsOFPK5YqnYl7k+o2eaNGW8ioQoyhVergSW/MKrUZcwsLQGa7IyQtcVuSyIENfCabm1KlFH33ZrReptDMRFuVIxgKmF07NdjhDQN2uXGhy0g5W0DvgguTpw5rKaXC9oVfInI1pVlsjedC6XLjWwVxeD+I9/SgpSWXaxGf34n48Y2LxpYfPgC5sXkUnqFQx0reEhq1dwAZ2XP/4pbWjJiLRZioBofGwrQ32CgeVwA+R0TZAlkocaI4gqrqLv0y1K20UJGW3HvoLUaQx0qRov6xpeRd+wVYng0XZpVWlgJfC171chNyTrGtlGBe6c4DMvfrB5EZFMXDA/jT832YiRyQZ2MA5Wz7btxoqgkSaL7l5OvXfvlkVET+KM+Wn/s4/9jExSu2MuMzdj6Lx1ZNIO8O44LWi0KdPlaRRDY/z8JVAPxIJY2xGtP34OG9E55Wd0Z8xldEu0Rtn8/joycxy0r8hnGWpcFXeIPPfERz8jkokPLeTfuPMerEP4NGVGy2eLhE7j0xLXIXzOIqYj+LS0Rchns4AGdiwOvjUnn5z9j7hD5fiHz92DuQPxhZ27jxqM7vcY3RqVtY5UF9MtUZx87SqohwJJvTVyl/ywbo/ULz+v+zxexyK3y0Z2c6QeOdh+Ggd/NSu2CmR40r1H45D68LnHjjZxe/73Jw1uf+txOx6FxYZW936SC+7NYZscKIN6JJDc3dH75Ed3R7SO+dndb7ELFSWaAaFVn6Uk/AEhSMXg2htYJfNdk2nIkwbB1833Lup15qYN7NCVptWcCBmP/ZnHnz/psb/rkbOvdsq82tQZ7by6RUKnzastcR0yr55FTEfMqy1tEc6rmwU0sMfj4LpZAVV1tibzughJdUTiXEp3HbrtNY/S+8e3f2RQetCjdFsU5rpiaUOsi+otYZ+AwTqoRwN53dMBTvmh3RmxZ35yf+uRG8n0r02gjW/o079AGQ3s2Tj4zjwg1us8o0EX4+knHvvIw/j0oc2NGJmkpjyMJ6J12pLrgrw1OrNblVD/EojyZEe45Yf51sh98+N8u4dzoyNss4Eei9wrB+lbusASY36swOFUnVFUD9tTD26diHlz5IP79hrYvuv9niqjM9RenmUpcskcQmNaYDHqlkD4RsLq2c+XHkb3foTOYu51qxRW8/bV6bowOrZqNbCxOPhLe6ckJ0tloUKmUsYcVkmVIaPpClS9W0evPrE3RvQkdj5t3jqanLyRTFIT3oC2CWHWOGRbGul2jS4sv0Rj3jzLU38MxAflSH8lZX6iUI30C1ZCbfPGqdEILbJHp43R2OIMSZMJsAxKqq5A2lzxKoqQxx3YpnacuJFI2h/OvDi1CbPWxjnoKejca9fkoiah8Wu2ekbSBLC1McTO/SxtCMkDPzv/5S0E+nmIFtis1ELq2ypHfYKBnnWqLKVlXavrGj4cYsdNdQklpLabajawRxPgmvZvkN/Ofwd38uDSxRObMDcdLhz5fMJIh//10iFEu2g6T7sJEZJdzTWpvYEpgXJ4CZTjjwpEw8v8qlP/48WFHrIVdmT8PMT+7djYFGuNjZtC7rw5OoZDbL8lPt6Ng2RFYXgRkhzDVaEbG2fOH/6NERu3mp9OHdtx3oiNs15soLzwMQXRpiA3LlBd+ATUorYExsSGkDr2p4MWQu/+VHjLu8pFuOS+RYR9kYtoyX1AKeodDMRFQdV4Ae21fMtpZtUL55S2ajWwG8Cff998TpBkJJ60nhMkm57cc1Cf/vdnznvg75o+MWauob4DgNRc22/0XZ24u3jyctMOZ15/9xMjSaa9OwM7kd6FbHs0kvYkz37xMoHsVuRVqKGeCkyjAx3jmj+vJjvCP3+i3endUkD7Y/1CZNozIHQ/1i9cDHW8bW6E9qfShQhsnjWh+6n0KhU1sKcAyC0oJu1rNSscx8cm/t+LyqnbjhrZS93vRWXHHIeWK7mOOA4t13nPBkbmoY5yzx+bBzrGR3903utFJ9q1jQuVascnurWNVyeIOtkWobd1lGvNMbqnY6xridIsWPYDeb0kygxP2u+UcINy9IM3xmJuUE7fPb5nkTFJPd4NVrVvM/vEdOb03q3eTmb2fLZ7EZk016nZaYtynZojk7Zlzj4ZRbRObd4CqPsCw3R7pOb4w3MborP4KrRQ272w3BypTXY4/ioqa+wsPNSWhSgXe19ZVHP2oVrsvSAVDexkF1jBO8HV8qYhJ54u/eE/9iwiehJvWfn3+7u7yST1sXc7bj06Rx1hdLMwN55UNAbOWZTaFxhJKJcYBOvxxxCiJQbzLG/+rmMnD8LfdYLV2GGD6HeduWo2sIkusNRZqcLLnCYruMPK9H++1E0k7Q/jl85PGddU5zxyEL41xvl131LkIoPorTHB1aitgawg/LWjTYefEUS/dlyprrlOz4YD4Tq9Nhk2FYjW6QUWa2DnEuBvFF0iNahqaZ5RNLIsK2Tw+7tI80Vc5u0K5+VfZI7uwx1kZp45dDhG9CTuet68KbF934/IJPW0N0s+jDB1FV2i3R7osqzQwT3QpmzzUsPpgc7RfS5xhxAN81+QPur9QEb/2MHO+qk+0qEe+3PgX73p+VQHW2wnx8EO9dXJmhfjoNt8hNFQhzs5MX34yR8RSfvDhZdPjGJkknrDG3WHEPruiHHxryBysL0QdTnw5VMI11R4EvxAIlpTMUdJ6jVvhF0XRtM2ImXUndrXse9joEer1dlsvcyIpRrKWYRXu6kgqllEYLEGlgJLVE1WIFmDmiJwxsWoxfP41hdGMZfuc0++adDdwHJg+d8JoihIFfInisxBVYXeNqO7P2ja5uFtx46Z21zsAn/h22jjkCCKpNkddHcwM/G7Td4ORl85bi5kG/MmIr9GdzRshbSrMGMopC2Fbsz8As3RmVdx6kjgFALl06Bz6/LnEaqnQReog7rZG/5/FZU7dnDdFIUjzsD9Xhf4+izoGVMCF7zTl9+ZaALv+DOTBnibPfB+GbqLhj4Xu5FQTWwuTd0RCB3K25tzqfIjh+r25oJUUKMecAgX5M+lycYN0YL8K1duYJ93gW/MAptd2eXtVOPypMfb/qnXf2fwts3jDeV65Vl6cfboMIdovfL8qlN3BWKH8mnWKwjzk4fqadaFCjEnSDZ84U+Q3EdtLf7CnSC1Fp9zvFvHDDMuf2c+2zbl8XfmwNt3RT7eGfoiGu+aS3fOeGeqiny8C1YR7XhnaopkvGuq3MDe6gLELLAxPOuyduH1XX/wWJs6Om1cFZoG2qyFbyDDsy5q4RrYVJk6HEgayjccziHKDxqqNxwuRAT1mXe36ReR+GJjtj50LxzKLi4GPfY7/+2FpDZWYzsfNLC6z7pku/2BXTEyaT7QYWOF8IEO+8XtLStEET3QEVCK2hQIDsLlEc0i/KAgWh4xZ1HqjAeGGE7jNglV9N1alf4UAAD//9ANEzk=" + } + '''; +} diff --git a/app_dart/test/src/utilities/entity_generators.dart b/packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart similarity index 97% rename from app_dart/test/src/utilities/entity_generators.dart rename to packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart index db320f0ac8..c37a9595ad 100644 --- a/app_dart/test/src/utilities/entity_generators.dart +++ b/packages/cocoon_integration_test/lib/src/utilities/entity_generators.dart @@ -18,7 +18,7 @@ import 'package:cocoon_service/src/service/firestore.dart'; import 'package:fixnum/fixnum.dart'; import 'package:github/github.dart' as github; -import '../service/fake_scheduler.dart'; +import '../fakes/fake_scheduler.dart'; Task generateFirestoreTask( int i, { @@ -319,7 +319,8 @@ PresubmitGuard generatePresubmitGuard({ String commitSha = 'abc', int creationTime = 1, String author = 'dash', - int buildCount = 1, + int remainingBuilds = -1, + int failedBuilds = 0, Map? builds, }) { return PresubmitGuard( @@ -330,8 +331,10 @@ PresubmitGuard generatePresubmitGuard({ commitSha: commitSha, creationTime: creationTime, author: author, - remainingBuilds: buildCount, - failedBuilds: 0, + remainingBuilds: remainingBuilds >= 0 + ? remainingBuilds + : (builds?.length ?? 0), + failedBuilds: failedBuilds, builds: builds, ); } diff --git a/packages/cocoon_integration_test/lib/src/utilities/matchers.dart b/packages/cocoon_integration_test/lib/src/utilities/matchers.dart new file mode 100644 index 0000000000..ce8605a15b --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/utilities/matchers.dart @@ -0,0 +1,15 @@ +// Copyright 2021 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:test/test.dart'; + +Matcher throwsExceptionWith(String messageSubString) { + return throwsA( + isA().having( + (T e) => e.toString(), + 'description', + contains(messageSubString), + ), + ); +} diff --git a/app_dart/test/src/utilities/mocks.dart b/packages/cocoon_integration_test/lib/src/utilities/mocks.dart similarity index 97% rename from app_dart/test/src/utilities/mocks.dart rename to packages/cocoon_integration_test/lib/src/utilities/mocks.dart index 0a36bc2db6..a5ccfc38a2 100644 --- a/app_dart/test/src/utilities/mocks.dart +++ b/packages/cocoon_integration_test/lib/src/utilities/mocks.dart @@ -26,7 +26,7 @@ import 'package:mockito/annotations.dart'; import 'package:neat_cache/neat_cache.dart'; import 'package:process/process.dart'; -import '../../service/cache_service_test.dart'; +import '../fakes/fake_entry.dart'; export 'mocks.mocks.dart'; @@ -68,8 +68,6 @@ export 'mocks.mocks.dart'; // ), ], ) -void main() {} - // ignore: unreachable_from_main class ThrowingGitHub implements GitHub { @override diff --git a/app_dart/test/src/utilities/mocks.mocks.dart b/packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart similarity index 99% rename from app_dart/test/src/utilities/mocks.mocks.dart rename to packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart index d915ef8736..1890f6e94b 100644 --- a/app_dart/test/src/utilities/mocks.mocks.dart +++ b/packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart @@ -1,5 +1,5 @@ // Mocks generated by Mockito 5.4.6 from annotations -// in cocoon_service/test/src/utilities/mocks.dart. +// in cocoon_integration_test/src/utilities/mocks.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes @@ -10,6 +10,7 @@ import 'dart:typed_data' as _i27; import 'package:buildbucket/buildbucket_pb.dart' as _i6; import 'package:cocoon_common/rpc_model.dart' as _i19; +import 'package:cocoon_integration_test/src/fakes/fake_entry.dart' as _i26; import 'package:cocoon_service/cocoon_service.dart' as _i17; import 'package:cocoon_service/src/foundation/github_checks_util.dart' as _i10; import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart' as _i41; @@ -54,8 +55,6 @@ import 'package:mockito/src/dummies.dart' as _i20; import 'package:neat_cache/neat_cache.dart' as _i16; import 'package:process/src/interface/process_manager.dart' as _i38; -import '../../service/cache_service_test.dart' as _i26; - // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters diff --git a/packages/cocoon_integration_test/lib/src/utilities/webhook_generators.dart b/packages/cocoon_integration_test/lib/src/utilities/webhook_generators.dart new file mode 100644 index 0000000000..7896dd6d85 --- /dev/null +++ b/packages/cocoon_integration_test/lib/src/utilities/webhook_generators.dart @@ -0,0 +1,1331 @@ +// 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 'dart:convert'; + +import 'package:cocoon_service/protos.dart' as pb; +import 'package:cocoon_service/src/model/github/checks.dart'; +import 'package:cocoon_service/src/model/luci/pubsub_message.dart'; +import 'package:cocoon_service/src/service/config.dart'; +import 'package:github/github.dart'; +import 'package:github/hooks.dart'; +import 'package:test/test.dart'; + +PushMessage generateGithubWebhookMessage({ + String event = 'pull_request', + String action = 'merged', + int number = 123, + String? baseRef, + String baseSha = '4cd12fc8b7d4cc2d8609182e1c4dea5cddc86890', + String headSha = 'be6ff099a4ee56e152a5fa2f37edd10f79d1269a', + String login = 'dash', + String headRef = 'abc', + bool isDraft = false, + bool merged = false, + bool mergeable = true, + String mergeCommitSha = 'fd6b46416c18de36ce87d0241994b2da180cab4c', + RepositorySlug? slug, + bool includeChanges = false, + bool withAutosubmit = false, + bool withRevertOf = false, + DateTime? closedAt, + Iterable additionalLabels = const [], +}) { + final data = + (pb.GithubWebhookMessage.create() + ..event = event + ..payload = _generatePullRequestEvent( + action, + number, + baseRef, + baseSha: baseSha, + headSha: headSha, + login: login, + headRef: headRef, + isDraft: isDraft, + merged: merged, + isMergeable: mergeable, + slug: slug, + mergeCommitSha: mergeCommitSha, + includeChanges: includeChanges, + withAutosubmit: withAutosubmit, + withRevertOf: withRevertOf, + closedAt: closedAt, + additionalLabels: additionalLabels, + )) + .writeToJson(); + return PushMessage(data: data, messageId: 'abc123'); +} + +String _generatePullRequestEvent( + String action, + int number, + String? baseRef, { + RepositorySlug? slug, + String login = 'flutter', + String baseSha = '4cd12fc8b7d4cc2d8609182e1c4dea5cddc86890', + String headRef = 'wait_for_reassemble', + required String headSha, + bool includeCqLabel = false, + bool isDraft = false, + bool merged = false, + bool isMergeable = true, + String mergeCommitSha = 'fd6b46416c18de36ce87d0241994b2da180cab4c', + bool includeChanges = false, + DateTime? closedAt, + required bool withAutosubmit, + required bool withRevertOf, + Iterable additionalLabels = const [], +}) { + slug ??= Config.flutterSlug; + baseRef ??= Config.defaultBranch(slug); + + var labelId = 1000; + Map generateLabel(String name) { + labelId++; + return { + 'id': labelId, + 'node_id': base64Encode('$labelId'.codeUnits), + 'url': 'https://api.github.com/repos/${slug!.fullName}/labels/$name', + 'name': name, + 'color': '207de5', + 'default': false, + }; + } + + final labels = [ + if (includeCqLabel) generateLabel('cla: yes'), + if (withAutosubmit) generateLabel('autosubmit'), + if (withRevertOf) generateLabel('revert of'), + + // This matches the behavior of this function before refactoring to have a + // more structured way to add test labels. It would be nice to refactor + // these out. + generateLabel('framework'), + generateLabel('tool'), + ...additionalLabels.map(generateLabel), + ]; + + return '''{ + "action": "$action", + "number": $number, + "pull_request": { + "url": "https://api.github.com/repos/${slug.fullName}/pulls/$number", + "id": 294034, + "node_id": "MDExOlB1bGxSZXF1ZXN0Mjk0MDMzODQx", + "html_url": "https://github.com/${slug.fullName}/pull/$number", + "diff_url": "https://github.com/${slug.fullName}/pull/$number.diff", + "patch_url": "https://github.com/${slug.fullName}/pull/$number.patch", + "issue_url": "https://api.github.com/repos/${slug.fullName}/issues/$number", + "number": $number, + "state": "open", + "locked": false, + "title": "Defer reassemble until reload is finished", + "user": { + "login": "$login", + "id": 862741, + "node_id": "MDQ6VXNlcjg2MjA3NDE=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + }, + "draft" : "$isDraft", + "body": "The body", + "created_at": "2019-07-03T07:14:35Z", + "updated_at": "2019-07-03T16:34:53Z", + "closed_at": ${closedAt == null ? 'null' : '"${closedAt.toUtc().toIso8601String()}"'}, + "merged_at": "2019-07-03T16:34:53Z", + "merge_commit_sha": "$mergeCommitSha", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": ${const JsonEncoder.withIndent(' ').convert(labels)}, + "milestone": null, + "commits_url": "https://api.github.com/repos/${slug.fullName}/pulls/$number/commits", + "review_comments_url": "https://api.github.com/repos/${slug.fullName}/pulls/$number/comments", + "review_comment_url": "https://api.github.com/repos/${slug.fullName}/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/${slug.fullName}/issues/$number/comments", + "statuses_url": "https://api.github.com/repos/${slug.fullName}/statuses/be6ff099a4ee56e152a5fa2f37edd10f79d1269a", + "head": { + "label": "$login:$headRef", + "ref": "$headRef", + "sha": "$headSha", + "user": { + "login": "$login", + "id": 8620741, + "node_id": "MDQ6VXNlcjg2MjA3NDE=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 131232406, + "node_id": "MDEwOlJlcG9zaXRvcnkxMzEyMzI0MDY=", + "name": "${slug.name}", + "full_name": "${slug.fullName}", + "private": false, + "owner": { + "login": "flutter", + "id": 8620741, + "node_id": "MDQ6VXNlcjg2MjA3NDE=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8620741?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/${slug.fullName}", + "description": "Flutter makes it easy and fast to build beautiful mobile apps.", + "fork": true, + "url": "https://api.github.com/repos/${slug.fullName}", + "forks_url": "https://api.github.com/repos/${slug.fullName}/forks", + "keys_url": "https://api.github.com/repos/${slug.fullName}/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/${slug.fullName}/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/${slug.fullName}/teams", + "hooks_url": "https://api.github.com/repos/${slug.fullName}/hooks", + "issue_events_url": "https://api.github.com/repos/${slug.fullName}/issues/events{/number}", + "events_url": "https://api.github.com/repos/${slug.fullName}/events", + "assignees_url": "https://api.github.com/repos/${slug.fullName}/assignees{/user}", + "branches_url": "https://api.github.com/repos/${slug.fullName}/branches{/branch}", + "tags_url": "https://api.github.com/repos/${slug.fullName}/tags", + "blobs_url": "https://api.github.com/repos/${slug.fullName}/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/${slug.fullName}/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/${slug.fullName}/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/${slug.fullName}/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/${slug.fullName}/statuses/{sha}", + "languages_url": "https://api.github.com/repos/${slug.fullName}/languages", + "stargazers_url": "https://api.github.com/repos/${slug.fullName}/stargazers", + "contributors_url": "https://api.github.com/repos/${slug.fullName}/contributors", + "subscribers_url": "https://api.github.com/repos/${slug.fullName}/subscribers", + "subscription_url": "https://api.github.com/repos/${slug.fullName}/subscription", + "commits_url": "https://api.github.com/repos/${slug.fullName}/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/${slug.fullName}/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/${slug.fullName}/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/${slug.fullName}/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/${slug.fullName}/contents/{+path}", + "compare_url": "https://api.github.com/repos/${slug.fullName}/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/${slug.fullName}/merges", + "archive_url": "https://api.github.com/repos/${slug.fullName}/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/${slug.fullName}/downloads", + "issues_url": "https://api.github.com/repos/${slug.fullName}/issues{/number}", + "pulls_url": "https://api.github.com/repos/${slug.fullName}/pulls{/number}", + "milestones_url": "https://api.github.com/repos/${slug.fullName}/milestones{/number}", + "notifications_url": "https://api.github.com/repos/${slug.fullName}/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/${slug.fullName}/labels{/name}", + "releases_url": "https://api.github.com/repos/${slug.fullName}/releases{/id}", + "deployments_url": "https://api.github.com/repos/${slug.fullName}/deployments", + "created_at": "2018-04-27T02:03:08Z", + "updated_at": "2019-06-27T06:56:59Z", + "pushed_at": "2019-07-03T19:40:11Z", + "git_url": "git://github.com/${slug.fullName}.git", + "ssh_url": "git@github.com:${slug.fullName}.git", + "clone_url": "https://github.com/${slug.fullName}.git", + "svn_url": "https://github.com/${slug.fullName}", + "homepage": "https://flutter.io", + "size": 94508, + "stargazers_count": 1, + "watchers_count": 1, + "language": "Dart", + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "other", + "name": "Other", + "spdx_id": "NOASSERTION", + "url": null, + "node_id": "MDc6TGljZW5zZTA=" + }, + "forks": 0, + "open_issues": 0, + "watchers": 1, + "default_branch": "$kDefaultBranchName" + } + }, + "base": { + "label": "flutter:$baseRef", + "ref": "$baseRef", + "sha": "$baseSha", + "user": { + "login": "flutter", + "id": 14101776, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MTAxNzc2", + "avatar_url": "https://avatars3.githubblahblahblah", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 31792824, + "node_id": "MDEwOlJlcG9zaXRvcnkzMTc5MjgyNA==", + "name": "${slug.name}", + "full_name": "${slug.fullName}", + "private": false, + "owner": { + "login": "flutter", + "id": 14101776, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MTAxNzc2", + "avatar_url": "https://avatars3.githubblahblahblah", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/${slug.fullName}", + "description": "Flutter makes it easy and fast to build beautiful mobile apps.", + "fork": false, + "url": "https://api.github.com/repos/${slug.fullName}", + "forks_url": "https://api.github.com/repos/${slug.fullName}/forks", + "keys_url": "https://api.github.com/repos/${slug.fullName}/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/${slug.fullName}/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/${slug.fullName}/teams", + "hooks_url": "https://api.github.com/repos/${slug.fullName}/hooks", + "issue_events_url": "https://api.github.com/repos/${slug.fullName}/issues/events{/number}", + "events_url": "https://api.github.com/repos/${slug.fullName}/events", + "assignees_url": "https://api.github.com/repos/${slug.fullName}/assignees{/user}", + "branches_url": "https://api.github.com/repos/${slug.fullName}/branches{/branch}", + "tags_url": "https://api.github.com/repos/${slug.fullName}/tags", + "blobs_url": "https://api.github.com/repos/${slug.fullName}/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/${slug.fullName}/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/${slug.fullName}/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/${slug.fullName}/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/${slug.fullName}/statuses/{sha}", + "languages_url": "https://api.github.com/repos/${slug.fullName}/languages", + "stargazers_url": "https://api.github.com/repos/${slug.fullName}/stargazers", + "contributors_url": "https://api.github.com/repos/${slug.fullName}/contributors", + "subscribers_url": "https://api.github.com/repos/${slug.fullName}/subscribers", + "subscription_url": "https://api.github.com/repos/${slug.fullName}/subscription", + "commits_url": "https://api.github.com/repos/${slug.fullName}/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/${slug.fullName}/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/${slug.fullName}/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/${slug.fullName}/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/${slug.fullName}/contents/{+path}", + "compare_url": "https://api.github.com/repos/${slug.fullName}/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/${slug.fullName}/merges", + "archive_url": "https://api.github.com/repos/${slug.fullName}/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/${slug.fullName}/downloads", + "issues_url": "https://api.github.com/repos/${slug.fullName}/issues{/number}", + "pulls_url": "https://api.github.com/repos/${slug.fullName}/pulls{/number}", + "milestones_url": "https://api.github.com/repos/${slug.fullName}/milestones{/number}", + "notifications_url": "https://api.github.com/repos/${slug.fullName}/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/${slug.fullName}/labels{/name}", + "releases_url": "https://api.github.com/repos/${slug.fullName}/releases{/id}", + "deployments_url": "https://api.github.com/repos/${slug.fullName}/deployments", + "created_at": "2015-03-06T22:54:58Z", + "updated_at": "2019-07-04T02:08:44Z", + "pushed_at": "2019-07-04T02:03:04Z", + "git_url": "git://github.com/${slug.fullName}.git", + "ssh_url": "git@github.com:${slug.fullName}.git", + "clone_url": "https://github.com/${slug.fullName}.git", + "svn_url": "https://github.com/${slug.fullName}", + "homepage": "https://flutter.dev", + "size": 65507, + "stargazers_count": 68944, + "watchers_count": 68944, + "language": "Dart", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 7987, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 6536, + "license": { + "key": "other", + "name": "Other", + "spdx_id": "NOASSERTION", + "url": null, + "node_id": "MDc6TGljZW5zZTA=" + }, + "forks": 7987, + "open_issues": 6536, + "watchers": 68944, + "default_branch": "$kDefaultBranchName" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/${slug.fullName}/pulls/$number" + }, + "html": { + "href": "https://github.com/${slug.fullName}/pull/$number" + }, + "issue": { + "href": "https://api.github.com/repos/${slug.fullName}/issues/$number" + }, + "comments": { + "href": "https://api.github.com/repos/${slug.fullName}/issues/$number/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/${slug.fullName}/pulls/$number/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/${slug.fullName}/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/${slug.fullName}/pulls/$number/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/${slug.fullName}/statuses/deadbeef" + } + }, + "author_association": "MEMBER", + "draft" : $isDraft, + "merged": $merged, + "mergeable": $isMergeable, + "rebaseable": true, + "mergeable_state": "draft", + "merged_by": null, + "comments": 1, + "review_comments": 0, + "maintainer_can_modify": true, + "commits": 5, + "additions": 55, + "deletions": 36, + "changed_files": 5 + }, + ${includeChanges ? ''' + "changes": { + "base": { + "ref": { + "from": "master" + }, + "sha": { + "from": "b3af5d64d3e6e2110b07d71909fc432537339659" + } + } + },''' : ''} + "repository": { + "id": 1868532, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "${slug.name}", + "full_name": "${slug.fullName}", + "private": false, + "owner": { + "login": "flutter", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/${slug.fullName}", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/${slug.fullName}", + "forks_url": "https://api.github.com/repos/${slug.fullName}/forks", + "keys_url": "https://api.github.com/repos/${slug.fullName}/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/${slug.fullName}/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/${slug.fullName}/teams", + "hooks_url": "https://api.github.com/repos/${slug.fullName}/hooks", + "issue_events_url": "https://api.github.com/repos/${slug.fullName}/issues/events{/number}", + "events_url": "https://api.github.com/repos/${slug.fullName}/events", + "assignees_url": "https://api.github.com/repos/${slug.fullName}/assignees{/user}", + "branches_url": "https://api.github.com/repos/${slug.fullName}/branches{/branch}", + "tags_url": "https://api.github.com/repos/${slug.fullName}/tags", + "blobs_url": "https://api.github.com/repos/${slug.fullName}/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/${slug.fullName}/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/${slug.fullName}/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/${slug.fullName}/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/${slug.fullName}/statuses/{sha}", + "languages_url": "https://api.github.com/repos/${slug.fullName}/languages", + "stargazers_url": "https://api.github.com/repos/${slug.fullName}/stargazers", + "contributors_url": "https://api.github.com/repos/${slug.fullName}/contributors", + "subscribers_url": "https://api.github.com/repos/${slug.fullName}/subscribers", + "subscription_url": "https://api.github.com/repos/${slug.fullName}/subscription", + "commits_url": "https://api.github.com/repos/${slug.fullName}/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/${slug.fullName}/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/${slug.fullName}/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/${slug.fullName}/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/${slug.fullName}/contents/{+path}", + "compare_url": "https://api.github.com/repos/${slug.fullName}/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/${slug.fullName}/merges", + "archive_url": "https://api.github.com/repos/${slug.fullName}/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/${slug.fullName}/downloads", + "issues_url": "https://api.github.com/repos/${slug.fullName}/issues{/number}", + "pulls_url": "https://api.github.com/repos/${slug.fullName}/pulls{/number}", + "milestones_url": "https://api.github.com/repos/${slug.fullName}/milestones{/number}", + "notifications_url": "https://api.github.com/repos/${slug.fullName}/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/${slug.fullName}/labels{/name}", + "releases_url": "https://api.github.com/repos/${slug.fullName}/releases{/id}", + "deployments_url": "https://api.github.com/repos/${slug.fullName}/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/${slug.fullName}.git", + "ssh_url": "git@github.com:${slug.fullName}.git", + "clone_url": "https://github.com/${slug.fullName}.git", + "svn_url": "https://github.com/${slug.fullName}", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "$kDefaultBranchName" + }, + "sender": { + "login": "$login", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + } +}'''; +} + +PushMessage generateCheckRunEvent({ + String action = 'created', + int numberOfPullRequests = 1, +}) { + var data = + '''{ + "action": "$action", + "check_run": { + "id": 128620228, + "node_id": "MDg6Q2hlY2tSdW4xMjg2MjAyMjg=", + "head_sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "external_id": "", + "url": "https://api.github.com/repos/flutter/flutter/check-runs/128620228", + "html_url": "https://github.com/flutter/flutter/runs/128620228", + "details_url": "https://octocoders.io", + "status": "queued", + "conclusion": null, + "started_at": "2019-05-15T15:21:12Z", + "completed_at": null, + "output": { + "title": null, + "summary": null, + "text": null, + "annotations_count": 0, + "annotations_url": "https://api.github.com/repos/flutter/flutter/check-runs/128620228/annotations" + }, + "name": "Octocoders-linter", + "check_suite": { + "id": 118578147, + "node_id": "MDEwOkNoZWNrU3VpdGUxMTg1NzgxNDc=", + "head_branch": "changes", + "head_sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "status": "queued", + "conclusion": null, + "url": "https://api.github.com/repos/flutter/flutter/check-suites/118578147", + "before": "6113728f27ae82c7b1a177c8d03f9e96e0adf246", + "after": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "pull_requests": [ + { + "url": "https://api.github.com/repos/flutter/flutter/pulls/2", + "id": 279147437, + "number": 2, + "head": { + "ref": "changes", + "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "repo": { + "id": 186853002, + "url": "https://api.github.com/repos/flutter/flutter", + "name": "flutter" + } + }, + "base": { + "ref": "master", + "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", + "repo": { + "id": 186853002, + "url": "https://api.github.com/repos/flutter/flutter", + "name": "flutter" + } + } + } + ], + "app": { + "id": 29310, + "node_id": "MDM6QXBwMjkzMTA=", + "owner": { + "login": "Octocoders", + "id": 38302899, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM4MzAyODk5", + "avatar_url": "https://avatars1.githubusercontent.com/u/38302899?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Octocoders", + "html_url": "https://github.com/Octocoders", + "followers_url": "https://api.github.com/users/Octocoders/followers", + "following_url": "https://api.github.com/users/Octocoders/following{/other_user}", + "gists_url": "https://api.github.com/users/Octocoders/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Octocoders/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Octocoders/subscriptions", + "organizations_url": "https://api.github.com/users/Octocoders/orgs", + "repos_url": "https://api.github.com/users/Octocoders/repos", + "events_url": "https://api.github.com/users/Octocoders/events{/privacy}", + "received_events_url": "https://api.github.com/users/Octocoders/received_events", + "type": "Organization", + "site_admin": false + }, + "name": "octocoders-linter", + "description": "", + "external_url": "https://octocoders.io", + "html_url": "https://github.com/apps/octocoders-linter", + "created_at": "2019-04-19T19:36:24Z", + "updated_at": "2019-04-19T19:36:56Z", + "permissions": { + "administration": "write", + "checks": "write", + "contents": "write", + "deployments": "write", + "issues": "write", + "members": "write", + "metadata": "read", + "organization_administration": "write", + "organization_hooks": "write", + "organization_plan": "read", + "organization_projects": "write", + "organization_user_blocking": "write", + "pages": "write", + "pull_requests": "write", + "repository_hooks": "write", + "repository_projects": "write", + "statuses": "write", + "team_discussions": "write", + "vulnerability_alerts": "read" + }, + "events": [] + }, + "created_at": "2019-05-15T15:20:31Z", + "updated_at": "2019-05-15T15:20:31Z" + }, + "app": { + "id": 29310, + "node_id": "MDM6QXBwMjkzMTA=", + "owner": { + "login": "Octocoders", + "id": 38302899, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjM4MzAyODk5", + "avatar_url": "https://avatars1.githubusercontent.com/u/38302899?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Octocoders", + "html_url": "https://github.com/Octocoders", + "followers_url": "https://api.github.com/users/Octocoders/followers", + "following_url": "https://api.github.com/users/Octocoders/following{/other_user}", + "gists_url": "https://api.github.com/users/Octocoders/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Octocoders/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Octocoders/subscriptions", + "organizations_url": "https://api.github.com/users/Octocoders/orgs", + "repos_url": "https://api.github.com/users/Octocoders/repos", + "events_url": "https://api.github.com/users/Octocoders/events{/privacy}", + "received_events_url": "https://api.github.com/users/Octocoders/received_events", + "type": "Organization", + "site_admin": false + }, + "name": "octocoders-linter", + "description": "", + "external_url": "https://octocoders.io", + "html_url": "https://github.com/apps/octocoders-linter", + "created_at": "2019-04-19T19:36:24Z", + "updated_at": "2019-04-19T19:36:56Z", + "permissions": { + "administration": "write", + "checks": "write", + "contents": "write", + "deployments": "write", + "issues": "write", + "members": "write", + "metadata": "read", + "organization_administration": "write", + "organization_hooks": "write", + "organization_plan": "read", + "organization_projects": "write", + "organization_user_blocking": "write", + "pages": "write", + "pull_requests": "write", + "repository_hooks": "write", + "repository_projects": "write", + "statuses": "write", + "team_discussions": "write", + "vulnerability_alerts": "read" + }, + "events": [] + }, + "pull_requests": ['''; + + for (var i = 0; i < numberOfPullRequests; i++) { + data += + '''{ + "url": "https://api.github.com/repos/flutter/flutter/pulls/2", + "id": 279147437, + "number": ${i + 2}, + "head": { + "ref": "changes", + "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "repo": { + "id": 186853002, + "url": "https://api.github.com/repos/flutter/flutter", + "name": "flutter" + } + }, + "base": { + "ref": "master", + "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", + "repo": { + "id": 186853002, + "url": "https://api.github.com/repos/flutter/flutter", + "name": "flutter" + } + } + }'''; + if (i < numberOfPullRequests - 1) { + data += ','; + } + } + data += '''], + "deployment": { + "url": "https://api.github.com/repos/flutter/flutter/deployments/326191728", + "id": 326191728, + "node_id": "MDEwOkRlcGxveW1lbnQzMjYxOTE3Mjg=", + "task": "deploy", + "original_environment": "lab", + "environment": "lab", + "description": null, + "created_at": "2021-02-18T08:22:48Z", + "updated_at": "2021-02-18T09:47:16Z", + "statuses_url": "https://api.github.com/repos/flutter/flutter/deployments/326191728/statuses", + "repository_url": "https://api.github.com/repos/flutter/flutter" + } + }, + "repository": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "flutter", + "full_name": "flutter/flutter", + "private": false, + "owner": { + "login": "flutter", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/flutter/flutter", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/flutter/flutter", + "forks_url": "https://api.github.com/repos/flutter/flutter/forks", + "keys_url": "https://api.github.com/repos/flutter/flutter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/flutter/flutter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/flutter/flutter/teams", + "hooks_url": "https://api.github.com/repos/flutter/flutter/hooks", + "issue_events_url": "https://api.github.com/repos/flutter/flutter/issues/events{/number}", + "events_url": "https://api.github.com/repos/flutter/flutter/events", + "assignees_url": "https://api.github.com/repos/flutter/flutter/assignees{/user}", + "branches_url": "https://api.github.com/repos/flutter/flutter/branches{/branch}", + "tags_url": "https://api.github.com/repos/flutter/flutter/tags", + "blobs_url": "https://api.github.com/repos/flutter/flutter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/flutter/flutter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/flutter/flutter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/flutter/flutter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/flutter/flutter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/flutter/flutter/languages", + "stargazers_url": "https://api.github.com/repos/flutter/flutter/stargazers", + "contributors_url": "https://api.github.com/repos/flutter/flutter/contributors", + "subscribers_url": "https://api.github.com/repos/flutter/flutter/subscribers", + "subscription_url": "https://api.github.com/repos/flutter/flutter/subscription", + "commits_url": "https://api.github.com/repos/flutter/flutter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/flutter/flutter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/flutter/flutter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/flutter/flutter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/flutter/flutter/contents/{+path}", + "compare_url": "https://api.github.com/repos/flutter/flutter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/flutter/flutter/merges", + "archive_url": "https://api.github.com/repos/flutter/flutter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/flutter/flutter/downloads", + "issues_url": "https://api.github.com/repos/flutter/flutter/issues{/number}", + "pulls_url": "https://api.github.com/repos/flutter/flutter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/flutter/flutter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/flutter/flutter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/flutter/flutter/labels{/name}", + "releases_url": "https://api.github.com/repos/flutter/flutter/releases{/id}", + "deployments_url": "https://api.github.com/repos/flutter/flutter/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:21:03Z", + "pushed_at": "2019-05-15T15:20:57Z", + "git_url": "git://github.com/flutter/flutter.git", + "ssh_url": "git@github.com:flutter/flutter.git", + "clone_url": "https://github.com/flutter/flutter.git", + "svn_url": "https://github.com/flutter/flutter", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 1, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "sender": { + "login": "flutter", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/flutter", + "html_url": "https://github.com/flutter", + "followers_url": "https://api.github.com/users/flutter/followers", + "following_url": "https://api.github.com/users/flutter/following{/other_user}", + "gists_url": "https://api.github.com/users/flutter/gists{/gist_id}", + "starred_url": "https://api.github.com/users/flutter/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/flutter/subscriptions", + "organizations_url": "https://api.github.com/users/flutter/orgs", + "repos_url": "https://api.github.com/users/flutter/repos", + "events_url": "https://api.github.com/users/flutter/events{/privacy}", + "received_events_url": "https://api.github.com/users/flutter/received_events", + "type": "User", + "site_admin": false + } +}'''; + final message = pb.GithubWebhookMessage(event: 'check_run', payload: data); + return PushMessage(data: message.writeToJson(), messageId: 'abc123'); +} + +PushMessage generateCreateBranchMessage( + String branchName, + String repository, { + bool forked = false, +}) { + final createEvent = generateCreateBranchEvent( + branchName, + repository, + forked: forked, + ); + final message = pb.GithubWebhookMessage( + event: 'create', + payload: jsonEncode(createEvent), + ); + return PushMessage(data: message.writeToJson(), messageId: 'abc123'); +} + +CreateEvent generateCreateBranchEvent( + String branchName, + String repository, { + bool forked = false, +}) => CreateEvent.fromJson( + jsonDecode(''' +{ + "ref": "$branchName", + "ref_type": "branch", + "master_branch": "master", + "description": null, + "pusher_type": "user", + "repository": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "${repository.split('/')[1]}", + "full_name": "$repository", + "private": false, + "owner": { + "login": "${repository.split('/')[0]}", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/$repository", + "description": null, + "fork": $forked, + "url": "https://api.github.com/repos/$repository", + "forks_url": "https://api.github.com/repos/$repository/forks", + "keys_url": "https://api.github.com/repos/$repository/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/$repository/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/$repository/teams", + "hooks_url": "https://api.github.com/repos/$repository/hooks", + "issue_events_url": "https://api.github.com/repos/$repository/issues/events{/number}", + "events_url": "https://api.github.com/repos/$repository/events", + "assignees_url": "https://api.github.com/repos/$repository/assignees{/user}", + "branches_url": "https://api.github.com/repos/$repository/branches{/branch}", + "tags_url": "https://api.github.com/repos/$repository/tags", + "blobs_url": "https://api.github.com/repos/$repository/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/$repository/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/$repository/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/$repository/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/$repository/statuses/{sha}", + "languages_url": "https://api.github.com/repos/$repository/languages", + "stargazers_url": "https://api.github.com/repos/$repository/stargazers", + "contributors_url": "https://api.github.com/repos/$repository/contributors", + "subscribers_url": "https://api.github.com/repos/$repository/subscribers", + "subscription_url": "https://api.github.com/repos/$repository/subscription", + "commits_url": "https://api.github.com/repos/$repository/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/$repository/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/$repository/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/$repository/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/$repository/contents/{+path}", + "compare_url": "https://api.github.com/repos/$repository/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/$repository/merges", + "archive_url": "https://api.github.com/repos/$repository/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/$repository/downloads", + "issues_url": "https://api.github.com/repos/$repository/issues{/number}", + "pulls_url": "https://api.github.com/repos/$repository/pulls{/number}", + "milestones_url": "https://api.github.com/repos/$repository/milestones{/number}", + "notifications_url": "https://api.github.com/repos/$repository/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/$repository/labels{/name}", + "releases_url": "https://api.github.com/repos/$repository/releases{/id}", + "deployments_url": "https://api.github.com/repos/$repository/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:20:41Z", + "pushed_at": "2019-05-15T15:20:56Z", + "git_url": "git://github.com/$repository.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/$repository.git", + "svn_url": "https://github.com/$repository", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 1, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "sender": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + } +}''') + as Map, +); + +PushMessage generatePushMessage( + String branch, + String organization, + String repository, +) { + final event = generatePushEvent(branch, organization, repository); + final message = pb.GithubWebhookMessage( + event: 'push', + payload: jsonEncode(event), + ); + return PushMessage(data: message.writeToJson(), messageId: 'abc123'); +} + +Map generatePushEvent( + String branch, + String organization, + String repository, { + String sha = 'def456def456def456', + String message = 'Commit-message', + String avatarUrl = 'https://fakegithubcontent.com/google_profile', + String username = 'googledotcom', +}) => + jsonDecode(''' +{ + "ref": "refs/heads/$branch", + "before": "abc123abc123abc123", + "after": "$sha", + "sender": { + "login": "$username", + "avatar_url": "$avatarUrl" + }, + "commits": [ + { + "id": "ba2f6608108d174c4a6e6e093a4ddcf313656748", + "message": "Adding null safety", + "timestamp": "2023-09-05T15:01:04-05:00", + "url": "https://github.com/org/repo/commit/abc123abc123abc123" + } + ], + "head_commit": { + "id": "$sha", + "message": "$message", + "timestamp": "2023-09-05T15:01:04-05:00", + "url": "https://github.com/org/repo/commit/abc123abc123abc123" + }, + "repository": { + "name": "$repository", + "full_name": "$organization/$repository" + } +} +''') + as Map; + +PushMessage generateMergeGroupMessage({ + required String repository, + required String action, + required String message, + DateTime? publishTime, + String? reason, + String? headSha, + String? headRef, +}) { + if (action == 'destroyed' && + !MergeGroupEvent.destroyReasons.contains(reason)) { + fail( + 'Invalid reason "$reason" for merge group "destroyed" event. The reason ' + 'must be one of: ${MergeGroupEvent.destroyReasons}', + ); + } + final webhookMessage = pb.GithubWebhookMessage( + event: 'merge_group', + payload: generateMergeGroupEventString( + action: action, + message: message, + repository: repository, + reason: reason, + headSha: headSha, + headRef: headRef, + ), + ); + publishTime ??= DateTime.now(); + return PushMessage( + data: webhookMessage.writeToJson(), + messageId: 'abc123', + publishTime: publishTime.toUtc().toIso8601String(), + ); +} + +String generateMergeGroupEventString({ + required String action, + required String message, + required String repository, + String? headSha, + String? headRef, + String? reason, +}) { + headSha ??= 'c9affbbb12aa40cb3afbe94b9ea6b119a256bebf'; + headRef ??= 'refs/heads/gh-readonly-queue/main/pr-15-$headSha'; + return ''' +{ +"action": "$action", +${reason != null ? '"reason": "$reason",' : ''} +"merge_group": { + "head_sha": "$headSha", + "head_ref": "$headRef", + "base_sha": "172355550dde5881b0269972ea4cbe5a6d0561bc", + "base_ref": "refs/heads/main", + "head_commit": { + "id": "c9affbbb12aa40cb3afbe94b9ea6b119a256bebf", + "tree_id": "556b9a8db18c974738d9d5e15988ae9a67e96b91", + "message": "$message", + "timestamp": "2024-10-15T20:24:16Z", + "author": { + "name": "John Doe", + "email": "johndoe@example.org" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + } +}, +"repository": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "${repository.split('/')[1]}", + "full_name": "$repository", + "private": false, + "owner": { + "login": "${repository.split('/')[0]}", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/$repository", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/$repository", + "forks_url": "https://api.github.com/repos/$repository/forks", + "keys_url": "https://api.github.com/repos/$repository/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/$repository/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/$repository/teams", + "hooks_url": "https://api.github.com/repos/$repository/hooks", + "issue_events_url": "https://api.github.com/repos/$repository/issues/events{/number}", + "events_url": "https://api.github.com/repos/$repository/events", + "assignees_url": "https://api.github.com/repos/$repository/assignees{/user}", + "branches_url": "https://api.github.com/repos/$repository/branches{/branch}", + "tags_url": "https://api.github.com/repos/$repository/tags", + "blobs_url": "https://api.github.com/repos/$repository/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/$repository/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/$repository/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/$repository/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/$repository/statuses/{sha}", + "languages_url": "https://api.github.com/repos/$repository/languages", + "stargazers_url": "https://api.github.com/repos/$repository/stargazers", + "contributors_url": "https://api.github.com/repos/$repository/contributors", + "subscribers_url": "https://api.github.com/repos/$repository/subscribers", + "subscription_url": "https://api.github.com/repos/$repository/subscription", + "commits_url": "https://api.github.com/repos/$repository/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/$repository/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/$repository/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/$repository/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/$repository/contents/{+path}", + "compare_url": "https://api.github.com/repos/$repository/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/$repository/merges", + "archive_url": "https://api.github.com/repos/$repository/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/$repository/downloads", + "issues_url": "https://api.github.com/repos/$repository/issues{/number}", + "pulls_url": "https://api.github.com/repos/$repository/pulls{/number}", + "milestones_url": "https://api.github.com/repos/$repository/milestones{/number}", + "notifications_url": "https://api.github.com/repos/$repository/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/$repository/labels{/name}", + "releases_url": "https://api.github.com/repos/$repository/releases{/id}", + "deployments_url": "https://api.github.com/repos/$repository/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:20:41Z", + "pushed_at": "2019-05-15T15:20:56Z", + "git_url": "git://github.com/$repository.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/$repository.git", + "svn_url": "https://github.com/$repository", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 1, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" +}, +"organization": { + "login": "flutter", + "id": 14101776, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MTAxNzc2", + "url": "https://api.github.com/orgs/flutter", + "repos_url": "https://api.github.com/orgs/flutter/repos", + "events_url": "https://api.github.com/orgs/flutter/events", + "hooks_url": "https://api.github.com/orgs/flutter/hooks", + "issues_url": "https://api.github.com/orgs/flutter/issues", + "members_url": "https://api.github.com/orgs/flutter/members{/member}", + "public_members_url": "https://api.github.com/orgs/flutter/public_members{/member}", + "avatar_url": "https://avatars.githubusercontent.com/u/14101776?v=4", + "description": "Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, desktop, and embedded devices from a single codebase." +}, +"enterprise": { + "id": 1732, + "slug": "alphabet", + "name": "Alphabet", + "node_id": "MDEwOkVudGVycHJpc2UxNzMy", + "avatar_url": "https://avatars.githubusercontent.com/b/1732?v=4", + "description": "", + "website_url": "https://abc.xyz/", + "html_url": "https://github.com/enterprises/alphabet", + "created_at": "2019-12-19T00:30:52Z", + "updated_at": "2024-07-18T11:54:37Z" +}, +"sender": { + "login": "johndoe", + "id": 1924313, + "node_id": "MDQ6VXNlcjE5MjQzMTM=", + "avatar_url": "https://avatars.githubusercontent.com/u/1924313?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/johndoe", + "html_url": "https://github.com/johndoe", + "followers_url": "https://api.github.com/users/johndoe/followers", + "following_url": "https://api.github.com/users/johndoe/following{/other_user}", + "gists_url": "https://api.github.com/users/johndoe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/johndoe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/johndoe/subscriptions", + "organizations_url": "https://api.github.com/users/johndoe/orgs", + "repos_url": "https://api.github.com/users/johndoe/repos", + "events_url": "https://api.github.com/users/johndoe/events{/privacy}", + "received_events_url": "https://api.github.com/users/johndoe/received_events", + "type": "User", + "site_admin": false +}, +"installation": { + "id": 10381585, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTAzODE1ODU=" +} +} +'''; +} diff --git a/packages/cocoon_integration_test/lib/testing.dart b/packages/cocoon_integration_test/lib/testing.dart new file mode 100644 index 0000000000..767ae23221 --- /dev/null +++ b/packages/cocoon_integration_test/lib/testing.dart @@ -0,0 +1,39 @@ +// Copyright 2026 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. + +export 'src/fakes/fake_auth_client.dart'; +export 'src/fakes/fake_big_query_service.dart'; +export 'src/fakes/fake_build_bucket_client.dart'; +export 'src/fakes/fake_build_status_service.dart'; +export 'src/fakes/fake_cache_service.dart'; +export 'src/fakes/fake_ci_yaml_fetcher.dart'; +export 'src/fakes/fake_config.dart'; +export 'src/fakes/fake_content_aware_hash_service.dart'; +export 'src/fakes/fake_dashboard_authentication.dart'; +export 'src/fakes/fake_entry.dart'; +export 'src/fakes/fake_firebase_jwt_validator.dart'; +export 'src/fakes/fake_firestore_service.dart'; + +export 'src/fakes/fake_gerrit_service.dart'; +export 'src/fakes/fake_get_files_changed.dart'; +export 'src/fakes/fake_github_service.dart'; +export 'src/fakes/fake_graphql_client.dart'; +export 'src/fakes/fake_http.dart'; +export 'src/fakes/fake_luci_build_service.dart'; +export 'src/fakes/fake_pubsub.dart'; +export 'src/fakes/fake_request_handler.dart'; +export 'src/fakes/fake_scheduler.dart'; +export 'src/fakes/fake_tabledata_resource.dart'; + +export 'src/integration_http_client.dart'; +export 'src/model/check_run_matcher.dart'; +export 'src/model/ci_yaml_matcher.dart'; +export 'src/model/firestore_matcher.dart'; +export 'src/model/ref_matcher.dart'; + +export 'src/utilities/build_bucket_messages.dart'; +export 'src/utilities/entity_generators.dart'; +export 'src/utilities/matchers.dart'; +export 'src/utilities/mocks.dart'; +export 'src/utilities/webhook_generators.dart'; diff --git a/packages/cocoon_integration_test/pubspec.yaml b/packages/cocoon_integration_test/pubspec.yaml new file mode 100644 index 0000000000..534e7d13b9 --- /dev/null +++ b/packages/cocoon_integration_test/pubspec.yaml @@ -0,0 +1,46 @@ +name: cocoon_integration_test +description: Integration testing environment for Cocoon. +version: 1.0.0 +publish_to: none + +resolution: workspace + +environment: + sdk: ^3.10.8 + +dependencies: + appengine: ^0.13.11 + buildbucket: + path: ../buildbucket-dart + cocoon_common: + path: ../cocoon_common + cocoon_server: + path: ../cocoon_server + cocoon_server_test: + path: ../cocoon_server_test + cocoon_service: + path: ../../app_dart + collection: ^1.19.1 + fixnum: 1.1.1 + github: 9.25.0 + googleapis: 14.0.0 + googleapis_auth: ^2.0.0 + gql: ^1.0.1 + graphql: ^5.2.3 + http: ^1.2.1 + jose_plus: ^0.4.7 + json_annotation: ^4.9.0 + logging: ^1.3.0 + meta: ^1.16.0 + mockito: ^5.6.3 + neat_cache: ^2.0.5 + path: ^1.9.1 + process: ^5.0.5 + retry: ^3.1.2 + test: ^1.26.3 + yaml: ^3.1.3 + +dev_dependencies: + build_runner: ^2.4.15 + json_serializable: ^6.9.4 + lints: ^6.0.0 \ No newline at end of file diff --git a/app_dart/test/src/service/fake_firestore_service_test.dart b/packages/cocoon_integration_test/test/fake_firestore_service_test.dart similarity index 99% rename from app_dart/test/src/service/fake_firestore_service_test.dart rename to packages/cocoon_integration_test/test/fake_firestore_service_test.dart index afe4e95d07..5fbca6b4a6 100644 --- a/app_dart/test/src/service/fake_firestore_service_test.dart +++ b/packages/cocoon_integration_test/test/fake_firestore_service_test.dart @@ -2,14 +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_integration_test/testing.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/service/firestore.dart'; import 'package:googleapis/firestore/v1.dart' as g; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'fake_firestore_service.dart'; - void main() { useTestLoggerPerTest(); diff --git a/packages/cocoon_integration_test/test/server_test.dart b/packages/cocoon_integration_test/test/server_test.dart new file mode 100644 index 0000000000..2663ce3f0e --- /dev/null +++ b/packages/cocoon_integration_test/test/server_test.dart @@ -0,0 +1,14 @@ +// Copyright 2026 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:cocoon_integration_test/cocoon_integration_test.dart'; +import 'package:test/test.dart'; + +void main() { + test('IntegrationServer starts', () async { + final server = IntegrationServer(); + expect(server.server, isNotNull); + expect(server.config, isNotNull); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index d5f1bb74ac..aa93cd6c0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ workspace: - packages/cocoon_common_test - packages/cocoon_server - packages/cocoon_server_test + - packages/cocoon_integration_test - dev/cocoon_code_health - dev/githubanalysis