test_utils.dart 6.92 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'package:file/file.dart';
import 'package:file/local.dart';
7
import 'package:flutter_tools/src/base/io.dart';
8
import 'package:flutter_tools/src/base/platform.dart';
9
import 'package:process/process.dart';
10
import 'package:vm_service/vm_service.dart';
11 12

import '../src/common.dart';
13
import 'test_driver.dart';
14

15 16 17 18 19 20 21
/// The [FileSystem] for the integration test environment.
const FileSystem fileSystem = LocalFileSystem();

/// The [Platform] for the integration test environment.
const Platform platform = LocalPlatform();

/// The [ProcessManager] for the integration test environment.
22
const ProcessManager processManager = LocalProcessManager();
23

24 25 26
/// Creates a temporary directory but resolves any symlinks to return the real
/// underlying path to avoid issues with breakpoints/hot reload.
/// https://github.com/flutter/flutter/pull/21741
27
Directory createResolvedTempDirectorySync(String prefix) {
28
  assert(prefix.endsWith('.'));
29 30
  final Directory tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_$prefix');
  return fileSystem.directory(tempDirectory.resolveSymbolicLinksSync());
31 32
}

33 34
void writeFile(String path, String content, {bool writeFutureModifiedDate = false}) {
  final File file = fileSystem.file(path)
35
    ..createSync(recursive: true)
36
    ..writeAsStringSync(content, flush: true);
37 38 39 40 41 42
    // Some integration tests on Windows to not see this file as being modified
    // recently enough for the hot reload to pick this change up unless the
    // modified time is written in the future.
    if (writeFutureModifiedDate) {
      file.setLastModifiedSync(DateTime.now().add(const Duration(seconds: 5)));
    }
43 44
}

45 46 47
void writeBytesFile(String path, List<int> content) {
  fileSystem.file(path)
    ..createSync(recursive: true)
48
    ..writeAsBytesSync(content, flush: true);
49 50
}

51
void writePackages(String folder) {
52 53
  writeFile(fileSystem.path.join(folder, '.packages'), '''
test:${fileSystem.path.join(fileSystem.currentDirectory.path, 'lib')}/
54 55 56 57 58
''');
}

Future<void> getPackages(String folder) async {
  final List<String> command = <String>[
59
    fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter'),
60
    'pub',
61
    'get',
62
  ];
63
  final ProcessResult result = await processManager.run(command, workingDirectory: folder);
64 65
  if (result.exitCode != 0) {
    throw Exception('flutter pub get failed: ${result.stderr}\n${result.stdout}');
66
  }
67
}
68 69

const String kLocalEngineEnvironment = 'FLUTTER_LOCAL_ENGINE';
70
const String kLocalEngineHostEnvironment = 'FLUTTER_LOCAL_ENGINE_HOST';
71 72 73 74 75 76 77 78
const String kLocalEngineLocation = 'FLUTTER_LOCAL_ENGINE_SRC_PATH';

List<String> getLocalEngineArguments() {
  return <String>[
    if (platform.environment.containsKey(kLocalEngineEnvironment))
      '--local-engine=${platform.environment[kLocalEngineEnvironment]}',
    if (platform.environment.containsKey(kLocalEngineLocation))
      '--local-engine-src-path=${platform.environment[kLocalEngineLocation]}',
79 80
    if (platform.environment.containsKey(kLocalEngineHostEnvironment))
      '--local-engine-host=${platform.environment[kLocalEngineHostEnvironment]}',
81 82
  ];
}
83 84

Future<void> pollForServiceExtensionValue<T>({
85 86 87 88
  required FlutterTestDriver testDriver,
  required String extension,
  required T continuePollingValue,
  required Matcher matches,
89 90
  String valueKey = 'value',
}) async {
91
  for (int i = 0; i < 10; i++) {
92
    final Response response = await testDriver.callServiceExtension(extension);
93
    if (response.json?[valueKey] as T == continuePollingValue) {
94 95
      await Future<void>.delayed(const Duration(seconds: 1));
    } else {
96
      expect(response.json?[valueKey] as T, matches);
97
      return;
98 99
    }
  }
100
  fail(
101 102
    "Did not find expected value for service extension '$extension'. All call"
    " attempts responded with '$continuePollingValue'.",
103
  );
104
}
105

106
abstract final class AppleTestUtils {
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
  static const List<String> requiredSymbols = <String>[
    '_kDartIsolateSnapshotData',
    '_kDartIsolateSnapshotInstructions',
    '_kDartVmSnapshotData',
    '_kDartVmSnapshotInstructions'
  ];

  static List<String> getExportedSymbols(String dwarfPath) {
    final ProcessResult nm = processManager.runSync(
      <String>[
        'nm',
        '--debug-syms',  // nm docs: 'Show all symbols, even debugger only'
        '--defined-only',
        '--just-symbol-name',
        dwarfPath,
        '-arch',
        'arm64',
      ],
    );
    final String nmOutput = (nm.stdout as String).trim();
    return nmOutput.isEmpty ? const <String>[] : nmOutput.split('\n');
  }
}
130 131 132 133

/// Matcher to be used for [ProcessResult] returned
/// from a process run
///
134 135
/// The default for [exitCode] will be 0 while
/// [stdoutPattern] and [stderrPattern] are both optional
136
class ProcessResultMatcher extends Matcher {
137 138 139 140
  const ProcessResultMatcher({
    this.exitCode = 0,
    this.stdoutPattern,
    this.stderrPattern,
141 142 143
  });

  /// The expected exit code to get returned from a process run
144
  final int exitCode;
145 146

  /// Substring to find in the process's stdout
147
  final Pattern? stdoutPattern;
148 149

  /// Substring to find in the process's stderr
150
  final Pattern? stderrPattern;
151 152 153

  @override
  Description describe(Description description) {
154 155 156
    description.add('a process with exit code $exitCode');
    if (stdoutPattern != null) {
      description.add(' and stdout: "$stdoutPattern"');
157
    }
158 159
    if (stderrPattern != null) {
      description.add(' and stderr: "$stderrPattern"');
160 161 162 163 164 165 166 167
    }

    return description;
  }

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
    final ProcessResult result = item as ProcessResult;
168 169 170 171 172 173 174 175 176 177 178
    bool foundStdout = true;
    bool foundStderr = true;

    final String stdout = result.stdout as String;
    final String stderr = result.stderr as String;
    if (stdoutPattern != null) {
      foundStdout = stdout.contains(stdoutPattern!);
      matchState['stdout'] = stdout;
    } else if (stdout.isNotEmpty) {
      // even if we were not asserting on stdout, show stdout for debug purposes
      matchState['stdout'] = stdout;
179 180
    }

181 182 183 184 185
    if (stderrPattern != null) {
      foundStderr = stderr.contains(stderrPattern!);
      matchState['stderr'] = stderr;
    } else if (stderr.isNotEmpty) {
      matchState['stderr'] = stderr;
186 187
    }

188
    return result.exitCode == exitCode && foundStdout && foundStderr;
189 190 191 192 193 194 195 196 197 198 199
  }

  @override
  Description describeMismatch(
    Object? item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    final ProcessResult result = item! as ProcessResult;

200 201
    if (result.exitCode != exitCode) {
      mismatchDescription.add('Actual exitCode was ${result.exitCode}\n');
202 203 204
    }

    if (matchState.containsKey('stdout')) {
205
      mismatchDescription.add('Actual stdout:\n${matchState["stdout"]}\n');
206 207 208
    }

    if (matchState.containsKey('stderr')) {
209
      mismatchDescription.add('Actual stderr:\n${matchState["stderr"]}\n');
210 211 212 213 214
    }

    return mismatchDescription;
  }
}