Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
1c35091a
Unverified
Commit
1c35091a
authored
Oct 19, 2020
by
Casey Hillers
Committed by
GitHub
Oct 19, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[devicelab] Cocoon client (#68333)
parent
e8dc7a2e
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
302 additions
and
6 deletions
+302
-6
run.dart
dev/devicelab/bin/run.dart
+22
-6
cocoon.dart
dev/devicelab/lib/framework/cocoon.dart
+152
-0
utils.dart
dev/devicelab/lib/framework/utils.dart
+31
-0
cocoon_test.dart
dev/devicelab/test/cocoon_test.dart
+88
-0
run_test.dart
dev/devicelab/test/run_test.dart
+9
-0
No files found.
dev/devicelab/bin/run.dart
View file @
1c35091a
...
@@ -9,6 +9,7 @@ import 'package:args/args.dart';
...
@@ -9,6 +9,7 @@ import 'package:args/args.dart';
import
'package:path/path.dart'
as
path
;
import
'package:path/path.dart'
as
path
;
import
'package:flutter_devicelab/framework/ab.dart'
;
import
'package:flutter_devicelab/framework/ab.dart'
;
import
'package:flutter_devicelab/framework/cocoon.dart'
;
import
'package:flutter_devicelab/framework/manifest.dart'
;
import
'package:flutter_devicelab/framework/manifest.dart'
;
import
'package:flutter_devicelab/framework/runner.dart'
;
import
'package:flutter_devicelab/framework/runner.dart'
;
import
'package:flutter_devicelab/framework/task_result.dart'
;
import
'package:flutter_devicelab/framework/task_result.dart'
;
...
@@ -18,8 +19,8 @@ ArgResults args;
...
@@ -18,8 +19,8 @@ ArgResults args;
List
<
String
>
_taskNames
=
<
String
>[];
List
<
String
>
_taskNames
=
<
String
>[];
///
Suppresses standard output, prints only standard error output
.
///
The device-id to run test on
.
bool
silent
;
String
deviceId
;
/// The build of the local engine to use.
/// The build of the local engine to use.
///
///
...
@@ -32,8 +33,13 @@ String localEngineSrcPath;
...
@@ -32,8 +33,13 @@ String localEngineSrcPath;
/// Whether to exit on first test failure.
/// Whether to exit on first test failure.
bool
exitOnFirstTestFailure
;
bool
exitOnFirstTestFailure
;
/// The device-id to run test on.
/// File containing a service account token.
String
deviceId
;
///
/// If passed, the test run results will be uploaded to Flutter infrastructure.
String
serviceAccountTokenFile
;
/// Suppresses standard output, prints only standard error output.
bool
silent
;
/// Runs tasks.
/// Runs tasks.
///
///
...
@@ -74,11 +80,12 @@ Future<void> main(List<String> rawArgs) async {
...
@@ -74,11 +80,12 @@ Future<void> main(List<String> rawArgs) async {
return
;
return
;
}
}
silent
=
args
[
'silent'
]
as
bool
;
deviceId
=
args
[
'device-id'
]
as
String
;
localEngine
=
args
[
'local-engine'
]
as
String
;
localEngine
=
args
[
'local-engine'
]
as
String
;
localEngineSrcPath
=
args
[
'local-engine-src-path'
]
as
String
;
localEngineSrcPath
=
args
[
'local-engine-src-path'
]
as
String
;
exitOnFirstTestFailure
=
args
[
'exit'
]
as
bool
;
exitOnFirstTestFailure
=
args
[
'exit'
]
as
bool
;
deviceId
=
args
[
'device-id'
]
as
String
;
serviceAccountTokenFile
=
args
[
'service-account-token-file'
]
as
String
;
silent
=
args
[
'silent'
]
as
bool
;
if
(
args
.
wasParsed
(
'ab'
))
{
if
(
args
.
wasParsed
(
'ab'
))
{
await
_runABTest
();
await
_runABTest
();
...
@@ -102,6 +109,11 @@ Future<void> _runTasks() async {
...
@@ -102,6 +109,11 @@ Future<void> _runTasks() async {
print
(
const
JsonEncoder
.
withIndent
(
' '
).
convert
(
result
));
print
(
const
JsonEncoder
.
withIndent
(
' '
).
convert
(
result
));
section
(
'Finished task "
$taskName
"'
);
section
(
'Finished task "
$taskName
"'
);
if
(
serviceAccountTokenFile
!=
null
)
{
final
Cocoon
cocoon
=
Cocoon
(
serviceAccountTokenPath:
serviceAccountTokenFile
);
await
cocoon
.
sendTaskResult
(
taskName:
taskName
,
result:
result
);
}
if
(!
result
.
succeeded
)
{
if
(!
result
.
succeeded
)
{
exitCode
=
1
;
exitCode
=
1
;
if
(
exitOnFirstTestFailure
)
{
if
(
exitOnFirstTestFailure
)
{
...
@@ -334,6 +346,10 @@ final ArgParser _argParser = ArgParser()
...
@@ -334,6 +346,10 @@ final ArgParser _argParser = ArgParser()
'on a windows host). Each test publishes its '
'on a windows host). Each test publishes its '
'`required_agent_capabilities`
\n
in the `manifest.yaml` file.'
,
'`required_agent_capabilities`
\n
in the `manifest.yaml` file.'
,
)
)
..
addOption
(
'service-account-token-file'
,
help:
'[Flutter infrastructure] Authentication for uploading results.'
,
)
..
addOption
(
..
addOption
(
'stage'
,
'stage'
,
abbr:
's'
,
abbr:
's'
,
...
...
dev/devicelab/lib/framework/cocoon.dart
0 → 100644
View file @
1c35091a
// Copyright 2014 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:convert'
show
json
;
import
'dart:io'
;
import
'package:file/file.dart'
;
import
'package:file/local.dart'
;
import
'package:http/http.dart'
;
import
'package:logging/logging.dart'
;
import
'package:meta/meta.dart'
;
import
'task_result.dart'
;
import
'utils.dart'
;
/// Class for test runner to interact with Flutter's infrastructure service, Cocoon.
///
/// Cocoon assigns bots to run these devicelab tasks on real devices.
/// To retrieve these results, the test runner needs to send results back so so the database can be updated.
class
Cocoon
{
Cocoon
({
String
serviceAccountTokenPath
,
@visibleForTesting
Client
httpClient
,
@visibleForTesting
FileSystem
filesystem
,
})
:
_httpClient
=
AuthenticatedCocoonClient
(
serviceAccountTokenPath
,
httpClient:
httpClient
,
filesystem:
filesystem
);
/// Client to make http requests to Cocoon.
final
AuthenticatedCocoonClient
_httpClient
;
/// Url used to send results to.
static
const
String
baseCocoonApiUrl
=
'https://flutter-dashboard.appspot.com/api'
;
static
final
Logger
logger
=
Logger
(
'CocoonClient'
);
String
get
commitSha
=>
_commitSha
??
_readCommitSha
();
String
_commitSha
;
/// Parse the local repo for the current running commit.
String
_readCommitSha
()
{
final
ProcessResult
result
=
Process
.
runSync
(
'git'
,
<
String
>[
'rev-parse'
,
'HEAD'
]);
if
(
result
.
exitCode
!=
0
)
{
throw
Exception
(
result
.
stderr
);
}
_commitSha
=
result
.
stdout
as
String
;
return
_commitSha
;
}
/// Send [TaskResult] to Cocoon.
Future
<
void
>
sendTaskResult
({
String
taskName
,
TaskResult
result
})
async
{
// Skip logging on test runs
Logger
.
root
.
level
=
Level
.
ALL
;
Logger
.
root
.
onRecord
.
listen
((
LogRecord
rec
)
{
print
(
'
${rec.level.name}
:
${rec.time}
:
${rec.message}
'
);
});
final
Map
<
String
,
dynamic
>
status
=
<
String
,
dynamic
>{
'CommitSha'
:
commitSha
,
'TaskName'
:
taskName
,
'NewStatus'
:
result
.
succeeded
?
'Succeeded'
:
'Failed'
,
};
// Make a copy of result data because we may alter it for validation below.
status
[
'ResultData'
]
=
result
.
data
;
final
List
<
String
>
validScoreKeys
=
<
String
>[];
if
(
result
.
benchmarkScoreKeys
!=
null
)
{
for
(
final
String
scoreKey
in
result
.
benchmarkScoreKeys
)
{
final
Object
score
=
result
.
data
[
scoreKey
];
if
(
score
is
num
)
{
// Convert all metrics to double, which provide plenty of precision
// without having to add support for multiple numeric types in Cocoon.
result
.
data
[
scoreKey
]
=
score
.
toDouble
();
validScoreKeys
.
add
(
scoreKey
);
}
}
}
status
[
'BenchmarkScoreKeys'
]
=
validScoreKeys
;
final
Map
<
String
,
dynamic
>
response
=
await
_sendCocoonRequest
(
'update-task-status'
,
status
);
if
(
response
[
'Name'
]
!=
null
)
{
logger
.
info
(
'Updated Cocoon with results from this task'
);
}
else
{
logger
.
info
(
response
);
logger
.
severe
(
'Failed to updated Cocoon with results from this task'
);
}
}
/// Make an API request to Cocoon.
Future
<
Map
<
String
,
dynamic
>>
_sendCocoonRequest
(
String
apiPath
,
[
dynamic
jsonData
])
async
{
final
String
url
=
'
$baseCocoonApiUrl
/
$apiPath
'
;
/// Retry requests to Cocoon as sometimes there are issues with the servers, such
/// as version changes to the backend, datastore issues, or latency issues.
final
Response
response
=
await
retry
(
()
=>
_httpClient
.
post
(
url
,
body:
json
.
encode
(
jsonData
)),
retryIf:
(
Exception
e
)
=>
e
is
SocketException
||
e
is
TimeoutException
||
e
is
ClientException
,
maxAttempts:
5
,
);
return
json
.
decode
(
response
.
body
)
as
Map
<
String
,
dynamic
>;
}
}
/// [HttpClient] for sending authenticated requests to Cocoon.
class
AuthenticatedCocoonClient
extends
BaseClient
{
AuthenticatedCocoonClient
(
this
.
_serviceAccountTokenPath
,
{
@visibleForTesting
Client
httpClient
,
@visibleForTesting
FileSystem
filesystem
,
})
:
_delegate
=
httpClient
??
Client
(),
_fs
=
filesystem
??
const
LocalFileSystem
();
/// Authentication token to have the ability to upload and record test results.
///
/// This is intended to only be passed on automated runs on LUCI post-submit.
final
String
_serviceAccountTokenPath
;
/// Underlying [HttpClient] to send requests to.
final
Client
_delegate
;
/// Underlying [FileSystem] to use.
final
FileSystem
_fs
;
/// Value contained in the service account token file that can be used in http requests.
String
get
serviceAccountToken
=>
_serviceAccountToken
??
_readServiceAccountTokenFile
();
String
_serviceAccountToken
;
/// Get [serviceAccountToken] from the given service account file.
String
_readServiceAccountTokenFile
()
{
return
_serviceAccountToken
=
_fs
.
file
(
_serviceAccountTokenPath
).
readAsStringSync
().
trim
();
}
@override
Future
<
StreamedResponse
>
send
(
BaseRequest
request
)
async
{
request
.
headers
[
'Service-Account-Token'
]
=
serviceAccountToken
;
final
StreamedResponse
response
=
await
_delegate
.
send
(
request
);
if
(
response
.
statusCode
!=
200
)
{
throw
ClientException
(
'AuthenticatedClientError:
\n
'
' URI:
${request.url}
\n
'
' HTTP Status:
${response.statusCode}
\n
'
' Response body:
\n
'
'
${(await Response.fromStream(response)).body}
'
,
request
.
url
);
}
return
response
;
}
}
dev/devicelab/lib/framework/utils.dart
View file @
1c35091a
...
@@ -736,3 +736,34 @@ Future<int> gitClone({String path, String repo}) async {
...
@@ -736,3 +736,34 @@ Future<int> gitClone({String path, String repo}) async {
()
=>
exec
(
'git'
,
<
String
>[
'clone'
,
repo
]),
()
=>
exec
(
'git'
,
<
String
>[
'clone'
,
repo
]),
);
);
}
}
/// Call [fn] retrying so long as [retryIf] return `true` for the exception
/// thrown and [maxAttempts] has not been reached.
///
/// If no [retryIf] function is given this will retry any for any [Exception]
/// thrown. To retry on an [Error], the error must be caught and _rethrown_
/// as an [Exception].
///
/// Waits a constant duration of [delayDuration] between every retry attempt.
Future
<
T
>
retry
<
T
>(
FutureOr
<
T
>
Function
()
fn
,
{
FutureOr
<
bool
>
Function
(
Exception
)
retryIf
,
int
maxAttempts
=
5
,
Duration
delayDuration
=
const
Duration
(
seconds:
3
),
})
async
{
int
attempt
=
0
;
while
(
true
)
{
attempt
++;
// first invocation is the first attempt
try
{
return
await
fn
();
}
on
Exception
catch
(
e
)
{
if
(
attempt
>=
maxAttempts
||
(
retryIf
!=
null
&&
!(
await
retryIf
(
e
))))
{
rethrow
;
}
}
// Sleep for a delay
await
Future
<
void
>.
delayed
(
delayDuration
);
}
}
dev/devicelab/test/cocoon_test.dart
0 → 100644
View file @
1c35091a
// Copyright 2014 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:file/file.dart'
;
import
'package:file/memory.dart'
;
import
'package:http/http.dart'
;
import
'package:http/testing.dart'
;
import
'package:flutter_devicelab/framework/cocoon.dart'
;
import
'package:flutter_devicelab/framework/task_result.dart'
;
import
'common.dart'
;
void
main
(
)
{
group
(
'Cocoon'
,
()
{
const
String
serviceAccountTokenPath
=
'test_account_file'
;
const
String
serviceAccountToken
=
'test_token'
;
Client
mockClient
;
Cocoon
cocoon
;
FileSystem
fs
;
setUp
(()
{
fs
=
MemoryFileSystem
();
final
File
serviceAccountFile
=
fs
.
file
(
serviceAccountTokenPath
)..
createSync
();
serviceAccountFile
.
writeAsStringSync
(
serviceAccountToken
);
});
test
(
'sends expected request from successful task'
,
()
async
{
mockClient
=
MockClient
((
Request
request
)
async
=>
Response
(
'{}'
,
200
));
cocoon
=
Cocoon
(
serviceAccountTokenPath:
serviceAccountTokenPath
,
filesystem:
fs
,
httpClient:
mockClient
,
);
final
TaskResult
result
=
TaskResult
.
success
(<
String
,
dynamic
>{});
// This should not throw an error.
await
cocoon
.
sendTaskResult
(
taskName:
'taskAbc'
,
result:
result
);
});
test
(
'throws client exception on non-200 responses'
,
()
async
{
mockClient
=
MockClient
((
Request
request
)
async
=>
Response
(
''
,
500
));
cocoon
=
Cocoon
(
serviceAccountTokenPath:
serviceAccountTokenPath
,
filesystem:
fs
,
httpClient:
mockClient
,
);
final
TaskResult
result
=
TaskResult
.
success
(<
String
,
dynamic
>{});
expect
(()
=>
cocoon
.
sendTaskResult
(
taskName:
'taskAbc'
,
result:
result
),
throwsA
(
isA
<
ClientException
>()));
});
});
group
(
'AuthenticatedCocoonClient'
,
()
{
const
String
serviceAccountPath
=
'test_account_file'
;
const
String
serviceAccountToken
=
'test_token'
;
FileSystem
fs
;
setUp
(()
{
fs
=
MemoryFileSystem
();
final
File
serviceAccountFile
=
fs
.
file
(
serviceAccountPath
)..
createSync
();
serviceAccountFile
.
writeAsStringSync
(
serviceAccountToken
);
});
test
(
'reads token from service account file'
,
()
{
final
AuthenticatedCocoonClient
client
=
AuthenticatedCocoonClient
(
serviceAccountPath
,
filesystem:
fs
);
expect
(
client
.
serviceAccountToken
,
serviceAccountToken
);
});
test
(
'reads token from service account file with whitespace'
,
()
{
final
File
serviceAccountFile
=
fs
.
file
(
serviceAccountPath
)..
createSync
();
serviceAccountFile
.
writeAsStringSync
(
serviceAccountToken
+
'
\n
'
);
final
AuthenticatedCocoonClient
client
=
AuthenticatedCocoonClient
(
serviceAccountPath
,
filesystem:
fs
);
expect
(
client
.
serviceAccountToken
,
serviceAccountToken
);
});
test
(
'throws error when service account file not found'
,
()
{
final
AuthenticatedCocoonClient
client
=
AuthenticatedCocoonClient
(
'idontexist'
,
filesystem:
fs
);
expect
(()
=>
client
.
serviceAccountToken
,
throwsA
(
isA
<
FileSystemException
>()));
});
});
}
dev/devicelab/test/run_test.dart
View file @
1c35091a
...
@@ -138,5 +138,14 @@ void main() {
...
@@ -138,5 +138,14 @@ void main() {
),
),
);
);
});
});
test
(
'fails to upload results to Cocoon if flags given'
,
()
async
{
// CocoonClient will fail to find test-file, and will not send any http requests.
final
ProcessResult
result
=
await
runScript
(
<
String
>[
'smoke_test_success'
],
<
String
>[
'--service-account-file=test-file'
,
'--task-key=task123'
],
);
expect
(
result
.
exitCode
,
1
);
});
});
});
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment