Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[core, iOS] Fix MGLOfflinePack invalidate crash #15582

Merged
merged 10 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions platform/darwin/src/MGLOfflinePack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,15 @@ - (void)suspend {
- (void)invalidate {
MGLLogInfo(@"Invalidating pack.");
MGLAssert(_state != MGLOfflinePackStateInvalid, @"Cannot invalidate an already invalid offline pack.");
MGLAssert(self.mbglOfflineRegion, @"Should have a valid region");

self.state = MGLOfflinePackStateInvalid;
_mbglFileSource->setOfflineRegionObserver(*self.mbglOfflineRegion, nullptr);
self.mbglOfflineRegion = nil;
@synchronized (self) {
self.state = MGLOfflinePackStateInvalid;
if (self.mbglOfflineRegion) {
_mbglFileSource->setOfflineRegionObserver(*self.mbglOfflineRegion, nullptr);
}
self.mbglOfflineRegion = nil;
}
}

- (void)setState:(MGLOfflinePackState)state {
Expand Down
4 changes: 4 additions & 0 deletions platform/darwin/src/MGLOfflineStorage.mm
Original file line number Diff line number Diff line change
Expand Up @@ -426,11 +426,15 @@ - (void)removePack:(MGLOfflinePack *)pack withCompletionHandler:(MGLOfflinePackR

- (void)_removePack:(MGLOfflinePack *)pack withCompletionHandler:(MGLOfflinePackRemovalCompletionHandler)completion {
mbgl::OfflineRegion *mbglOfflineRegion = pack.mbglOfflineRegion;

[pack invalidate];

if (!mbglOfflineRegion) {
MGLAssert(pack.state == MGLOfflinePackStateInvalid, @"State should be invalid");
completion(nil);
return;
}

_mbglFileSource->deleteOfflineRegion(std::move(*mbglOfflineRegion), [&, completion](std::exception_ptr exception) {
NSError *error;
if (exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#import <Mapbox/Mapbox.h>
#import <XCTest/XCTest.h>
#import "MGLOfflinePack_Private.h"
#import "MGLTestAssertionHandler.h"

@interface MGLOfflinePackTests : XCTestCase

Expand All @@ -18,6 +20,22 @@ - (void)testInvalidation {
XCTAssertThrowsSpecificNamed([invalidPack suspend], NSException, MGLInvalidOfflinePackException, @"Invalid offline pack should raise an exception when being suspended.");
}

- (void)testInvalidatingAnInvalidPack {
MGLOfflinePack *invalidPack = [[MGLOfflinePack alloc] init];

XCTAssertThrowsSpecificNamed([invalidPack invalidate], NSException, NSInternalInconsistencyException, @"Invalid offline pack should raise an exception when being invalidated.");

// Now try again, without asserts
NSAssertionHandler *oldHandler = [NSAssertionHandler currentHandler];
MGLTestAssertionHandler *newHandler = [[MGLTestAssertionHandler alloc] initWithTestCase:self];
[[[NSThread currentThread] threadDictionary] setValue:newHandler forKey:NSAssertionHandlerKey];

// Make sure this doesn't crash without asserts
[invalidPack invalidate];

[[[NSThread currentThread] threadDictionary] setValue:oldHandler forKey:NSAssertionHandlerKey];
}

- (void)testProgressBoxing {
MGLOfflinePackProgress progress = {
.countOfResourcesCompleted = 3,
Expand Down
198 changes: 195 additions & 3 deletions platform/darwin/test/MGLOfflineStorageTests.mm
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
#import <Mapbox/Mapbox.h>
#import <XCTest/XCTest.h>

#import "MGLOfflineStorage_Private.h"
#import "NSBundle+MGLAdditions.h"
#import "NSDate+MGLAdditions.h"

#import <XCTest/XCTest.h>
#import "MGLTestAssertionHandler.h"

#include <mbgl/util/run_loop.hpp>

#pragma clang diagnostic ignored "-Wshadow"


@interface MGLOfflineStorageTests : XCTestCase <MGLOfflineStorageDelegate>
@end

Expand All @@ -34,7 +35,7 @@ + (void)tearDown {

- (void)setUp {
[super setUp];

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
XCTestExpectation *expectation = [self keyValueObservingExpectationForObject:[MGLOfflineStorage sharedOfflineStorage] keyPath:@"packs" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) {
Expand Down Expand Up @@ -310,6 +311,197 @@ - (void)testRemovePack {
XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage].packs.count, countOfPacks - 1, @"Removed pack should have been removed from the canonical collection of packs owned by the shared offline storage object. This assertion can fail if this test is run before -testAAALoadPacks or -testAddPack.");
}

- (void)addPacks:(NSInteger)count {

XCTestExpectation *expectation = [self expectationWithDescription:@"added packs"];

NSURL *styleURL = [MGLStyle lightStyleURLWithVersion:8];

MGLCoordinateBounds bounds[] = {
{{51.5, -0.2}, {51.6, -0.1}}, // London
{{60.1, 24.8}, {60.3, 25.1}}, // Helsinki
{{38.9, -77.1}, {38.9, -77.0}}, // DC
{{37.7, -122.5}, {37.9, -122.4}} // SF
};

int arraySize = sizeof(bounds)/sizeof(bounds[0]);

count = MIN(count, arraySize);

dispatch_group_t group = dispatch_group_create();

for (int i = 0; i < count; i++) {

dispatch_group_enter(group);
MGLTilePyramidOfflineRegion *region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds[i] fromZoomLevel:20 toZoomLevel:20];
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{
@"index": @(i)
}];

[[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region
withContext:context
completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) {
XCTAssertNotNil(pack);
XCTAssertNil(error);

dispatch_group_leave(group);
}];
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[expectation fulfill];
});

[self waitForExpectations:@[expectation] timeout:1.0];
}

- (void)testRemovePackTwiceInSuccession {

[self addPacks:1];

NSUInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count;

MGLOfflinePack *pack = [MGLOfflineStorage sharedOfflineStorage].packs.lastObject;
XCTAssertNotNil(pack, @"Added pack should still exist.");

[self keyValueObservingExpectationForObject:[MGLOfflineStorage sharedOfflineStorage] keyPath:@"packs" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) {
const auto changeKind = static_cast<NSKeyValueChange>([change[NSKeyValueChangeKindKey] unsignedLongValue]);
NSIndexSet *indices = change[NSKeyValueChangeIndexesKey];
return changeKind == NSKeyValueChangeRemoval && indices.count == 1;
}];

XCTestExpectation *completionHandlerExpectation = [self expectationWithDescription:@"remove pack completion handler"];

[[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:nil];

NSAssertionHandler *oldHandler = [NSAssertionHandler currentHandler];
MGLTestAssertionHandler *newHandler = [[MGLTestAssertionHandler alloc] initWithTestCase:self];

[[[NSThread currentThread] threadDictionary] setValue:newHandler forKey:NSAssertionHandlerKey];

[[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:^(NSError * _Nullable error) {
XCTAssertEqual(pack.state, MGLOfflinePackStateInvalid, @"Removed pack should be invalid in the completion handler.");
[completionHandlerExpectation fulfill];
}];

[self waitForExpectationsWithTimeout:5 handler:nil];

[[[NSThread currentThread] threadDictionary] setValue:oldHandler forKey:NSAssertionHandlerKey];

XCTAssertEqual(pack.state, MGLOfflinePackStateInvalid, @"Removed pack should have been invalidated synchronously.");

XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage].packs.count, countOfPacks - 1, @"Removed pack should have been removed from the canonical collection of packs owned by the shared offline storage object. This assertion can fail if this test is run before -testAAALoadPacks or -testAddPack.");

NSLog(@"Test `%@` complete", NSStringFromSelector(_cmd));
}

- (void)test15536RemovePacksWhileReloading {

// This test triggers
//
// throw std::runtime_error("Malformed offline region definition");
//
// in offline.cpp
//
// Reloading packs, while trying to remove them is currently problematic.

[self addPacks:4];

NSInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count;
XCTAssert(countOfPacks > 0);

// Now delete packs one by one
XCTestExpectation *expectation = [self expectationWithDescription:@"All packs removed"];
expectation.expectedFulfillmentCount = countOfPacks;

MGLOfflineStorage *storage = [MGLOfflineStorage sharedOfflineStorage];
NSArray *packs = [storage.packs copy];

// Simulate what happens the first time sharedOfflineStorage is accessed
[storage reloadPacks];

NSArray *validPacks = [packs filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
MGLOfflinePack *pack = (MGLOfflinePack*)evaluatedObject;
return pack.state != MGLOfflinePackStateInvalid;
}]];

NSAssertionHandler *oldHandler = [NSAssertionHandler currentHandler];
MGLTestAssertionHandler *newHandler = [[MGLTestAssertionHandler alloc] initWithTestCase:self];

[[[NSThread currentThread] threadDictionary] setValue:newHandler forKey:NSAssertionHandlerKey];

for (MGLOfflinePack *pack in validPacks) {
[storage removePack:pack withCompletionHandler:^(NSError * _Nullable error) {
[expectation fulfill];
}];
}

[[[NSThread currentThread] threadDictionary] setValue:oldHandler forKey:NSAssertionHandlerKey];

[self waitForExpectations:@[expectation] timeout:10.0];

// TODO: What should we expect here? All packs removed?

NSLog(@"Test `%@` complete", NSStringFromSelector(_cmd));
}

// Test to explore https://github.com/mapbox/mapbox-gl-native/issues/15536
- (void)test15536RemovePacksOnBackgroundQueueWhileReloading {

[self addPacks:4];

NSInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count;
XCTAssert(countOfPacks > 0);

// Now delete packs one by one
dispatch_queue_t queue = dispatch_queue_create("com.mapbox.testRemovePacks", DISPATCH_QUEUE_SERIAL);

XCTestExpectation *expectation = [self expectationWithDescription:@"all packs removed"];
expectation.expectedFulfillmentCount = countOfPacks;

MGLOfflineStorage *storage = [MGLOfflineStorage sharedOfflineStorage];

// Simulate what happens the first time sharedOfflineStorage is accessed
[storage reloadPacks];

// NSArray *packs = [storage.packs copy];

dispatch_async(queue, ^{
NSArray *packs = storage.packs;
NSAssertionHandler *oldHandler = [NSAssertionHandler currentHandler];
MGLTestAssertionHandler *newHandler = [[MGLTestAssertionHandler alloc] initWithTestCase:self];

[[[NSThread currentThread] threadDictionary] setValue:newHandler forKey:NSAssertionHandlerKey];

NSArray *validPacks = [packs filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
MGLOfflinePack *pack = (MGLOfflinePack*)evaluatedObject;
return pack.state != MGLOfflinePackStateInvalid;
}]];

for (MGLOfflinePack *pack in validPacks) {
// NOTE: pack can be invalid, as we have two threads potentially
// modifying the same MGLOfflinePack.

dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[storage removePack:pack withCompletionHandler:^(NSError * _Nullable error) {
dispatch_group_leave(group);
}];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

[expectation fulfill];
}

[[[NSThread currentThread] threadDictionary] setValue:oldHandler forKey:NSAssertionHandlerKey];
});

[self waitForExpectations:@[expectation] timeout:60.0];

// TODO: What should we expect here? All packs removed?

NSLog(@"Test `%@` complete", NSStringFromSelector(_cmd));
}

- (void)testCountOfBytesCompleted {
XCTAssertGreaterThan([MGLOfflineStorage sharedOfflineStorage].countOfBytesCompleted, 0UL);
}
Expand Down
18 changes: 18 additions & 0 deletions platform/darwin/test/MGLTestAssertionHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>

NS_ASSUME_NONNULL_BEGIN

// Use to catch or log assertions that occur in dispatch blocks, timers or
// other asynchronous operations.
@interface MGLTestAssertionHandler : NSAssertionHandler

- (instancetype)initWithTestCase:(XCTestCase *)testCase;
@property (nonatomic, weak) XCTestCase *testCase;

// If YES, use `_XCTPreformattedFailureHandler` to "fail" the test,
// otherwise log the assert.
@property (nonatomic) BOOL shouldFail;
@end

NS_ASSUME_NONNULL_END
77 changes: 77 additions & 0 deletions platform/darwin/test/MGLTestAssertionHandler.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#import "MGLTestAssertionHandler.h"

@implementation MGLTestAssertionHandler

- (instancetype)initWithTestCase:(XCTestCase *)testCase {
if ((self = [super init])) {
_testCase = testCase;
}
return self;
}

- (void)handleFailureInMethod:(SEL)selector
object:(id)object
file:(NSString *)fileName
lineNumber:(NSInteger)line
description:(NSString *)format, ...
{
va_list args;
va_start(args, format);
NSString *description = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);

NSString *condition = [NSString stringWithFormat:
@"`[%@ %@]`",
object, NSStringFromSelector(selector)
];

if (self.testCase && self.shouldFail) {
_XCTPreformattedFailureHandler(self.testCase,
YES,
fileName,
line,
condition,
description
);
}
else {
NSLog(@"Assertion Failure: %@:%lu: %@ - %@",
fileName,
line,
condition,
description);
}
}

- (void)handleFailureInFunction:(NSString *)functionName
file:(NSString *)fileName
lineNumber:(NSInteger)line
description:(NSString *)format, ...
{
va_list args;
va_start(args, format);
NSString *description = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);

NSString *condition = [NSString stringWithFormat:
@"`%@`",
functionName];

if (self.testCase && self.shouldFail) {
_XCTPreformattedFailureHandler(self.testCase,
YES,
fileName,
line,
condition,
description);
}
else {
NSLog(@"Assertion Failure: %@:%lu: %@ - %@",
fileName,
line,
condition,
description);
}
}
@end

Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class OfflineDatabase : private util::noncopyable {
void initialize();
void handleError(const mapbox::sqlite::Exception&, const char* action);
void handleError(const util::IOException&, const char* action);
void handleError(const std::runtime_error& ex, const char* action);

void removeExisting();
void removeOldCacheTable();
Expand Down
Loading