Browse Source

Code formatting

tags/v5.9.0
Cédric Belin 1 month ago
parent
commit
6642ddc2ca
47 changed files with 1164 additions and 1161 deletions
  1. +6
    -3
      .editorconfig
  2. +2
    -2
      .github/workflows/build.yaml
  3. +5
    -5
      .vscode/settings.json
  4. +1
    -1
      README.md
  5. +1
    -1
      analysis_options.yaml
  6. +29
    -29
      bin/coveralls.dart
  7. +0
    -4
      doc/about/see_also.md
  8. +1
    -1
      doc/features.md
  9. +4
    -4
      doc/index.md
  10. +8
    -8
      doc/installation.md
  11. +15
    -15
      doc/usage/api.md
  12. +4
    -4
      doc/usage/cli.md
  13. +9
    -5
      etc/mkdocs.yaml
  14. +11
    -11
      example/main.dart
  15. +2
    -2
      lib/coveralls.dart
  16. +4
    -4
      lib/src/cli.dart
  17. +3
    -3
      lib/src/cli/options.dart
  18. +4
    -4
      lib/src/cli/usage.dart
  19. +27
    -27
      lib/src/io.dart
  20. +113
    -113
      lib/src/io/client.dart
  21. +157
    -157
      lib/src/io/configuration.dart
  22. +82
    -82
      lib/src/io/git.dart
  23. +39
    -39
      lib/src/io/job.dart
  24. +24
    -24
      lib/src/io/parsers/clover.dart
  25. +26
    -26
      lib/src/io/parsers/lcov.dart
  26. +15
    -15
      lib/src/io/services/appveyor.dart
  27. +8
    -8
      lib/src/io/services/circleci.dart
  28. +7
    -7
      lib/src/io/services/codeship.dart
  29. +11
    -11
      lib/src/io/services/github.dart
  30. +6
    -6
      lib/src/io/services/gitlab_ci.dart
  31. +8
    -8
      lib/src/io/services/jenkins.dart
  32. +6
    -6
      lib/src/io/services/semaphore.dart
  33. +10
    -10
      lib/src/io/services/solano_ci.dart
  34. +4
    -4
      lib/src/io/services/surf.dart
  35. +12
    -12
      lib/src/io/services/travis_ci.dart
  36. +5
    -5
      lib/src/io/services/wercker.dart
  37. +24
    -24
      lib/src/io/source_file.dart
  38. +1
    -1
      pubspec.yaml
  39. +13
    -13
      test/client_test.dart
  40. +95
    -95
      test/configuration_test.dart
  41. +47
    -47
      test/fixtures/clover.xml
  42. +161
    -161
      test/git_test.dart
  43. +57
    -57
      test/job_test.dart
  44. +23
    -23
      test/parsers/clover_test.dart
  45. +23
    -23
      test/parsers/lcov_test.dart
  46. +48
    -48
      test/source_file_test.dart
  47. +3
    -3
      tool/clean.ps1

+ 6
- 3
.editorconfig View File

@@ -3,11 +3,14 @@ root = true

[*]
charset = utf-8
indent_size = 2
indent_style = space
indent_style = tab
insert_final_newline = true
tab_width = 2
trim_trailing_whitespace = true

[*.md]
indent_size = 4
trim_trailing_whitespace = false

[*.{yaml,yml}]
indent_size = 2
indent_style = space

+ 2
- 2
.github/workflows/build.yaml View File

@@ -3,13 +3,13 @@ on:
pull_request:
push:
schedule:
- cron: '0 0 1 * *'
- cron: "0 0 1 * *"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up Dart
uses: cedx/setup-dart@v1
uses: cedx/setup-dart@v2
- name: Check environment
run: |
dart --version


+ 5
- 5
.vscode/settings.json View File

@@ -1,7 +1,7 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
"editor.insertSpaces": false,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
}

+ 1
- 1
README.md View File

@@ -1,5 +1,5 @@
# Coveralls for Dart
![Runtime](https://badgen.net/badge/dart/%3E%3D2.8.0/green) ![Release](https://img.shields.io/pub/v/coveralls.svg) ![License](https://badgen.net/badge/license/MIT/blue) ![Coverage](https://badgen.net/coveralls/c/github/cedx/coveralls.dart) ![Build](https://badgen.net/github/checks/cedx/coveralls.dart)
![Runtime](https://badgen.net/pub/sdk-version/coveralls) ![Release](https://badgen.net/pub/v/coveralls) ![License](https://badgen.net/pub/license/coveralls) ![Likes](https://badgen.net/pub/likes/coveralls) ![Coverage](https://badgen.net/coveralls/c/github/cedx/coveralls.dart) ![Build](https://badgen.net/github/checks/cedx/coveralls.dart)

Send [LCOV](http://ltp.sourceforge.net/coverage/lcov.php) and [Clover](https://www.atlassian.com/software/clover) coverage reports to the [Coveralls](https://coveralls.io) service, in [Dart](https://dart.dev).



+ 1
- 1
analysis_options.yaml View File

@@ -90,6 +90,7 @@ linter:
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_double_quotes
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
@@ -112,7 +113,6 @@ linter:
- prefer_mixin
- prefer_null_aware_operators
- prefer_relative_imports
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null


+ 29
- 29
bin/coveralls.dart View File

@@ -1,41 +1,41 @@
#!/usr/bin/env dart

// ignore_for_file: avoid_print
import 'dart:io';
import 'package:coveralls/coveralls.dart';
import 'package:coveralls/src/cli.dart';
import 'package:coveralls/src/version.dart';
import "dart:io";
import "package:coveralls/coveralls.dart";
import "package:coveralls/src/cli.dart";
import "package:coveralls/src/version.dart";

/// Application entry point.
Future<void> main(List<String> args) async {
// Parse the command line arguments.
Options options;
// Parse the command line arguments.
Options options;

try {
options = parseOptions(args);
if (options.help) return print(usage);
if (options.version) return print(packageVersion);
if (options.rest.isEmpty) throw const FormatException('A coverage report must be provided.');
}
try {
options = parseOptions(args);
if (options.help) return print(usage);
if (options.version) return print(packageVersion);
if (options.rest.isEmpty) throw const FormatException("A coverage report must be provided.");
}

on FormatException {
print(usage);
exitCode = 64;
return;
}
on FormatException {
print(usage);
exitCode = 64;
return;
}

// Run the program.
try {
final endPoint = Platform.environment['COVERALLS_ENDPOINT'];
final client = Client(endPoint != null ? Uri.parse(endPoint) : Client.defaultEndPoint);
// Run the program.
try {
final endPoint = Platform.environment["COVERALLS_ENDPOINT"];
final client = Client(endPoint != null ? Uri.parse(endPoint) : Client.defaultEndPoint);

final coverage = await File(options.rest.first).readAsString();
print('[Coveralls] Submitting to ${client.endPoint}');
await client.upload(coverage);
}
final coverage = await File(options.rest.first).readAsString();
print("[Coveralls] Submitting to ${client.endPoint}");
await client.upload(coverage);
}

on Exception catch (err) {
print(err);
exitCode = 1;
}
on Exception catch (err) {
print(err);
exitCode = 1;
}
}

+ 0
- 4
doc/about/see_also.md View File

@@ -8,7 +8,3 @@
## Testing
- [Continuous integration](https://github.com/cedx/coveralls.dart/actions)
- [Code coverage](https://coveralls.io/github/cedx/coveralls.dart)

## Other implementations
- JavaScript: [Coveralls for JS](https://docs.belin.io/coveralls.js)
- PHP: [Coveralls for PHP](https://docs.belin.io/coveralls.php)

+ 1
- 1
doc/features.md View File

@@ -21,7 +21,7 @@ This project has been tested with [Travis CI](https://travis-ci.com) service, bu
- [Wercker](https://app.wercker.com)

!!! tip
You can find an [example workflow for GitHub Actions](https://git.belin.io/cedx/coveralls.dart/src/branch/master/.github/workflows/build.yaml) in the sources of this project.
You can find an [example workflow for GitHub Actions](https://git.belin.io/cedx/coveralls.dart/src/branch/master/.github/workflows/build.yaml) in the sources of this project.

## Environment variables
If your build system is not supported, you can still use this package.


+ 4
- 4
doc/index.md View File

@@ -1,5 +1,5 @@
# Coveralls <small>for Dart</small>
![Runtime](https://badgen.net/badge/dart/%3E%3D2.8.0/green) ![Release](https://img.shields.io/pub/v/coveralls.svg) ![License](https://badgen.net/badge/license/MIT/blue) ![Coverage](https://badgen.net/coveralls/c/github/cedx/coveralls.dart) ![Build](https://badgen.net/github/checks/cedx/coveralls.dart)
![Runtime](https://badgen.net/pub/sdk-version/coveralls) ![Release](https://badgen.net/pub/v/coveralls) ![License](https://badgen.net/pub/license/coveralls) ![Likes](https://badgen.net/pub/likes/coveralls) ![Coverage](https://badgen.net/coveralls/c/github/cedx/coveralls.dart) ![Build](https://badgen.net/github/checks/cedx/coveralls.dart)

Send [LCOV](http://ltp.sourceforge.net/coverage/lcov.php) and [Clover](https://www.atlassian.com/software/clover) coverage reports to the [Coveralls](https://coveralls.io) service, in [Dart](https://dart.dev).

@@ -8,14 +8,14 @@ Send [LCOV](http://ltp.sourceforge.net/coverage/lcov.php) and [Clover](https://w
## Quick start
Append the following line to your project's `pubspec.yaml` file:

```yaml
``` yaml
dependencies:
coveralls: *
coveralls: *
```

Install the latest version of **Coveralls for Dart** with [Pub](https://dart.dev/tools/pub):

```shell
``` shell
pub get
```



+ 8
- 8
doc/installation.md View File

@@ -6,7 +6,7 @@ and [Pub](https://dart.dev/tools/pub), the Dart package manager, up and running.

You can verify if you're already good to go with the following commands:

```shell
``` shell
dart --version
# Dart VM version: 2.8.1 (stable) (Thu Apr 30 09:25:21 2020 +0200) on "windows_x64"

@@ -15,29 +15,29 @@ pub --version
```

!!! info
If you plan to play with the package sources, you will also need
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).
If you plan to play with the package sources, you will also need
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).

## Installing with Pub package manager

### 1. Depend on it
Add this to your project's `pubspec.yaml` file:

```yaml
``` yaml
dependencies:
coveralls: *
coveralls: *
```

### 2. Install it
Install this package and its dependencies from a command prompt:

```shell
``` shell
pub get
```

### 3. Import it
Now in your [Dart](https://dart.dev) code, you can use:

```dart
import 'package:coveralls/coveralls.dart';
``` dart
import "package:coveralls/coveralls.dart";
```

+ 15
- 15
doc/usage/api.md View File

@@ -6,20 +6,20 @@ source: lib/src/io/client.dart
# Application programming interface
The hard way. Use the `Client` class to upload your coverage reports:

```dart
import 'dart:io';
import 'package:coveralls/coveralls.dart';
``` dart
import "dart:io";
import "package:coveralls/coveralls.dart";

Future<void> main() async {
try {
var coverage = File('/path/to/coverage.report');
await Client().upload(await coverage.readAsString());
print('The report was sent successfully.');
}
on Exception catch (err) {
print('An error occurred: $err');
}
try {
var coverage = File("/path/to/coverage.report");
await Client().upload(await coverage.readAsString());
print("The report was sent successfully.");
}
on Exception catch (err) {
print("An error occurred: $err");
}
}
```

@@ -36,12 +36,12 @@ The `Client` class triggers some events during its life cycle:

These events are exposed as [`Stream`](https://api.dart.dev/stable/dart-async/Stream-class.html), you can listen to them using the `on<EventName>` properties:

```dart
``` dart
client.onRequest.listen(
(request) => print('Client request: ${request.url}')
(request) => print("Client request: ${request.url}")
);

client.onResponse.listen(
(response) => print('Server response: ${response.statusCode}')
(response) => print("Server response: ${response.statusCode}")
);
```

+ 4
- 4
doc/usage/cli.md View File

@@ -6,15 +6,15 @@ source: lib/src/cli/options.dart
# Command line interface
The easy way. From a command prompt, install the `coveralls` executable:

```shell
``` shell
pub global activate coveralls
```
!!! tip
Consider adding the [`pub global`](https://dart.dev/tools/pub/cmd/pub-global) executables directory to your system path.
Consider adding the [`pub global`](https://dart.dev/tools/pub/cmd/pub-global) executables directory to your system path.

Then use it to upload your coverage reports:

```shell
``` shell
$ coveralls --help

Send a coverage report to the Coveralls service.
@@ -28,6 +28,6 @@ Options:

For example:

```shell
``` shell
coveralls build/lcov.info
```

+ 9
- 5
etc/mkdocs.yaml View File

@@ -8,19 +8,23 @@ site_dir: ../www

repo_name: git.belin.io
repo_url: https://git.belin.io/cedx/coveralls.dart
edit_uri: ''
edit_uri: ""

copyright: Copyright &copy; 2017 - 2020 Cédric Belin
extra:
social:
- icon: fontawesome/solid/globe
link: 'https://belin.io'
link: "https://belin.io"
name: Belin.io
- icon: fontawesome/brands/github
link: 'https://github.com/cedx'
link: "https://github.com/cedx"
name: GitHub
- icon: fontawesome/brands/twitter
link: 'https://twitter.com/cedxbelin'
link: "https://twitter.com/cedxbelin"
name: Twitter
- icon: fontawesome/brands/linkedin
link: 'https://linkedin.com/in/cedxbelin'
link: "https://linkedin.com/in/cedxbelin"
name: LinkedIn

markdown_extensions:
- admonition


+ 11
- 11
example/main.dart View File

@@ -1,17 +1,17 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:coveralls/coveralls.dart';
import "dart:io";
import "package:coveralls/coveralls.dart";

/// Uploads a coverage report.
Future<void> main() async {
try {
final coverage = File('/path/to/coverage.report');
await Client().upload(await coverage.readAsString());
print('The report was sent successfully.');
}
try {
final coverage = File("/path/to/coverage.report");
await Client().upload(await coverage.readAsString());
print("The report was sent successfully.");
}

on Exception catch (err) {
print('An error occurred: $err');
if (err is ClientException) print('From: ${err.uri}');
}
on Exception catch (err) {
print("An error occurred: $err");
if (err is ClientException) print("From: ${err.uri}");
}
}

+ 2
- 2
lib/coveralls.dart View File

@@ -1,5 +1,5 @@
/// Send [LCOV](http://ltp.sourceforge.net/coverage/lcov.php) coverage reports to the [Coveralls](https://coveralls.io) service.
library coveralls;

export 'package:http/http.dart' show ClientException;
export 'src/io.dart';
export "package:http/http.dart" show ClientException;
export "src/io.dart";

+ 4
- 4
lib/src/cli.dart View File

@@ -1,11 +1,11 @@
/// Provides the command line interface.
library coveralls.cli;

import 'package:build_cli_annotations/build_cli_annotations.dart';
import "package:build_cli_annotations/build_cli_annotations.dart";

part 'cli.g.dart';
part 'cli/options.dart';
part 'cli/usage.dart';
part "cli.g.dart";
part "cli/options.dart";
part "cli/usage.dart";

/// The command line argument parser.
ArgParser get argParser => _$parserForOptions;

+ 3
- 3
lib/src/cli/options.dart View File

@@ -1,4 +1,4 @@
part of '../cli.dart';
part of "../cli.dart";

/// The parsed command line arguments.
@CliOptions()
@@ -8,13 +8,13 @@ class Options {
Options({this.help, this.rest, this.version});

/// Value indicating whether to output usage information.
@CliOption(abbr: 'h', help: 'Output usage information.', negatable: false)
@CliOption(abbr: "h", help: "Output usage information.", negatable: false)
final bool help;

/// The remaining command-line arguments that were not parsed as options or flags.
final List<String> rest;

/// Value indicating whether to output the version number.
@CliOption(abbr: 'v', help: 'Output the version number.', negatable: false)
@CliOption(abbr: "v", help: "Output the version number.", negatable: false)
final bool version;
}

+ 4
- 4
lib/src/cli/usage.dart View File

@@ -1,9 +1,9 @@
part of '../cli.dart';
part of "../cli.dart";

/// The usage information.
final String usage = (StringBuffer()
..writeln('Send a coverage report to the Coveralls service.')..writeln()
..writeln('Usage: coveralls [options] <file>')..writeln()
..writeln('Options:')
..writeln("Send a coverage report to the Coveralls service.")..writeln()
..writeln("Usage: coveralls [options] <file>")..writeln()
..writeln("Options:")
..write(argParser.usage))
.toString();

+ 27
- 27
lib/src/io.dart View File

@@ -1,32 +1,32 @@
/// Provides the I/O support.
library coveralls.io;

import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart';
import 'package:where/where.dart';
import 'package:yaml/yaml.dart';
import "dart:async";
import "dart:collection";
import "dart:convert";
import "dart:io";
import "package:http/http.dart" as http;
import "package:json_annotation/json_annotation.dart";
import "package:where/where.dart";
import "package:yaml/yaml.dart";

import 'io/parsers/clover.dart' deferred as clover;
import 'io/parsers/lcov.dart' deferred as lcov;
import 'io/services/appveyor.dart' deferred as appveyor;
import 'io/services/circleci.dart' deferred as circleci;
import 'io/services/codeship.dart' deferred as codeship;
import 'io/services/github.dart' deferred as github;
import 'io/services/gitlab_ci.dart' deferred as gitlab_ci;
import 'io/services/jenkins.dart' deferred as jenkins;
import 'io/services/semaphore.dart' deferred as semaphore;
import 'io/services/solano_ci.dart' deferred as solano_ci;
import 'io/services/surf.dart' deferred as surf;
import 'io/services/travis_ci.dart' deferred as travis_ci;
import 'io/services/wercker.dart' deferred as wercker;
import "io/parsers/clover.dart" deferred as clover;
import "io/parsers/lcov.dart" deferred as lcov;
import "io/services/appveyor.dart" deferred as appveyor;
import "io/services/circleci.dart" deferred as circleci;
import "io/services/codeship.dart" deferred as codeship;
import "io/services/github.dart" deferred as github;
import "io/services/gitlab_ci.dart" deferred as gitlab_ci;
import "io/services/jenkins.dart" deferred as jenkins;
import "io/services/semaphore.dart" deferred as semaphore;
import "io/services/solano_ci.dart" deferred as solano_ci;
import "io/services/surf.dart" deferred as surf;
import "io/services/travis_ci.dart" deferred as travis_ci;
import "io/services/wercker.dart" deferred as wercker;

part 'io.g.dart';
part 'io/client.dart';
part 'io/configuration.dart';
part 'io/git.dart';
part 'io/job.dart';
part 'io/source_file.dart';
part "io.g.dart";
part "io/client.dart";
part "io/configuration.dart";
part "io/git.dart";
part "io/job.dart";
part "io/source_file.dart";

+ 113
- 113
lib/src/io/client.dart View File

@@ -1,118 +1,118 @@
part of '../io.dart';
part of "../io.dart";

/// Uploads code coverage reports to the [Coveralls](https://coveralls.io) service.
class Client {

/// Creates a new client.
Client([Uri endPoint]): endPoint = endPoint ?? defaultEndPoint;
/// The URL of the default API end point.
static final Uri defaultEndPoint = Uri.https('coveralls.io', '/api/v1/');
/// The handler of "request" events.
final StreamController<http.MultipartRequest> _onRequest = StreamController<http.MultipartRequest>.broadcast();
/// The handler of "response" events.
final StreamController<http.Response> _onResponse = StreamController<http.Response>.broadcast();
/// The URL of the API end point.
final Uri endPoint;
/// The stream of "request" events.
Stream<http.MultipartRequest> get onRequest => _onRequest.stream;
/// The stream of "response" events.
Stream<http.Response> get onResponse => _onResponse.stream;
/// Uploads the specified code [coverage] report to the Coveralls service.
/// A [config] object provides the environment settings.
///
/// Completes with a [FormatException] if the format of the specified coverage report is not supported.
Future<void> upload(String coverage, [Configuration config]) async {
assert(coverage.isNotEmpty);
Job job;
final report = coverage.trim();
if (report.startsWith('<?xml') || report.startsWith('<coverage')) {
await clover.loadLibrary();
job = await clover.parseReport(report);
}
else if (report.startsWith('TN:') || report.startsWith('SF:')) {
await lcov.loadLibrary();
job = await lcov.parseReport(report);
}
if (job == null) throw FormatException('The specified coverage format is not supported.', report);
_updateJob(job, config ?? await Configuration.loadDefaults());
job.runAt ??= DateTime.now();
try {
await where('git');
final git = await GitData.fromRepository();
final branch = job.git != null ? job.git.branch : '';
if (git.branch == 'HEAD' && branch.isNotEmpty) git.branch = branch;
job.git = git;
}
on FinderException { /* Noop */ }
return uploadJob(job);
}
/// Uploads the specified [job] to the Coveralls service.
///
/// Completes with a [FormatException] if the job does not meet the requirements.
/// Completes with a [http.ClientException] if the remote service does not respond successfully.
Future<void> uploadJob(Job job) async {
if (job.repoToken == null && job.serviceName == null) throw FormatException('The job does not meet the requirements.', job);
final httpClient = http.Client();
final request = http.MultipartRequest('POST', endPoint.resolve('jobs'))
..files.add(http.MultipartFile.fromString('json_file', jsonEncode(job), filename: 'coveralls.json'));
try {
_onRequest.add(request);
final response = await http.Response.fromStream(await httpClient.send(request));
_onResponse.add(response);
if ((response.statusCode ~/ 100) != 2) throw http.ClientException(response.body, request.url);
return response.body;
}
on Exception catch (err) {
if (err is http.ClientException) rethrow;
throw http.ClientException(err.toString(), request.url);
}
finally {
httpClient.close();
}
}
/// Updates the properties of the specified [job] using the given configuration parameters.
void _updateJob(Job job, Configuration config) {
if (config.containsKey('repo_token')) job.repoToken = config['repo_token'];
else if (config.containsKey('repo_secret_token')) job.repoToken = config['repo_secret_token'];
if (config.containsKey('parallel')) job.isParallel = config['parallel'] == 'true';
if (config.containsKey('run_at')) job.runAt = DateTime.parse(config['run_at']);
if (config.containsKey('service_job_id')) job.serviceJobId = config['service_job_id'];
if (config.containsKey('service_name')) job.serviceName = config['service_name'];
if (config.containsKey('service_number')) job.serviceNumber = config['service_number'];
if (config.containsKey('service_pull_request')) job.servicePullRequest = config['service_pull_request'];
final hasGitData = config.keys.any((key) => key == 'service_branch' || key.startsWith('git_'));
if (!hasGitData) job.commitSha = config['commit_sha'] ?? '';
else {
final commit = GitCommit(
config['commit_sha'] ?? '',
authorEmail: config['git_author_email'] ?? '',
authorName: config['git_author_name'] ?? '',
committerEmail: config['git_committer_email'] ?? '',
committerName: config['git_committer_email'] ?? '',
message: config['git_message'] ?? ''
);
job.git = GitData(commit, branch: config['service_branch'] ?? '');
}
}
/// Creates a new client.
Client([Uri endPoint]): endPoint = endPoint ?? defaultEndPoint;
/// The URL of the default API end point.
static final Uri defaultEndPoint = Uri.https("coveralls.io", "/api/v1/");
/// The handler of "request" events.
final StreamController<http.MultipartRequest> _onRequest = StreamController<http.MultipartRequest>.broadcast();
/// The handler of "response" events.
final StreamController<http.Response> _onResponse = StreamController<http.Response>.broadcast();
/// The URL of the API end point.
final Uri endPoint;
/// The stream of "request" events.
Stream<http.MultipartRequest> get onRequest => _onRequest.stream;
/// The stream of "response" events.
Stream<http.Response> get onResponse => _onResponse.stream;
/// Uploads the specified code [coverage] report to the Coveralls service.
/// A [config] object provides the environment settings.
///
/// Completes with a [FormatException] if the format of the specified coverage report is not supported.
Future<void> upload(String coverage, [Configuration config]) async {
assert(coverage.isNotEmpty);
Job job;
final report = coverage.trim();
if (report.startsWith("<?xml") || report.startsWith("<coverage")) {
await clover.loadLibrary();
job = await clover.parseReport(report);
}
else if (report.startsWith("TN:") || report.startsWith("SF:")) {
await lcov.loadLibrary();
job = await lcov.parseReport(report);
}
if (job == null) throw FormatException("The specified coverage format is not supported.", report);
_updateJob(job, config ?? await Configuration.loadDefaults());
job.runAt ??= DateTime.now();
try {
await where("git");
final git = await GitData.fromRepository();
final branch = job.git != null ? job.git.branch : "";
if (git.branch == "HEAD" && branch.isNotEmpty) git.branch = branch;
job.git = git;
}
on FinderException { /* Noop */ }
return uploadJob(job);
}
/// Uploads the specified [job] to the Coveralls service.
///
/// Completes with a [FormatException] if the job does not meet the requirements.
/// Completes with a [http.ClientException] if the remote service does not respond successfully.
Future<void> uploadJob(Job job) async {
if (job.repoToken == null && job.serviceName == null) throw FormatException("The job does not meet the requirements.", job);
final httpClient = http.Client();
final request = http.MultipartRequest("POST", endPoint.resolve("jobs"))
..files.add(http.MultipartFile.fromString("json_file", jsonEncode(job), filename: "coveralls.json"));
try {
_onRequest.add(request);
final response = await http.Response.fromStream(await httpClient.send(request));
_onResponse.add(response);
if ((response.statusCode ~/ 100) != 2) throw http.ClientException(response.body, request.url);
return response.body;
}
on Exception catch (err) {
if (err is http.ClientException) rethrow;
throw http.ClientException(err.toString(), request.url);
}
finally {
httpClient.close();
}
}
/// Updates the properties of the specified [job] using the given configuration parameters.
void _updateJob(Job job, Configuration config) {
if (config.containsKey("repo_token")) job.repoToken = config["repo_token"];
else if (config.containsKey("repo_secret_token")) job.repoToken = config["repo_secret_token"];
if (config.containsKey("parallel")) job.isParallel = config["parallel"] == "true";
if (config.containsKey("run_at")) job.runAt = DateTime.parse(config["run_at"]);
if (config.containsKey("service_job_id")) job.serviceJobId = config["service_job_id"];
if (config.containsKey("service_name")) job.serviceName = config["service_name"];
if (config.containsKey("service_number")) job.serviceNumber = config["service_number"];
if (config.containsKey("service_pull_request")) job.servicePullRequest = config["service_pull_request"];
final hasGitData = config.keys.any((key) => key == "service_branch" || key.startsWith("git_"));
if (!hasGitData) job.commitSha = config["commit_sha"] ?? "";
else {
final commit = GitCommit(
config["commit_sha"] ?? "",
authorEmail: config["git_author_email"] ?? "",
authorName: config["git_author_name"] ?? "",
committerEmail: config["git_committer_email"] ?? "",
committerName: config["git_committer_email"] ?? "",
message: config["git_message"] ?? ""
);
job.git = GitData(commit, branch: config["service_branch"] ?? "");
}
}
}

+ 157
- 157
lib/src/io/configuration.dart View File

@@ -1,162 +1,162 @@
part of '../io.dart';
part of "../io.dart";

/// Provides access to the coverage settings.
class Configuration extends Object with MapMixin<String, String> { // ignore: prefer_mixin

/// Creates a new configuration from the specified [map].
Configuration([Map<String, String> map]): _params = map ?? <String, String>{};
/// Creates a new configuration from the specified YAML [document].
/// Throws a [FormatException] if the specified document is invalid.
Configuration.fromYaml(String document): assert(document.isNotEmpty), _params = <String, String>{} {
if (document == null || document.trim().isEmpty) throw const FormatException('The specified YAML document is empty.');
try {
final map = loadYaml(document);
if (map is! Map) throw FormatException('The specified YAML document is unsupported.', document);
addAll(Map<String, String>.from(map));
}
on YamlException {
throw FormatException('The specified YAML document is invalid.', document);
}
}
/// The coverage parameters.
final Map<String, String> _params;
/// Creates a new configuration from the variables of the specified environment.
/// If [env] is not provided, it defaults to `Platform.environment`.
static Future<Configuration> fromEnvironment([Map<String, String> env]) async {
env ??= Platform.environment;
final config = Configuration();
// Standard.
final serviceName = env['CI_NAME'] ?? '';
if (serviceName.isNotEmpty) config['service_name'] = serviceName;
if (env.containsKey('CI_BRANCH')) config['service_branch'] = env['CI_BRANCH'];
if (env.containsKey('CI_BUILD_NUMBER')) config['service_number'] = env['CI_BUILD_NUMBER'];
if (env.containsKey('CI_BUILD_URL')) config['service_build_url'] = env['CI_BUILD_URL'];
if (env.containsKey('CI_COMMIT')) config['commit_sha'] = env['CI_COMMIT'];
if (env.containsKey('CI_JOB_ID')) config['service_job_id'] = env['CI_JOB_ID'];
if (env.containsKey('CI_PULL_REQUEST')) {
final matches = RegExp(r'(\d+)$').allMatches(env['CI_PULL_REQUEST']);
if (matches.isNotEmpty && matches.first.groupCount >= 1) config['service_pull_request'] = matches.first[1];
}
// Coveralls.
if (env.containsKey('COVERALLS_REPO_TOKEN') || env.containsKey('COVERALLS_TOKEN'))
config['repo_token'] = env['COVERALLS_REPO_TOKEN'] ?? env['COVERALLS_TOKEN'];
if (env.containsKey('COVERALLS_COMMIT_SHA')) config['commit_sha'] = env['COVERALLS_COMMIT_SHA'];
if (env.containsKey('COVERALLS_FLAG_NAME')) config['flag_name'] = env['COVERALLS_FLAG_NAME'];
if (env.containsKey('COVERALLS_PARALLEL')) config['parallel'] = env['COVERALLS_PARALLEL'];
if (env.containsKey('COVERALLS_RUN_AT')) config['run_at'] = env['COVERALLS_RUN_AT'];
if (env.containsKey('COVERALLS_SERVICE_BRANCH')) config['service_branch'] = env['COVERALLS_SERVICE_BRANCH'];
if (env.containsKey('COVERALLS_SERVICE_JOB_ID')) config['service_job_id'] = env['COVERALLS_SERVICE_JOB_ID'];
if (env.containsKey('COVERALLS_SERVICE_NAME')) config['service_name'] = env['COVERALLS_SERVICE_NAME'];
// Git.
if (env.containsKey('GIT_AUTHOR_EMAIL')) config['git_author_email'] = env['GIT_AUTHOR_EMAIL'];
if (env.containsKey('GIT_AUTHOR_NAME')) config['git_author_name'] = env['GIT_AUTHOR_NAME'];
if (env.containsKey('GIT_BRANCH')) config['service_branch'] = env['GIT_BRANCH'];
if (env.containsKey('GIT_COMMITTER_EMAIL')) config['git_committer_email'] = env['GIT_COMMITTER_EMAIL'];
if (env.containsKey('GIT_COMMITTER_NAME')) config['git_committer_name'] = env['GIT_COMMITTER_NAME'];
if (env.containsKey('GIT_ID')) config['commit_sha'] = env['GIT_ID'];
if (env.containsKey('GIT_MESSAGE')) config['git_message'] = env['GIT_MESSAGE'];
// CI services.
if (env.containsKey('TRAVIS')) {
await travis_ci.loadLibrary();
config.merge(travis_ci.getConfiguration(env));
if (serviceName.isNotEmpty && serviceName != 'travis-ci') config['service_name'] = serviceName;
}
else if (env.containsKey('APPVEYOR')) {
await appveyor.loadLibrary();
config.merge(appveyor.getConfiguration(env));
}
else if (env.containsKey('CIRCLECI')) {
await circleci.loadLibrary();
config.merge(circleci.getConfiguration(env));
}
else if (serviceName == 'codeship') {
await codeship.loadLibrary();
config.merge(codeship.getConfiguration(env));
}
else if (env.containsKey('GITHUB_WORKFLOW')) {
await github.loadLibrary();
config.merge(github.getConfiguration(env));
}
else if (env.containsKey('GITLAB_CI')) {
await gitlab_ci.loadLibrary();
config.merge(gitlab_ci.getConfiguration(env));
}
else if (env.containsKey('JENKINS_URL')) {
await jenkins.loadLibrary();
config.merge(jenkins.getConfiguration(env));
}
else if (env.containsKey('SEMAPHORE')) {
await semaphore.loadLibrary();
config.merge(semaphore.getConfiguration(env));
}
else if (env.containsKey('SURF_SHA1')) {
await surf.loadLibrary();
config.merge(surf.getConfiguration(env));
}
else if (env.containsKey('TDDIUM')) {
await solano_ci.loadLibrary();
config.merge(solano_ci.getConfiguration(env));
}
else if (env.containsKey('WERCKER')) {
await wercker.loadLibrary();
config.merge(wercker.getConfiguration(env));
}
return config;
}
/// Loads the default configuration.
/// The default values are read from the environment variables and an optional `.coveralls.yml` file.
static Future<Configuration> loadDefaults([String coverallsFile = '.coveralls.yml']) async {
assert(coverallsFile.isNotEmpty);
final defaults = await Configuration.fromEnvironment();
try {
defaults.merge(Configuration.fromYaml(await File(coverallsFile).readAsString()));
return defaults;
}
on Exception {
return defaults;
}
}
/// The keys of this configuration.
@override
Iterable<String> get keys => _params.keys;
/// Returns the value for the given [key] or `null` if [key] is not in this configuration.
@override
String operator [](Object key) => _params[key];
/// Associates the [key] with the given [value].
@override
void operator []=(String key, String value) => _params[key] = value;
/// Removes all pairs from this configuration.
@override
void clear() => _params.clear();
/// Adds all entries of the specified configuration to this one, ignoring `null` values.
void merge(Configuration config) {
for (final entry in config.entries)
if (entry.value != null) this[entry.key] = entry.value;
}
/// Removes the specified [key] and its associated value from this configuration.
/// Returns the value associated with [key] before it was removed.
@override
String remove(Object key) => _params.remove(key);
/// Creates a new configuration from the specified [map].
Configuration([Map<String, String> map]): _params = map ?? <String, String>{};
/// Creates a new configuration from the specified YAML [document].
/// Throws a [FormatException] if the specified document is invalid.
Configuration.fromYaml(String document): assert(document.isNotEmpty), _params = <String, String>{} {
if (document == null || document.trim().isEmpty) throw const FormatException("The specified YAML document is empty.");
try {
final map = loadYaml(document);
if (map is! Map) throw FormatException("The specified YAML document is unsupported.", document);
addAll(Map<String, String>.from(map));
}
on YamlException {
throw FormatException("The specified YAML document is invalid.", document);
}
}
/// The coverage parameters.
final Map<String, String> _params;
/// Creates a new configuration from the variables of the specified environment.
/// If [env] is not provided, it defaults to `Platform.environment`.
static Future<Configuration> fromEnvironment([Map<String, String> env]) async {
env ??= Platform.environment;
final config = Configuration();
// Standard.
final serviceName = env["CI_NAME"] ?? "";
if (serviceName.isNotEmpty) config["service_name"] = serviceName;
if (env.containsKey("CI_BRANCH")) config["service_branch"] = env["CI_BRANCH"];
if (env.containsKey("CI_BUILD_NUMBER")) config["service_number"] = env["CI_BUILD_NUMBER"];
if (env.containsKey("CI_BUILD_URL")) config["service_build_url"] = env["CI_BUILD_URL"];
if (env.containsKey("CI_COMMIT")) config["commit_sha"] = env["CI_COMMIT"];
if (env.containsKey("CI_JOB_ID")) config["service_job_id"] = env["CI_JOB_ID"];
if (env.containsKey("CI_PULL_REQUEST")) {
final matches = RegExp(r"(\d+)$").allMatches(env["CI_PULL_REQUEST"]);
if (matches.isNotEmpty && matches.first.groupCount >= 1) config["service_pull_request"] = matches.first[1];
}
// Coveralls.
if (env.containsKey("COVERALLS_REPO_TOKEN") || env.containsKey("COVERALLS_TOKEN"))
config["repo_token"] = env["COVERALLS_REPO_TOKEN"] ?? env["COVERALLS_TOKEN"];
if (env.containsKey("COVERALLS_COMMIT_SHA")) config["commit_sha"] = env["COVERALLS_COMMIT_SHA"];
if (env.containsKey("COVERALLS_FLAG_NAME")) config["flag_name"] = env["COVERALLS_FLAG_NAME"];
if (env.containsKey("COVERALLS_PARALLEL")) config["parallel"] = env["COVERALLS_PARALLEL"];
if (env.containsKey("COVERALLS_RUN_AT")) config["run_at"] = env["COVERALLS_RUN_AT"];
if (env.containsKey("COVERALLS_SERVICE_BRANCH")) config["service_branch"] = env["COVERALLS_SERVICE_BRANCH"];
if (env.containsKey("COVERALLS_SERVICE_JOB_ID")) config["service_job_id"] = env["COVERALLS_SERVICE_JOB_ID"];
if (env.containsKey("COVERALLS_SERVICE_NAME")) config["service_name"] = env["COVERALLS_SERVICE_NAME"];
// Git.
if (env.containsKey("GIT_AUTHOR_EMAIL")) config["git_author_email"] = env["GIT_AUTHOR_EMAIL"];
if (env.containsKey("GIT_AUTHOR_NAME")) config["git_author_name"] = env["GIT_AUTHOR_NAME"];
if (env.containsKey("GIT_BRANCH")) config["service_branch"] = env["GIT_BRANCH"];
if (env.containsKey("GIT_COMMITTER_EMAIL")) config["git_committer_email"] = env["GIT_COMMITTER_EMAIL"];
if (env.containsKey("GIT_COMMITTER_NAME")) config["git_committer_name"] = env["GIT_COMMITTER_NAME"];
if (env.containsKey("GIT_ID")) config["commit_sha"] = env["GIT_ID"];
if (env.containsKey("GIT_MESSAGE")) config["git_message"] = env["GIT_MESSAGE"];
// CI services.
if (env.containsKey("TRAVIS")) {
await travis_ci.loadLibrary();
config.merge(travis_ci.getConfiguration(env));
if (serviceName.isNotEmpty && serviceName != "travis-ci") config["service_name"] = serviceName;
}
else if (env.containsKey("APPVEYOR")) {
await appveyor.loadLibrary();
config.merge(appveyor.getConfiguration(env));
}
else if (env.containsKey("CIRCLECI")) {
await circleci.loadLibrary();
config.merge(circleci.getConfiguration(env));
}
else if (serviceName == "codeship") {
await codeship.loadLibrary();
config.merge(codeship.getConfiguration(env));
}
else if (env.containsKey("GITHUB_WORKFLOW")) {
await github.loadLibrary();
config.merge(github.getConfiguration(env));
}
else if (env.containsKey("GITLAB_CI")) {
await gitlab_ci.loadLibrary();
config.merge(gitlab_ci.getConfiguration(env));
}
else if (env.containsKey("JENKINS_URL")) {
await jenkins.loadLibrary();
config.merge(jenkins.getConfiguration(env));
}
else if (env.containsKey("SEMAPHORE")) {
await semaphore.loadLibrary();
config.merge(semaphore.getConfiguration(env));
}
else if (env.containsKey("SURF_SHA1")) {
await surf.loadLibrary();
config.merge(surf.getConfiguration(env));
}
else if (env.containsKey("TDDIUM")) {
await solano_ci.loadLibrary();
config.merge(solano_ci.getConfiguration(env));
}
else if (env.containsKey("WERCKER")) {
await wercker.loadLibrary();
config.merge(wercker.getConfiguration(env));
}
return config;
}
/// Loads the default configuration.
/// The default values are read from the environment variables and an optional `.coveralls.yml` file.
static Future<Configuration> loadDefaults([String coverallsFile = ".coveralls.yml"]) async {
assert(coverallsFile.isNotEmpty);
final defaults = await Configuration.fromEnvironment();
try {
defaults.merge(Configuration.fromYaml(await File(coverallsFile).readAsString()));
return defaults;
}
on Exception {
return defaults;
}
}
/// The keys of this configuration.
@override
Iterable<String> get keys => _params.keys;
/// Returns the value for the given [key] or `null` if [key] is not in this configuration.
@override
String operator [](Object key) => _params[key];
/// Associates the [key] with the given [value].
@override
void operator []=(String key, String value) => _params[key] = value;
/// Removes all pairs from this configuration.
@override
void clear() => _params.clear();
/// Adds all entries of the specified configuration to this one, ignoring `null` values.
void merge(Configuration config) {
for (final entry in config.entries)
if (entry.value != null) this[entry.key] = entry.value;
}
/// Removes the specified [key] and its associated value from this configuration.
/// Returns the value associated with [key] before it was removed.
@override
String remove(Object key) => _params.remove(key);
}

+ 82
- 82
lib/src/io/git.dart View File

@@ -1,112 +1,112 @@
part of '../io.dart';
part of "../io.dart";

/// Represents a Git commit.
@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false)
class GitCommit {

/// Creates a new commit.
const GitCommit(this.id, {this.authorEmail, this.authorName, this.committerEmail, this.committerName, this.message});
/// Creates a new commit.
const GitCommit(this.id, {this.authorEmail, this.authorName, this.committerEmail, this.committerName, this.message});

/// Creates a new commit from the specified [map] in JSON format.
factory GitCommit.fromJson(Map<String, dynamic> map) => _$GitCommitFromJson(map);
/// Creates a new commit from the specified [map] in JSON format.
factory GitCommit.fromJson(Map<String, dynamic> map) => _$GitCommitFromJson(map);

/// The author mail address.
final String authorEmail;
/// The author mail address.
final String authorEmail;

/// The author name.
final String authorName;
/// The author name.
final String authorName;

/// The committer mail address.
final String committerEmail;
/// The committer mail address.
final String committerEmail;

/// The committer name.
final String committerName;
/// The committer name.
final String committerName;

/// The commit identifier.
final String id;
/// The commit identifier.
final String id;

/// The commit message.
final String message;
/// The commit message.
final String message;

/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$GitCommitToJson(this);
/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$GitCommitToJson(this);
}

/// Represents Git data that can be used to display more information to users.
@JsonSerializable(explicitToJson: true)
class GitData {

/// Creates a new data.
GitData(this.commit, {this.branch = '', Iterable<GitRemote> remotes}): remotes = remotes?.toList() ?? <GitRemote>[];
/// Creates a new data from the specified [map] in JSON format.
factory GitData.fromJson(Map<String, dynamic> map) => _$GitDataFromJson(map);
/// The branch name.
@JsonKey(defaultValue: '')
String branch;
/// The Git commit.
@JsonKey(name: 'head')
final GitCommit commit;
/// The remote repositories.
@JsonKey(defaultValue: [])
final List<GitRemote> remotes;
/// Creates a new Git data from a repository located at the specified [path] (defaulting to the current working directory).
/// This method relies on the availability of the Git executable in the system path.
static Future<GitData> fromRepository([String path = '']) async {
final commands = {
'authorEmail': 'log -1 --pretty=format:%ae',
'authorName': 'log -1 --pretty=format:%aN',
'branch': 'rev-parse --abbrev-ref HEAD',
'committerEmail': 'log -1 --pretty=format:%ce',
'committerName': 'log -1 --pretty=format:%cN',
'id': 'log -1 --pretty=format:%H',
'message': 'log -1 --pretty=format:%s',
'remotes': 'remote -v'
};
final workingDir = path.isNotEmpty ? path : Directory.current.path;
for (final key in commands.keys) {
final result = await Process.run('git', commands[key].split(' '), workingDirectory: workingDir);
commands[key] = result.stdout.trim();
}
final remotes = <String, GitRemote>{};
for (final remote in commands['remotes'].split(RegExp(r'\r?\n'))) {
final parts = remote.replaceAll(RegExp(r'\s+'), ' ').split(' ');
remotes.putIfAbsent(parts.first, () => GitRemote(parts.first, parts.length > 1 ? parts[1] : null));
}
return GitData(GitCommit.fromJson(commands), branch: commands['branch'], remotes: remotes.values);
}
/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$GitDataToJson(this);
/// Creates a new data.
GitData(this.commit, {this.branch = "", Iterable<GitRemote> remotes}): remotes = remotes?.toList() ?? <GitRemote>[];
/// Creates a new data from the specified [map] in JSON format.
factory GitData.fromJson(Map<String, dynamic> map) => _$GitDataFromJson(map);
/// The branch name.
@JsonKey(defaultValue: "")
String branch;
/// The Git commit.
@JsonKey(name: "head")
final GitCommit commit;
/// The remote repositories.
@JsonKey(defaultValue: [])
final List<GitRemote> remotes;
/// Creates a new Git data from a repository located at the specified [path] (defaulting to the current working directory).
/// This method relies on the availability of the Git executable in the system path.
static Future<GitData> fromRepository([String path = ""]) async {
final commands = {
"authorEmail": "log -1 --pretty=format:%ae",
"authorName": "log -1 --pretty=format:%aN",
"branch": "rev-parse --abbrev-ref HEAD",
"committerEmail": "log -1 --pretty=format:%ce",
"committerName": "log -1 --pretty=format:%cN",
"id": "log -1 --pretty=format:%H",
"message": "log -1 --pretty=format:%s",
"remotes": "remote -v"
};
final workingDir = path.isNotEmpty ? path : Directory.current.path;
for (final key in commands.keys) {
final result = await Process.run("git", commands[key].split(" "), workingDirectory: workingDir);
commands[key] = result.stdout.trim();
}
final remotes = <String, GitRemote>{};
for (final remote in commands["remotes"].split(RegExp(r"\r?\n"))) {
final parts = remote.replaceAll(RegExp(r"\s+"), " ").split(" ");
remotes.putIfAbsent(parts.first, () => GitRemote(parts.first, parts.length > 1 ? parts[1] : null));
}
return GitData(GitCommit.fromJson(commands), branch: commands["branch"], remotes: remotes.values);
}
/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$GitDataToJson(this);
}

/// Represents a Git remote repository.
@JsonSerializable()
class GitRemote {

/// Creates a new remote repository.
GitRemote(this.name, [url]): url = url is! String ? url : Uri.parse(RegExp(r'^\w+://').hasMatch(url) ? url : url.replaceFirstMapped(
RegExp(r'^([^@]+@)?([^:]+):(.+)$'),
(match) => 'ssh://${match[1]}${match[2]}/${match[3]}')
);
/// Creates a new remote repository.
GitRemote(this.name, [url]): url = url is! String ? url : Uri.parse(RegExp(r"^\w+://").hasMatch(url) ? url : url.replaceFirstMapped(
RegExp(r"^([^@]+@)?([^:]+):(.+)$"),
(match) => "ssh://${match[1]}${match[2]}/${match[3]}")
);

/// Creates a new source file from the specified [map] in JSON format.
factory GitRemote.fromJson(Map<String, dynamic> map) => _$GitRemoteFromJson(map);
/// Creates a new source file from the specified [map] in JSON format.
factory GitRemote.fromJson(Map<String, dynamic> map) => _$GitRemoteFromJson(map);

/// The remote's name.
@JsonKey(defaultValue: '')
final String name;
/// The remote's name.
@JsonKey(defaultValue: "")
final String name;

/// The remote's URL.
final Uri url;
/// The remote's URL.
final Uri url;

/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$GitRemoteToJson(this);
/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$GitRemoteToJson(this);
}

+ 39
- 39
lib/src/io/job.dart View File

@@ -1,58 +1,58 @@
part of '../io.dart';
part of "../io.dart";

/// Represents the coverage data from a single run of a test suite.
@JsonSerializable(explicitToJson: true, includeIfNull: false)
class Job {

/// Creates a new job.
Job({this.repoToken, this.serviceJobId, this.serviceName, Iterable<SourceFile> sourceFiles}): sourceFiles = sourceFiles?.toList() ?? <SourceFile>[];
/// Creates a new job.
Job({this.repoToken, this.serviceJobId, this.serviceName, Iterable<SourceFile> sourceFiles}): sourceFiles = sourceFiles?.toList() ?? <SourceFile>[];

/// Creates a new job from the specified [map] in JSON format.
factory Job.fromJson(Map<String, dynamic> map) => _$JobFromJson(map);
/// Creates a new job from the specified [map] in JSON format.
factory Job.fromJson(Map<String, dynamic> map) => _$JobFromJson(map);

/// The current SHA of the commit being built to override the [git] property.
@JsonKey(name: 'commit_sha')
String commitSha;
/// The current SHA of the commit being built to override the [git] property.
@JsonKey(name: "commit_sha")
String commitSha;

/// The job name.
@JsonKey(name: 'flag_name')
String flagName;
/// The job name.
@JsonKey(name: "flag_name")
String flagName;

/// A map of Git data that can be used to display more information to users.
GitData git;
/// A map of Git data that can be used to display more information to users.
GitData git;

/// Value indicating whether the build will not be considered done until a webhook has been sent to Coveralls.
@JsonKey(name: 'parallel')
bool isParallel;
/// Value indicating whether the build will not be considered done until a webhook has been sent to Coveralls.
@JsonKey(name: "parallel")
bool isParallel;

/// The secret token for the repository.
@JsonKey(name: 'repo_token')
String repoToken;
/// The secret token for the repository.
@JsonKey(name: "repo_token")
String repoToken;

/// A timestamp of when the job ran.
@JsonKey(name: 'run_at')
DateTime runAt;
/// A timestamp of when the job ran.
@JsonKey(name: "run_at")
DateTime runAt;

/// A unique identifier of the job on the CI service.
@JsonKey(name: 'service_job_id')
String serviceJobId;
/// A unique identifier of the job on the CI service.
@JsonKey(name: "service_job_id")
String serviceJobId;

/// The CI service or other environment in which the test suite was run.
@JsonKey(name: 'service_name')
String serviceName;
/// The CI service or other environment in which the test suite was run.
@JsonKey(name: "service_name")
String serviceName;

/// The build number.
@JsonKey(name: 'service_number')
String serviceNumber;
/// The build number.
@JsonKey(name: "service_number")
String serviceNumber;

/// The associated pull request ID of the build.
@JsonKey(name: 'service_pull_request')
String servicePullRequest;
/// The associated pull request ID of the build.
@JsonKey(name: "service_pull_request")
String servicePullRequest;

/// The list of source files.
@JsonKey(defaultValue: [], name: 'source_files')
final List<SourceFile> sourceFiles;
/// The list of source files.
@JsonKey(defaultValue: [], name: "source_files")
final List<SourceFile> sourceFiles;

/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$JobToJson(this);
/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$JobToJson(this);
}

+ 24
- 24
lib/src/io/parsers/clover.dart View File

@@ -1,34 +1,34 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as p;
import 'package:xml/xml.dart' as xml;
import '../../io.dart';
import "dart:io";
import "dart:math" as math;
import "package:crypto/crypto.dart";
import "package:path/path.dart" as p;
import "package:xml/xml.dart" as xml;
import "../../io.dart";

/// Parses the specified [Clover](https://www.atlassian.com/software/clover) coverage report.
/// Throws a [FormatException] if a parsing error occurred.
Future<Job> parseReport(String report) async {
final coverage = xml.parse(report);
if (coverage.findAllElements('project').isEmpty) throw FormatException('The specified Clover report is invalid.', report);
final coverage = xml.parse(report);
if (coverage.findAllElements("project").isEmpty) throw FormatException("The specified Clover report is invalid.", report);

final sourceFiles = <SourceFile>[];
for (final file in xml.parse(report).findAllElements('file')) {
final sourceFile = file.getAttribute('name');
if (sourceFile == null || sourceFile.isEmpty) throw FormatException('Invalid file data: ${file.toXmlString()}', report);
final sourceFiles = <SourceFile>[];
for (final file in xml.parse(report).findAllElements("file")) {
final sourceFile = file.getAttribute("name");
if (sourceFile == null || sourceFile.isEmpty) throw FormatException("Invalid file data: ${file.toXmlString()}", report);

final source = await File(sourceFile).readAsString();
final coverage = List<int>(source.split(RegExp(r'\r?\n')).length);
final source = await File(sourceFile).readAsString();
final coverage = List<int>(source.split(RegExp(r"\r?\n")).length);

for (final line in file.findAllElements('line')) {
if (line.getAttribute('type') != 'stmt') continue;
final lineNumber = math.max(1, int.parse(line.getAttribute('num'), radix: 10));
coverage[lineNumber - 1] = math.max(0, int.parse(line.getAttribute('count'), radix: 10));
}
for (final line in file.findAllElements("line")) {
if (line.getAttribute("type") != "stmt") continue;
final lineNumber = math.max(1, int.parse(line.getAttribute("num"), radix: 10));
coverage[lineNumber - 1] = math.max(0, int.parse(line.getAttribute("count"), radix: 10));
}

final filename = p.isAbsolute(sourceFile) ? p.relative(sourceFile) : p.normalize(sourceFile);
final digest = md5.convert(source.codeUnits).toString();
sourceFiles.add(SourceFile(filename, digest, coverage: coverage, source: source));
}
final filename = p.isAbsolute(sourceFile) ? p.relative(sourceFile) : p.normalize(sourceFile);
final digest = md5.convert(source.codeUnits).toString();
sourceFiles.add(SourceFile(filename, digest, coverage: coverage, source: source));
}

return Job(sourceFiles: sourceFiles);
return Job(sourceFiles: sourceFiles);
}

+ 26
- 26
lib/src/io/parsers/lcov.dart View File

@@ -1,33 +1,33 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:lcov/lcov.dart';
import 'package:path/path.dart' as p;
import '../../io.dart';
import "dart:io";
import "package:crypto/crypto.dart";
import "package:lcov/lcov.dart";
import "package:path/path.dart" as p;
import "../../io.dart";

/// Parses the specified [LCOV](http://ltp.sourceforge.net/coverage/lcov.php) coverage report.
Future<Job> parseReport(String report) async {
final sourceFiles = <SourceFile>[];
for (final record in Report.fromCoverage(report).records) {
final source = await File(record.sourceFile).readAsString();
final lineCoverage = List<int>(source.split(RegExp(r'\r?\n')).length);
if (record.lines != null) for (final lineData in record.lines.data) lineCoverage[lineData.lineNumber - 1] = lineData.executionCount;
final sourceFiles = <SourceFile>[];
for (final record in Report.fromCoverage(report).records) {
final source = await File(record.sourceFile).readAsString();
final lineCoverage = List<int>(source.split(RegExp(r"\r?\n")).length);
if (record.lines != null) for (final lineData in record.lines.data) lineCoverage[lineData.lineNumber - 1] = lineData.executionCount;

final branchCoverage = <int>[];
if (record.branches != null) for (final branchData in record.branches.data) branchCoverage.addAll(<int>[
branchData.lineNumber,
branchData.blockNumber,
branchData.branchNumber,
branchData.taken
]);
final branchCoverage = <int>[];
if (record.branches != null) for (final branchData in record.branches.data) branchCoverage.addAll(<int>[
branchData.lineNumber,
branchData.blockNumber,
branchData.branchNumber,
branchData.taken
]);

sourceFiles.add(SourceFile(
p.isAbsolute(record.sourceFile) ? p.relative(record.sourceFile) : p.normalize(record.sourceFile),
md5.convert(source.codeUnits).toString(),
branches: branchCoverage.isEmpty ? null : branchCoverage,
coverage: lineCoverage,
source: source
));
}
sourceFiles.add(SourceFile(
p.isAbsolute(record.sourceFile) ? p.relative(record.sourceFile) : p.normalize(record.sourceFile),
md5.convert(source.codeUnits).toString(),
branches: branchCoverage.isEmpty ? null : branchCoverage,
coverage: lineCoverage,
source: source
));
}

return Job(sourceFiles: sourceFiles);
return Job(sourceFiles: sourceFiles);
}

+ 15
- 15
lib/src/io/services/appveyor.dart View File

@@ -1,20 +1,20 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [AppVeyor](https://www.appveyor.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) {
final repoName = env['APPVEYOR_REPO_NAME'];
final serviceNumber = env['APPVEYOR_BUILD_VERSION'];
final repoName = env["APPVEYOR_REPO_NAME"];
final serviceNumber = env["APPVEYOR_BUILD_VERSION"];

return Configuration({
'commit_sha': env['APPVEYOR_REPO_COMMIT'],
'git_author_email': env['APPVEYOR_REPO_COMMIT_AUTHOR_EMAIL'],
'git_author_name': env['APPVEYOR_REPO_COMMIT_AUTHOR'],
'git_message': env['APPVEYOR_REPO_COMMIT_MESSAGE'],
'service_branch': env['APPVEYOR_REPO_BRANCH'],
'service_build_url': repoName != null && serviceNumber != null ? 'https://ci.appveyor.com/project/$repoName/build/$serviceNumber' : null,
'service_job_id': env['APPVEYOR_BUILD_ID'],
'service_job_number': env['APPVEYOR_BUILD_NUMBER'],
'service_name': 'appveyor',
'service_number': serviceNumber
});
return Configuration({
"commit_sha": env["APPVEYOR_REPO_COMMIT"],
"git_author_email": env["APPVEYOR_REPO_COMMIT_AUTHOR_EMAIL"],
"git_author_name": env["APPVEYOR_REPO_COMMIT_AUTHOR"],
"git_message": env["APPVEYOR_REPO_COMMIT_MESSAGE"],
"service_branch": env["APPVEYOR_REPO_BRANCH"],
"service_build_url": repoName != null && serviceNumber != null ? "https://ci.appveyor.com/project/$repoName/build/$serviceNumber" : null,
"service_job_id": env["APPVEYOR_BUILD_ID"],
"service_job_number": env["APPVEYOR_BUILD_NUMBER"],
"service_name": "appveyor",
"service_number": serviceNumber
});
}

+ 8
- 8
lib/src/io/services/circleci.dart View File

@@ -1,12 +1,12 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [CircleCI](https://circleci.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['CIRCLE_SHA1'],
'parallel': int.tryParse(env['CIRCLE_NODE_TOTAL'] ?? '0', radix: 10) ?? 0 > 1 ? 'true' : 'false',
'service_branch': env['CIRCLE_BRANCH'],
'service_build_url': env['CIRCLE_BUILD_URL'],
'service_job_number': env['CIRCLE_NODE_INDEX'],
'service_name': 'circleci',
'service_number': env['CIRCLE_BUILD_NUM']
"commit_sha": env["CIRCLE_SHA1"],
"parallel": int.tryParse(env["CIRCLE_NODE_TOTAL"] ?? "0", radix: 10) ?? 0 > 1 ? "true" : "false",
"service_branch": env["CIRCLE_BRANCH"],
"service_build_url": env["CIRCLE_BUILD_URL"],
"service_job_number": env["CIRCLE_NODE_INDEX"],
"service_name": "circleci",
"service_number": env["CIRCLE_BUILD_NUM"]
});

+ 7
- 7
lib/src/io/services/codeship.dart View File

@@ -1,11 +1,11 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Codeship](https://codeship.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['CI_COMMIT_ID'],
'git_committer_email': env['CI_COMMITTER_EMAIL'],
'git_committer_name': env['CI_COMMITTER_NAME'],
'git_message': env['CI_COMMIT_MESSAGE'],
'service_job_id': env['CI_BUILD_NUMBER'],
'service_name': 'codeship'
"commit_sha": env["CI_COMMIT_ID"],
"git_committer_email": env["CI_COMMITTER_EMAIL"],
"git_committer_name": env["CI_COMMITTER_NAME"],
"git_message": env["CI_COMMIT_MESSAGE"],
"service_job_id": env["CI_BUILD_NUMBER"],
"service_name": "codeship"
});

+ 11
- 11
lib/src/io/services/github.dart View File

@@ -1,17 +1,17 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [GitHub](https://github.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) {
final commitSha = env['GITHUB_SHA'];
final repository = env['GITHUB_REPOSITORY'];
final commitSha = env["GITHUB_SHA"];
final repository = env["GITHUB_REPOSITORY"];

final gitRef = env['GITHUB_REF'] ?? '';
final gitRegex = RegExp(r'^refs/\w+/');
final gitRef = env["GITHUB_REF"] ?? "";
final gitRegex = RegExp(r"^refs/\w+/");

return Configuration({
'commit_sha': commitSha,
'service_branch': gitRegex.hasMatch(gitRef) ? gitRef.replaceFirst(gitRegex, '') : null,
'service_build_url': commitSha != null && repository != null ? 'https://github.com/$repository/commit/$commitSha/checks' : null,
'service_name': 'github'
});
return Configuration({
"commit_sha": commitSha,
"service_branch": gitRegex.hasMatch(gitRef) ? gitRef.replaceFirst(gitRegex, "") : null,
"service_build_url": commitSha != null && repository != null ? "https://github.com/$repository/commit/$commitSha/checks" : null,
"service_name": "github"
});
}

+ 6
- 6
lib/src/io/services/gitlab_ci.dart View File

@@ -1,10 +1,10 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [GitLab CI](https://gitlab.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['CI_BUILD_REF'],
'service_branch': env['CI_BUILD_REF_NAME'],
'service_job_id': env['CI_BUILD_ID'],
'service_job_number': env['CI_BUILD_NAME'],
'service_name': 'gitlab-ci'
"commit_sha": env["CI_BUILD_REF"],
"service_branch": env["CI_BUILD_REF_NAME"],
"service_job_id": env["CI_BUILD_ID"],
"service_job_number": env["CI_BUILD_NAME"],
"service_name": "gitlab-ci"
});

+ 8
- 8
lib/src/io/services/jenkins.dart View File

@@ -1,12 +1,12 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Jenkins](https://jenkins.io) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['GIT_COMMIT'],
'service_branch': env['GIT_BRANCH'],
'service_build_url': env['BUILD_URL'],
'service_job_id': env['BUILD_ID'],
'service_name': 'jenkins',
'service_number': env['BUILD_NUMBER'],
'service_pull_request': env['ghprbPullId']
"commit_sha": env["GIT_COMMIT"],
"service_branch": env["GIT_BRANCH"],
"service_build_url": env["BUILD_URL"],
"service_job_id": env["BUILD_ID"],
"service_name": "jenkins",
"service_number": env["BUILD_NUMBER"],
"service_pull_request": env["ghprbPullId"]
});

+ 6
- 6
lib/src/io/services/semaphore.dart View File

@@ -1,10 +1,10 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Semaphore](https://semaphoreci.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['REVISION'],
'service_branch': env['BRANCH_NAME'],
'service_name': 'semaphore',
'service_number': env['SEMAPHORE_BUILD_NUMBER'],
'service_pull_request': env['PULL_REQUEST_NUMBER']
"commit_sha": env["REVISION"],
"service_branch": env["BRANCH_NAME"],
"service_name": "semaphore",
"service_number": env["SEMAPHORE_BUILD_NUMBER"],
"service_pull_request": env["PULL_REQUEST_NUMBER"]
});

+ 10
- 10
lib/src/io/services/solano_ci.dart View File

@@ -1,14 +1,14 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Solano CI](https://ci.solanolabs.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) {
final serviceNumber = env['TDDIUM_SESSION_ID'];
return Configuration({
'service_branch': env['TDDIUM_CURRENT_BRANCH'],
'service_build_url': serviceNumber != null ? 'https://ci.solanolabs.com/reports/$serviceNumber' : null,
'service_job_number': env['TDDIUM_TID'],
'service_name': 'tddium',
'service_number': serviceNumber,
'service_pull_request': env['TDDIUM_PR_ID']
});
final serviceNumber = env["TDDIUM_SESSION_ID"];
return Configuration({
"service_branch": env["TDDIUM_CURRENT_BRANCH"],
"service_build_url": serviceNumber != null ? "https://ci.solanolabs.com/reports/$serviceNumber" : null,
"service_job_number": env["TDDIUM_TID"],
"service_name": "tddium",
"service_number": serviceNumber,
"service_pull_request": env["TDDIUM_PR_ID"]
});
}

+ 4
- 4
lib/src/io/services/surf.dart View File

@@ -1,8 +1,8 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Surf](https://github.com/surf-build/surf) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['SURF_SHA1'],
'service_branch': env['SURF_REF'],
'service_name': 'surf'
"commit_sha": env["SURF_SHA1"],
"service_branch": env["SURF_REF"],
"service_name": "surf"
});

+ 12
- 12
lib/src/io/services/travis_ci.dart View File

@@ -1,18 +1,18 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Travis CI](https://travis-ci.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) {
final config = Configuration({
'commit_sha': env['TRAVIS_COMMIT'],
'flag_name': env['TRAVIS_JOB_NAME'],
'service_branch': env['TRAVIS_BRANCH'],
'service_build_url': env['TRAVIS_BUILD_WEB_URL'],
'service_job_id': env['TRAVIS_JOB_ID'],
'service_name': 'travis-ci'
});
final config = Configuration({
"commit_sha": env["TRAVIS_COMMIT"],
"flag_name": env["TRAVIS_JOB_NAME"],
"service_branch": env["TRAVIS_BRANCH"],
"service_build_url": env["TRAVIS_BUILD_WEB_URL"],
"service_job_id": env["TRAVIS_JOB_ID"],
"service_name": "travis-ci"
});

if (env.containsKey('TRAVIS_PULL_REQUEST') && env['TRAVIS_PULL_REQUEST'] != 'false')
config['service_pull_request'] = env['TRAVIS_PULL_REQUEST'];
if (env.containsKey("TRAVIS_PULL_REQUEST") && env["TRAVIS_PULL_REQUEST"] != "false")
config["service_pull_request"] = env["TRAVIS_PULL_REQUEST"];

return config;
return config;
}

+ 5
- 5
lib/src/io/services/wercker.dart View File

@@ -1,9 +1,9 @@
import '../../io.dart';
import "../../io.dart";

/// Gets the [Wercker](https://app.wercker.com) configuration parameters from the specified environment.
Configuration getConfiguration(Map<String, String> env) => Configuration({
'commit_sha': env['WERCKER_GIT_COMMIT'],
'service_branch': env['WERCKER_GIT_BRANCH'],
'service_job_id': env['WERCKER_BUILD_ID'],
'service_name': 'wercker'
"commit_sha": env["WERCKER_GIT_COMMIT"],
"service_branch": env["WERCKER_GIT_BRANCH"],
"service_job_id": env["WERCKER_BUILD_ID"],
"service_name": "wercker"
});

+ 24
- 24
lib/src/io/source_file.dart View File

@@ -1,37 +1,37 @@
part of '../io.dart';
part of "../io.dart";

/// Represents a source code file and its coverage data for a single job.
@JsonSerializable()
class SourceFile {

/// Creates a new source file.
SourceFile(this.name, this.sourceDigest, {Iterable<int> branches, Iterable<int> coverage, this.source}):
branches = branches?.toList(),
coverage = coverage?.toList() ?? <int>[];
/// Creates a new source file.
SourceFile(this.name, this.sourceDigest, {Iterable<int> branches, Iterable<int> coverage, this.source}):
branches = branches?.toList(),
coverage = coverage?.toList() ?? <int>[];

/// Creates a new source file from the specified [map] in JSON format.
factory SourceFile.fromJson(Map<String, dynamic> map) => _$SourceFileFromJson(map);
/// Creates a new source file from the specified [map] in JSON format.
factory SourceFile.fromJson(Map<String, dynamic> map) => _$SourceFileFromJson(map);

/// The branch data for this file's job.
@JsonKey(includeIfNull: false)
final List<int> branches;
/// The branch data for this file's job.
@JsonKey(includeIfNull: false)
final List<int> branches;

/// The coverage data for this file's job.
@JsonKey(defaultValue: [])
final List<int> coverage;
/// The coverage data for this file's job.
@JsonKey(defaultValue: [])
final List<int> coverage;

/// The file path of this source file.
@JsonKey(defaultValue: '')
final String name;
/// The file path of this source file.
@JsonKey(defaultValue: "")
final String name;

/// The contents of this source file.
@JsonKey(includeIfNull: false)
final String source;
/// The contents of this source file.
@JsonKey(includeIfNull: false)
final String source;

/// The MD5 digest of the full source code of this file.
@JsonKey(defaultValue: '', name: 'source_digest')
final String sourceDigest;
/// The MD5 digest of the full source code of this file.
@JsonKey(defaultValue: "", name: "source_digest")
final String sourceDigest;

/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$SourceFileToJson(this);
/// Converts this object to a [Map] in JSON format.
Map<String, dynamic> toJson() => _$SourceFileToJson(this);
}

+ 1
- 1
pubspec.yaml View File

@@ -7,7 +7,7 @@ issue_tracker: https://git.belin.io/cedx/coveralls.dart/issues
repository: https://git.belin.io/cedx/coveralls.dart

environment:
sdk: '>=2.8.0 <3.0.0'
sdk: ">=2.8.0 <3.0.0"

executables:
coveralls: coveralls


+ 13
- 13
test/client_test.dart View File

@@ -1,17 +1,17 @@
import 'package:coveralls/coveralls.dart';
import 'package:test/test.dart';
import "package:coveralls/coveralls.dart";
import "package:test/test.dart";

/// Tests the features of the [Client] class.
void main() => group('Client', () {
group('.upload()', () {
test('should throw an exception with an invalid coverage report', () {
expect(Client().upload('end_of_record'), throwsFormatException);
});
});
void main() => group("Client", () {
group(".upload()", () {
test("should throw an exception with an invalid coverage report", () {
expect(Client().upload("end_of_record"), throwsFormatException);
});
});

group('.uploadJob()', () {
test('should throw an exception with an empty coverage job', () {
expect(Client().uploadJob(Job()), throwsFormatException);
});
});
group(".uploadJob()", () {
test("should throw an exception with an empty coverage job", () {
expect(Client().uploadJob(Job()), throwsFormatException);
});
});
});

+ 95
- 95
test/configuration_test.dart View File

@@ -1,112 +1,112 @@
import 'package:coveralls/coveralls.dart';
import 'package:test/test.dart';
import "package:coveralls/coveralls.dart";
import "package:test/test.dart";

/// Tests the features of the [Configuration] class.
void main() => group('Configuration', () {
group('.keys', () {
test('should return an empty array for an empty configuration', () {
expect(Configuration().keys, isEmpty);
});
void main() => group("Configuration", () {
group(".keys", () {
test("should return an empty array for an empty configuration", () {
expect(Configuration().keys, isEmpty);
});

test('should return the list of keys for a non-empty configuration', () {
final keys = Configuration({'foo': 'bar', 'baz': 'qux'}).keys.toList();
expect(keys, hasLength(2));
expect(keys.first, 'foo');
expect(keys.last, 'baz');
});
});
test("should return the list of keys for a non-empty configuration", () {
final keys = Configuration({"foo": "bar", "baz": "qux"}).keys.toList();
expect(keys, hasLength(2));
expect(keys.first, "foo");
expect(keys.last, "baz");
});
});

group('operator []', () {
test('should properly get and set the configuration entries', () {
final config = Configuration();
expect(config['foo'], isNull);
group("operator []", () {
test("should properly get and set the configuration entries", () {
final config = Configuration();
expect(config["foo"], isNull);

config['foo'] = 'bar';
expect(config['foo'], 'bar');
});
});
config["foo"] = "bar";
expect(config["foo"], "bar");
});
});

group('.clear()', () {
test('should be empty when there is no CI environment variables', () {
final config = Configuration({'foo': 'bar', 'baz': 'qux'});
expect(config, hasLength(2));
config.clear();
expect(config, hasLength(0));
});
});
group(".clear()", () {
test("should be empty when there is no CI environment variables", () {
final config = Configuration({"foo": "bar", "baz": "qux"});
expect(config, hasLength(2));
config.clear();
expect(config, hasLength(0));
});
});