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

import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:flutter_devicelab/framework/framework.dart';
9
import 'package:flutter_devicelab/framework/task_result.dart';
10 11
import 'package:flutter_devicelab/framework/utils.dart';

12 13 14
/// Combines several TaskFunctions with trivial success value into one.
TaskFunction combine(List<TaskFunction> tasks) {
  return () async {
15
    for (final TaskFunction task in tasks) {
16 17 18 19 20
      final TaskResult result = await task();
      if (result.failed) {
        return result;
      }
    }
21
    return TaskResult.success(null);
22 23 24
  };
}

25 26
/// Defines task that creates new Flutter project, adds a local and remote
/// plugin, and then builds the specified [buildTarget].
27
class PluginTest {
28
  PluginTest(this.buildTarget, this.options, { this.pluginCreateEnvironment, this.appCreateEnvironment });
29

30
  final String buildTarget;
31
  final List<String> options;
32 33
  final Map<String, String> pluginCreateEnvironment;
  final Map<String, String> appCreateEnvironment;
34 35

  Future<TaskResult> call() async {
36 37
    final Directory tempDir =
        Directory.systemTemp.createTempSync('flutter_devicelab_plugin_test.');
38
    try {
39 40
      section('Create plugin');
      final _FlutterProject plugin = await _FlutterProject.create(
41
          tempDir, options, buildTarget,
42
          name: 'plugintest', template: 'plugin', environment: pluginCreateEnvironment);
43 44 45
      section('Test plugin');
      await plugin.test();
      section('Create Flutter app');
46
      final _FlutterProject app = await _FlutterProject.create(tempDir, options, buildTarget,
47
          name: 'plugintestapp', template: 'app', environment: appCreateEnvironment);
48
      try {
49 50 51 52 53 54 55 56
        section('Add plugins');
        await app.addPlugin('plugintest',
            pluginPath: path.join('..', 'plugintest'));
        await app.addPlugin('path_provider');
        section('Build app');
        await app.build(buildTarget);
        section('Test app');
        await app.test();
57
      } finally {
58 59
        await plugin.delete();
        await app.delete();
60
      }
61
      return TaskResult.success(null);
62
    } catch (e) {
63
      return TaskResult.failure(e.toString());
64
    } finally {
65
      rmTree(tempDir);
66 67 68 69
    }
  }
}

70 71
class _FlutterProject {
  _FlutterProject(this.parent, this.name);
72 73 74 75 76 77

  final Directory parent;
  final String name;

  String get rootPath => path.join(parent.path, name);

78
  Future<void> addPlugin(String plugin, {String pluginPath}) async {
79
    final File pubspec = File(path.join(rootPath, 'pubspec.yaml'));
80
    String content = await pubspec.readAsString();
81 82
    final String dependency =
        pluginPath != null ? '$plugin:\n    path: $pluginPath' : '$plugin:';
83 84
    content = content.replaceFirst(
      '\ndependencies:\n',
85
      '\ndependencies:\n  $dependency\n',
86 87 88 89
    );
    await pubspec.writeAsString(content, flush: true);
  }

90 91 92 93 94 95 96
  Future<void> test() async {
    await inDirectory(Directory(rootPath), () async {
      await flutter('test');
    });
  }

  static Future<_FlutterProject> create(
97 98
      Directory directory,
      List<String> options,
99
      String target,
100 101 102 103 104
      {
        String name,
        String template,
        Map<String, String> environment,
      }) async {
105 106 107 108 109 110 111 112
    await inDirectory(directory, () async {
      await flutter(
        'create',
        options: <String>[
          '--template=$template',
          '--org',
          'io.flutter.devicelab',
          ...options,
113
          name,
114
        ],
115
        environment: environment,
116 117
      );
    });
118 119

    final _FlutterProject project = _FlutterProject(directory, name);
120 121
    if (template == 'plugin' && (target == 'ios' || target == 'macos')) {
      project._reduceDarwinPluginMinimumVersion(name, target);
122 123 124 125 126 127
    }
    return project;
  }

  // Make the platform version artificially low to test that the "deployment
  // version too low" warning is never emitted.
128 129
  void _reduceDarwinPluginMinimumVersion(String plugin, String target) {
    final File podspec = File(path.join(rootPath, target, '$plugin.podspec'));
130 131 132
    if (!podspec.existsSync()) {
      throw TaskResult.failure('podspec file missing at ${podspec.path}');
    }
133 134 135
    final String versionString = target == 'ios'
        ? "s.platform = :ios, '8.0'"
        : "s.platform = :osx, '10.11'";
136 137
    String podspecContent = podspec.readAsStringSync();
    if (!podspecContent.contains(versionString)) {
138
      throw TaskResult.failure('Update this test to match plugin minimum $target deployment version');
139 140 141
    }
    podspecContent = podspecContent.replaceFirst(
      versionString,
142 143 144
      target == 'ios'
          ? "s.platform = :ios, '7.0'"
          : "s.platform = :osx, '10.8'"
145 146
    );
    podspec.writeAsStringSync(podspecContent, flush: true);
147 148
  }

149
  Future<void> build(String target) async {
150
    await inDirectory(Directory(rootPath), () async {
151 152
      final String buildOutput =  await evalFlutter('build', options: <String>[target, '-v']);

153
      if (target == 'ios' || target == 'macos') {
154 155 156 157
        // This warning is confusing and shouldn't be emitted. Plugins often support lower versions than the
        // Flutter app, but as long as they support the minimum it will work.
        // warning: The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0,
        // but the range of supported deployment target versions is 9.0 to 14.0.99.
158 159
        //
        // (or "The macOS deployment target 'MACOSX_DEPLOYMENT_TARGET'"...)
160 161 162
        if (buildOutput.contains('the range of supported deployment target versions')) {
          throw TaskResult.failure('Minimum plugin version warning present');
        }
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183

        final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj'));
        if (!podsProject.existsSync()) {
          throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}');
        }

        final String podsProjectContent = podsProject.readAsStringSync();
        // This may be a bit brittle, IPHONEOS_DEPLOYMENT_TARGET appears in the
        // Pods Xcode project file 6 times. If this number changes, make sure
        // it's not a regression in the IPHONEOS_DEPLOYMENT_TARGET override logic.
        // The plugintest target should not have IPHONEOS_DEPLOYMENT_TARGET set.
        // See _reduceDarwinPluginMinimumVersion for details.
        if (target == 'ios' && 'IPHONEOS_DEPLOYMENT_TARGET'.allMatches(podsProjectContent).length != 6) {
          throw TaskResult.failure('plugintest may contain IPHONEOS_DEPLOYMENT_TARGET');
        }

        // Same for macOS, but 12.
        // The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set.
        if (target == 'macos' && 'MACOSX_DEPLOYMENT_TARGET'.allMatches(podsProjectContent).length != 12) {
          throw TaskResult.failure('plugintest may contain MACOSX_DEPLOYMENT_TARGET');
        }
184
      }
185 186 187
    });
  }

188
  Future<void> delete() async {
189 190 191
    if (Platform.isWindows) {
      // A running Gradle daemon might prevent us from deleting the project
      // folder on Windows.
192 193 194 195 196
      final String wrapperPath =
          path.absolute(path.join(rootPath, 'android', 'gradlew.bat'));
      if (File(wrapperPath).existsSync()) {
        await exec(wrapperPath, <String>['--stop'], canFail: true);
      }
197
      // TODO(ianh): Investigating if flakiness is timing dependent.
198
      await Future<void>.delayed(const Duration(seconds: 10));
199
    }
200
    rmTree(parent);
201 202
  }
}