// 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.

library test.domain.analysis;

import 'dart:async';

import 'package:analysis_server/plugin/protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/constants.dart';
import 'package:analysis_server/src/domain_analysis.dart';
import 'package:analysis_server/src/plugin/server_plugin.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:plugin/manager.dart';
import 'package:plugin/plugin.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'analysis_abstract.dart';
import 'mock_sdk.dart';
import 'mocks.dart';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AnalysisDomainTest);
    defineReflectiveTests(SetSubscriptionsTest);
  });

  MockServerChannel serverChannel;
  MemoryResourceProvider resourceProvider;
  AnalysisServer server;
  AnalysisDomainHandler handler;

  void processRequiredPlugins(ServerPlugin serverPlugin) {
    List<Plugin> plugins = <Plugin>[];
    plugins.addAll(AnalysisEngine.instance.requiredPlugins);
    plugins.add(serverPlugin);

    ExtensionManager manager = new ExtensionManager();
    manager.processPlugins(plugins);
  }

  setUp(() {
    serverChannel = new MockServerChannel();
    resourceProvider = new MemoryResourceProvider();
    ServerPlugin serverPlugin = new ServerPlugin();
    processRequiredPlugins(serverPlugin);
    // Create an SDK in the mock file system.
    new MockSdk(resourceProvider: resourceProvider);
    server = new AnalysisServer(
        serverChannel,
        resourceProvider,
        new MockPackageMapProvider(),
        null,
        serverPlugin,
        new AnalysisServerOptions(),
        new DartSdkManager('/', false),
        InstrumentationService.NULL_SERVICE);
    handler = new AnalysisDomainHandler(server);
  });

  group('updateContent', testUpdateContent);

  group('AnalysisDomainHandler', () {
    group('getReachableSources', () {
      test('valid sources', () async {
        String fileA = '/project/a.dart';
        String fileB = '/project/b.dart';
        resourceProvider.newFile(fileA, 'import "b.dart";');
        resourceProvider.newFile(fileB, '');

        server.setAnalysisRoots('0', ['/project/'], [], {});

        await server.onAnalysisComplete;

        var request =
            new AnalysisGetReachableSourcesParams(fileA).toRequest('0');
        var response = handler.handleRequest(request);

        Map json = response.toJson()[Response.RESULT];

        // Sanity checks.
        expect(json['sources'], hasLength(6));
        expect(json['sources']['file:///project/a.dart'],
            unorderedEquals(['dart:core', 'file:///project/b.dart']));
        expect(json['sources']['file:///project/b.dart'], ['dart:core']);
      });

      test('invalid source', () async {
        resourceProvider.newFile('/project/a.dart', 'import "b.dart";');
        server.setAnalysisRoots('0', ['/project/'], [], {});

        await server.onAnalysisComplete;

        var request =
            new AnalysisGetReachableSourcesParams('/does/not/exist.dart')
                .toRequest('0');
        var response = handler.handleRequest(request);
        expect(response.error, isNotNull);
        expect(response.error.code,
            RequestErrorCode.GET_REACHABLE_SOURCES_INVALID_FILE);
      });
    });

    group('setAnalysisRoots', () {
      Response testSetAnalysisRoots(
          List<String> included, List<String> excluded) {
        Request request = new AnalysisSetAnalysisRootsParams(included, excluded)
            .toRequest('0');
        return handler.handleRequest(request);
      }

      group('excluded', () {
        test('excluded folder', () async {
          String fileA = '/project/aaa/a.dart';
          String fileB = '/project/bbb/b.dart';
          resourceProvider.newFile(fileA, '// a');
          resourceProvider.newFile(fileB, '// b');
          var response = testSetAnalysisRoots(['/project'], ['/project/bbb']);
          var serverRef = server;
          expect(response, isResponseSuccess('0'));
          // unit "a" is resolved eventually
          // unit "b" is not resolved
          await server.onAnalysisComplete;
          expect(await serverRef.getResolvedCompilationUnit(fileA), isNotNull);
          expect(await serverRef.getResolvedCompilationUnit(fileB), isNull);
        });

        test('not absolute', () async {
          var response = testSetAnalysisRoots([], ['foo/bar']);
          expect(
              response,
              isResponseFailure(
                  '0', RequestErrorCode.INVALID_FILE_PATH_FORMAT));
        });

        test('not normalized', () async {
          var response = testSetAnalysisRoots([], ['/foo/../bar']);
          expect(
              response,
              isResponseFailure(
                  '0', RequestErrorCode.INVALID_FILE_PATH_FORMAT));
        });
      });

      group('included', () {
        test('new folder', () async {
          String file = '/project/bin/test.dart';
          resourceProvider.newFile('/project/pubspec.yaml', 'name: project');
          resourceProvider.newFile(file, 'main() {}');
          var response = testSetAnalysisRoots(['/project'], []);
          var serverRef = server;
          expect(response, isResponseSuccess('0'));
          // verify that unit is resolved eventually
          await server.onAnalysisComplete;
          var unit = await serverRef.getResolvedCompilationUnit(file);
          expect(unit, isNotNull);
        });

        test('nonexistent folder', () async {
          String fileB = '/project_b/b.dart';
          resourceProvider.newFile(fileB, '// b');
          var response = testSetAnalysisRoots(['/project_a', '/project_b'], []);
          var serverRef = server;
          expect(response, isResponseSuccess('0'));
          // Non-existence of /project_a should not prevent files in /project_b
          // from being analyzed.
          await server.onAnalysisComplete;
          var unit = await serverRef.getResolvedCompilationUnit(fileB);
          expect(unit, isNotNull);
        });

        test('not absolute', () async {
          var response = testSetAnalysisRoots(['foo/bar'], []);
          expect(
              response,
              isResponseFailure(
                  '0', RequestErrorCode.INVALID_FILE_PATH_FORMAT));
        });

        test('not normalized', () async {
          var response = testSetAnalysisRoots(['/foo/../bar'], []);
          expect(
              response,
              isResponseFailure(
                  '0', RequestErrorCode.INVALID_FILE_PATH_FORMAT));
        });
      });
    });

    group('setPriorityFiles', () {
      test('invalid', () {
        // TODO(paulberry): under the "eventual consistency" model this request
        // should not be invalid.
        var request = new AnalysisSetPriorityFilesParams(['/project/lib.dart'])
            .toRequest('0');
        var response = handler.handleRequest(request);
        expect(response, isResponseFailure('0'));
      });

      test('valid', () {
        resourceProvider.newFolder('/p1');
        resourceProvider.newFile('/p1/a.dart', 'library a;');
        resourceProvider.newFolder('/p2');
        resourceProvider.newFile('/p2/b.dart', 'library b;');
        resourceProvider.newFile('/p2/c.dart', 'library c;');

        var setRootsRequest =
            new AnalysisSetAnalysisRootsParams(['/p1', '/p2'], [])
                .toRequest('0');
        var setRootsResponse = handler.handleRequest(setRootsRequest);
        expect(setRootsResponse, isResponseSuccess('0'));

        void setPriorityFiles(List<String> fileList) {
          var request =
              new AnalysisSetPriorityFilesParams(fileList).toRequest('0');
          var response = handler.handleRequest(request);
          expect(response, isResponseSuccess('0'));
          // TODO(brianwilkerson) Enable the line below after getPriorityFiles
          // has been implemented.
          // expect(server.getPriorityFiles(), unorderedEquals(fileList));
        }

        setPriorityFiles(['/p1/a.dart', '/p2/b.dart']);
        setPriorityFiles(['/p2/b.dart', '/p2/c.dart']);
        setPriorityFiles([]);
      });
    });

    group('updateOptions', () {
      test('invalid', () {
        var request = new Request('0', ANALYSIS_UPDATE_OPTIONS, {
          OPTIONS: {'not-an-option': true}
        });
        var response = handler.handleRequest(request);
        // Invalid options should be silently ignored.
        expect(response, isResponseSuccess('0'));
      });

      test('null', () {
        // null is allowed as a synonym for {}.
        var request =
            new Request('0', ANALYSIS_UPDATE_OPTIONS, {OPTIONS: null});
        var response = handler.handleRequest(request);
        expect(response, isResponseSuccess('0'));
      });
    });
  });
}

testUpdateContent() {
  test('bad type', () {
    AnalysisTestHelper helper = new AnalysisTestHelper();
    helper.createSingleFileProject('// empty');
    return helper.onAnalysisComplete.then((_) {
      Request request = new Request('0', ANALYSIS_UPDATE_CONTENT, {
        'files': {
          helper.testFile: {
            TYPE: 'foo',
          }
        }
      });
      Response response = helper.handler.handleRequest(request);
      expect(response, isResponseFailure('0'));
    });
  });

  test('full content', () {
    AnalysisTestHelper helper = new AnalysisTestHelper();
    helper.createSingleFileProject('// empty');
    return helper.onAnalysisComplete.then((_) {
      // no errors initially
      List<AnalysisError> errors = helper.getTestErrors();
      expect(errors, isEmpty);
      // update code
      helper.sendContentChange(new AddContentOverlay('library lib'));
      // wait, there is an error
      return helper.onAnalysisComplete.then((_) {
        List<AnalysisError> errors = helper.getTestErrors();
        expect(errors, hasLength(1));
      });
    });
  });

  test('incremental', () {
    AnalysisTestHelper helper = new AnalysisTestHelper();
    String initialContent = 'library A;';
    helper.createSingleFileProject(initialContent);
    return helper.onAnalysisComplete.then((_) {
      // no errors initially
      List<AnalysisError> errors = helper.getTestErrors();
      expect(errors, isEmpty);
      // Add the file to the cache
      helper.sendContentChange(new AddContentOverlay(initialContent));
      // update code
      helper.sendContentChange(new ChangeContentOverlay(
          [new SourceEdit('library '.length, 'A;'.length, 'lib')]));
      // wait, there is an error
      return helper.onAnalysisComplete.then((_) {
        List<AnalysisError> errors = helper.getTestErrors();
        expect(errors, hasLength(1));
      });
    });
  });

  test('change on disk, normal', () {
    AnalysisTestHelper helper = new AnalysisTestHelper();
    helper.createSingleFileProject('library A;');
    return helper.onAnalysisComplete.then((_) {
      // There should be no errors
      expect(helper.getTestErrors(), hasLength(0));
      // Change file on disk, adding a syntax error.
      helper.resourceProvider.modifyFile(helper.testFile, 'library lib');
      // There should be errors now.
      return pumpEventQueue().then((_) {
        return helper.onAnalysisComplete.then((_) {
          expect(helper.getTestErrors(), hasLength(1));
        });
      });
    });
  });

  test('change on disk, during override', () {
    AnalysisTestHelper helper = new AnalysisTestHelper();
    helper.createSingleFileProject('library A;');
    return helper.onAnalysisComplete.then((_) {
      // update code
      helper.sendContentChange(new AddContentOverlay('library B;'));
      // There should be no errors
      return helper.onAnalysisComplete.then((_) {
        expect(helper.getTestErrors(), hasLength(0));
        // Change file on disk, adding a syntax error.
        helper.resourceProvider.modifyFile(helper.testFile, 'library lib');
        // There should still be no errors (file should not have been reread).
        return helper.onAnalysisComplete.then((_) {
          expect(helper.getTestErrors(), hasLength(0));
          // Send a content change with a null content param--file should be
          // reread from disk.
          helper.sendContentChange(new RemoveContentOverlay());
          // There should be errors now.
          return helper.onAnalysisComplete.then((_) {
            expect(helper.getTestErrors(), hasLength(1));
          });
        });
      });
    });
  });

  group('out of range', () {
    Future outOfRangeTest(SourceEdit edit) {
      AnalysisTestHelper helper = new AnalysisTestHelper();
      helper.createSingleFileProject('library A;');
      return helper.onAnalysisComplete.then((_) {
        helper.sendContentChange(new AddContentOverlay('library B;'));
        return helper.onAnalysisComplete.then((_) {
          ChangeContentOverlay contentChange = new ChangeContentOverlay([edit]);
          Request request =
              new AnalysisUpdateContentParams({helper.testFile: contentChange})
                  .toRequest('0');
          Response response = helper.handler.handleRequest(request);
          expect(response,
              isResponseFailure('0', RequestErrorCode.INVALID_OVERLAY_CHANGE));
        });
      });
    }

    test('negative length', () {
      return outOfRangeTest(new SourceEdit(3, -1, 'foo'));
    });

    test('negative offset', () {
      return outOfRangeTest(new SourceEdit(-1, 3, 'foo'));
    });

    test('beyond end', () {
      return outOfRangeTest(new SourceEdit(6, 6, 'foo'));
    });
  });
}

@reflectiveTest
class AnalysisDomainTest extends AbstractAnalysisTest {
  Map<String, List<AnalysisError>> filesErrors = {};

  void processNotification(Notification notification) {
    if (notification.event == ANALYSIS_ERRORS) {
      var decoded = new AnalysisErrorsParams.fromNotification(notification);
      filesErrors[decoded.file] = decoded.errors;
    }
  }

  test_setRoots_packages() {
    // prepare package
    String pkgFile = '/packages/pkgA/libA.dart';
    resourceProvider.newFile(
        pkgFile,
        '''
library lib_a;
class A {}
''');
    resourceProvider.newFile(
        '/project/.packages', 'pkgA:file:///packages/pkgA');
    addTestFile('''
import 'package:pkgA/libA.dart';
main(A a) {
}
''');
    // create project and wait for analysis
    createProject();
    return waitForTasksFinished().then((_) {
      // if 'package:pkgA/libA.dart' was resolved, then there are no errors
      expect(filesErrors[testFile], isEmpty);
      // errors are not reported for packages
      expect(filesErrors[pkgFile], isNull);
    });
  }
}

/**
 * A helper to test 'analysis.*' requests.
 */
class AnalysisTestHelper {
  MockServerChannel serverChannel;
  MemoryResourceProvider resourceProvider;
  AnalysisServer server;
  AnalysisDomainHandler handler;

  Map<AnalysisService, List<String>> analysisSubscriptions = {};

  Map<String, List<AnalysisError>> filesErrors = {};
  Map<String, List<HighlightRegion>> filesHighlights = {};
  Map<String, List<NavigationRegion>> filesNavigation = {};

  String testFile = '/project/bin/test.dart';
  String testCode;

  AnalysisTestHelper() {
    ServerPlugin serverPlugin = new ServerPlugin();
    processRequiredPlugins(serverPlugin);
    serverChannel = new MockServerChannel();
    resourceProvider = new MemoryResourceProvider();
    // Create an SDK in the mock file system.
    new MockSdk(resourceProvider: resourceProvider);
    server = new AnalysisServer(
        serverChannel,
        resourceProvider,
        new MockPackageMapProvider(),
        null,
        serverPlugin,
        new AnalysisServerOptions(),
        new DartSdkManager('/', false),
        InstrumentationService.NULL_SERVICE);
    handler = new AnalysisDomainHandler(server);
    // listen for notifications
    Stream<Notification> notificationStream =
        serverChannel.notificationController.stream;
    notificationStream.listen((Notification notification) {
      if (notification.event == ANALYSIS_ERRORS) {
        var decoded = new AnalysisErrorsParams.fromNotification(notification);
        filesErrors[decoded.file] = decoded.errors;
      }
      if (notification.event == ANALYSIS_HIGHLIGHTS) {
        var params =
            new AnalysisHighlightsParams.fromNotification(notification);
        filesHighlights[params.file] = params.regions;
      }
      if (notification.event == ANALYSIS_NAVIGATION) {
        var params =
            new AnalysisNavigationParams.fromNotification(notification);
        filesNavigation[params.file] = params.regions;
      }
    });
  }

  /**
   * Returns a [Future] that completes when the server's analysis is complete.
   */
  Future get onAnalysisComplete {
    return server.onAnalysisComplete;
  }

  void addAnalysisSubscription(AnalysisService service, String file) {
    // add file to subscription
    var files = analysisSubscriptions[service];
    if (files == null) {
      files = <String>[];
      analysisSubscriptions[service] = files;
    }
    files.add(file);
    // set subscriptions
    Request request = new AnalysisSetSubscriptionsParams(analysisSubscriptions)
        .toRequest('0');
    handleSuccessfulRequest(request);
  }

  void addAnalysisSubscriptionHighlights(String file) {
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file);
  }

  void addAnalysisSubscriptionNavigation(String file) {
    addAnalysisSubscription(AnalysisService.NAVIGATION, file);
  }

  /**
   * Creates an empty project `/project`.
   */
  void createEmptyProject() {
    resourceProvider.newFolder('/project');
    Request request =
        new AnalysisSetAnalysisRootsParams(['/project'], []).toRequest('0');
    handleSuccessfulRequest(request);
  }

  /**
   * Creates a project with a single Dart file `/project/bin/test.dart` with
   * the given [code].
   */
  void createSingleFileProject(code) {
    this.testCode = _getCodeString(code);
    resourceProvider.newFolder('/project');
    resourceProvider.newFile(testFile, testCode);
    Request request =
        new AnalysisSetAnalysisRootsParams(['/project'], []).toRequest('0');
    handleSuccessfulRequest(request);
  }

  /**
   * Returns the offset of [search] in [testCode].
   * Fails if not found.
   */
  int findOffset(String search) {
    int offset = testCode.indexOf(search);
    expect(offset, isNot(-1));
    return offset;
  }

  /**
   * Returns [AnalysisError]s recorded for the given [file].
   * May be empty, but not `null`.
   */
  List<AnalysisError> getErrors(String file) {
    List<AnalysisError> errors = filesErrors[file];
    if (errors != null) {
      return errors;
    }
    return <AnalysisError>[];
  }

  /**
   * Returns highlights recorded for the given [file].
   * May be empty, but not `null`.
   */
  List<HighlightRegion> getHighlights(String file) {
    List<HighlightRegion> highlights = filesHighlights[file];
    if (highlights != null) {
      return highlights;
    }
    return [];
  }

  /**
   * Returns navigation regions recorded for the given [file].
   * May be empty, but not `null`.
   */
  List<NavigationRegion> getNavigation(String file) {
    List<NavigationRegion> navigation = filesNavigation[file];
    if (navigation != null) {
      return navigation;
    }
    return [];
  }

  /**
   * Returns [AnalysisError]s recorded for the [testFile].
   * May be empty, but not `null`.
   */
  List<AnalysisError> getTestErrors() {
    return getErrors(testFile);
  }

  /**
   * Returns highlights recorded for the given [testFile].
   * May be empty, but not `null`.
   */
  List<HighlightRegion> getTestHighlights() {
    return getHighlights(testFile);
  }

  /**
   * Returns navigation information recorded for the given [testFile].
   * May be empty, but not `null`.
   */
  List<NavigationRegion> getTestNavigation() {
    return getNavigation(testFile);
  }

  /**
   * Validates that the given [request] is handled successfully.
   */
  void handleSuccessfulRequest(Request request) {
    Response response = handler.handleRequest(request);
    expect(response, isResponseSuccess('0'));
  }

  void processRequiredPlugins(ServerPlugin serverPlugin) {
    List<Plugin> plugins = <Plugin>[];
    plugins.addAll(AnalysisEngine.instance.requiredPlugins);
    plugins.add(serverPlugin);

    ExtensionManager manager = new ExtensionManager();
    manager.processPlugins(plugins);
  }

  /**
   * Send an `updateContent` request for [testFile].
   */
  void sendContentChange(dynamic contentChange) {
    Request request = new AnalysisUpdateContentParams({testFile: contentChange})
        .toRequest('0');
    handleSuccessfulRequest(request);
  }

  String setFileContent(String path, String content) {
    resourceProvider.newFile(path, content);
    return path;
  }

  /**
   * Stops the associated server.
   */
  void stopServer() {
    server.done();
  }

  static String _getCodeString(code) {
    if (code is List<String>) {
      code = code.join('\n');
    }
    return code as String;
  }
}

@reflectiveTest
class SetSubscriptionsTest extends AbstractAnalysisTest {
  Map<String, List<HighlightRegion>> filesHighlights = {};

  void processNotification(Notification notification) {
    if (notification.event == ANALYSIS_HIGHLIGHTS) {
      var params = new AnalysisHighlightsParams.fromNotification(notification);
      filesHighlights[params.file] = params.regions;
    }
  }

  test_afterAnalysis() async {
    addTestFile('int V = 42;');
    createProject();
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[testFile], isNull);
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile);
    await server.onAnalysisComplete;
    // there are results
    expect(filesHighlights[testFile], isNotEmpty);
  }

  test_afterAnalysis_noSuchFile() async {
    String file = '/no-such-file.dart';
    addTestFile('// no matter');
    createProject();
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[testFile], isNull);
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file);
    await server.onAnalysisComplete;
    // there are results
    expect(filesHighlights[file], isNull);
  }

  test_afterAnalysis_packageFile_external() async {
    String pkgFile = '/packages/pkgA/lib/libA.dart';
    resourceProvider.newFile(
        pkgFile,
        '''
library lib_a;
class A {}
''');
    resourceProvider.newFile(
        '/project/.packages', 'pkgA:file:///packages/pkgA/lib');
    //
    addTestFile('''
import 'package:pkgA/libA.dart';
main() {
  new A();
}
''');
    createProject();
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[pkgFile], isNull);
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFile);
    await server.onAnalysisComplete;
    // there are results
    expect(filesHighlights[pkgFile], isNotEmpty);
  }

  test_afterAnalysis_packageFile_inRoot() async {
    String pkgA = '/pkgA';
    String pkgB = '/pkgA';
    String pkgFileA = '$pkgA/lib/libA.dart';
    String pkgFileB = '$pkgA/lib/libB.dart';
    resourceProvider.newFile(
        pkgFileA,
        '''
library lib_a;
class A {}
''');
    resourceProvider.newFile(
        pkgFileB,
        '''
import 'package:pkgA/libA.dart';
main() {
  new A();
}
''');
    packageMapProvider.packageMap = {
      'pkgA': [
        resourceProvider.newFolder('$pkgA/lib'),
        resourceProvider.newFolder('$pkgB/lib')
      ]
    };
    // add 'pkgA' and 'pkgB' as projects
    {
      resourceProvider.newFolder(projectPath);
      handleSuccessfulRequest(
          new AnalysisSetAnalysisRootsParams([pkgA, pkgB], []).toRequest('0'));
    }
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[pkgFileA], isNull);
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFileA);
    await server.onAnalysisComplete;
    // there are results
    expect(filesHighlights[pkgFileA], isNotEmpty);
  }

  test_afterAnalysis_packageFile_notUsed() async {
    String pkgFile = '/packages/pkgA/lib/libA.dart';
    resourceProvider.newFile(
        pkgFile,
        '''
library lib_a;
class A {}
''');
    resourceProvider.newFile('/project/.packages', 'pkgA:/packages/pkgA/lib');
    //
    addTestFile('// no "pkgA" reference');
    createProject();
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[pkgFile], isNull);
    // make it a priority file, so make analyzable
    server.setPriorityFiles('0', [pkgFile]);
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFile);
    await server.onAnalysisComplete;
    // there are results
    expect(filesHighlights[pkgFile], isNotEmpty);
  }

  test_afterAnalysis_sdkFile() async {
    String file = '/lib/core/core.dart';
    addTestFile('// no matter');
    createProject();
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[file], isNull);
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file);
    await server.onAnalysisComplete;
    // there are results
    expect(filesHighlights[file], isNotEmpty);
  }

  test_beforeAnalysis() async {
    addTestFile('int V = 42;');
    createProject();
    // subscribe
    addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile);
    // wait for analysis
    await waitForTasksFinished();
    expect(filesHighlights[testFile], isNotEmpty);
  }
}
