1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert' show Encoding, json;
7import 'dart:io';
8
9import 'package:file/file.dart';
10import 'package:file/local.dart';
11import 'package:logging/logging.dart';
12import 'package:meta/meta.dart';
13
14import 'task_result.dart';
15
16/// Class for test runner to write device-lab metrics results for Skia Perf.
17interface class MetricsResultWriter {
18 MetricsResultWriter({
19 @visibleForTesting this.fs = const LocalFileSystem(),
20 @visibleForTesting this.processRunSync = Process.runSync,
21 });
22
23 final ProcessResult Function(
24 String,
25 List<String>, {
26 Map<String, String>? environment,
27 bool includeParentEnvironment,
28 bool runInShell,
29 Encoding? stderrEncoding,
30 Encoding? stdoutEncoding,
31 String? workingDirectory,
32 })
33 processRunSync;
34
35 /// Threshold to auto retry a failed test.
36 static const int retryNumber = 2;
37
38 /// Underlying [FileSystem] to use.
39 final FileSystem fs;
40
41 static final Logger logger = Logger('CocoonClient');
42
43 String get commitSha => _commitSha ?? _readCommitSha();
44 String? _commitSha;
45
46 /// Parse the local repo for the current running commit.
47 String _readCommitSha() {
48 final ProcessResult result = processRunSync('git', <String>['rev-parse', 'HEAD']);
49 if (result.exitCode != 0) {
50 throw CocoonException(result.stderr as String);
51 }
52
53 return _commitSha = result.stdout as String;
54 }
55
56 /// Write the given parameters into an update task request and store the JSON in [resultsPath].
57 Future<void> writeTaskResultToFile({
58 String? builderName,
59 String? gitBranch,
60 required TaskResult result,
61 required String resultsPath,
62 }) async {
63 final Map<String, dynamic> updateRequest = _constructUpdateRequest(
64 gitBranch: gitBranch,
65 builderName: builderName,
66 result: result,
67 );
68 final File resultFile = fs.file(resultsPath);
69 if (resultFile.existsSync()) {
70 resultFile.deleteSync();
71 }
72 logger.fine('Writing results: ${json.encode(updateRequest)}');
73 resultFile.createSync();
74 resultFile.writeAsStringSync(json.encode(updateRequest));
75 }
76
77 Map<String, dynamic> _constructUpdateRequest({
78 String? builderName,
79 required TaskResult result,
80 String? gitBranch,
81 }) {
82 final Map<String, dynamic> updateRequest = <String, dynamic>{
83 'CommitBranch': gitBranch,
84 'CommitSha': commitSha,
85 'BuilderName': builderName,
86 'NewStatus': result.succeeded ? 'Succeeded' : 'Failed',
87 };
88 logger.fine('Update request: $updateRequest');
89
90 // Make a copy of result data because we may alter it for validation below.
91 updateRequest['ResultData'] = result.data;
92
93 final List<String> validScoreKeys = <String>[];
94 if (result.benchmarkScoreKeys != null) {
95 for (final String scoreKey in result.benchmarkScoreKeys!) {
96 final Object score = result.data![scoreKey] as Object;
97 if (score is num) {
98 // Convert all metrics to double, which provide plenty of precision
99 // without having to add support for multiple numeric types in Cocoon.
100 result.data![scoreKey] = score.toDouble();
101 validScoreKeys.add(scoreKey);
102 }
103 }
104 }
105 updateRequest['BenchmarkScoreKeys'] = validScoreKeys;
106
107 return updateRequest;
108 }
109}
110
111class CocoonException implements Exception {
112 CocoonException(this.message);
113
114 /// The message to show to the issuer to explain the error.
115 final String message;
116
117 @override
118 String toString() => 'CocoonException: $message';
119}
120