Add unit tests for the layout engine (#424)

* Build testing platform & tests for the layout engine

* Add our license header to debugbreak.

* Remove thing

* Address review comments

* Beef up the logging

* Update -[ASLayout isEqual:]

* testLayoutTransitionWithAsyncMeasurement passes now

* Disable testASetNeedsLayoutInterferingWithTheCurrentTransition

* Fix build errors
This commit is contained in:
Adlai Holler 2017-12-01 09:05:47 -08:00 committed by Huy Nguyen
parent bccde6cf0f
commit 0dc7002f0b
16 changed files with 1176 additions and 8 deletions

View File

@ -405,6 +405,9 @@
CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; };
CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */; };
CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */; };
CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */; };
CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */; };
CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */; };
CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; };
DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; };
DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; };
@ -899,6 +902,12 @@
CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = "<group>"; };
CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = "<group>"; };
CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIntegerMapTests.m; sourceTree = "<group>"; };
CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutEngineTests.mm; sourceTree = "<group>"; };
CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTestNode.h; sourceTree = "<group>"; };
CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTestNode.mm; sourceTree = "<group>"; };
CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = debugbreak.h; sourceTree = "<group>"; };
CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTLayoutFixture.h; sourceTree = "<group>"; };
CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTLayoutFixture.mm; sourceTree = "<group>"; };
D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = "<group>"; };
D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = "<group>"; };
D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = "<group>"; };
@ -1175,6 +1184,11 @@
CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */,
CC051F1E1D7A286A006434CB /* ASCALayerTests.m */,
CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */,
CCE4F9B41F0DA4F300062E4E /* ASLayoutEngineTests.mm */,
CCE4F9B61F0DBA5000062E4E /* ASLayoutTestNode.h */,
CCE4F9B71F0DBA5000062E4E /* ASLayoutTestNode.mm */,
CCE4F9BC1F0ECE5200062E4E /* ASTLayoutFixture.h */,
CCE4F9BD1F0ECE5200062E4E /* ASTLayoutFixture.mm */,
CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */,
CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */,
CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */,
@ -1573,6 +1587,7 @@
CC583ABF1EF9BAB400134156 /* Common */ = {
isa = PBXGroup;
children = (
CCE4F9BB1F0EA67F00062E4E /* debugbreak.h */,
CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.m */,
CC583AC11EF9BAB400134156 /* ASTestCase.h */,
CC583AC21EF9BAB400134156 /* ASTestCase.m */,
@ -2187,6 +2202,7 @@
ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */,
CC8B05D61D73836400F54286 /* ASPerformanceTestContext.m in Sources */,
CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.m in Sources */,
CCE4F9B51F0DA4F300062E4E /* ASLayoutEngineTests.mm in Sources */,
69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */,
ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */,
7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */,
@ -2195,12 +2211,14 @@
254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */,
05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */,
ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */,
CCE4F9BA1F0DBB5000062E4E /* ASLayoutTestNode.mm in Sources */,
81E95C141D62639600336598 /* ASTextNodeSnapshotTests.m in Sources */,
3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */,
AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.m in Sources */,
254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */,
058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */,
CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */,
CCE4F9BE1F0ECE5200062E4E /* ASTLayoutFixture.mm in Sources */,
058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */,
DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m in Sources */,
058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */,

View File

@ -14,6 +14,7 @@
- [Layout] Fix an issue that causes a pending layout to be applied multiple times. [Huy Nguyen](https://github.com/nguyenhuy) [#695](https://github.com/TextureGroup/Texture/pull/695)
- [ASScrollNode] Ensure the node respects the given size range while calculating its layout. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy)
- [ASScrollNode] Invalidate the node's calculated layout if its scrollable directions changed. Also add unit tests for the class. [#637](https://github.com/TextureGroup/Texture/pull/637) [Huy Nguyen](https://github.com/nguyenhuy)
- Add new unit testing to the layout engine. [Adlai Holler](https://github.com/Adlai-Holler) [#424](https://github.com/TextureGroup/Texture/pull/424)
## 2.6
- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon)

View File

@ -85,6 +85,7 @@
layout = [self calculateLayoutThatFits:constrainedSize
restrictedToSize:self.style.size
relativeToParentSize:parentSize];
as_log_verbose(ASLayoutLog(), "Established pending layout for %@ in %s", self, sel_getName(_cmd));
_pendingDisplayNodeLayout = std::make_shared<ASDisplayNodeLayout>(layout, constrainedSize, parentSize, version);
ASDisplayNodeAssertNotNil(layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newly calculated layout should not be nil! %@", self);
}

View File

@ -2762,7 +2762,8 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) {
}
}
ASDisplayNodeLogEvent(self, @"setHierarchyState: oldState = %@, newState = %@", NSStringFromASHierarchyState(oldState), NSStringFromASHierarchyState(newState));
ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState));
as_log_verbose(ASNodeLog(), "%s%@ %@", sel_getName(_cmd), NSStringFromASHierarchyStateChange(oldState, newState), self);
}
- (void)willEnterHierarchy

View File

@ -25,6 +25,7 @@
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASLog.h>
@implementation _ASDisplayLayer
{
@ -93,6 +94,7 @@
- (void)setNeedsLayout
{
ASDisplayNodeAssertMainThread();
as_log_verbose(ASNodeLog(), "%s on %@", sel_getName(_cmd), self);
[super setNeedsLayout];
}
#endif

View File

@ -23,6 +23,7 @@
#import <AsyncDisplayKit/ASLayoutSpecUtilities.h>
#import <AsyncDisplayKit/ASLayoutSpec+Subclasses.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASRectTable.h>
@ -281,11 +282,12 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
}
if (!CGSizeEqualToSize(_size, layout.size)) return NO;
if (!CGPointEqualToPoint(_position, layout.position)) return NO;
if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position))
|| CGPointEqualToPoint(self.position, layout.position))) return NO;
if (_layoutElement != layout.layoutElement) return NO;
NSArray *sublayouts = layout.sublayouts;
if (sublayouts != _sublayouts && (sublayouts == nil || _sublayouts == nil || ![_sublayouts isEqual:sublayouts])) {
if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) {
return NO;
}

View File

@ -99,6 +99,29 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat
return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]];
}
#define HIERARCHY_STATE_DELTA(Name) ({ \
if ((oldState & ASHierarchyState##Name) != (newState & ASHierarchyState##Name)) { \
[changes appendFormat:@"%c%s ", (newState & ASHierarchyState##Name ? '+' : '-'), #Name]; \
} \
})
__unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarchyState oldState, ASHierarchyState newState)
{
if (oldState == newState) {
return @"{ }";
}
NSMutableString *changes = [NSMutableString stringWithString:@"{ "];
HIERARCHY_STATE_DELTA(Rasterized);
HIERARCHY_STATE_DELTA(RangeManaged);
HIERARCHY_STATE_DELTA(TransitioningSupernodes);
HIERARCHY_STATE_DELTA(LayoutPending);
[changes appendString:@"}"];
return changes;
}
#undef HIERARCHY_STATE_DELTA
@interface ASDisplayNode () <ASDescriptionProvider, ASDebugDescriptionProvider>
{
@protected

View File

@ -0,0 +1,517 @@
//
// ASLayoutEngineTests.mm
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASTestCase.h"
#import "ASLayoutTestNode.h"
#import "ASXCTExtensions.h"
#import "ASTLayoutFixture.h"
@interface ASLayoutEngineTests : ASTestCase
@end
@implementation ASLayoutEngineTests {
ASLayoutTestNode *nodeA;
ASLayoutTestNode *nodeB;
ASLayoutTestNode *nodeC;
ASLayoutTestNode *nodeD;
ASLayoutTestNode *nodeE;
ASTLayoutFixture *fixture1;
ASTLayoutFixture *fixture2;
ASTLayoutFixture *fixture3;
ASTLayoutFixture *fixture4;
// fixtures 1 and 3 share the same exact node A layout spec block.
// we don't want the infra to call -setNeedsLayout when we switch fixtures
// so we need to use the same exact block.
ASLayoutSpecBlock fixture1and3NodeALayoutSpecBlock;
UIWindow *window;
UIViewController *vc;
NSArray<ASLayoutTestNode *> *allNodes;
NSTimeInterval verifyDelay;
// See -stubCalculatedLayoutDidChange.
BOOL stubbedCalculatedLayoutDidChange;
}
- (void)setUp
{
[super setUp];
verifyDelay = 3;
window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 10, 1)];
vc = [[UIViewController alloc] init];
nodeA = [ASLayoutTestNode new];
nodeA.backgroundColor = [UIColor redColor];
// NOTE: nodeB has flexShrink, the others don't
nodeB = [ASLayoutTestNode new];
nodeB.style.flexShrink = 1;
nodeB.backgroundColor = [UIColor orangeColor];
nodeC = [ASLayoutTestNode new];
nodeC.backgroundColor = [UIColor yellowColor];
nodeD = [ASLayoutTestNode new];
nodeD.backgroundColor = [UIColor greenColor];
nodeE = [ASLayoutTestNode new];
nodeE.backgroundColor = [UIColor blueColor];
allNodes = @[ nodeA, nodeB, nodeC, nodeD, nodeE ];
ASSetDebugNames(nodeA, nodeB, nodeC, nodeD, nodeE);
ASLayoutSpecBlock b = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeD ]];
};
fixture1and3NodeALayoutSpecBlock = b;
fixture1 = [self createFixture1];
fixture2 = [self createFixture2];
fixture3 = [self createFixture3];
fixture4 = [self createFixture4];
nodeA.frame = vc.view.bounds;
nodeA.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[vc.view addSubnode:nodeA];
window.rootViewController = vc;
[window makeKeyAndVisible];
}
- (void)tearDown
{
nodeA.layoutSpecBlock = nil;
for (ASLayoutTestNode *node in allNodes) {
OCMVerifyAllWithDelay(node.mock, verifyDelay);
}
[super tearDown];
}
- (void)testFirstLayoutPassWhenInWindow
{
[self runFirstLayoutPassWithFixture:fixture1];
}
- (void)testSetNeedsLayoutAndNormalLayoutPass
{
[self runFirstLayoutPassWithFixture:fixture1];
[fixture2 apply];
// skip nodeB because its layout doesn't change.
for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) {
[fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread();
}];
OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread();
}
[window layoutIfNeeded];
[self verifyFixture:fixture2];
}
/**
* Transition from fixture1 to Fixture2 on node A.
*
* Expect A and D to calculate once off main, and
* to receive calculatedLayoutDidChange on main,
* then to get the measurement completion call on main,
* then to get animateLayoutTransition: and didCompleteLayoutTransition: on main.
*/
- (void)testLayoutTransitionWithAsyncMeasurement
{
[self stubCalculatedLayoutDidChange];
[self runFirstLayoutPassWithFixture:fixture1];
[fixture2 apply];
// Expect A, C, E to calculate new layouts off-main
// dispatch_once onto main to run our injectedMainThread work while the transition calculates.
__block dispatch_block_t injectedMainThreadWork = nil;
for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) {
[fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange])
.offMainThread()
.andDo(^(NSInvocation *inv) {
// On first calculateLayoutThatFits, schedule our injected main thread work.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_async(dispatch_get_main_queue(), ^{
injectedMainThreadWork();
});
});
});
}];
}
// The code in this section is designed to move in time order, all on the main thread:
OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread();
OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread();
// Trigger the layout transition.
__block dispatch_block_t measurementCompletionBlock = nil;
[nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{
measurementCompletionBlock();
}];
// This block will get run after bg layout calculate starts, but before measurementCompletion
__block BOOL injectedMainThreadWorkDone = NO;
injectedMainThreadWork = ^{
injectedMainThreadWorkDone = YES;
[window layoutIfNeeded];
// Ensure we're still on the old layout. We should stay on this until the transition completes.
[self verifyFixture:fixture1];
};
measurementCompletionBlock = ^{
XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran.");
};
for (ASLayoutTestNode *node in allNodes) {
OCMVerifyAllWithDelay(node.mock, verifyDelay);
}
[self verifyFixture:fixture2];
}
/**
* Start at fixture 1.
* Trigger an async transition to fixture 2.
* While it's measuring, on main switch to fixture 4 (setNeedsLayout A, D) and run a CA layout pass.
*
* Correct behavior, we end up at fixture 4 since it's newer.
* Current incorrect behavior, we end up at fixture 2 and we remeasure surviving node C.
* Note: incorrect behavior likely introduced by the early check in __layout added in
* https://github.com/facebookarchive/AsyncDisplayKit/pull/2657
*/
- (void)DISABLE_testASetNeedsLayoutInterferingWithTheCurrentTransition
{
static BOOL enforceCorrectBehavior = NO;
[self stubCalculatedLayoutDidChange];
[self runFirstLayoutPassWithFixture:fixture1];
[fixture2 apply];
// Expect A, C, E to calculate new layouts off-main
// dispatch_once onto main to run our injectedMainThread work while the transition calculates.
__block dispatch_block_t injectedMainThreadWork = nil;
for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) {
[fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange])
.offMainThread()
.andDo(^(NSInvocation *inv) {
// On first calculateLayoutThatFits, schedule our injected main thread work.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_async(dispatch_get_main_queue(), ^{
injectedMainThreadWork();
});
});
});
}];
}
// The code in this section is designed to move in time order, all on the main thread:
// With the current behavior, the transition will continue and complete.
if (!enforceCorrectBehavior) {
OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread();
OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread();
}
// Trigger the layout transition.
__block dispatch_block_t measurementCompletionBlock = nil;
[nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{
measurementCompletionBlock();
}];
// Injected block will get run on main after bg layout calculate starts, but before measurementCompletion
__block BOOL injectedMainThreadWorkDone = NO;
injectedMainThreadWork = ^{
as_log_verbose(OS_LOG_DEFAULT, "Begin injectedMainThreadWork");
injectedMainThreadWorkDone = YES;
[fixture4 apply];
as_log_verbose(OS_LOG_DEFAULT, "Did apply new fixture");
if (enforceCorrectBehavior) {
// Correct measurement behavior here is unclear, may depend on whether the layouts which
// are common to both fixture2 and fixture4 are available from the cache.
} else {
// Incorrect behavior: nodeC will get measured against its new bounds on main.
auto cPendingSize = [fixture2 layoutForNode:nodeC].size;
OCMExpect([nodeC.mock calculateLayoutThatFits:ASSizeRangeMake(cPendingSize)]).onMainThread();
}
[window layoutIfNeeded];
as_log_verbose(OS_LOG_DEFAULT, "End injectedMainThreadWork");
};
measurementCompletionBlock = ^{
XCTAssert(injectedMainThreadWorkDone, @"We hoped to get onto the main thread before the measurementCompletion callback ran.");
};
for (ASLayoutTestNode *node in allNodes) {
OCMVerifyAllWithDelay(node.mock, verifyDelay);
}
// Incorrect behavior: The transition will "win" even though its transitioning to stale data.
if (enforceCorrectBehavior) {
[self verifyFixture:fixture4];
} else {
[self verifyFixture:fixture2];
}
}
/**
* Start on fixture 3 where nodeB is force-shrunk via multipass layout.
* Apply fixture 1, which just changes nodeB's size and calls -setNeedsLayout on it.
*
* This behavior is currently broken. See implementation for correct behavior and incorrect behavior.
*/
- (void)testCallingSetNeedsLayoutOnANodeThatWasSubjectToMultipassLayout
{
static BOOL const enforceCorrectBehavior = NO;
[self stubCalculatedLayoutDidChange];
[self runFirstLayoutPassWithFixture:fixture3];
// Switch to fixture 1, updating nodeB's desired size and calling -setNeedsLayout
// Now nodeB will fit happily into the stack.
[fixture1 apply];
if (enforceCorrectBehavior) {
/*
* Correct behavior: nodeB is remeasured against the first (unconstrained) size
* and when it's discovered that now nodeB fits, nodeA will re-layout and we'll
* end up correctly at fixture1.
*/
OCMExpect([nodeB.mock calculateLayoutThatFits:[fixture3 firstSizeRangeForNode:nodeB]]);
[fixture1 withSizeRangesForNode:nodeA block:^(ASSizeRange sizeRange) {
OCMExpect([nodeA.mock calculateLayoutThatFits:sizeRange]);
}];
[window layoutIfNeeded];
[self verifyFixture:fixture1];
} else {
/*
* Incorrect behavior: nodeB is remeasured against the second (fixed-width) constraint.
* The returned value (8) is clamped to the fixed with (7), and then compared to the previous
* width (7) and we decide not to propagate up the invalidation, and we stay stuck on the old
* layout (fixture3).
*/
OCMExpect([nodeB.mock calculateLayoutThatFits:nodeB.constrainedSizeForCalculatedLayout]);
[window layoutIfNeeded];
[self verifyFixture:fixture3];
}
}
#pragma mark - Helpers
- (void)verifyFixture:(ASTLayoutFixture *)fixture
{
auto expected = fixture.layout;
// Ensure expected == frames
auto frames = [fixture.rootNode currentLayoutBasedOnFrames];
if (![expected isEqual:frames]) {
XCTFail(@"\n*** Layout verification failed frames don't match expected. ***\nGot:\n%@\nExpected:\n%@", [frames recursiveDescription], [expected recursiveDescription]);
}
// Ensure expected == calculatedLayout
auto calculated = fixture.rootNode.calculatedLayout;
if (![expected isEqual:calculated]) {
XCTFail(@"\n*** Layout verification failed calculated layout doesn't match expected. ***\nGot:\n%@\nExpected:\n%@", [calculated recursiveDescription], [expected recursiveDescription]);
}
}
/**
* Stubs calculatedLayoutDidChange for all nodes.
*
* It's not really a core layout engine method, and it's also
* currently bugged and gets called a lot so for most
* tests its better not to have expectations about it littered around.
* https://github.com/TextureGroup/Texture/issues/422
*/
- (void)stubCalculatedLayoutDidChange
{
stubbedCalculatedLayoutDidChange = YES;
for (ASLayoutTestNode *node in allNodes) {
OCMStub([node.mock calculatedLayoutDidChange]);
}
}
/**
* Fixture 1: A basic horizontal stack, all single-pass.
*
* [A: HorizStack([B, C, D])]. B is (1x1), C is (2x1), D is (1x1)
*/
- (ASTLayoutFixture *)createFixture1
{
auto fixture = [[ASTLayoutFixture alloc] init];
// nodeB
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB];
auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil];
// nodeC
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC];
auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{4,0} sublayouts:nil];
// nodeD
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD];
auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil];
[fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA];
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]];
fixture.layout = layoutA;
[fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA];
return fixture;
}
/**
* Fixture 2: A simple transition away from fixture 1.
*
* [A: HorizStack([B, C, E])]. B is (1x1), C is (4x1), E is (1x1)
*
* From fixture 1:
* B survives with same layout
* C survives with new layout
* D is removed
* E joins with first layout
*/
- (ASTLayoutFixture *)createFixture2
{
auto fixture = [[ASTLayoutFixture alloc] init];
// nodeB
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB];
auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil];
// nodeC
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC];
auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{4,1} position:{3,0} sublayouts:nil];
// nodeE
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE];
auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil];
[fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA];
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutE ]];
fixture.layout = layoutA;
ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeE ]];
};
[fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA];
return fixture;
}
/**
* Fixture 3: Multipass stack layout
*
* [A: HorizStack([B, C, D])]. B is (7x1), C is (2x1), D is (1x1)
*
* nodeB (which has flexShrink=1) will return 8x1 for its size during the first
* stack pass, and it'll be subject to a second pass where it returns 7x1.
*
*/
- (ASTLayoutFixture *)createFixture3
{
auto fixture = [[ASTLayoutFixture alloc] init];
// nodeB wants 8,1 but it will settle for 7,1
[fixture setReturnedSize:{8,1} forNode:nodeB];
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB];
[fixture addSizeRange:{{7, 0}, {7, 1}} forNode:nodeB];
auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{7,1} position:{0,0} sublayouts:nil];
// nodeC
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC];
auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{7,0} sublayouts:nil];
// nodeD
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD];
auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil];
[fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA];
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]];
fixture.layout = layoutA;
[fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA];
return fixture;
}
/**
* Fixture 4: A different simple transition away from fixture 1.
*
* [A: HorizStack([B, D, E])]. B is (1x1), D is (2x1), E is (1x1)
*
* From fixture 1:
* B survives with same layout
* C is removed
* D survives with new layout
* E joins with first layout
*
* From fixture 2:
* B survives with same layout
* C is removed
* D joins with first layout
* E survives with same layout
*/
- (ASTLayoutFixture *)createFixture4
{
auto fixture = [[ASTLayoutFixture alloc] init];
// nodeB
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB];
auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil];
// nodeD
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD];
auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{2,1} position:{4,0} sublayouts:nil];
// nodeE
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeE];
auto layoutE = [ASLayout layoutWithLayoutElement:nodeE size:{1,1} position:{9,0} sublayouts:nil];
[fixture addSizeRange:{{10, 1}, {10, 1}} forNode:nodeA];
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutD, layoutE ]];
fixture.layout = layoutA;
ASLayoutSpecBlock specBlockA = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeD, nodeE ]];
};
[fixture.layoutSpecBlocks setObject:specBlockA forKey:nodeA];
return fixture;
}
- (void)runFirstLayoutPassWithFixture:(ASTLayoutFixture *)fixture
{
[fixture apply];
for (ASLayoutTestNode *node in fixture.allNodes) {
[fixture withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread();
}];
if (!stubbedCalculatedLayoutDidChange) {
OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread();
}
}
// Trigger CA layout pass.
[window layoutIfNeeded];
// Make sure it went through.
[self verifyFixture:fixture];
}
@end

42
Tests/ASLayoutTestNode.h Normal file
View File

@ -0,0 +1,42 @@
//
// ASLayoutTestNode.h
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import <OCMock/OCMock.h>
@interface ASLayoutTestNode : ASDisplayNode
/**
* Mocking ASDisplayNodes directly isn't very safe because when you pump mock objects
* into the guts of the framework, bad things happen e.g. direct-ivar-access on mock
* objects will return garbage data.
*
* Instead we create a strict mock for each node, and forward a selected set of calls to it.
*/
@property (nonatomic, strong, readonly) id mock;
/**
* The size that this node will return in calculateLayoutThatFits (if it doesn't have a layoutSpecBlock).
*
* Changing this value will call -setNeedsLayout on the node.
*/
@property (nonatomic) CGSize testSize;
/**
* Generate a layout based on the frame of this node and its subtree.
*
* The root layout will be unpositioned. This is so that the returned layout can be directly
* compared to `calculatedLayout`
*/
- (ASLayout *)currentLayoutBasedOnFrames;
@end

92
Tests/ASLayoutTestNode.mm Normal file
View File

@ -0,0 +1,92 @@
//
// ASLayoutTestNode.mm
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASLayoutTestNode.h"
#import <OCMock/OCMock.h>
#import "OCMockObject+ASAdditions.h"
@implementation ASLayoutTestNode
- (instancetype)init
{
if (self = [super init]) {
_mock = OCMStrictClassMock([ASDisplayNode class]);
// If errors occur (e.g. unexpected method) we need to quickly figure out
// which node is at fault, so we inject the node name into the mock instance
// description.
__weak __typeof(self) weakSelf = self;
[_mock setModifyDescriptionBlock:^(id mock, NSString *baseDescription){
return [NSString stringWithFormat:@"Mock(%@)", weakSelf.description];
}];
}
return self;
}
- (ASLayout *)currentLayoutBasedOnFrames
{
return [self _currentLayoutBasedOnFramesForRootNode:YES];
}
- (ASLayout *)_currentLayoutBasedOnFramesForRootNode:(BOOL)isRootNode
{
auto sublayouts = [NSMutableArray<ASLayout *> array];
for (ASLayoutTestNode *subnode in self.subnodes) {
[sublayouts addObject:[subnode _currentLayoutBasedOnFramesForRootNode:NO]];
}
CGPoint rootPosition = isRootNode ? ASPointNull : self.frame.origin;
return [ASLayout layoutWithLayoutElement:self size:self.frame.size position:rootPosition sublayouts:sublayouts];
}
- (void)setTestSize:(CGSize)testSize
{
if (!CGSizeEqualToSize(testSize, _testSize)) {
_testSize = testSize;
[self setNeedsLayout];
}
}
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
[_mock calculateLayoutThatFits:constrainedSize];
// If we have a layout spec block, or no test size, return super.
if (self.layoutSpecBlock || CGSizeEqualToSize(self.testSize, CGSizeZero)) {
return [super calculateLayoutThatFits:constrainedSize];
} else {
// Interestingly, the infra will auto-clamp sizes from calculateSizeThatFits, but not from calculateLayoutThatFits.
auto size = ASSizeRangeClamp(constrainedSize, self.testSize);
return [ASLayout layoutWithLayoutElement:self size:size];
}
}
#pragma mark - Forwarding to mock
- (void)calculatedLayoutDidChange
{
[_mock calculatedLayoutDidChange];
[super calculatedLayoutDidChange];
}
- (void)didCompleteLayoutTransition:(id<ASContextTransitioning>)context
{
[_mock didCompleteLayoutTransition:context];
[super didCompleteLayoutTransition:context];
}
- (void)animateLayoutTransition:(id<ASContextTransitioning>)context
{
[_mock animateLayoutTransition:context];
[super animateLayoutTransition:context];
}
@end

61
Tests/ASTLayoutFixture.h Normal file
View File

@ -0,0 +1,61 @@
//
// ASTLayoutFixture.h
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import "ASTestCase.h"
#import "ASLayoutTestNode.h"
NS_ASSUME_NONNULL_BEGIN
AS_SUBCLASSING_RESTRICTED
@interface ASTLayoutFixture : NSObject
/// The correct layout. The root should be unpositioned (same as -calculatedLayout).
@property (nonatomic, strong, nullable) ASLayout *layout;
/// The layoutSpecBlocks for non-leaf nodes.
@property (nonatomic, strong, readonly) NSMapTable<ASDisplayNode *, ASLayoutSpecBlock> *layoutSpecBlocks;
@property (nonatomic, strong, readonly) ASLayoutTestNode *rootNode;
@property (nonatomic, strong, readonly) NSSet<ASLayoutTestNode *> *allNodes;
/// Get the (correct) layout for the specified node.
- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node;
/// Add this to the list of expected size ranges for the given node.
- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node;
/// If you have a node that wants a size different than it gets, set it here.
/// For any leaf nodes that you don't call this on, the node will return the correct size
/// based on the fixture's layout. This is useful for triggering multipass stack layout.
- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node;
/// Get the first expected size range for the node.
- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node;
/// Enumerate all the size ranges for the node.
- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange sizeRange))block;
/// Configure the nodes for this fixture. Set testSize on leaf nodes, layoutSpecBlock on container nodes.
- (void)apply;
@end
@interface ASLayout (TestHelpers)
@property (nonatomic, readonly) NSArray<ASDisplayNode *> *allNodes;
@end
NS_ASSUME_NONNULL_END

134
Tests/ASTLayoutFixture.mm Normal file
View File

@ -0,0 +1,134 @@
//
// ASTLayoutFixture.mm
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASTLayoutFixture.h"
@interface ASTLayoutFixture ()
/// The size ranges against which nodes are expected to be measured.
@property (nonatomic, strong, readonly) NSMapTable<ASDisplayNode *, NSMutableArray<NSValue *> *> *sizeRanges;
/// The overridden returned sizes for nodes where you want to trigger multipass layout.
@property (nonatomic, strong, readonly) NSMapTable<ASDisplayNode *, NSValue *> *returnedSizes;
@end
@implementation ASTLayoutFixture
- (instancetype)init
{
if (self = [super init]) {
_sizeRanges = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory];
_layoutSpecBlocks = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory];
_returnedSizes = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory];
}
return self;
}
- (void)addSizeRange:(ASSizeRange)sizeRange forNode:(ASLayoutTestNode *)node
{
auto ranges = [_sizeRanges objectForKey:node];
if (ranges == nil) {
ranges = [NSMutableArray array];
[_sizeRanges setObject:ranges forKey:node];
}
[ranges addObject:[NSValue valueWithBytes:&sizeRange objCType:@encode(ASSizeRange)]];
}
- (void)setReturnedSize:(CGSize)size forNode:(ASLayoutTestNode *)node
{
[_returnedSizes setObject:[NSValue valueWithCGSize:size] forKey:node];
}
- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node
{
auto val = [_sizeRanges objectForKey:node].firstObject;
ASSizeRange r;
[val getValue:&r];
return r;
}
- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange))block
{
for (NSValue *value in [_sizeRanges objectForKey:node]) {
ASSizeRange r;
[value getValue:&r];
block(r);
}
}
- (ASLayout *)layoutForNode:(ASLayoutTestNode *)node
{
NSMutableArray *allLayouts = [NSMutableArray array];
[ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts];
for (ASLayout *layout in allLayouts) {
if (layout.layoutElement == node) {
return layout;
}
}
return nil;
}
/// A very dumb tree iteration approach. NSEnumerator or something would be way better.
+ (void)collectAllLayoutsFromLayout:(ASLayout *)layout array:(NSMutableArray<ASLayout *> *)array
{
[array addObject:layout];
for (ASLayout *sublayout in layout.sublayouts) {
[self collectAllLayoutsFromLayout:sublayout array:array];
}
}
- (ASLayoutTestNode *)rootNode
{
return (ASLayoutTestNode *)self.layout.layoutElement;
}
- (NSSet<ASLayoutTestNode *> *)allNodes
{
auto allLayouts = [NSMutableArray array];
[ASTLayoutFixture collectAllLayoutsFromLayout:self.layout array:allLayouts];
return [NSSet setWithArray:[allLayouts valueForKey:@"layoutElement"]];
}
- (void)apply
{
// Update layoutSpecBlock for parent nodes, set automatic subnode management
for (ASDisplayNode *node in _layoutSpecBlocks) {
auto block = [_layoutSpecBlocks objectForKey:node];
if (node.layoutSpecBlock != block) {
node.automaticallyManagesSubnodes = YES;
node.layoutSpecBlock = block;
[node setNeedsLayout];
}
}
[self setTestSizesOfLeafNodesInLayout:self.layout];
}
/// Go through the given layout, and for all the leaf nodes, set their preferredSize
/// to the layout size if needed, then call -setNeedsLayout
- (void)setTestSizesOfLeafNodesInLayout:(ASLayout *)layout
{
auto node = (ASLayoutTestNode *)layout.layoutElement;
if (layout.sublayouts.count == 0) {
auto override = [self.returnedSizes objectForKey:node];
node.testSize = override ? override.CGSizeValue : layout.size;
} else {
node.testSize = CGSizeZero;
for (ASLayout *sublayout in layout.sublayouts) {
[self setTestSizesOfLeafNodesInLayout:sublayout];
}
}
}
@end

View File

@ -12,6 +12,11 @@
#import <XCTest/XCTest.h>
// Not strictly necessary, but convenient
#import <OCMock/OCMock.h>
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import "OCMockObject+ASAdditions.h"
NS_ASSUME_NONNULL_BEGIN
@interface ASTestCase : XCTestCase

View File

@ -10,7 +10,7 @@
// http://www.apache.org/licenses/LICENSE-2.0
//
#import <OCMock/OCMockObject.h>
#import <OCMock/OCMock.h>
@interface OCMockObject (ASAdditions)
@ -30,4 +30,31 @@
*/
- (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... NS_REQUIRES_NIL_TERMINATION;
/// An optional block to modify description text. Only used in OCClassMockObject currently.
@property (atomic) NSString *(^modifyDescriptionBlock)(OCMockObject *object, NSString *baseDescription);
@end
/**
* Additional stub recorders useful in ASDK.
*/
@interface OCMStubRecorder (ASProperties)
/**
* Add a debug-break side effect to this stub/expectation.
*
* You will usually need to jump to frame 12 "fr s 12"
*/
#define andDebugBreak() _andDebugBreak()
@property (nonatomic, readonly) OCMStubRecorder *(^ _andDebugBreak)(void);
#define ignoringNonObjectArgs() _ignoringNonObjectArgs()
@property (nonatomic, readonly) OCMStubRecorder *(^ _ignoringNonObjectArgs)(void);
#define onMainThread() _onMainThread()
@property (nonatomic, readonly) OCMStubRecorder *(^ _onMainThread)(void);
#define offMainThread() _offMainThread()
@property (nonatomic, readonly) OCMStubRecorder *(^ _offMainThread)(void);
@end

View File

@ -15,6 +15,8 @@
#import <OCMock/OCMock.h>
#import <objc/runtime.h>
#import "ASTestCase.h"
#import <AsyncDisplayKit/ASAssert.h>
#import "debugbreak.h"
@interface ASTestCase (OCMockObjectRegistering)
@ -32,9 +34,18 @@
method_exchangeImplementations(orig, new);
// init <-> swizzled_init
Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init));
Method newInit = class_getInstanceMethod(self, @selector(swizzled_init));
method_exchangeImplementations(origInit, newInit);
{
Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init));
Method newInit = class_getInstanceMethod(self, @selector(swizzled_init));
method_exchangeImplementations(origInit, newInit);
}
// (class mock) description <-> swizzled_classMockDescription
{
Method orig = class_getInstanceMethod(OCMockObject.classMockObjectClass, @selector(description));
Method new = class_getInstanceMethod(self, @selector(swizzled_classMockDescription));
method_exchangeImplementations(orig, new);
}
}
/// Since OCProtocolMockObject is private, use this method to get the class.
@ -49,6 +60,18 @@
return c;
}
/// Since OCClassMockObject is private, use this method to get the class.
+ (Class)classMockObjectClass
{
static Class c;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
c = NSClassFromString(@"OCClassMockObject");
NSAssert(c != Nil, nil);
});
return c;
}
/// Whether the user has opted-in to specify which optional methods are implemented for this object.
- (BOOL)hasSpecifiedOptionalProtocolMethods
{
@ -142,4 +165,77 @@
return self;
}
- (NSString *)swizzled_classMockDescription
{
NSString *orig = [self swizzled_classMockDescription];
__auto_type block = self.modifyDescriptionBlock;
if (block) {
return block(self, orig);
}
return orig;
}
- (void)setModifyDescriptionBlock:(NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock
{
objc_setAssociatedObject(self, @selector(modifyDescriptionBlock), modifyDescriptionBlock, OBJC_ASSOCIATION_COPY);
}
- (NSString *(^)(OCMockObject *, NSString *))modifyDescriptionBlock
{
return objc_getAssociatedObject(self, _cmd);
}
@end
@implementation OCMStubRecorder (ASProperties)
@dynamic _ignoringNonObjectArgs;
- (OCMStubRecorder *(^)(void))_ignoringNonObjectArgs
{
id (^theBlock)(void) = ^ ()
{
return [self ignoringNonObjectArgs];
};
return theBlock;
}
@dynamic _onMainThread;
- (OCMStubRecorder *(^)(void))_onMainThread
{
id (^theBlock)(void) = ^ ()
{
return [self andDo:^(NSInvocation *invocation) {
ASDisplayNodeAssertMainThread();
}];
};
return theBlock;
}
@dynamic _offMainThread;
- (OCMStubRecorder *(^)(void))_offMainThread
{
id (^theBlock)(void) = ^ ()
{
return [self andDo:^(NSInvocation *invocation) {
ASDisplayNodeAssertNotMainThread();
}];
};
return theBlock;
}
@dynamic _andDebugBreak;
- (OCMStubRecorder *(^)(void))_andDebugBreak
{
id (^theBlock)(void) = ^ ()
{
return [self andDo:^(NSInvocation *invocation) {
debug_break();
}];
};
return theBlock;
}
@end

146
Tests/Common/debugbreak.h Normal file
View File

@ -0,0 +1,146 @@
//
// debugbreak.h
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
/* Copyright (c) 2011-2015, Scott Tsai
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef DEBUG_BREAK_H
#define DEBUG_BREAK_H
#ifdef _MSC_VER
#define debug_break __debugbreak
#else
#include <signal.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
/* gcc optimizers consider code after __builtin_trap() dead.
* Making __builtin_trap() unsuitable for breaking into the debugger */
DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP = 0,
};
#if defined(__i386__) || defined(__x86_64__)
enum { HAVE_TRAP_INSTRUCTION = 1, };
__attribute__((gnu_inline, always_inline))
__inline__ static void trap_instruction(void)
{
__asm__ volatile("int $0x03");
}
#elif defined(__thumb__)
enum { HAVE_TRAP_INSTRUCTION = 1, };
/* FIXME: handle __THUMB_INTERWORK__ */
__attribute__((gnu_inline, always_inline))
__inline__ static void trap_instruction(void)
{
/* See 'arm-linux-tdep.c' in GDB source.
* Both instruction sequences below work. */
#if 1
/* 'eabi_linux_thumb_le_breakpoint' */
__asm__ volatile(".inst 0xde01");
#else
/* 'eabi_linux_thumb2_le_breakpoint' */
__asm__ volatile(".inst.w 0xf7f0a000");
#endif
/* Known problem:
* After a breakpoint hit, can't stepi, step, or continue in GDB.
* 'step' stuck on the same instruction.
*
* Workaround: a new GDB command,
* 'debugbreak-step' is defined in debugbreak-gdb.py
* that does:
* (gdb) set $instruction_len = 2
* (gdb) tbreak *($pc + $instruction_len)
* (gdb) jump *($pc + $instruction_len)
*/
}
#elif defined(__arm__) && !defined(__thumb__)
enum { HAVE_TRAP_INSTRUCTION = 1, };
__attribute__((gnu_inline, always_inline))
__inline__ static void trap_instruction(void)
{
/* See 'arm-linux-tdep.c' in GDB source,
* 'eabi_linux_arm_le_breakpoint' */
__asm__ volatile(".inst 0xe7f001f0");
/* Has same known problem and workaround
* as Thumb mode */
}
#elif defined(__aarch64__)
enum { HAVE_TRAP_INSTRUCTION = 1, };
__attribute__((gnu_inline, always_inline))
__inline__ static void trap_instruction(void)
{
/* See 'aarch64-tdep.c' in GDB source,
* 'aarch64_default_breakpoint' */
__asm__ volatile(".inst 0xd4200000");
}
#else
enum { HAVE_TRAP_INSTRUCTION = 0, };
#endif
__attribute__((gnu_inline, always_inline))
__inline__ static void debug_break(void)
{
if (HAVE_TRAP_INSTRUCTION) {
trap_instruction();
} else if (DEBUG_BREAK_PREFER_BUILTIN_TRAP_TO_SIGTRAP) {
/* raises SIGILL on Linux x86{,-64}, to continue in gdb:
* (gdb) handle SIGILL stop nopass
* */
__builtin_trap();
} else {
#ifdef _WIN32
/* SIGTRAP available only on POSIX-compliant operating systems
* use builtin trap instead */
__builtin_trap();
#else
raise(SIGTRAP);
#endif
}
}
#ifdef __cplusplus
}
#endif
#endif
#endif