Flutter iOS Embedder
FlutterEngineTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #import <Foundation/Foundation.h>
6 #import <OCMock/OCMock.h>
7 #import <XCTest/XCTest.h>
8 
9 #import <objc/runtime.h>
10 
11 #import "flutter/common/settings.h"
12 #include "flutter/fml/synchronization/sync_switch.h"
18 
20 
22 
23 @end
24 
25 /// FlutterBinaryMessengerRelay used for testing that setting FlutterEngine.binaryMessenger to
26 /// the current instance doesn't trigger a use-after-free bug.
27 ///
28 /// See: testSetBinaryMessengerToSameBinaryMessenger
30 @property(nonatomic, assign) BOOL failOnDealloc;
31 @end
32 
33 @implementation FakeBinaryMessengerRelay
34 - (void)dealloc {
35  if (_failOnDealloc) {
36  XCTFail("FakeBinaryMessageRelay should not be deallocated");
37  }
38 }
39 @end
40 
41 @interface FlutterEngineTest : XCTestCase
42 @end
43 
44 @implementation FlutterEngineTest
45 
46 - (void)setUp {
47 }
48 
49 - (void)tearDown {
50 }
51 
52 - (void)testCreate {
53  FlutterDartProject* project = [[FlutterDartProject alloc] init];
54  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
55  XCTAssertNotNil(engine);
56 }
57 
58 - (void)testInfoPlist {
59  // Check the embedded Flutter.framework Info.plist, not the linked dylib.
60  NSURL* flutterFrameworkURL =
61  [NSBundle.mainBundle.privateFrameworksURL URLByAppendingPathComponent:@"Flutter.framework"];
62  NSBundle* flutterBundle = [NSBundle bundleWithURL:flutterFrameworkURL];
63  XCTAssertEqualObjects(flutterBundle.bundleIdentifier, @"io.flutter.flutter");
64 
65  NSDictionary<NSString*, id>* infoDictionary = flutterBundle.infoDictionary;
66 
67  // OS version can have one, two, or three digits: "8", "8.0", "8.0.0"
68  NSError* regexError = NULL;
69  NSRegularExpression* osVersionRegex =
70  [NSRegularExpression regularExpressionWithPattern:@"((0|[1-9]\\d*)\\.)*(0|[1-9]\\d*)"
71  options:NSRegularExpressionCaseInsensitive
72  error:&regexError];
73  XCTAssertNil(regexError);
74 
75  // Smoke test the test regex.
76  NSString* testString = @"9";
77  NSUInteger versionMatches =
78  [osVersionRegex numberOfMatchesInString:testString
79  options:NSMatchingAnchored
80  range:NSMakeRange(0, testString.length)];
81  XCTAssertEqual(versionMatches, 1UL);
82  testString = @"9.1";
83  versionMatches = [osVersionRegex numberOfMatchesInString:testString
84  options:NSMatchingAnchored
85  range:NSMakeRange(0, testString.length)];
86  XCTAssertEqual(versionMatches, 1UL);
87  testString = @"9.0.1";
88  versionMatches = [osVersionRegex numberOfMatchesInString:testString
89  options:NSMatchingAnchored
90  range:NSMakeRange(0, testString.length)];
91  XCTAssertEqual(versionMatches, 1UL);
92  testString = @".0.1";
93  versionMatches = [osVersionRegex numberOfMatchesInString:testString
94  options:NSMatchingAnchored
95  range:NSMakeRange(0, testString.length)];
96  XCTAssertEqual(versionMatches, 0UL);
97 
98  // Test Info.plist values.
99  NSString* minimumOSVersion = infoDictionary[@"MinimumOSVersion"];
100  versionMatches = [osVersionRegex numberOfMatchesInString:minimumOSVersion
101  options:NSMatchingAnchored
102  range:NSMakeRange(0, minimumOSVersion.length)];
103  XCTAssertEqual(versionMatches, 1UL);
104 
105  // SHA length is 40.
106  XCTAssertEqual(((NSString*)infoDictionary[@"FlutterEngine"]).length, 40UL);
107 
108  // {clang_version} placeholder is 15 characters. The clang string version
109  // is longer than that, so check if the placeholder has been replaced, without
110  // actually checking a literal string, which could be different on various machines.
111  XCTAssertTrue(((NSString*)infoDictionary[@"ClangVersion"]).length > 15UL);
112 }
113 
114 - (void)testDeallocated {
115  __weak FlutterEngine* weakEngine = nil;
116  @autoreleasepool {
117  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
118  weakEngine = engine;
119  [engine run];
120  XCTAssertNotNil(weakEngine);
121  }
122  XCTAssertNil(weakEngine);
123 }
124 
125 - (void)testSendMessageBeforeRun {
126  FlutterDartProject* project = [[FlutterDartProject alloc] init];
127  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
128  XCTAssertNotNil(engine);
129  XCTAssertThrows([engine.binaryMessenger
130  sendOnChannel:@"foo"
131  message:[@"bar" dataUsingEncoding:NSUTF8StringEncoding]
132  binaryReply:nil]);
133 }
134 
135 - (void)testSetMessageHandlerBeforeRun {
136  FlutterDartProject* project = [[FlutterDartProject alloc] init];
137  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
138  XCTAssertNotNil(engine);
139  XCTAssertThrows([engine.binaryMessenger
140  setMessageHandlerOnChannel:@"foo"
141  binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply){
142 
143  }]);
144 }
145 
146 - (void)testNilSetMessageHandlerBeforeRun {
147  FlutterDartProject* project = [[FlutterDartProject alloc] init];
148  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
149  XCTAssertNotNil(engine);
150  XCTAssertNoThrow([engine.binaryMessenger setMessageHandlerOnChannel:@"foo"
151  binaryMessageHandler:nil]);
152 }
153 
154 - (void)testNotifyPluginOfDealloc {
155  id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
156  OCMStub([plugin detachFromEngineForRegistrar:[OCMArg any]]);
157  {
158  FlutterDartProject* project = [[FlutterDartProject alloc] init];
159  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:project];
160  NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"plugin"];
161  [registrar publish:plugin];
162  engine = nil;
163  }
164  OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]);
165 }
166 
167 - (void)testSetBinaryMessengerToSameBinaryMessenger {
168  FakeBinaryMessengerRelay* fakeBinaryMessenger = [[FakeBinaryMessengerRelay alloc] init];
169 
170  FlutterEngine* engine = [[FlutterEngine alloc] init];
171  [engine setBinaryMessenger:fakeBinaryMessenger];
172 
173  // Verify that the setter doesn't free the old messenger before setting the new messenger.
174  fakeBinaryMessenger.failOnDealloc = YES;
175  [engine setBinaryMessenger:fakeBinaryMessenger];
176 
177  // Don't fail when ARC releases the binary messenger.
178  fakeBinaryMessenger.failOnDealloc = NO;
179 }
180 
181 - (void)testRunningInitialRouteSendsNavigationMessage {
182  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
183 
184  FlutterEngine* engine = [[FlutterEngine alloc] init];
185  [engine setBinaryMessenger:mockBinaryMessenger];
186 
187  // Run with an initial route.
188  [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
189 
190  // Now check that an encoded method call has been made on the binary messenger to set the
191  // initial route to "test".
192  FlutterMethodCall* setInitialRouteMethodCall =
193  [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
194  NSData* encodedSetInitialRouteMethod =
195  [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
196  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
197  message:encodedSetInitialRouteMethod]);
198 }
199 
200 - (void)testInitialRouteSettingsSendsNavigationMessage {
201  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
202 
203  auto settings = FLTDefaultSettingsForBundle();
204  settings.route = "test";
205  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
206  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
207  [engine setBinaryMessenger:mockBinaryMessenger];
208  [engine run];
209 
210  // Now check that an encoded method call has been made on the binary messenger to set the
211  // initial route to "test".
212  FlutterMethodCall* setInitialRouteMethodCall =
213  [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"];
214  NSData* encodedSetInitialRouteMethod =
215  [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall];
216  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation"
217  message:encodedSetInitialRouteMethod]);
218 }
219 
220 - (void)testPlatformViewsControllerRenderingMetalBackend {
221  FlutterEngine* engine = [[FlutterEngine alloc] init];
222  [engine run];
223  flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
224 
225  XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kMetal);
226 }
227 
228 - (void)testPlatformViewsControllerRenderingSoftware {
229  auto settings = FLTDefaultSettingsForBundle();
230  settings.enable_software_rendering = true;
231  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
232  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
233  [engine run];
234  flutter::IOSRenderingAPI renderingApi = [engine platformViewsRenderingAPI];
235 
236  XCTAssertEqual(renderingApi, flutter::IOSRenderingAPI::kSoftware);
237 }
238 
239 - (void)testWaitForFirstFrameTimeout {
240  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
241  [engine run];
242  XCTestExpectation* timeoutFirstFrame = [self expectationWithDescription:@"timeoutFirstFrame"];
243  [engine waitForFirstFrame:0.1
244  callback:^(BOOL didTimeout) {
245  if (timeoutFirstFrame) {
246  [timeoutFirstFrame fulfill];
247  }
248  }];
249  [self waitForExpectationsWithTimeout:5 handler:nil];
250 }
251 
252 - (void)testSpawn {
253  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
254  [engine run];
255  FlutterEngine* spawn = [engine spawnWithEntrypoint:nil
256  libraryURI:nil
257  initialRoute:nil
258  entrypointArgs:nil];
259  XCTAssertNotNil(spawn);
260 }
261 
262 - (void)testDeallocNotification {
263  XCTestExpectation* deallocNotification = [self expectationWithDescription:@"deallocNotification"];
264  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
265  id<NSObject> observer;
266  @autoreleasepool {
267  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
268  observer = [center addObserverForName:kFlutterEngineWillDealloc
269  object:engine
270  queue:[NSOperationQueue mainQueue]
271  usingBlock:^(NSNotification* note) {
272  [deallocNotification fulfill];
273  }];
274  }
275  [self waitForExpectationsWithTimeout:1 handler:nil];
276  [center removeObserver:observer];
277 }
278 
279 - (void)testSetHandlerAfterRun {
280  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
281  XCTestExpectation* gotMessage = [self expectationWithDescription:@"gotMessage"];
282  dispatch_async(dispatch_get_main_queue(), ^{
283  NSObject<FlutterPluginRegistrar>* registrar = [engine registrarForPlugin:@"foo"];
284  fml::AutoResetWaitableEvent latch;
285  [engine run];
286  flutter::Shell& shell = engine.shell;
287  engine.shell.GetTaskRunners().GetUITaskRunner()->PostTask([&latch, &shell] {
288  flutter::Engine::Delegate& delegate = shell;
289  auto message = std::make_unique<flutter::PlatformMessage>("foo", nullptr);
290  delegate.OnEngineHandlePlatformMessage(std::move(message));
291  latch.Signal();
292  });
293  latch.Wait();
294  [registrar.messenger setMessageHandlerOnChannel:@"foo"
295  binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) {
296  [gotMessage fulfill];
297  }];
298  });
299  [self waitForExpectationsWithTimeout:1 handler:nil];
300 }
301 
302 - (void)testThreadPrioritySetCorrectly {
303  XCTestExpectation* prioritiesSet = [self expectationWithDescription:@"prioritiesSet"];
304  prioritiesSet.expectedFulfillmentCount = 3;
305 
306  IMP mockSetThreadPriority =
307  imp_implementationWithBlock(^(NSThread* thread, double threadPriority) {
308  if ([thread.name hasSuffix:@".ui"]) {
309  XCTAssertEqual(threadPriority, 1.0);
310  [prioritiesSet fulfill];
311  } else if ([thread.name hasSuffix:@".raster"]) {
312  XCTAssertEqual(threadPriority, 1.0);
313  [prioritiesSet fulfill];
314  } else if ([thread.name hasSuffix:@".io"]) {
315  XCTAssertEqual(threadPriority, 0.5);
316  [prioritiesSet fulfill];
317  }
318  });
319  Method method = class_getInstanceMethod([NSThread class], @selector(setThreadPriority:));
320  IMP originalSetThreadPriority = method_getImplementation(method);
321  method_setImplementation(method, mockSetThreadPriority);
322 
323  FlutterEngine* engine = [[FlutterEngine alloc] init];
324  [engine run];
325  [self waitForExpectationsWithTimeout:1 handler:nil];
326 
327  method_setImplementation(method, originalSetThreadPriority);
328 }
329 
330 - (void)testCanEnableDisableEmbedderAPIThroughInfoPlist {
331  {
332  // Not enable embedder API by default
333  auto settings = FLTDefaultSettingsForBundle();
334  settings.enable_software_rendering = true;
335  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
336  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
337  XCTAssertFalse(engine.enableEmbedderAPI);
338  }
339  {
340  // Enable embedder api
341  id mockMainBundle = OCMPartialMock([NSBundle mainBundle]);
342  OCMStub([mockMainBundle objectForInfoDictionaryKey:@"FLTEnableIOSEmbedderAPI"])
343  .andReturn(@"YES");
344  auto settings = FLTDefaultSettingsForBundle();
345  settings.enable_software_rendering = true;
346  FlutterDartProject* project = [[FlutterDartProject alloc] initWithSettings:settings];
347  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
348  XCTAssertTrue(engine.enableEmbedderAPI);
349  }
350 }
351 
352 - (void)testFlutterTextInputViewDidResignFirstResponderWillCallTextInputClientConnectionClosed {
353  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
354  FlutterEngine* engine = [[FlutterEngine alloc] init];
355  [engine setBinaryMessenger:mockBinaryMessenger];
356  [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
357  [engine flutterTextInputView:nil didResignFirstResponderWithTextInputClient:1];
358  FlutterMethodCall* methodCall =
359  [FlutterMethodCall methodCallWithMethodName:@"TextInputClient.onConnectionClosed"
360  arguments:@[ @(1) ]];
361  NSData* encodedMethodCall = [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:methodCall];
362  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/textinput" message:encodedMethodCall]);
363 }
364 
365 - (void)testFlutterEngineUpdatesDisplays {
366  FlutterEngine* engine = [[FlutterEngine alloc] init];
367  id mockEngine = OCMPartialMock(engine);
368 
369  [engine run];
370  OCMVerify(times(1), [mockEngine updateDisplays]);
371  engine.viewController = nil;
372  OCMVerify(times(2), [mockEngine updateDisplays]);
373 }
374 
375 - (void)testLifeCycleNotificationDidEnterBackground {
376  FlutterDartProject* project = [[FlutterDartProject alloc] init];
377  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
378  [engine run];
379  NSNotification* sceneNotification =
380  [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
381  object:nil
382  userInfo:nil];
383  NSNotification* applicationNotification =
384  [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
385  object:nil
386  userInfo:nil];
387  id mockEngine = OCMPartialMock(engine);
388  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
389  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
390 #if APPLICATION_EXTENSION_API_ONLY
391  OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]);
392 #else
393  OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]);
394 #endif
395  XCTAssertTrue(engine.isGpuDisabled);
396  bool switch_value = false;
397  [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
398  fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
399  switch_value = false;
400  }));
401  XCTAssertTrue(switch_value);
402 }
403 
404 - (void)testLifeCycleNotificationWillEnterForeground {
405  FlutterDartProject* project = [[FlutterDartProject alloc] init];
406  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
407  [engine run];
408  NSNotification* sceneNotification =
409  [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
410  object:nil
411  userInfo:nil];
412  NSNotification* applicationNotification =
413  [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
414  object:nil
415  userInfo:nil];
416  id mockEngine = OCMPartialMock(engine);
417  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
418  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
419 #if APPLICATION_EXTENSION_API_ONLY
420  OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]);
421 #else
422  OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]);
423 #endif
424  XCTAssertFalse(engine.isGpuDisabled);
425  bool switch_value = true;
426  [engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
427  fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
428  switch_value = false;
429  }));
430  XCTAssertFalse(switch_value);
431 }
432 
433 @end
FLUTTER_ASSERT_ARC
#define FLUTTER_ASSERT_ARC
Definition: FlutterMacros.h:44
FlutterEngine
Definition: FlutterEngine.h:59
FlutterPlugin-p
Definition: FlutterPlugin.h:189
FlutterTextInputDelegate-p
Definition: FlutterTextInputDelegate.h:33
-[FlutterEngine waitForFirstFrame:callback:]
void waitForFirstFrame:callback:(NSTimeInterval timeout,[callback] void(^ callback)(BOOL didTimeout))
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
-[FlutterEngine platformViewsRenderingAPI]
flutter::IOSRenderingAPI platformViewsRenderingAPI()
FlutterEngine_Test.h
FlutterTextInputPlugin.h
FlutterMacros.h
-[FlutterEngine setBinaryMessenger:]
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
-[FlutterEngine shell]
flutter::Shell & shell()
FlutterBinaryMessengerRelay.h
FakeBinaryMessengerRelay::failOnDealloc
BOOL failOnDealloc
Definition: FlutterEngineTest.mm:30
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterEngineTest
Definition: FlutterEngineTest.mm:41
flutter::IOSRenderingAPI
IOSRenderingAPI
Definition: rendering_api_selection.h:14
-[FlutterPluginRegistry-p registrarForPlugin:]
nullable NSObject< FlutterPluginRegistrar > * registrarForPlugin:(NSString *pluginKey)
flutter::IOSRenderingAPI::kMetal
@ kMetal
FakeBinaryMessengerRelay
Definition: FlutterEngineTest.mm:29
engine
id engine
Definition: FlutterTextInputPluginTest.mm:89
FlutterDartProject_Internal.h
FlutterBinaryMessengerRelay
Definition: FlutterBinaryMessengerRelay.h:14
FlutterJSONMethodCodec
Definition: FlutterCodecs.h:453
FLTDefaultSettingsForBundle
flutter::Settings FLTDefaultSettingsForBundle(NSBundle *bundle, NSProcessInfo *processInfoOrNil)
Definition: FlutterDartProject.mm:55
FlutterDartProject
Definition: FlutterDartProject.mm:262
-[FlutterEngine runWithEntrypoint:initialRoute:]
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
-[FlutterEngine spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:]
FlutterEngine * spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:(/*nullable */NSString *entrypoint,[libraryURI]/*nullable */NSString *libraryURI,[initialRoute]/*nullable */NSString *initialRoute,[entrypointArgs]/*nullable */NSArray< NSString * > *entrypointArgs)
-[FlutterEngine run]
BOOL run()
Definition: FlutterEngine.mm:938
flutter::IOSRenderingAPI::kSoftware
@ kSoftware
FlutterBinaryReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)