build_system_test.dart 8.42 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
// Copyright 2019 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 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:mockito/mockito.dart';

13 14 15
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/testbed.dart';
16 17 18 19 20 21

void main() {
  setUpAll(() {
    Cache.disableLocking();
  });

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  const BuildSystem buildSystem = BuildSystem();
  Testbed testbed;
  MockPlatform mockPlatform;
  Environment environment;
  Target fooTarget;
  Target barTarget;
  Target fizzTarget;
  Target sharedTarget;
  int fooInvocations;
  int barInvocations;
  int shared;

  setUp(() {
    fooInvocations = 0;
    barInvocations = 0;
    shared = 0;
    mockPlatform = MockPlatform();
    // Keep file paths the same.
    when(mockPlatform.isWindows).thenReturn(false);

    /// Create various testing targets.
    fooTarget = TestTarget((List<File> inputFiles, Environment environment) async {
      environment
        .buildDir
        .childFile('out')
        ..createSync(recursive: true)
        ..writeAsStringSync('hey');
      fooInvocations++;
    })
      ..name = 'foo'
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out'),
      ]
      ..dependencies = <Target>[];
    barTarget = TestTarget((List<File> inputFiles, Environment environment) async {
      environment.buildDir
        .childFile('bar')
        ..createSync(recursive: true)
        ..writeAsStringSync('there');
      barInvocations++;
    })
      ..name = 'bar'
      ..inputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/bar'),
      ]
      ..dependencies = <Target>[];
    fizzTarget = TestTarget((List<File> inputFiles, Environment environment) async {
      throw Exception('something bad happens');
    })
      ..name = 'fizz'
      ..inputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/fizz'),
      ]
      ..dependencies = <Target>[fooTarget];
    sharedTarget = TestTarget((List<File> inputFiles, Environment environment) async {
      shared += 1;
    })
      ..name = 'shared'
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ];
    testbed = Testbed(
      setup: () {
94
        environment = Environment(
95
          outputDir: fs.currentDirectory,
96 97
          projectDir: fs.currentDirectory,
        );
98 99 100 101 102 103 104 105 106
        fs.file('foo.dart')
          ..createSync(recursive: true)
          ..writeAsStringSync('');
        fs.file('pubspec.yaml').createSync();
      },
      overrides: <Type, Generator>{
        Platform: () => mockPlatform,
      }
    );
107 108
  });

109 110 111 112
  test('Throws exception if asked to build with missing inputs', () => testbed.run(() async {
    // Delete required input file.
    fs.file('foo.dart').deleteSync();
    final BuildResult buildResult = await buildSystem.build(fooTarget, environment);
113

114 115 116
    expect(buildResult.hasException, true);
    expect(buildResult.exceptions.values.single.exception, isInstanceOf<MissingInputException>());
  }));
117

118 119 120 121 122 123 124 125 126
  test('Throws exception if it does not produce a specified output', () => testbed.run(() async {
    final Target badTarget = TestTarget((List<File> inputFiles, Environment environment) async {})
      ..inputs = const <Source>[
        Source.pattern('{PROJECT_DIR}/foo.dart'),
      ]
      ..outputs = const <Source>[
        Source.pattern('{BUILD_DIR}/out')
      ];
    final BuildResult result = await buildSystem.build(badTarget, environment);
127

128 129 130
    expect(result.hasException, true);
    expect(result.exceptions.values.single.exception, isInstanceOf<MissingOutputException>());
  }));
131

132 133
  test('Saves a stamp file with inputs and outputs', () => testbed.run(() async {
    await buildSystem.build(fooTarget, environment);
134

135 136
    final File stampFile = fs.file(fs.path.join(environment.buildDir.path, 'foo.stamp'));
    expect(stampFile.existsSync(), true);
137

138 139 140
    final Map<String, Object> stampContents = json.decode(stampFile.readAsStringSync());
    expect(stampContents['inputs'], <Object>['/foo.dart']);
  }));
141

142 143 144 145 146 147 148 149
  test('Creates a BuildResult with inputs and outputs', () => testbed.run(() async {
    final BuildResult result = await buildSystem.build(fooTarget, environment);

    expect(result.inputFiles.single.path, fs.path.absolute('foo.dart'));
    expect(result.outputFiles.single.path,
        fs.path.absolute(fs.path.join(environment.buildDir.path, 'out')));
  }));

150 151 152
  test('Does not re-invoke build if stamp is valid', () => testbed.run(() async {
    await buildSystem.build(fooTarget, environment);
    await buildSystem.build(fooTarget, environment);
153

154 155
    expect(fooInvocations, 1);
  }));
156

157 158
  test('Re-invoke build if input is modified', () => testbed.run(() async {
    await buildSystem.build(fooTarget, environment);
159

160
    fs.file('foo.dart').writeAsStringSync('new contents');
161

162 163 164
    await buildSystem.build(fooTarget, environment);
    expect(fooInvocations, 2);
  }));
165

166 167
  test('does not re-invoke build if input timestamp changes', () => testbed.run(() async {
    await buildSystem.build(fooTarget, environment);
168

169
    fs.file('foo.dart').writeAsStringSync('');
170

171 172 173
    await buildSystem.build(fooTarget, environment);
    expect(fooInvocations, 1);
  }));
174

175 176
  test('does not re-invoke build if output timestamp changes', () => testbed.run(() async {
    await buildSystem.build(fooTarget, environment);
177

178
    environment.buildDir.childFile('out').writeAsStringSync('hey');
179

180 181 182
    await buildSystem.build(fooTarget, environment);
    expect(fooInvocations, 1);
  }));
183 184


185 186
  test('Re-invoke build if output is modified', () => testbed.run(() async {
    await buildSystem.build(fooTarget, environment);
187

188
    environment.buildDir.childFile('out').writeAsStringSync('Something different');
189

190 191 192
    await buildSystem.build(fooTarget, environment);
    expect(fooInvocations, 2);
  }));
193

194 195
  test('Runs dependencies of targets', () => testbed.run(() async {
    barTarget.dependencies.add(fooTarget);
196

197
    await buildSystem.build(barTarget, environment);
198

199 200 201 202
    expect(fs.file(fs.path.join(environment.buildDir.path, 'bar')).existsSync(), true);
    expect(fooInvocations, 1);
    expect(barInvocations, 1);
  }));
203

204 205 206 207
  test('Only invokes shared dependencies once', () => testbed.run(() async {
    fooTarget.dependencies.add(sharedTarget);
    barTarget.dependencies.add(sharedTarget);
    barTarget.dependencies.add(fooTarget);
208

209
    await buildSystem.build(barTarget, environment);
210

211 212
    expect(shared, 1);
  }));
213 214


215 216
  test('handles a throwing build action', () => testbed.run(() async {
    final BuildResult result = await buildSystem.build(fizzTarget, environment);
217

218 219
    expect(result.hasException, true);
  }));
220

221 222 223 224 225 226 227 228 229 230 231 232 233 234
  test('Can describe itself with JSON output', () => testbed.run(() {
    environment.buildDir.createSync(recursive: true);
    expect(fooTarget.toJson(environment), <String, dynamic>{
      'inputs':  <Object>[
        '/foo.dart'
      ],
      'outputs': <Object>[
        fs.path.join(environment.buildDir.path, 'out'),
      ],
      'dependencies': <Object>[],
      'name':  'foo',
      'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
    });
  }));
235 236

  test('Can find dependency cycles', () {
237 238
    final Target barTarget = TestTarget()..name = 'bar';
    final Target fooTarget = TestTarget()..name = 'foo';
239 240
    barTarget.dependencies.add(fooTarget);
    fooTarget.dependencies.add(barTarget);
241

242 243 244 245 246 247 248 249 250
    expect(() => checkCycles(barTarget), throwsA(isInstanceOf<CycleException>()));
  });
}

class MockPlatform extends Mock implements Platform {}

// Work-around for silly lint check.
T nonconst<T>(T input) => input;

251 252 253 254 255 256 257 258 259 260 261 262 263 264
class TestTarget extends Target {
  TestTarget([this._build]);

  final Future<void> Function(List<File> inputFiles, Environment environment) _build;

  @override
  Future<void> build(List<File> inputFiles, Environment environment) => _build(inputFiles, environment);

  @override
  List<Target> dependencies = <Target>[];

  @override
  List<Source> inputs = <Source>[];

265
  @override
266
  String name = 'test';
267 268

  @override
269
  List<Source> outputs = <Source>[];
270
}