gradle_plugin_test.dart 11 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// Copyright (c) 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.

import 'dart:async';
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';

12
String javaHome;
13
String errorMessage;
14

15 16
/// Runs the given [testFunction] on a freshly generated Flutter project.
Future<void> runProjectTest(Future<void> testFunction(FlutterProject project)) async {
Florian Loitsch's avatar
Florian Loitsch committed
17 18 19 20
  final Directory tmp = await Directory.systemTemp.createTemp('gradle');
  final FlutterProject project = await FlutterProject.create(tmp, 'hello');

  try {
21
    await testFunction(project);
Florian Loitsch's avatar
Florian Loitsch committed
22 23 24 25 26
  } finally {
    project.parent.deleteSync(recursive: true);
  }
}

27 28 29 30 31 32 33 34 35 36 37 38
/// Runs the given [testFunction] on a freshly generated Flutter plugin project.
Future<void> runPluginProjectTest(Future<void> testFunction(FlutterPluginProject pluginProject)) async {
  final Directory tmp = await Directory.systemTemp.createTemp('gradle');
  final FlutterPluginProject pluginProject = await FlutterPluginProject.create(tmp, 'aaa');

  try {
    await testFunction(pluginProject);
  } finally {
    pluginProject.parent.deleteSync(recursive: true);
  }
}

39 40
void main() async {
  await task(() async {
41 42 43 44 45 46
    section('Find Java');

    javaHome = await findJavaHome();
    if (javaHome == null)
      return new TaskResult.failure('Could not find Java');
    print('\nUsing JAVA_HOME=$javaHome');
47

48
    try {
49
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
50 51 52 53 54 55 56 57
        section('gradlew assembleDebug');
        await project.runGradleTask('assembleDebug');
        errorMessage = _validateSnapshotDependency(project, 'build/app.dill');
        if (errorMessage != null) {
          throw new TaskResult.failure(errorMessage);
        }
      });

58
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
59 60 61 62 63
        section('gradlew assembleDebug no-preview-dart-2');
        await project.runGradleTask('assembleDebug',
            options: <String>['-Ppreview-dart-2=false']);
      });

64
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
65 66 67 68
        section('gradlew assembleProfile');
        await project.runGradleTask('assembleProfile');
      });

69
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
70 71 72 73
        section('gradlew assembleRelease');
        await project.runGradleTask('assembleRelease');
      });

74
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
75 76 77 78 79
        section('gradlew assembleLocal (custom debug build)');
        await project.addCustomBuildType('local', initWith: 'debug');
        await project.runGradleTask('assembleLocal');
      });

80
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
81 82 83 84 85
        section('gradlew assembleBeta (custom release build)');
        await project.addCustomBuildType('beta', initWith: 'release');
        await project.runGradleTask('assembleBeta');
      });

86
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
87 88 89 90 91
        section('gradlew assembleFreeDebug (product flavor)');
        await project.addProductFlavor('free');
        await project.runGradleTask('assembleFreeDebug');
      });

92
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
93
        section('gradlew on build script with error');
94
        await project.introduceError();
Florian Loitsch's avatar
Florian Loitsch committed
95 96
        final ProcessResult result =
            await project.resultOfGradleTask('assembleRelease');
97
        if (result.exitCode == 0)
98
          throw _failure(
Florian Loitsch's avatar
Florian Loitsch committed
99
              'Gradle did not exit with error as expected', result);
100
        final String output = result.stdout + '\n' + result.stderr;
Florian Loitsch's avatar
Florian Loitsch committed
101 102 103
        if (output.contains('GradleException') ||
            output.contains('Failed to notify') ||
            output.contains('at org.gradle'))
104
          throw _failure(
Florian Loitsch's avatar
Florian Loitsch committed
105
              'Gradle output should not contain stacktrace', result);
106
        if (!output.contains('Build failed') || !output.contains('builTypes'))
107
          throw _failure(
Florian Loitsch's avatar
Florian Loitsch committed
108 109 110
              'Gradle output should contain a readable error message',
              result);
      });
111

112
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
113
        section('flutter build apk on build script with error');
114
        await project.introduceError();
115 116
        final ProcessResult result = await project.resultOfFlutterCommand('build', <String>['apk']);
        if (result.exitCode == 0)
117
          throw _failure(
Florian Loitsch's avatar
Florian Loitsch committed
118
              'flutter build apk should fail when Gradle does', result);
119 120
        final String output = result.stdout + '\n' + result.stderr;
        if (!output.contains('Build failed') || !output.contains('builTypes'))
121
          throw _failure(
Florian Loitsch's avatar
Florian Loitsch committed
122 123
              'flutter build apk output should contain a readable Gradle error message',
              result);
124
        if (_hasMultipleOccurrences(output, 'builTypes'))
125
          throw _failure(
Florian Loitsch's avatar
Florian Loitsch committed
126 127 128 129
              'flutter build apk should not invoke Gradle repeatedly on error',
              result);
      });

130
      await runPluginProjectTest((FlutterPluginProject pluginProject) async {
Florian Loitsch's avatar
Florian Loitsch committed
131 132 133 134 135 136
        section('gradlew assembleDebug on plugin example');
        await pluginProject.runGradleTask('assembleDebug');
        if (!pluginProject.hasDebugApk)
          throw new TaskResult.failure(
              'Gradle did not produce an apk file at the expected place');
      });
137

138
      return new TaskResult.success(null);
Florian Loitsch's avatar
Florian Loitsch committed
139 140
    } on TaskResult catch (taskResult) {
      return taskResult;
141
    } catch (e) {
142 143 144 145
      return new TaskResult.failure(e.toString());
    }
  });
}
146

147 148 149 150 151 152 153
TaskResult _failure(String message, ProcessResult result) {
  print('Unexpected process result:');
  print('Exit code: ${result.exitCode}');
  print('Std out  :\n${result.stdout}');
  print('Std err  :\n${result.stderr}');
  return new TaskResult.failure(message);
}
154

155 156
bool _hasMultipleOccurrences(String text, Pattern pattern) {
  return text.indexOf(pattern) != text.lastIndexOf(pattern);
157 158 159
}

class FlutterProject {
160
  FlutterProject(this.parent, this.name);
161

162 163
  final Directory parent;
  final String name;
164 165 166 167 168

  static Future<FlutterProject> create(Directory directory, String name) async {
    await inDirectory(directory, () async {
      await flutter('create', options: <String>[name]);
    });
169
    return new FlutterProject(directory, name);
170 171 172 173 174 175 176 177 178
  }

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

  Future<Null> addCustomBuildType(String name, {String initWith}) async {
    final File buildScript = new File(
      path.join(androidPath, 'app', 'build.gradle'),
    );
179 180

    buildScript.openWrite(mode: FileMode.append).write('''
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

android {
    buildTypes {
        $name {
            initWith $initWith
        }
    }
}
    ''');
  }

  Future<Null> addProductFlavor(String name) async {
    final File buildScript = new File(
      path.join(androidPath, 'app', 'build.gradle'),
    );
196 197

    buildScript.openWrite(mode: FileMode.append).write('''
198 199

android {
200
    flavorDimensions "mode"
201 202 203 204 205 206 207 208 209 210
    productFlavors {
        $name {
            applicationIdSuffix ".$name"
            versionNameSuffix "-$name"
        }
    }
}
    ''');
  }

211 212 213
  Future<Null> introduceError() async {
    final File buildScript = new File(
      path.join(androidPath, 'app', 'build.gradle'),
214
    );
215 216 217
    await buildScript.writeAsString((await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes'));
  }

218 219
  Future<Null> runGradleTask(String task, {List<String> options}) async {
    return _runGradleTask(workingDirectory: androidPath, task: task, options: options);
220 221
  }

222 223
  Future<ProcessResult> resultOfGradleTask(String task, {List<String> options}) {
    return _resultOfGradleTask(workingDirectory: androidPath, task: task, options: options);
224 225 226 227 228 229 230 231
  }

  Future<ProcessResult> resultOfFlutterCommand(String command, List<String> options) {
    return Process.run(
      path.join(flutterDirectory.path, 'bin', 'flutter'),
      <String>[command]..addAll(options),
      workingDirectory: rootPath,
    );
232 233
  }
}
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252

class FlutterPluginProject {
  FlutterPluginProject(this.parent, this.name);

  final Directory parent;
  final String name;

  static Future<FlutterPluginProject> create(Directory directory, String name) async {
    await inDirectory(directory, () async {
      await flutter('create', options: <String>['-t', 'plugin', name]);
    });
    return new FlutterPluginProject(directory, name);
  }

  String get rootPath => path.join(parent.path, name);
  String get examplePath => path.join(rootPath, 'example');
  String get exampleAndroidPath => path.join(examplePath, 'android');
  String get debugApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'debug', 'app-debug.apk');

253 254
  Future<Null> runGradleTask(String task, {List<String> options}) async {
    return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options);
255 256 257 258 259
  }

  bool get hasDebugApk => new File(debugApkPath).existsSync();
}

260 261 262 263 264
Future<Null> _runGradleTask({String workingDirectory, String task, List<String> options}) async {
  final ProcessResult result = await _resultOfGradleTask(
      workingDirectory: workingDirectory,
      task: task,
      options: options);
265 266 267 268 269 270 271 272 273 274
  if (result.exitCode != 0) {
    print('stdout:');
    print(result.stdout);
    print('stderr:');
    print(result.stderr);
  }
  if (result.exitCode != 0)
    throw 'Gradle exited with error';
}

275 276 277 278 279 280
Future<ProcessResult> _resultOfGradleTask({String workingDirectory, String task,
    List<String> options}) {
  final List<String> args = <String>['app:$task'];
  if (options != null) {
    args.addAll(options);
  }
281 282
  return Process.run(
    './gradlew',
283
    args,
284 285 286 287
    workingDirectory: workingDirectory,
    environment: <String, String>{ 'JAVA_HOME': javaHome }
  );
}
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319

class _Dependencies {
  String target;
  Set<String> dependencies;
  _Dependencies(String depfilePath) {
    final RegExp _separatorExpr = new RegExp(r'([^\\]) ');
    final RegExp _escapeExpr = new RegExp(r'\\(.)');

    // Depfile format:
    // outfile1 outfile2 : file1.dart file2.dart file3.dart file\ 4.dart
    final String contents = new File(depfilePath).readAsStringSync();
    final List<String> colonSeparated = contents.split(': ');
    target = colonSeparated[0].trim();
    dependencies = colonSeparated[1]
        // Put every file on right-hand side on the separate line
        .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
        .split('\n')
        // Expand escape sequences, so that '\ ', for example,ß becomes ' '
        .map((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
        .where((String path) => path.isNotEmpty)
        .toSet();
  }
}

/// Returns [null] if target matches [expectedTarget], otherwise returns an error message.
String _validateSnapshotDependency(FlutterProject project, String expectedTarget) {
  final _Dependencies deps = new _Dependencies(
      path.join(project.rootPath, 'build', 'app', 'intermediates',
          'flutter', 'debug', 'snapshot_blob.bin.d'));
  return deps.target == expectedTarget ? null :
    'Dependency file should have $expectedTarget as target. Instead has ${deps.target}';
}