// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. 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'; import 'dart:io'; import 'package:path/path.dart' as path; import 'package:vm_service/vm_service.dart'; import 'package:yaml/yaml.dart'; // TODO(cbracken) make generic /// Retries the specified function with the specified interval and returns /// the result on successful completion. Future retry(Future Function() f, Duration interval, {Duration? timeout}) async { var keepGoing = true; Future withTimeout(Future Function() f, {Duration? duration}) { if (duration == null) { return f(); } return f().timeout(duration, onTimeout: () { keepGoing = false; final msg = duration.inSeconds == 0 ? '${duration.inMilliseconds}ms' : '${duration.inSeconds}s'; throw StateError('Failed to complete within $msg'); }); } return withTimeout(() async { while (keepGoing) { try { return await f(); } catch (_) { if (keepGoing) { await Future.delayed(interval); } } } }, duration: timeout); } /// Scrapes and returns the Dart VM service URI from a string, or null if not /// found. /// /// Potentially useful as a means to extract it from log statements. Uri? extractVMServiceUri(String str) { final listeningMessageRegExp = RegExp( r'(?:Observatory|The Dart VM service is) listening on ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)', ); final match = listeningMessageRegExp.firstMatch(str); if (match != null) { return Uri.parse(match[1]!); } return null; } final multiLineIgnoreStart = RegExp(r'//\s*coverage:ignore-start[\w\d\s]*$'); final multiLineIgnoreEnd = RegExp(r'//\s*coverage:ignore-end[\w\d\s]*$'); final singleLineIgnore = RegExp(r'//\s*coverage:ignore-line[\w\d\s]*$'); final ignoreFile = RegExp(r'//\s*coverage:ignore-file[\w\d\s]*$'); /// Return list containing inclusive ranges of lines to be ignored by coverage. /// If there is a error in balancing the statements it will throw a /// [FormatException], /// unless `coverage:ignore-file` is found. /// Return null if the whole file is ignored. /// /// ``` /// 1. final str = ''; // coverage:ignore-line /// 2. final str = ''; /// 3. final str = ''; // coverage:ignore-start /// 4. final str = ''; /// 5. final str = ''; // coverage:ignore-end /// ``` /// /// Returns /// ``` /// [ /// [1,1], /// [3,5], /// ] /// ``` /// /// The returned ranges are in sorted order, and never overlap. List>? getIgnoredLines(String filePath, List? lines) { final ignoredLines = >[]; if (lines == null) return ignoredLines; FormatException? err; var i = 0; while (i < lines.length) { if (lines[i].contains(ignoreFile)) return null; if (lines[i].contains(multiLineIgnoreEnd)) { err ??= FormatException( 'unmatched coverage:ignore-end found at $filePath:${i + 1}', ); } if (lines[i].contains(singleLineIgnore)) ignoredLines.add([i + 1, i + 1]); if (lines[i].contains(multiLineIgnoreStart)) { final start = i; var isUnmatched = true; ++i; while (i < lines.length) { if (lines[i].contains(ignoreFile)) return null; if (lines[i].contains(multiLineIgnoreStart)) { err ??= FormatException( 'coverage:ignore-start found at $filePath:${i + 1}' ' before previous coverage:ignore-start ended', ); break; } if (lines[i].contains(multiLineIgnoreEnd)) { ignoredLines.add([start + 1, i + 1]); isUnmatched = false; break; } ++i; } if (isUnmatched) { err ??= FormatException( 'coverage:ignore-start found at $filePath:${start + 1}' ' has no matching coverage:ignore-end', ); } } ++i; } if (err == null) { return ignoredLines; } throw err; } extension IgnoredLinesContains on List> { /// Returns whether this list of line ranges contains the given line. bool ignoredContains(int line) { if (length == 0 || this[0][0] > line) return false; // Binary search for the range with the largest start value that is <= line. var lo = 0; var hi = length; while (lo < hi - 1) { final mid = lo + (hi - lo) ~/ 2; if (this[mid][0] <= line) { lo = mid; } else { hi = mid; } } return this[lo][1] >= line; } } extension StandardOutExtension on Stream> { Stream lines() => transform(const SystemEncoding().decoder).transform(const LineSplitter()); } Future serviceUriFromProcess(Stream procStdout) { // Capture the VM service URI. final serviceUriCompleter = Completer(); procStdout.listen((line) { if (!serviceUriCompleter.isCompleted) { final serviceUri = extractVMServiceUri(line); if (serviceUri != null) { serviceUriCompleter.complete(serviceUri); } } }); return serviceUriCompleter.future; } Future> getAllIsolates(VmService service) async => (await service.getVM()).isolates ?? []; String getPubspecPath(String root) => path.join(root, 'pubspec.yaml'); List getAllWorkspaceNames(String packageRoot) => _getAllWorkspaceNames(packageRoot, []); List _getAllWorkspaceNames(String packageRoot, List results) { final pubspec = _loadPubspec(packageRoot); results.add(pubspec['name'] as String); for (final workspace in pubspec['workspace'] as YamlList? ?? []) { _getAllWorkspaceNames(path.join(packageRoot, workspace as String), results); } return results; } YamlMap _loadPubspec(String packageRoot) { final pubspecPath = getPubspecPath(packageRoot); final yaml = File(pubspecPath).readAsStringSync(); return loadYaml(yaml, sourceUrl: Uri.file(pubspecPath)) as YamlMap; }