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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app_dart/lib/src/model/common/checks_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,14 @@ extension ChecksExtension on TaskStatus {
_ => .failed,
};
}

/// Converts a [TaskConclusion] to a [TaskStatus].
static TaskStatus fromTaskConclusion(TaskConclusion conclusion) {
return switch (conclusion) {
TaskConclusion.unknown || TaskConclusion.failure => TaskStatus.failed,
TaskConclusion.scheduled => TaskStatus.waitingForBackfill,
TaskConclusion.success => TaskStatus.succeeded,
TaskConclusion.neutral => TaskStatus.neutral,
};
}
}
16 changes: 16 additions & 0 deletions app_dart/lib/src/model/firestore/ci_staging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ final class CiStaging extends AppDocument<CiStaging> {
return CiStaging.fromDocument(document);
}

/// Queries for [CiStaging] records by [slug] and [sha].
///
/// This is used to get both `_engine` and `_fusion` documents.
static Future<List<CiStaging>> getCiStagingForCommitSha({
required FirestoreService firestoreService,
required RepositorySlug slug,
required String sha,
}) async {
final filterMap = {
'$fieldRepoFullPath =': slug.fullName,
'$fieldCommitSha =': sha,
};
final documents = await firestoreService.query(_collectionId, filterMap);
return documents.map(CiStaging.fromDocument).toList();
}

/// Create [CiStaging] from a Commit Document.
CiStaging.fromDocument(Document other) {
this
Expand Down
81 changes: 78 additions & 3 deletions app_dart/lib/src/request_handlers/get_presubmit_guard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';

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/common/checks_extension.dart';
import '../model/firestore/ci_staging.dart';
import '../request_handling/public_api_request_handler.dart';
import '../service/firestore/unified_check_run.dart';

Expand Down Expand Up @@ -95,9 +98,7 @@ final class GetPresubmitGuard extends PublicApiRequestHandler {
);

if (guards.isEmpty) {
return Response.json({
'error': 'No guard found for slug $slug and sha $sha',
}, statusCode: HttpStatus.notFound);
return _getCiStagingFallback(slug, sha);
}

// Consolidate metadata from the first record.
Expand Down Expand Up @@ -134,4 +135,78 @@ final class GetPresubmitGuard extends PublicApiRequestHandler {

return Response.json(response);
}

Future<Response> _getCiStagingFallback(
RepositorySlug slug,
String sha,
) async {
final ciStagings = await CiStaging.getCiStagingForCommitSha(
firestoreService: _firestore,
slug: slug,
sha: sha,
);

if (ciStagings.isEmpty) {
return Response.json({
'error': 'No guard found for slug $slug and sha $sha',
}, statusCode: HttpStatus.notFound);
}

final totalFailed = ciStagings.fold<int>(0, (sum, g) => sum + g.failed);
final totalRemaining = ciStagings.fold<int>(
0,
(sum, g) => sum + g.remaining,
);
final totalBuilds = ciStagings.fold<int>(0, (sum, g) => sum + g.total);

final guardStatus = GuardStatus.calculate(
failedBuilds: totalFailed,
remainingBuilds: totalRemaining,
totalBuilds: totalBuilds,
);

var checkRunId = -1;
final guardJsonStr = ciStagings.first.checkRunGuard;
if (guardJsonStr.isNotEmpty) {
try {
// Try to extract the check-run id from the json string.
final guardJson = jsonDecode(guardJsonStr) as Map<String, Object?>;
checkRunId = guardJson['id'] as int? ?? 0;
} catch (_) {
// ignore
}
}
Comment thread
jtmcdole marked this conversation as resolved.

var prNum = 0;
var author = '';

final prInfo = await PrCheckRuns.findPullRequestForSha(_firestore, sha);
if (prInfo != null) {
prNum = prInfo.number ?? 0;
author = prInfo.user?.login ?? '';
}

final response = rpc_model.PresubmitGuardResponse(
prNum: prNum,
checkRunId: checkRunId,
author: author,
guardStatus: guardStatus,
enableGeminiLogAnalysis: config.flags.enableGeminiLogAnalysis,
stages: [
for (final g in ciStagings)
rpc_model.PresubmitGuardStage(
name: g.stage?.name ?? 'unknown',
createdAt:
DateTime.tryParse(g.createTime ?? '')?.millisecondsSinceEpoch ??
0,
jobs: {
for (final MapEntry(:key, :value) in g.checkRuns.entries)
key: ChecksExtension.fromTaskConclusion(value),
},
),
],
);

return Response.json(response);
}
}
23 changes: 23 additions & 0 deletions app_dart/test/model/common/checks_extension_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ void main() {
TaskStatus.neutral,
);
});

test('fromTaskConclusion mapping', () {
expect(
ChecksExtension.fromTaskConclusion(TaskConclusion.success),
TaskStatus.succeeded,
);
expect(
ChecksExtension.fromTaskConclusion(TaskConclusion.neutral),
TaskStatus.neutral,
);
expect(
ChecksExtension.fromTaskConclusion(TaskConclusion.failure),
TaskStatus.failed,
);
expect(
ChecksExtension.fromTaskConclusion(TaskConclusion.scheduled),
TaskStatus.waitingForBackfill,
);
expect(
ChecksExtension.fromTaskConclusion(TaskConclusion.unknown),
TaskStatus.failed,
);
});
});

group('TaskConclusion', () {
Expand Down
50 changes: 50 additions & 0 deletions app_dart/test/request_handlers/get_presubmit_guard_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ 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/model/firestore/ci_staging.dart';
import 'package:cocoon_service/src/request_handlers/get_presubmit_guard.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/firestore.dart';
import 'package:github/github.dart';
import 'package:googleapis/firestore/v1.dart' hide Status;
import 'package:test/test.dart';

import '../src/request_handling/request_handler_tester.dart';
Expand Down Expand Up @@ -213,4 +216,51 @@ void main() {
final response = await tester.get(handler);
expect(response.statusCode, HttpStatus.ok);
});

test('falls back to ciStaging if no guards found', () async {
final slug = RepositorySlug('flutter', 'flutter');
const sha = 'abc';

final staging = CiStaging.fromDocument(
Document(
name: CiStaging.documentNameFor(
slug: slug,
sha: sha,
stage: CiStage.fusionEngineBuild,
),
fields: {
CiStaging.kTotalField: 2.toValue(),
CiStaging.kRemainingField: 1.toValue(),
CiStaging.kFailedField: 0.toValue(),
CiStaging.kCheckRunGuardField: '{"id": 0}'.toValue(),
CiStaging.fieldRepoFullPath: slug.fullName.toValue(),
CiStaging.fieldCommitSha: sha.toValue(),
CiStaging.fieldStage: CiStage.fusionEngineBuild.name.toValue(),
'job1': TaskConclusion.success.name.toValue(),
'job2': TaskConclusion.scheduled.name.toValue(),
},
),
);

firestore.putDocuments([staging]);

tester.request = FakeHttpRequest(
queryParametersValue: {
GetPresubmitGuard.kOwnerParam: 'flutter',
GetPresubmitGuard.kRepoParam: 'flutter',
GetPresubmitGuard.kShaParam: sha,
},
);

final result = (await getResponse())!;
expect(result.prNum, 0);
expect(result.checkRunId, 0);
expect(result.guardStatus, GuardStatus.inProgress);
expect(result.stages.length, 1);
expect(result.stages.first.name, CiStage.fusionEngineBuild.name);
expect(result.stages.first.jobs, {
'job1': TaskStatus.succeeded,
'job2': TaskStatus.waitingForBackfill,
});
});
}
6 changes: 4 additions & 2 deletions dashboard/test/views/presubmit_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1183,8 +1183,9 @@ void main() {
for (var i = 0; i < 20; i++) {
await tester.pump();
await Future<void>.delayed(const Duration(milliseconds: 50));
if (find.textContaining('linux_analyze').evaluate().isNotEmpty)
if (find.textContaining('linux_analyze').evaluate().isNotEmpty) {
break;
}
}
});
await tester.pump();
Expand Down Expand Up @@ -1243,8 +1244,9 @@ void main() {
for (var i = 0; i < 20; i++) {
await tester.pump();
await Future<void>.delayed(const Duration(milliseconds: 50));
if (find.textContaining('linux_analyze').evaluate().isNotEmpty)
if (find.textContaining('linux_analyze').evaluate().isNotEmpty) {
break;
}
}
});
await tester.pump();
Expand Down
Loading