Files
MapLibreParticleLab/platform/darwin/test/MLNOfflineStorageTests.mm
nitrag e27ea3a5f5 Expose Offline Region ID property on MLNOfflinePack for Darwin/iOS (#4161)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-10 21:53:36 +00:00

947 lines
46 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import <Mapbox.h>
#import <XCTest/XCTest.h>
#import "MLNOfflineStorage_Private.h"
#import "MLNTestAssertionHandler.h"
#import "NSBundle+MLNAdditions.h"
#import "NSDate+MLNAdditions.h"
#include <mbgl/storage/resource.hpp>
#include <mbgl/util/run_loop.hpp>
#pragma clang diagnostic ignored "-Wshadow"
@interface MLNOfflineStorageTests : XCTestCase <MLNOfflineStorageDelegate>
@end
@implementation MLNOfflineStorageTests
+ (void)tearDown {
NSURL *cacheDirectoryURL =
[[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:NO
error:nil];
NSString *bundleIdentifier = [NSBundle mgl_applicationBundleIdentifier];
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:bundleIdentifier];
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:@".mapbox"];
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:cacheDirectoryURL.path],
@"Directory containing database should exist.");
NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:@"cache.db"];
XCTAssertEqualObjects(cacheURL, MLNOfflineStorage.sharedOfflineStorage.databaseURL);
[[NSFileManager defaultManager] removeItemAtURL:cacheURL error:nil];
XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:cacheURL.path],
@"Database should not exist.");
}
- (void)setUp {
[super setUp];
[MLNSettings useWellKnownTileServer:MLNMapTiler];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
XCTestExpectation *expectation = [self
keyValueObservingExpectationForObject:[MLNOfflineStorage sharedOfflineStorage]
keyPath:@"packs"
handler:^BOOL(id _Nonnull observedObject,
NSDictionary *_Nonnull change) {
const auto changeKind = static_cast<NSKeyValueChange>(
[change[NSKeyValueChangeKindKey] unsignedLongValue]);
return changeKind == NSKeyValueChangeSetting;
}];
if ([MLNOfflineStorage sharedOfflineStorage].packs) {
[expectation fulfill];
[self waitForExpectationsWithTimeout:0 handler:nil];
} else {
[self waitForExpectationsWithTimeout:10 handler:nil];
}
XCTAssertNotNil(
[MLNOfflineStorage sharedOfflineStorage].packs,
@"Shared offline storage object should have a non-nil collection of packs by this point.");
});
}
- (NSURL *)offlineStorage:(MLNOfflineStorage *)storage
URLForResourceOfKind:(MLNResourceKind)kind
withURL:(NSURL *)url {
if ([url.scheme isEqual:@"test"] && [url.host isEqual:@"api"]) {
return [NSURL URLWithString:@"https://api.mapbox.com"];
} else {
return url;
}
}
- (void)testSharedObject {
XCTAssertEqual([MLNOfflineStorage sharedOfflineStorage], [MLNOfflineStorage sharedOfflineStorage],
@"There should only be one shared offline storage object.");
}
- (void)testAddPackForBounds {
NSUInteger countOfPacks = [MLNOfflineStorage sharedOfflineStorage].packs.count;
NSURL *styleURL = [[MLNStyle predefinedStyle:@"Bright"] url];
/// Somewhere near Grape Grove, Ohio, United States.
MLNCoordinateBounds bounds = {
{.latitude = 39.70358155855172, .longitude = -83.69506472545841},
{.latitude = 39.703818870225376, .longitude = -83.69420641857361},
};
double zoomLevel = 20;
MLNTilePyramidOfflineRegion *region =
[[MLNTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL
bounds:bounds
fromZoomLevel:zoomLevel
toZoomLevel:zoomLevel];
NSString *nameKey = @"Name";
NSString *name = @"🍇 Grape Grove";
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{
nameKey : name,
}];
__block MLNOfflinePack *pack;
[self keyValueObservingExpectationForObject:[MLNOfflineStorage 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 == NSKeyValueChangeInsertion &&
indices.count == 1;
}];
XCTestExpectation *additionCompletionHandlerExpectation =
[self expectationWithDescription:@"add pack completion handler"];
[[MLNOfflineStorage sharedOfflineStorage]
addPackForRegion:region
withContext:context
completionHandler:^(MLNOfflinePack *_Nullable completionHandlerPack,
NSError *_Nullable error) {
XCTAssertNotNil(completionHandlerPack, @"Added pack should exist.");
XCTAssertEqual(completionHandlerPack.state, MLNOfflinePackStateInactive,
@"New pack should initially have inactive state.");
pack = completionHandlerPack;
[additionCompletionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
XCTAssertEqual([MLNOfflineStorage sharedOfflineStorage].packs.count, countOfPacks + 1,
@"Added pack should have been added to the canonical collection of packs owned by "
@"the shared offline storage object. This assertion can fail if this test is run "
@"before -testAAALoadPacks.");
XCTAssertEqual(pack, [MLNOfflineStorage sharedOfflineStorage].packs.lastObject,
@"Pack should be appended to end of packs array.");
XCTAssertEqualObjects(pack.region, region, @"Added pack’s region has changed.");
NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
XCTAssert([userInfo isKindOfClass:[NSDictionary class]],
@"Context of offline pack isn’t a dictionary.");
XCTAssert([userInfo[nameKey] isKindOfClass:[NSString class]],
@"Name of offline pack isn’t a string.");
XCTAssertEqualObjects(userInfo[nameKey], name, @"Name of offline pack has changed.");
XCTAssertEqual(pack.state, MLNOfflinePackStateInactive,
@"New pack should initially have inactive state.");
[self keyValueObservingExpectationForObject:pack
keyPath:@"state"
handler:^BOOL(id _Nonnull observedObject,
NSDictionary *_Nonnull change) {
const auto changeKind = static_cast<NSKeyValueChange>(
[change[NSKeyValueChangeKindKey] unsignedLongValue]);
const auto state = static_cast<MLNOfflinePackState>(
[change[NSKeyValueChangeNewKey] longValue]);
return changeKind == NSKeyValueChangeSetting &&
state == MLNOfflinePackStateInactive;
}];
[self
expectationForNotification:MLNOfflinePackProgressChangedNotification
object:pack
handler:^BOOL(NSNotification *_Nonnull notification) {
MLNOfflinePack *notificationPack = notification.object;
XCTAssert([notificationPack isKindOfClass:[MLNOfflinePack class]],
@"Object of notification should be an MLNOfflinePack.");
NSDictionary *userInfo = notification.userInfo;
XCTAssertNotNil(
userInfo,
@"Progress change notification should have a userInfo dictionary.");
NSNumber *stateNumber = userInfo[MLNOfflinePackUserInfoKeyState];
XCTAssert(
[stateNumber isKindOfClass:[NSNumber class]],
@"Progress change notification’s state should be an NSNumber.");
XCTAssertEqual(stateNumber.integerValue, pack.state,
@"State in a progress change notification should match "
@"the pack’s state.");
NSValue *progressValue = userInfo[MLNOfflinePackUserInfoKeyProgress];
XCTAssert(
[progressValue isKindOfClass:[NSValue class]],
@"Progress change notification’s progress should be an NSValue.");
XCTAssertEqualObjects(
progressValue,
[NSValue valueWithMLNOfflinePackProgress:pack.progress],
@"Progress change notification’s progress should match pack’s "
@"progress.");
return notificationPack == pack &&
pack.state == MLNOfflinePackStateInactive;
}];
[pack requestProgress];
[self waitForExpectationsWithTimeout:5 handler:nil];
XCTAssertEqualObjects(
pack.context, context,
@"Offline pack context should match the context specified by the application.");
NSString *newName = @"🍑 Peach Grove";
NSData *newContext = [NSKeyedArchiver archivedDataWithRootObject:@{
nameKey : newName,
}];
XCTestExpectation *contextCompletionHandlerExpectation =
[self expectationWithDescription:@"set pack completion context handler"];
__weak MLNOfflinePack *weakPack = pack;
[pack setContext:newContext
completionHandler:^(NSError *_Nullable error) {
XCTAssertNil(error);
XCTAssertNotNil(weakPack);
XCTAssertEqualObjects(
weakPack.context, newContext,
@"Offline pack context should match the updated context specified by the application.");
[contextCompletionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testAddPackForGeometry {
NSUInteger countOfPacks = [MLNOfflineStorage sharedOfflineStorage].packs.count;
NSURL *styleURL = [[MLNStyle predefinedStyle:@"Bright"] url];
double zoomLevel = 20;
NSString *geojson =
@"{ \"type\": \"Polygon\", \"coordinates\": [ [ [ 5.1299285888671875, 52.10365839097971 ], [ "
@"5.103063583374023, 52.110037078604236 ], [ 5.080232620239258, 52.09548601177304 ], [ "
@"5.106925964355469, 52.07987524347506 ], [ 5.1299285888671875, 52.10365839097971 ] ] ]}";
NSError *error;
MLNShape *shape = [MLNShape shapeWithData:[geojson dataUsingEncoding:NSUTF8StringEncoding]
encoding:NSUTF8StringEncoding
error:&error];
XCTAssertNil(error);
MLNShapeOfflineRegion *region = [[MLNShapeOfflineRegion alloc] initWithStyleURL:styleURL
shape:shape
fromZoomLevel:zoomLevel
toZoomLevel:zoomLevel];
region.includesIdeographicGlyphs = NO;
NSString *nameKey = @"Name";
NSString *name = @"Utrecht centrum";
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{nameKey : name}];
__block MLNOfflinePack *pack;
[self keyValueObservingExpectationForObject:[MLNOfflineStorage 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 == NSKeyValueChangeInsertion &&
indices.count == 1;
}];
XCTestExpectation *additionCompletionHandlerExpectation =
[self expectationWithDescription:@"add pack completion handler"];
[[MLNOfflineStorage sharedOfflineStorage]
addPackForRegion:region
withContext:context
completionHandler:^(MLNOfflinePack *_Nullable completionHandlerPack,
NSError *_Nullable error) {
XCTAssertNotNil(completionHandlerPack, @"Added pack should exist.");
XCTAssertEqual(completionHandlerPack.state, MLNOfflinePackStateInactive,
@"New pack should initially have inactive state.");
pack = completionHandlerPack;
[additionCompletionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
XCTAssertEqual([MLNOfflineStorage sharedOfflineStorage].packs.count, countOfPacks + 1,
@"Added pack should have been added to the canonical collection of packs owned by "
@"the shared offline storage object. This assertion can fail if this test is run "
@"before -testAAALoadPacks.");
XCTAssertEqual(pack, [MLNOfflineStorage sharedOfflineStorage].packs.lastObject,
@"Pack should be appended to end of packs array.");
XCTAssertEqualObjects(pack.region, region, @"Added pack’s region has changed.");
NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
XCTAssert([userInfo isKindOfClass:[NSDictionary class]],
@"Context of offline pack isn’t a dictionary.");
XCTAssert([userInfo[nameKey] isKindOfClass:[NSString class]],
@"Name of offline pack isn’t a string.");
XCTAssertEqualObjects(userInfo[nameKey], name, @"Name of offline pack has changed.");
XCTAssertEqual(pack.state, MLNOfflinePackStateInactive,
@"New pack should initially have inactive state.");
[self keyValueObservingExpectationForObject:pack
keyPath:@"state"
handler:^BOOL(id _Nonnull observedObject,
NSDictionary *_Nonnull change) {
const auto changeKind = static_cast<NSKeyValueChange>(
[change[NSKeyValueChangeKindKey] unsignedLongValue]);
const auto state = static_cast<MLNOfflinePackState>(
[change[NSKeyValueChangeNewKey] longValue]);
return changeKind == NSKeyValueChangeSetting &&
state == MLNOfflinePackStateInactive;
}];
[self
expectationForNotification:MLNOfflinePackProgressChangedNotification
object:pack
handler:^BOOL(NSNotification *_Nonnull notification) {
MLNOfflinePack *notificationPack = notification.object;
XCTAssert([notificationPack isKindOfClass:[MLNOfflinePack class]],
@"Object of notification should be an MLNOfflinePack.");
NSDictionary *userInfo = notification.userInfo;
XCTAssertNotNil(
userInfo,
@"Progress change notification should have a userInfo dictionary.");
NSNumber *stateNumber = userInfo[MLNOfflinePackUserInfoKeyState];
XCTAssert(
[stateNumber isKindOfClass:[NSNumber class]],
@"Progress change notification’s state should be an NSNumber.");
XCTAssertEqual(stateNumber.integerValue, pack.state,
@"State in a progress change notification should match "
@"the pack’s state.");
NSValue *progressValue = userInfo[MLNOfflinePackUserInfoKeyProgress];
XCTAssert(
[progressValue isKindOfClass:[NSValue class]],
@"Progress change notification’s progress should be an NSValue.");
XCTAssertEqualObjects(
progressValue,
[NSValue valueWithMLNOfflinePackProgress:pack.progress],
@"Progress change notification’s progress should match pack’s "
@"progress.");
return notificationPack == pack &&
pack.state == MLNOfflinePackStateInactive;
}];
[pack requestProgress];
[self waitForExpectationsWithTimeout:5 handler:nil];
pack = nil;
}
- (void)testInvalidatePack {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Expect offline pack to be invalidated without an error."];
MLNCoordinateBounds bounds = {
{.latitude = 48.8660, .longitude = 2.3306},
{.latitude = 48.8603, .longitude = 2.3213},
};
NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner"
withExtension:@"json"];
MLNTilePyramidOfflineRegion *region =
[[MLNTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL
bounds:bounds
fromZoomLevel:10
toZoomLevel:11];
NSString *nameKey = @"Name";
NSString *name = @"Paris square";
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{nameKey : name}];
[[MLNOfflineStorage sharedOfflineStorage]
addPackForRegion:region
withContext:context
completionHandler:^(MLNOfflinePack *_Nullable pack, NSError *_Nullable error) {
XCTAssertNotNil(pack);
[[MLNOfflineStorage sharedOfflineStorage] invalidatePack:pack
withCompletionHandler:^(NSError *_Nullable) {
XCTAssertNotNil(pack);
XCTAssertNil(error);
[expectation fulfill];
}];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testRemovePack {
NSUInteger countOfPacks = [MLNOfflineStorage sharedOfflineStorage].packs.count;
MLNOfflinePack *pack = [MLNOfflineStorage sharedOfflineStorage].packs.lastObject;
XCTAssertNotNil(pack, @"Added pack should still exist.");
[self keyValueObservingExpectationForObject:[MLNOfflineStorage 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"];
[[MLNOfflineStorage sharedOfflineStorage]
removePack:pack
withCompletionHandler:^(NSError *_Nullable error) {
XCTAssertEqual(pack.state, MLNOfflinePackStateInvalid,
@"Removed pack should be invalid in the completion handler.");
[completionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
XCTAssertEqual(pack.state, MLNOfflinePackStateInvalid,
@"Removed pack should have been invalidated synchronously.");
XCTAssertEqual([MLNOfflineStorage 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)testBackupExclusion {
NSURL *cacheDirectoryURL =
[[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:NO
error:nil];
// As of iOS SDK 12.2 unit tests now have a bundle id: com.apple.dt.xctest.tool
NSString *bundleIdentifier = [NSBundle mgl_applicationBundleIdentifier];
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:bundleIdentifier];
cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:@".mapbox"];
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:cacheDirectoryURL.path],
@"Cache subdirectory should exist.");
NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:@"cache.db"];
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:cacheURL.path],
@"Cache database should exist.");
NSError *error = nil;
NSNumber *exclusionFlag = nil;
[cacheDirectoryURL getResourceValue:&exclusionFlag
forKey:NSURLIsExcludedFromBackupKey
error:&error];
XCTAssertTrue(
exclusionFlag && [exclusionFlag boolValue],
@"Backup exclusion flag should be set for the directory containing the cache database.");
XCTAssertNil(error, @"No errors should be returned when checking backup exclusion flag.");
}
- (void)addPacks:(NSInteger)count {
XCTestExpectation *expectation = [self expectationWithDescription:@"added packs"];
NSURL *styleURL = [[MLNStyle predefinedStyle:@"Bright"] url];
MLNCoordinateBounds 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);
MLNTilePyramidOfflineRegion *region =
[[MLNTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL
bounds:bounds[i]
fromZoomLevel:20
toZoomLevel:20];
NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{@"index" : @(i)}];
[[MLNOfflineStorage sharedOfflineStorage]
addPackForRegion:region
withContext:context
completionHandler:^(MLNOfflinePack *_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 = [MLNOfflineStorage sharedOfflineStorage].packs.count;
MLNOfflinePack *pack = [MLNOfflineStorage sharedOfflineStorage].packs.lastObject;
XCTAssertNotNil(pack, @"Added pack should still exist.");
[self keyValueObservingExpectationForObject:[MLNOfflineStorage 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"];
[[MLNOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:nil];
NSAssertionHandler *oldHandler = [NSAssertionHandler currentHandler];
MLNTestAssertionHandler *newHandler = [[MLNTestAssertionHandler alloc] initWithTestCase:self];
[[[NSThread currentThread] threadDictionary] setValue:newHandler forKey:NSAssertionHandlerKey];
[[MLNOfflineStorage sharedOfflineStorage]
removePack:pack
withCompletionHandler:^(NSError *_Nullable error) {
XCTAssertEqual(pack.state, MLNOfflinePackStateInvalid,
@"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, MLNOfflinePackStateInvalid,
@"Removed pack should have been invalidated synchronously.");
XCTAssertEqual([MLNOfflineStorage 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 = [MLNOfflineStorage sharedOfflineStorage].packs.count;
XCTAssert(countOfPacks > 0);
// Now delete packs one by one
XCTestExpectation *expectation = [self expectationWithDescription:@"All packs removed"];
expectation.expectedFulfillmentCount = countOfPacks;
MLNOfflineStorage *storage = [MLNOfflineStorage 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) {
MLNOfflinePack *pack = (MLNOfflinePack *)evaluatedObject;
return pack.state != MLNOfflinePackStateInvalid;
}]];
NSAssertionHandler *oldHandler = [NSAssertionHandler currentHandler];
MLNTestAssertionHandler *newHandler = [[MLNTestAssertionHandler alloc] initWithTestCase:self];
[[[NSThread currentThread] threadDictionary] setValue:newHandler forKey:NSAssertionHandlerKey];
for (MLNOfflinePack *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 = [MLNOfflineStorage 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;
MLNOfflineStorage *storage = [MLNOfflineStorage 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];
MLNTestAssertionHandler *newHandler = [[MLNTestAssertionHandler 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) {
MLNOfflinePack *pack = (MLNOfflinePack *)evaluatedObject;
return pack.state != MLNOfflinePackStateInvalid;
}]];
for (MLNOfflinePack *pack in validPacks) {
// NOTE: pack can be invalid, as we have two threads potentially
// modifying the same MLNOfflinePack.
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([MLNOfflineStorage sharedOfflineStorage].countOfBytesCompleted, 0UL);
}
- (void)testResourceTransform {
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
[os setDelegate:self];
auto fs = os.mbglOnlineFileSource;
// Delegate returns "https://api.mapbox.com" as a replacement URL.
const mbgl::Resource resource{mbgl::Resource::Unknown, "test://api"};
std::unique_ptr<mbgl::AsyncRequest> req;
req = fs->request(resource, [&](mbgl::Response res) {
req.reset();
XCTAssertFalse(res.error.get(), @"Request should not return an error");
XCTAssertTrue(res.data.get(), @"Request should return data");
XCTAssertEqual("{\"api\":\"mapbox\"}", *res.data, @"Request did not return expected data");
CFRunLoopStop(CFRunLoopGetCurrent());
});
CFRunLoopRun();
[os setDelegate:nil];
}
- (void)testAddFileContent {
NSFileManager *fileManager = [NSFileManager defaultManager];
// Valid database
[XCTContext
runActivityNamed:@"Valid database"
block:^(id<XCTActivity> activity) {
NSURL *resourceURL =
[NSURL fileURLWithPath:[[NSBundle bundleForClass:[self class]]
pathForResource:@"sideload_sat"
ofType:@"db"]];
NSError *error;
NSDictionary *fileAttributes =
[fileManager attributesOfItemAtPath:resourceURL.path error:&error];
XCTAssertNil(error,
@"Getting the file's attributes should not return an error. (%@)",
resourceURL.path);
NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
long long fileSize = [fileSizeNumber longLongValue];
long long databaseFileSize = 19218432;
// Merging databases creates an empty file if the file does not exist at the
// given path.
XCTAssertEqual(fileSize, databaseFileSize,
@"The database file size must be:%lld actual size:%lld",
databaseFileSize, fileSize);
NSUInteger countOfPacks = [MLNOfflineStorage sharedOfflineStorage].packs.count;
[self
keyValueObservingExpectationForObject:[MLNOfflineStorage
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 ==
NSKeyValueChangeInsertion &&
indices.count == 1;
}];
XCTestExpectation *fileAdditionCompletionHandlerExpectation =
[self expectationWithDescription:@"add database content completion handler"];
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
[os addContentsOfURL:resourceURL
withCompletionHandler:^(NSURL *fileURL,
NSArray<MLNOfflinePack *> *_Nullable packs,
NSError *_Nullable error) {
XCTAssertNotNil(fileURL, @"The fileURL should not be nil.");
XCTAssertNotNil(
packs,
@"Adding the contents of the sideload_sat.db should update one pack.");
XCTAssertNil(error,
@"Adding contents to a file should not return an error.");
for (MLNOfflinePack *pack in [MLNOfflineStorage sharedOfflineStorage]
.packs) {
NSLog(@"PACK:%@", pack);
}
[fileAdditionCompletionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
// Depending on the database it may update or add a pack. For this case
// specifically the offline database adds one pack.
XCTAssertEqual([MLNOfflineStorage sharedOfflineStorage].packs.count,
countOfPacks + 1,
@"Adding contents of sideload_sat.db should add one pack.");
MLNOfflinePack *lastPack =
[MLNOfflineStorage sharedOfflineStorage].packs.lastObject;
XCTAssertNotNil(lastPack, @"Pack should exist after adding contents.");
XCTAssertNotNil(lastPack.regionId,
@"Region ID should not be nil after a pack is added.");
XCTAssertGreaterThan(
lastPack.regionId.longLongValue, 0,
@"Region ID should be greater than 0 after a pack is added.");
}];
// Invalid database type
[XCTContext
runActivityNamed:@"Invalid database type"
block:^(id<XCTActivity> activity) {
NSURL *resourceURL =
[NSURL fileURLWithPath:[[NSBundle bundleForClass:[self class]]
pathForResource:@"one-liner"
ofType:@"json"]];
XCTestExpectation *invalidFileCompletionHandlerExpectation = [self
expectationWithDescription:@"invalid content database completion handler"];
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
[os addContentsOfFile:resourceURL.path
withCompletionHandler:^(NSURL *fileURL,
NSArray<MLNOfflinePack *> *_Nullable packs,
NSError *_Nullable error) {
XCTAssertNotNil(
error,
@"Passing an invalid offline database file should return an error.");
XCTAssertNil(packs, @"Passing an invalid offline database file should not "
@"add packs to the offline database.");
[invalidFileCompletionHandlerExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}];
// File does not exist
[XCTContext runActivityNamed:@"File does not exist"
block:^(id<XCTActivity> activity) {
NSURL *resourceURL = [NSURL URLWithString:@"nonexistent.db"];
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
XCTAssertThrowsSpecificNamed(
[os addContentsOfURL:resourceURL withCompletionHandler:nil],
NSException, NSInvalidArgumentException,
"MLNOfflineStorage should rise an exception if an invalid database "
"file is passed.");
}];
// URL to a non-file
[XCTContext runActivityNamed:@"URL to a non-file"
block:^(id<XCTActivity> activity) {
NSURL *resourceURL = [NSURL URLWithString:@"https://www.mapbox.com"];
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
XCTAssertThrowsSpecificNamed(
[os addContentsOfURL:resourceURL withCompletionHandler:nil],
NSException, NSInvalidArgumentException,
"MLNOfflineStorage should rise an exception if an invalid URL file "
"is passed.");
}];
}
- (void)testPutResourceForURL {
NSURL *styleURL = [NSURL URLWithString:@"https://api.mapbox.com/some/thing"];
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
std::string testData("test data");
NSData *data = [NSData dataWithBytes:testData.c_str() length:testData.length()];
__block std::unique_ptr<mbgl::AsyncRequest> req;
[os preloadData:data
forURL:styleURL
modificationDate:nil
expirationDate:nil
eTag:nil
mustRevalidate:NO
completionHandler:^(NSURL *url, NSError *_Nullable error) {
XCTAssertFalse(error, @"preloadData should not return an error");
XCTAssertEqual(styleURL, url, @"Preloaded resource url is invalid");
auto fs = os.mbglDatabaseFileSource;
const mbgl::Resource resource{mbgl::Resource::Unknown, "https://api.mapbox.com/some/thing"};
req = fs->request(resource, [&](mbgl::Response res) {
XCTAssertFalse(res.error.get(), @"Request should not return an error");
XCTAssertTrue(res.data.get(), @"Request should return data");
XCTAssertFalse(res.modified, @"Request should not have a modification timestamp");
XCTAssertFalse(res.expires, @"Request should not have an expiration timestamp");
XCTAssertFalse(res.etag, @"Request should not have an entity tag");
XCTAssertFalse(res.mustRevalidate, @"Request should not require revalidation");
XCTAssertEqual("test data", *res.data, @"Request did not return expected data");
CFRunLoopStop(CFRunLoopGetCurrent());
});
}];
CFRunLoopRun();
}
- (void)testPutResourceForURLWithTimestamps {
NSURL *styleURL = [NSURL URLWithString:@"https://api.mapbox.com/some/thing1"];
MLNOfflineStorage *os = [MLNOfflineStorage sharedOfflineStorage];
std::string testData("test data");
NSData *data = [NSData dataWithBytes:testData.c_str() length:testData.length()];
__block NSDate *now = [NSDate date];
__block NSDate *future = [now dateByAddingTimeInterval:600];
__block std::unique_ptr<mbgl::AsyncRequest> req;
[os preloadData:data
forURL:styleURL
modificationDate:now
expirationDate:future
eTag:@"some etag"
mustRevalidate:YES
completionHandler:^(NSURL *url, NSError *_Nullable error) {
XCTAssertFalse(error, @"preloadData should not return an error");
XCTAssertEqual(styleURL, url, @"Preloaded resource url is invalid");
auto fs = os.mbglDatabaseFileSource;
const mbgl::Resource resource{mbgl::Resource::Unknown,
"https://api.mapbox.com/some/thing1"};
req = fs->request(resource, [&, tNow = now.timeIntervalSince1970,
tFuture = future.timeIntervalSince1970](mbgl::Response res) {
XCTAssertFalse(res.error.get(), @"Request should not return an error");
XCTAssertTrue(res.data.get(), @"Request should return data");
XCTAssertTrue(res.modified, @"Request should have a modification timestamp");
XCTAssertEqual(MLNTimeIntervalFromDuration(res.modified->time_since_epoch()), floor(tNow),
@"Modification timestamp should roundtrip");
XCTAssertTrue(res.expires, @"Request should have an expiration timestamp");
XCTAssertEqual(MLNTimeIntervalFromDuration(res.expires->time_since_epoch()),
floor(tFuture), @"Expiration timestamp should roundtrip");
XCTAssertTrue(res.etag, @"Request should have an entity tag");
XCTAssertEqual(*res.etag, "some etag", @"Entity tag should roundtrip");
XCTAssertTrue(res.mustRevalidate, @"Request should require revalidation");
XCTAssertEqual("test data", *res.data, @"Request did not return expected data");
CFRunLoopStop(CFRunLoopGetCurrent());
});
}];
CFRunLoopRun();
}
- (void)testSetMaximumAmbientCache {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Expect maximum cache size to be raised without an error."];
[[MLNOfflineStorage sharedOfflineStorage]
setMaximumAmbientCacheSize:0
withCompletionHandler:^(NSError *_Nullable error) {
XCTAssertNil(error);
[[MLNOfflineStorage sharedOfflineStorage]
setMaximumAmbientCacheSize:50 * 1024 * 1024
withCompletionHandler:^(NSError *_Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testInvalidateAmbientCache {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Expect cache to be invalidated without an error."];
[[MLNOfflineStorage sharedOfflineStorage]
invalidateAmbientCacheWithCompletionHandler:^(NSError *_Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testClearCache {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Expect cache to be cleared without an error."];
[[MLNOfflineStorage sharedOfflineStorage]
clearAmbientCacheWithCompletionHandler:^(NSError *_Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
- (void)testResetDatabase {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Expect database to be reset without an error."];
[[MLNOfflineStorage sharedOfflineStorage]
resetDatabaseWithCompletionHandler:^(NSError *_Nullable error) {
XCTAssertNil(error);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:10 handler:nil];
}
@end