Use @isTest and @isTestGroup to understand executable element as a test/group.

R=brianwilkerson@google.com, devoncarew@google.com

Bug: https://github.com/flutter/flutter-intellij/issues/2055
Change-Id: I7c8e7639d111eca63df0780ebdec182573493047
Reviewed-on: https://dart-review.googlesource.com/53690
Reviewed-by: Devon Carew <devoncarew@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/computer/computer_outline.dart b/pkg/analysis_server/lib/src/computer/computer_outline.dart
index 3516bbe..c086d92 100644
--- a/pkg/analysis_server/lib/src/computer/computer_outline.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_outline.dart
@@ -365,6 +365,9 @@
    * test package.
    */
   bool isGroup(engine.ExecutableElement element) {
+    if (element != null && element.hasIsTestGroup) {
+      return true;
+    }
     return element is engine.FunctionElement &&
         element.name == 'group' &&
         _isInsideTestPackage(element);
@@ -375,6 +378,9 @@
    * test package.
    */
   bool isTest(engine.ExecutableElement element) {
+    if (element != null && element.hasIsTest) {
+      return true;
+    }
     return element is engine.FunctionElement &&
         element.name == 'test' &&
         _isInsideTestPackage(element);
diff --git a/pkg/analysis_server/test/abstract_context.dart b/pkg/analysis_server/test/abstract_context.dart
index dccf6a6..eb77a4a 100644
--- a/pkg/analysis_server/test/abstract_context.dart
+++ b/pkg/analysis_server/test/abstract_context.dart
@@ -68,12 +68,24 @@
   Source addMetaPackageSource() => addPackageSource('meta', 'meta.dart', r'''
 library meta;
 
+const _IsTest isTest = const _IsTest();
+
+const _IsTestGroup isTestGroup = const _IsTestGroup();
+
 const Required required = const Required();
 
 class Required {
   final String reason;
   const Required([this.reason]);
 }
+
+class _IsTest {
+  const _IsTest();
+}
+
+class _IsTestGroup {
+  const _IsTestGroup();
+}
 ''');
 
   Source addPackageSource(String packageName, String filePath, String content) {
diff --git a/pkg/analysis_server/test/src/computer/outline_computer_test.dart b/pkg/analysis_server/test/src/computer/outline_computer_test.dart
index 316cf31..7f3a6ce 100644
--- a/pkg/analysis_server/test/src/computer/outline_computer_test.dart
+++ b/pkg/analysis_server/test/src/computer/outline_computer_test.dart
@@ -546,6 +546,124 @@
     expect(outline, isNotNull);
   }
 
+  test_isTest_isTestGroup() async {
+    addMetaPackageSource();
+    Outline outline = await _computeOutline('''
+import 'package:meta/meta.dart';
+
+@isTestGroup
+void myGroup(name, body()) {}
+
+@isTest
+void myTest(name) {}
+
+void main() {
+  myGroup('group1', () {
+    myGroup('group1_1', () {
+      myTest('test1_1_1');
+      myTest('test1_1_2');
+    });
+    myGroup('group1_2', () {
+      myTest('test1_2_1');
+    });
+  });
+  myGroup('group2', () {
+    myTest('test2_1');
+    myTest('test2_2');
+  });
+}
+''');
+    // unit
+    List<Outline> unit_children = outline.children;
+    expect(unit_children, hasLength(3));
+    // main
+    Outline main_outline = unit_children[2];
+    _expect(main_outline,
+        kind: ElementKind.FUNCTION,
+        name: 'main',
+        offset: testCode.indexOf("main() {"),
+        parameters: '()',
+        returnType: 'void');
+    List<Outline> main_children = main_outline.children;
+    expect(main_children, hasLength(2));
+    // group1
+    Outline group1_outline = main_children[0];
+    _expect(group1_outline,
+        kind: ElementKind.UNIT_TEST_GROUP,
+        length: 7,
+        name: 'group("group1")',
+        offset: testCode.indexOf("myGroup('group1'"));
+    List<Outline> group1_children = group1_outline.children;
+    expect(group1_children, hasLength(2));
+    // group1_1
+    Outline group1_1_outline = group1_children[0];
+    _expect(group1_1_outline,
+        kind: ElementKind.UNIT_TEST_GROUP,
+        length: 7,
+        name: 'group("group1_1")',
+        offset: testCode.indexOf("myGroup('group1_1'"));
+    List<Outline> group1_1_children = group1_1_outline.children;
+    expect(group1_1_children, hasLength(2));
+    // test1_1_1
+    Outline test1_1_1_outline = group1_1_children[0];
+    _expect(test1_1_1_outline,
+        kind: ElementKind.UNIT_TEST_TEST,
+        leaf: true,
+        length: 6,
+        name: 'test("test1_1_1")',
+        offset: testCode.indexOf("myTest('test1_1_1'"));
+    // test1_1_1
+    Outline test1_1_2_outline = group1_1_children[1];
+    _expect(test1_1_2_outline,
+        kind: ElementKind.UNIT_TEST_TEST,
+        leaf: true,
+        length: 6,
+        name: 'test("test1_1_2")',
+        offset: testCode.indexOf("myTest('test1_1_2'"));
+    // group1_2
+    Outline group1_2_outline = group1_children[1];
+    _expect(group1_2_outline,
+        kind: ElementKind.UNIT_TEST_GROUP,
+        length: 7,
+        name: 'group("group1_2")',
+        offset: testCode.indexOf("myGroup('group1_2'"));
+    List<Outline> group1_2_children = group1_2_outline.children;
+    expect(group1_2_children, hasLength(1));
+    // test2_1
+    Outline test1_2_1_outline = group1_2_children[0];
+    _expect(test1_2_1_outline,
+        kind: ElementKind.UNIT_TEST_TEST,
+        leaf: true,
+        length: 6,
+        name: 'test("test1_2_1")',
+        offset: testCode.indexOf("myTest('test1_2_1'"));
+    // group2
+    Outline group2_outline = main_children[1];
+    _expect(group2_outline,
+        kind: ElementKind.UNIT_TEST_GROUP,
+        length: 7,
+        name: 'group("group2")',
+        offset: testCode.indexOf("myGroup('group2'"));
+    List<Outline> group2_children = group2_outline.children;
+    expect(group2_children, hasLength(2));
+    // test2_1
+    Outline test2_1_outline = group2_children[0];
+    _expect(test2_1_outline,
+        kind: ElementKind.UNIT_TEST_TEST,
+        leaf: true,
+        length: 6,
+        name: 'test("test2_1")',
+        offset: testCode.indexOf("myTest('test2_1'"));
+    // test2_2
+    Outline test2_2_outline = group2_children[1];
+    _expect(test2_2_outline,
+        kind: ElementKind.UNIT_TEST_TEST,
+        leaf: true,
+        length: 6,
+        name: 'test("test2_2")',
+        offset: testCode.indexOf("myTest('test2_2'"));
+  }
+
   test_localFunctions() async {
     Outline unitOutline = await _computeOutline('''
 class A {
diff --git a/pkg/analyzer/lib/dart/element/element.dart b/pkg/analyzer/lib/dart/element/element.dart
index 5b2237b..ea8ebbb 100644
--- a/pkg/analyzer/lib/dart/element/element.dart
+++ b/pkg/analyzer/lib/dart/element/element.dart
@@ -618,6 +618,16 @@
   bool get hasFactory;
 
   /**
+   * Return `true` if this element has an annotation of the form `@isTest`.
+   */
+  bool get hasIsTest;
+
+  /**
+   * Return `true` if this element has an annotation of the form `@isTestGroup`.
+   */
+  bool get hasIsTestGroup;
+
+  /**
    * Return `true` if this element has an annotation of the form `@JS(..)`.
    */
   bool get hasJS;
@@ -889,6 +899,18 @@
   bool get isImmutable;
 
   /**
+   * Return `true` if this annotation marks the associated member as running
+   * a single test.
+   */
+  bool get isIsTest;
+
+  /**
+   * Return `true` if this annotation marks the associated member as running
+   * a test group.
+   */
+  bool get isIsTestGroup;
+
+  /**
    * Return `true` if this annotation marks the associated element with the `JS`
    * annotation.
    */
diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart
index 8fa0d0b..c81779b 100644
--- a/pkg/analyzer/lib/src/dart/element/element.dart
+++ b/pkg/analyzer/lib/src/dart/element/element.dart
@@ -2854,6 +2854,18 @@
   static String _IMMUTABLE_VARIABLE_NAME = "immutable";
 
   /**
+   * The name of the top-level variable used to mark a function as running
+   * a single test.
+   */
+  static String _IS_TEST_VARIABLE_NAME = "isTest";
+
+  /**
+   * The name of the top-level variable used to mark a function as running
+   * a test group.
+   */
+  static String _IS_TEST_GROUP_VARIABLE_NAME = "isTestGroup";
+
+  /**
    * The name of the class used to JS annotate an element.
    */
   static String _JS_CLASS_NAME = "JS";
@@ -2983,6 +2995,18 @@
       element.library?.name == _META_LIB_NAME;
 
   @override
+  bool get isIsTest =>
+      element is PropertyAccessorElement &&
+      element.name == _IS_TEST_VARIABLE_NAME &&
+      element.library?.name == _META_LIB_NAME;
+
+  @override
+  bool get isIsTestGroup =>
+      element is PropertyAccessorElement &&
+      element.name == _IS_TEST_GROUP_VARIABLE_NAME &&
+      element.library?.name == _META_LIB_NAME;
+
+  @override
   bool get isJS =>
       element is ConstructorElement &&
       element.enclosingElement.name == _JS_CLASS_NAME &&
@@ -3214,6 +3238,14 @@
   }
 
   @override
+  bool get hasIsTest =>
+      metadata.any((ElementAnnotation annotation) => annotation.isIsTest);
+
+  @override
+  bool get hasIsTestGroup =>
+      metadata.any((ElementAnnotation annotation) => annotation.isIsTestGroup);
+
+  @override
   bool get hasJS =>
       metadata.any((ElementAnnotation annotation) => annotation.isJS);
 
@@ -7556,6 +7588,12 @@
   bool get hasFactory => false;
 
   @override
+  bool get hasIsTest => false;
+
+  @override
+  bool get hasIsTestGroup => false;
+
+  @override
   bool get hasJS => false;
 
   @override
diff --git a/pkg/analyzer/lib/src/dart/element/handle.dart b/pkg/analyzer/lib/src/dart/element/handle.dart
index a5f7cbe..541f0d0 100644
--- a/pkg/analyzer/lib/src/dart/element/handle.dart
+++ b/pkg/analyzer/lib/src/dart/element/handle.dart
@@ -363,6 +363,12 @@
   int get hashCode => _location.hashCode;
 
   @override
+  bool get hasIsTest => actualElement.hasIsTest;
+
+  @override
+  bool get hasIsTestGroup => actualElement.hasIsTestGroup;
+
+  @override
   bool get hasJS => actualElement.hasJS;
 
   @override
diff --git a/pkg/analyzer/lib/src/dart/element/member.dart b/pkg/analyzer/lib/src/dart/element/member.dart
index 026a7ef..a304d9f 100644
--- a/pkg/analyzer/lib/src/dart/element/member.dart
+++ b/pkg/analyzer/lib/src/dart/element/member.dart
@@ -400,6 +400,12 @@
   bool get hasFactory => _baseElement.hasFactory;
 
   @override
+  bool get hasIsTest => _baseElement.hasIsTest;
+
+  @override
+  bool get hasIsTestGroup => _baseElement.hasIsTestGroup;
+
+  @override
   bool get hasJS => _baseElement.hasJS;
 
   @override