gradle_plugin_test.dart 10.7 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 {
17 18
  final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.');
  final FlutterProject project = await FlutterProject.create(tempDir, 'hello');
Florian Loitsch's avatar
Florian Loitsch committed
19 20

  try {
21
    await testFunction(project);
Florian Loitsch's avatar
Florian Loitsch committed
22
  } finally {
23
    rmTree(tempDir);
Florian Loitsch's avatar
Florian Loitsch committed
24 25 26
  }
}

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

  try {
    await testFunction(pluginProject);
  } finally {
35
    rmTree(tempDir);
36 37 38
  }
}

39
Future<void> main() async {
40
  await task(() async {
41 42 43 44
    section('Find Java');

    javaHome = await findJavaHome();
    if (javaHome == null)
45
      return TaskResult.failure('Could not find Java');
46
    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
        section('gradlew assembleDebug');
        await project.runGradleTask('assembleDebug');
        errorMessage = _validateSnapshotDependency(project, 'build/app.dill');
        if (errorMessage != null) {
54
          throw TaskResult.failure(errorMessage);
Florian Loitsch's avatar
Florian Loitsch committed
55 56 57
        }
      });

58
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
59 60 61 62
        section('gradlew assembleProfile');
        await project.runGradleTask('assembleProfile');
      });

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

68
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
69 70 71 72 73
        section('gradlew assembleLocal (custom debug build)');
        await project.addCustomBuildType('local', initWith: 'debug');
        await project.runGradleTask('assembleLocal');
      });

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

80
      await runProjectTest((FlutterProject project) async {
Florian Loitsch's avatar
Florian Loitsch committed
81 82 83 84 85
        section('gradlew assembleFreeDebug (product flavor)');
        await project.addProductFlavor('free');
        await project.runGradleTask('assembleFreeDebug');
      });

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

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

124
      await runPluginProjectTest((FlutterPluginProject pluginProject) async {
Florian Loitsch's avatar
Florian Loitsch committed
125 126 127
        section('gradlew assembleDebug on plugin example');
        await pluginProject.runGradleTask('assembleDebug');
        if (!pluginProject.hasDebugApk)
128
          throw TaskResult.failure(
Florian Loitsch's avatar
Florian Loitsch committed
129 130
              'Gradle did not produce an apk file at the expected place');
      });
131

132
      return TaskResult.success(null);
Florian Loitsch's avatar
Florian Loitsch committed
133 134
    } on TaskResult catch (taskResult) {
      return taskResult;
135
    } catch (e) {
136
      return TaskResult.failure(e.toString());
137 138 139
    }
  });
}
140

141 142 143 144 145
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}');
146
  return TaskResult.failure(message);
147
}
148

149 150
bool _hasMultipleOccurrences(String text, Pattern pattern) {
  return text.indexOf(pattern) != text.lastIndexOf(pattern);
151 152 153
}

class FlutterProject {
154
  FlutterProject(this.parent, this.name);
155

156 157
  final Directory parent;
  final String name;
158 159 160

  static Future<FlutterProject> create(Directory directory, String name) async {
    await inDirectory(directory, () async {
161
      await flutter('create', options: <String>['--template=app', name]);
162
    });
163
    return FlutterProject(directory, name);
164 165 166 167 168
  }

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

169
  Future<void> addCustomBuildType(String name, {String initWith}) async {
170
    final File buildScript = File(
171 172
      path.join(androidPath, 'app', 'build.gradle'),
    );
173 174

    buildScript.openWrite(mode: FileMode.append).write('''
175 176 177 178 179 180 181 182 183 184 185

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

186
  Future<void> addProductFlavor(String name) async {
187
    final File buildScript = File(
188 189
      path.join(androidPath, 'app', 'build.gradle'),
    );
190 191

    buildScript.openWrite(mode: FileMode.append).write('''
192 193

android {
194
    flavorDimensions "mode"
195 196 197 198 199 200 201 202 203 204
    productFlavors {
        $name {
            applicationIdSuffix ".$name"
            versionNameSuffix "-$name"
        }
    }
}
    ''');
  }

205
  Future<void> introduceError() async {
206
    final File buildScript = File(
207
      path.join(androidPath, 'app', 'build.gradle'),
208
    );
209 210 211
    await buildScript.writeAsString((await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes'));
  }

212
  Future<void> runGradleTask(String task, {List<String> options}) async {
213
    return _runGradleTask(workingDirectory: androidPath, task: task, options: options);
214 215
  }

216 217
  Future<ProcessResult> resultOfGradleTask(String task, {List<String> options}) {
    return _resultOfGradleTask(workingDirectory: androidPath, task: task, options: options);
218 219 220 221 222 223 224 225
  }

  Future<ProcessResult> resultOfFlutterCommand(String command, List<String> options) {
    return Process.run(
      path.join(flutterDirectory.path, 'bin', 'flutter'),
      <String>[command]..addAll(options),
      workingDirectory: rootPath,
    );
226 227
  }
}
228 229 230 231 232 233 234 235 236

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 {
237
      await flutter('create', options: <String>['--template=plugin', name]);
238
    });
239
    return FlutterPluginProject(directory, name);
240 241 242 243 244 245 246
  }

  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');

247
  Future<void> runGradleTask(String task, {List<String> options}) async {
248
    return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options);
249 250
  }

251
  bool get hasDebugApk => File(debugApkPath).existsSync();
252 253
}

254
Future<void> _runGradleTask({String workingDirectory, String task, List<String> options}) async {
255 256 257 258
  final ProcessResult result = await _resultOfGradleTask(
      workingDirectory: workingDirectory,
      task: task,
      options: options);
259 260 261 262 263 264 265 266 267 268
  if (result.exitCode != 0) {
    print('stdout:');
    print(result.stdout);
    print('stderr:');
    print(result.stderr);
  }
  if (result.exitCode != 0)
    throw 'Gradle exited with error';
}

269 270 271 272 273 274
Future<ProcessResult> _resultOfGradleTask({String workingDirectory, String task,
    List<String> options}) {
  final List<String> args = <String>['app:$task'];
  if (options != null) {
    args.addAll(options);
  }
275 276
  return Process.run(
    './gradlew',
277
    args,
278 279 280 281
    workingDirectory: workingDirectory,
    environment: <String, String>{ 'JAVA_HOME': javaHome }
  );
}
282 283 284

class _Dependencies {
  _Dependencies(String depfilePath) {
285 286
    final RegExp _separatorExpr = RegExp(r'([^\\]) ');
    final RegExp _escapeExpr = RegExp(r'\\(.)');
287 288 289

    // Depfile format:
    // outfile1 outfile2 : file1.dart file2.dart file3.dart file\ 4.dart
290
    final String contents = File(depfilePath).readAsStringSync();
291 292 293 294 295 296 297
    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 ' '
298
        .map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)).trim())
299 300 301
        .where((String path) => path.isNotEmpty)
        .toSet();
  }
302 303 304

  String target;
  Set<String> dependencies;
305 306 307 308
}

/// Returns [null] if target matches [expectedTarget], otherwise returns an error message.
String _validateSnapshotDependency(FlutterProject project, String expectedTarget) {
309
  final _Dependencies deps = _Dependencies(
310 311 312 313 314
      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}';
}