devfs_test.dart 17.1 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'dart:async';
import 'dart:convert';

8 9
import 'package:file/file.dart';
import 'package:file/memory.dart';
10
import 'package:flutter_tools/src/asset.dart';
11
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:flutter_tools/src/base/file_system.dart';
13
import 'package:flutter_tools/src/build_info.dart';
14
import 'package:flutter_tools/src/devfs.dart';
15
import 'package:flutter_tools/src/vmservice.dart';
16
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
17 18
import 'package:test/test.dart';

19
import 'src/common.dart';
20 21 22 23
import 'src/context.dart';
import 'src/mocks.dart';

void main() {
24 25 26
  FileSystem fs;
  String filePath;
  String filePath2;
27 28 29
  Directory tempDir;
  String basePath;
  DevFS devFS;
30 31
  final AssetBundle assetBundle = new AssetBundle();

32 33 34 35 36 37
  setUpAll(() {
    fs = new MemoryFileSystem();
    filePath = fs.path.join('lib', 'foo.txt');
    filePath2 = fs.path.join('foo', 'bar.txt');
  });

38 39
  group('DevFSContent', () {
    test('bytes', () {
40
      final DevFSByteContent content = new DevFSByteContent(<int>[4, 5, 6]);
41 42 43 44 45 46 47 48 49
      expect(content.bytes, orderedEquals(<int>[4, 5, 6]));
      expect(content.isModified, isTrue);
      expect(content.isModified, isFalse);
      content.bytes = <int>[7, 8, 9, 2];
      expect(content.bytes, orderedEquals(<int>[7, 8, 9, 2]));
      expect(content.isModified, isTrue);
      expect(content.isModified, isFalse);
    });
    test('string', () {
50
      final DevFSStringContent content = new DevFSStringContent('some string');
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
      expect(content.string, 'some string');
      expect(content.bytes, orderedEquals(UTF8.encode('some string')));
      expect(content.isModified, isTrue);
      expect(content.isModified, isFalse);
      content.string = 'another string';
      expect(content.string, 'another string');
      expect(content.bytes, orderedEquals(UTF8.encode('another string')));
      expect(content.isModified, isTrue);
      expect(content.isModified, isFalse);
      content.bytes = UTF8.encode('foo bar');
      expect(content.string, 'foo bar');
      expect(content.bytes, orderedEquals(UTF8.encode('foo bar')));
      expect(content.isModified, isTrue);
      expect(content.isModified, isFalse);
    });
  });

  group('devfs local', () {
69
    final MockDevFSOperations devFSOperations = new MockDevFSOperations();
70 71

    setUpAll(() {
72
      tempDir = _newTempDir(fs);
73
      basePath = tempDir.path;
74 75 76 77 78
    });
    tearDownAll(_cleanupTempDirs);

    testUsingContext('create dev file system', () async {
      // simulate workspace
79
      final File file = fs.file(fs.path.join(basePath, filePath));
80 81
      await file.parent.create(recursive: true);
      file.writeAsBytesSync(<int>[1, 2, 3]);
82
      _packages['my_project'] = fs.path.toUri('lib');
83 84

      // simulate package
85
      await _createPackage(fs, 'somepkg', 'somefile.txt');
86

87 88
      devFS = new DevFS.operations(devFSOperations, 'test', tempDir);
      await devFS.create();
89 90 91
      devFSOperations.expectMessages(<String>['create test']);
      expect(devFS.assetPathsToEvict, isEmpty);

92
      final int bytes = await devFS.update();
93
      devFSOperations.expectMessages(<String>[
94
        'writeFile test .packages',
95 96
        'writeFile test lib/foo.txt',
        'writeFile test packages/somepkg/somefile.txt',
97 98
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
99

100
      final List<String> packageSpecOnDevice = LineSplitter.split(UTF8.decode(
101
          await devFSOperations.devicePathToContent[fs.path.toUri('.packages')].contentsAsBytes()
102 103
      )).toList();
      expect(packageSpecOnDevice,
104
          unorderedEquals(<String>['my_project:lib/', 'somepkg:packages/somepkg/'])
105 106
      );

107
      expect(bytes, 48);
108 109
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
110
    });
111

112
    testUsingContext('add new file to local file system', () async {
113
      final File file = fs.file(fs.path.join(basePath, filePath2));
114 115
      await file.parent.create(recursive: true);
      file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6, 7]);
116
      final int bytes = await devFS.update();
117
      devFSOperations.expectMessages(<String>[
118
        'writeFile test foo/bar.txt',
119 120 121
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
      expect(bytes, 7);
122 123
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
124
    });
125

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
    testUsingContext('add new file to local file system and preserve unusal file name casing', () async {
      final String filePathWithUnusalCasing = fs.path.join('FooBar', 'TEST.txt');
      final File file = fs.file(fs.path.join(basePath, filePathWithUnusalCasing));
      await file.parent.create(recursive: true);
      file.writeAsBytesSync(<int>[1, 2, 3, 4, 5, 6, 7]);
      final int bytes = await devFS.update();
      devFSOperations.expectMessages(<String>[
        'writeFile test FooBar/TEST.txt',
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
      expect(bytes, 7);
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
    });

141
    testUsingContext('modify existing file on local file system', () async {
142
      await devFS.update();
143
      final File file = fs.file(fs.path.join(basePath, filePath));
144 145
      // Set the last modified time to 5 seconds in the past.
      updateFileModificationTime(file.path, new DateTime.now(), -5);
146 147 148 149 150
      int bytes = await devFS.update();
      devFSOperations.expectMessages(<String>[]);
      expect(devFS.assetPathsToEvict, isEmpty);
      expect(bytes, 0);

151
      await file.writeAsBytes(<int>[1, 2, 3, 4, 5, 6]);
152 153
      bytes = await devFS.update();
      devFSOperations.expectMessages(<String>[
154
        'writeFile test lib/foo.txt',
155 156 157
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
      expect(bytes, 6);
158 159
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
160
    });
161

162
    testUsingContext('delete a file from the local file system', () async {
163
      final File file = fs.file(fs.path.join(basePath, filePath));
164
      await file.delete();
165
      final int bytes = await devFS.update();
166
      devFSOperations.expectMessages(<String>[
167
        'deleteFile test lib/foo.txt',
168 169 170
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
      expect(bytes, 0);
171 172
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
173
    });
174

175
    testUsingContext('add new package', () async {
176
      await _createPackage(fs, 'newpkg', 'anotherfile.txt');
177
      final int bytes = await devFS.update();
178 179
      devFSOperations.expectMessages(<String>[
        'writeFile test .packages',
180
        'writeFile test packages/newpkg/anotherfile.txt',
181 182
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
183
      expect(bytes, 69);
184 185
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
186
    });
187

188
    testUsingContext('add new package with double slashes in URI', () async {
189
      final String packageName = 'doubleslashpkg';
190
      await _createPackage(fs, packageName, 'somefile.txt', doubleSlash: true);
191

192 193
      final Set<String> fileFilter = new Set<String>();
      final List<Uri> pkgUris = <Uri>[fs.path.toUri(basePath)]..addAll(_packages.values);
194 195 196 197 198 199 200
      for (Uri pkgUri in pkgUris) {
        if (!pkgUri.isAbsolute) {
          pkgUri = fs.path.toUri(fs.path.join(basePath, pkgUri.path));
        }
        fileFilter.addAll(fs.directory(pkgUri)
            .listSync(recursive: true)
            .where((FileSystemEntity file) => file is File)
201
            .map((FileSystemEntity file) => canonicalizePath(file.path))
202 203
            .toList());
      }
204
      final int bytes = await devFS.update(fileFilter: fileFilter);
205 206 207 208 209 210
      devFSOperations.expectMessages(<String>[
        'writeFile test .packages',
        'writeFile test packages/doubleslashpkg/somefile.txt',
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
      expect(bytes, 109);
211 212
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
213
    });
214

215 216
    testUsingContext('add an asset bundle', () async {
      assetBundle.entries['a.txt'] = new DevFSStringContent('abc');
217
      final int bytes = await devFS.update(bundle: assetBundle, bundleDirty: true);
218
      devFSOperations.expectMessages(<String>[
219
        'writeFile test ${_inAssetBuildDirectory(fs, 'a.txt')}',
220 221 222 223
      ]);
      expect(devFS.assetPathsToEvict, unorderedMatches(<String>['a.txt']));
      devFS.assetPathsToEvict.clear();
      expect(bytes, 3);
224 225
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
226
    });
227

228 229
    testUsingContext('add a file to the asset bundle - bundleDirty', () async {
      assetBundle.entries['b.txt'] = new DevFSStringContent('abcd');
230
      final int bytes = await devFS.update(bundle: assetBundle, bundleDirty: true);
231 232
      // Expect entire asset bundle written because bundleDirty is true
      devFSOperations.expectMessages(<String>[
233 234
        'writeFile test ${_inAssetBuildDirectory(fs, 'a.txt')}',
        'writeFile test ${_inAssetBuildDirectory(fs, 'b.txt')}',
235 236 237 238 239
      ]);
      expect(devFS.assetPathsToEvict, unorderedMatches(<String>[
        'a.txt', 'b.txt']));
      devFS.assetPathsToEvict.clear();
      expect(bytes, 7);
240 241
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
242
    });
243

244
    testUsingContext('add a file to the asset bundle', () async {
245
      assetBundle.entries['c.txt'] = new DevFSStringContent('12');
246
      final int bytes = await devFS.update(bundle: assetBundle);
247
      devFSOperations.expectMessages(<String>[
248
        'writeFile test ${_inAssetBuildDirectory(fs, 'c.txt')}',
249 250 251 252 253
      ]);
      expect(devFS.assetPathsToEvict, unorderedMatches(<String>[
        'c.txt']));
      devFS.assetPathsToEvict.clear();
      expect(bytes, 2);
254 255
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
256
    });
257

258
    testUsingContext('delete a file from the asset bundle', () async {
259
      assetBundle.entries.remove('c.txt');
260
      final int bytes = await devFS.update(bundle: assetBundle);
261
      devFSOperations.expectMessages(<String>[
262
        'deleteFile test ${_inAssetBuildDirectory(fs, 'c.txt')}',
263 264 265 266
      ]);
      expect(devFS.assetPathsToEvict, unorderedMatches(<String>['c.txt']));
      devFS.assetPathsToEvict.clear();
      expect(bytes, 0);
267 268
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
269
    });
270

271
    testUsingContext('delete all files from the asset bundle', () async {
272
      assetBundle.entries.clear();
273
      final int bytes = await devFS.update(bundle: assetBundle, bundleDirty: true);
274
      devFSOperations.expectMessages(<String>[
275 276
        'deleteFile test ${_inAssetBuildDirectory(fs, 'a.txt')}',
        'deleteFile test ${_inAssetBuildDirectory(fs, 'b.txt')}',
277 278 279 280 281 282
      ]);
      expect(devFS.assetPathsToEvict, unorderedMatches(<String>[
        'a.txt', 'b.txt'
      ]));
      devFS.assetPathsToEvict.clear();
      expect(bytes, 0);
283 284
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
285
    });
286

287 288
    testUsingContext('delete dev file system', () async {
      await devFS.destroy();
289 290
      devFSOperations.expectMessages(<String>['destroy test']);
      expect(devFS.assetPathsToEvict, isEmpty);
291 292
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
293
    });
294
  });
295 296 297 298 299

  group('devfs remote', () {
    MockVMService vmService;

    setUpAll(() async {
300
      tempDir = _newTempDir(fs);
301 302 303 304 305 306 307
      basePath = tempDir.path;
      vmService = new MockVMService();
      await vmService.setUp();
    });
    tearDownAll(() async {
      await vmService.tearDown();
      _cleanupTempDirs();
308
    });
309 310 311

    testUsingContext('create dev file system', () async {
      // simulate workspace
312
      final File file = fs.file(fs.path.join(basePath, filePath));
313 314 315 316
      await file.parent.create(recursive: true);
      file.writeAsBytesSync(<int>[1, 2, 3]);

      // simulate package
317
      await _createPackage(fs, 'somepkg', 'somefile.txt');
318 319 320 321 322 323

      devFS = new DevFS(vmService, 'test', tempDir);
      await devFS.create();
      vmService.expectMessages(<String>['create test']);
      expect(devFS.assetPathsToEvict, isEmpty);

324
      final int bytes = await devFS.update();
325 326
      vmService.expectMessages(<String>[
        'writeFile test .packages',
327 328
        'writeFile test lib/foo.txt',
        'writeFile test packages/somepkg/somefile.txt',
329 330
      ]);
      expect(devFS.assetPathsToEvict, isEmpty);
331
      expect(bytes, 48);
332 333
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
Dan Rubel's avatar
Dan Rubel committed
334
    });
335 336

    testUsingContext('delete dev file system', () async {
Dan Rubel's avatar
Dan Rubel committed
337
      expect(vmService.messages, isEmpty, reason: 'prior test timeout');
338
      await devFS.destroy();
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
      vmService.expectMessages(<String>['destroy test']);
      expect(devFS.assetPathsToEvict, isEmpty);
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
    });

    testUsingContext('cleanup preexisting file system', () async {
      // simulate workspace
      final File file = fs.file(fs.path.join(basePath, filePath));
      await file.parent.create(recursive: true);
      file.writeAsBytesSync(<int>[1, 2, 3]);

      // simulate package
      await _createPackage(fs, 'somepkg', 'somefile.txt');

      devFS = new DevFS(vmService, 'test', tempDir);
      await devFS.create();
      vmService.expectMessages(<String>['create test']);
      expect(devFS.assetPathsToEvict, isEmpty);

      // Try to create again.
      await devFS.create();
      vmService.expectMessages(<String>['create test', 'destroy test', 'create test']);
      expect(devFS.assetPathsToEvict, isEmpty);

      // Really destroy.
      await devFS.destroy();
      vmService.expectMessages(<String>['destroy test']);
367
      expect(devFS.assetPathsToEvict, isEmpty);
368 369
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
370
    });
371
  });
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
}

class MockVMService extends BasicMock implements VMService {
  Uri _httpAddress;
  HttpServer _server;
  MockVM _vm;

  MockVMService() {
    _vm = new MockVM(this);
  }

  @override
  Uri get httpAddress => _httpAddress;

  @override
  VM get vm => _vm;

  Future<Null> setUp() async {
390 391 392 393 394 395 396 397
    try {
      _server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V6, 0);
      _httpAddress = Uri.parse('http://[::1]:${_server.port}');
    } on SocketException {
      // Fall back to IPv4 if the host doesn't support binding to IPv6 localhost
      _server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0);
      _httpAddress = Uri.parse('http://127.0.0.1:${_server.port}');
    }
398
    _server.listen((HttpRequest request) {
399
      final String fsName = request.headers.value('dev_fs_name');
400
      final String devicePath = UTF8.decode(BASE64.decode(request.headers.value('dev_fs_uri_b64')));
401
      messages.add('writeFile $fsName $devicePath');
402
      request.drain<List<int>>().then<Null>((List<int> value) {
403 404 405 406 407 408 409 410
        request.response
          ..write('Got it')
          ..close();
      });
    });
  }

  Future<Null> tearDown() async {
411
    await _server?.close();
412 413 414 415 416 417 418 419 420
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

class MockVM implements VM {
  final MockVMService _service;
  final Uri _baseUri = Uri.parse('file:///tmp/devfs/test');
421 422 423
  bool _devFSExists = false;

  static const int kFileSystemAlreadyExists = 1001;
424 425 426 427 428 429

  MockVM(this._service);

  @override
  Future<Map<String, dynamic>> createDevFS(String fsName) async {
    _service.messages.add('create $fsName');
430 431 432 433
    if (_devFSExists) {
      throw new rpc.RpcException(kFileSystemAlreadyExists, 'File system already exists');
    }
    _devFSExists = true;
434 435 436
    return <String, dynamic>{'uri': '$_baseUri'};
  }

437 438 439 440 441 442 443
  @override
  Future<Map<String, dynamic>> deleteDevFS(String fsName) async {
    _service.messages.add('destroy $fsName');
    _devFSExists = false;
    return <String, dynamic>{'type': 'Success'};
  }

444
  @override
445 446 447 448 449
  Future<Map<String, dynamic>> invokeRpcRaw(String method, {
    Map<String, dynamic> params: const <String, dynamic>{},
    Duration timeout,
    bool timeoutFatal: true,
  }) async {
450 451 452 453 454 455 456 457 458 459
    _service.messages.add('$method $params');
    return <String, dynamic>{'success': true};
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}


final List<Directory> _tempDirs = <Directory>[];
460
final Map <String, Uri> _packages = <String, Uri>{};
461

462
Directory _newTempDir(FileSystem fs) {
463
  final Directory tempDir = fs.systemTempDirectory.createTempSync('devfs${_tempDirs.length}');
464 465 466 467 468
  _tempDirs.add(tempDir);
  return tempDir;
}

void _cleanupTempDirs() {
469
  while (_tempDirs.isNotEmpty) {
470 471 472 473
    _tempDirs.removeLast().deleteSync(recursive: true);
  }
}

474 475
Future<Null> _createPackage(FileSystem fs, String pkgName, String pkgFileName, { bool doubleSlash: false }) async {
  final Directory pkgTempDir = _newTempDir(fs);
476 477 478
  String pkgFilePath = fs.path.join(pkgTempDir.path, pkgName, 'lib', pkgFileName);
  if (doubleSlash) {
    // Force two separators into the path.
479
    final String doubleSlash = fs.path.separator + fs.path.separator;
480 481 482
    pkgFilePath = pkgTempDir.path + doubleSlash  + fs.path.join(pkgName, 'lib', pkgFileName);
  }
  final File pkgFile = fs.file(pkgFilePath);
483 484
  await pkgFile.parent.create(recursive: true);
  pkgFile.writeAsBytesSync(<int>[11, 12, 13]);
485
  _packages[pkgName] = fs.path.toUri(pkgFile.parent.path);
486
  final StringBuffer sb = new StringBuffer();
487 488
  _packages.forEach((String pkgName, Uri pkgUri) {
    sb.writeln('$pkgName:$pkgUri');
489
  });
490
  fs.file(fs.path.join(_tempDirs[0].path, '.packages')).writeAsStringSync(sb.toString());
491
}
492

493
String _inAssetBuildDirectory(FileSystem fs, String filename) {
494 495
  return '${fs.path.toUri(getAssetBuildDirectory()).path}/$filename';
}