{"action":"created","comment":{"url":"https://api.github.com/repos/TextureGroup/Texture/pulls/comments/126174782","pull_request_review_id":48636886,"id":126174782,"diff_hunk":"@@ -0,0 +1,521 @@\n+//\n+//  ASLayoutEngineTests.mm\n+//  Texture\n+//\n+//  Copyright (c) 2017-present, Pinterest, Inc.  All rights reserved.\n+//  Licensed under the Apache License, Version 2.0 (the \"License\");\n+//  you may not use this file except in compliance with the License.\n+//  You may obtain a copy of the License at\n+//\n+//      http://www.apache.org/licenses/LICENSE-2.0\n+//\n+\n+#import \"ASTestCase.h\"\n+#import \"ASLayoutTestNode.h\"\n+#import \"ASXCTExtensions.h\"\n+#import \"ASTLayoutFixture.h\"\n+\n+@interface ASLayoutEngineTests : ASTestCase\n+\n+@end\n+\n+@implementation ASLayoutEngineTests {\n+  ASLayoutTestNode *nodeA;\n+  ASLayoutTestNode *nodeB;\n+  ASLayoutTestNode *nodeC;\n+  ASLayoutTestNode *nodeD;\n+  ASLayoutTestNode *nodeE;\n+  ASTLayoutFixture *fixture1;\n+  ASTLayoutFixture *fixture2;\n+  ASTLayoutFixture *fixture3;\n+  ASTLayoutFixture *fixture4;\n+\n+  // fixtures 1 and 3 share the same exact node A layout spec block.\n+  // we don't want the infra to call -setNeedsLayout when we switch fixtures\n+  // so we need to use the same exact block.\n+  ASLayoutSpecBlock fixture1and3NodeALayoutSpecBlock;\n+\n+  UIWindow *window;\n+  UIViewController *vc;\n+  NSArray<ASLayoutTestNode *> *allNodes;\n+  NSTimeInterval verifyDelay;\n+  // See -stubCalculatedLayoutDidChange.\n+  BOOL stubbedCalculatedLayoutDidChange;\n+}\n+\n+- (void)setUp\n+{\n+  [super setUp];\n+  verifyDelay = 3;\n+  window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 10, 1)];\n+  vc = [[UIViewController alloc] init];\n+  nodeA = [ASLayoutTestNode new];\n+  nodeA.backgroundColor = [UIColor redColor];\n+\n+  // NOTE: nodeB has flexShrink, the others don't\n+  nodeB = [ASLayoutTestNode new];\n+  nodeB.style.flexShrink = 1;\n+  nodeB.backgroundColor = [UIColor orangeColor];\n+\n+  nodeC = [ASLayoutTestNode new];\n+  nodeC.backgroundColor = [UIColor yellowColor];\n+  nodeD = [ASLayoutTestNode new];\n+  nodeD.backgroundColor = [UIColor greenColor];\n+  nodeE = [ASLayoutTestNode new];\n+  nodeE.backgroundColor = [UIColor blueColor];\n+  allNodes = @[ nodeA, nodeB, nodeC, nodeD, nodeE ];\n+  ASSetDebugNames(nodeA, nodeB, nodeC, nodeD, nodeE);\n+  ASLayoutSpecBlock b = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {\n+    return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeD ]];\n+  };\n+  fixture1and3NodeALayoutSpecBlock = b;\n+  fixture1 = [self createFixture1];\n+  fixture2 = [self createFixture2];\n+  fixture3 = [self createFixture3];\n+  fixture4 = [self createFixture4];\n+\n+  nodeA.frame = vc.view.bounds;\n+  nodeA.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;\n+  [vc.view addSubnode:nodeA];\n+\n+  window.rootViewController = vc;\n+  [window makeKeyAndVisible];\n+}\n+\n+- (void)tearDown\n+{\n+  nodeA.layoutSpecBlock = nil;\n+  for (ASLayoutTestNode *node in allNodes) {\n+    OCMVerifyAllWithDelay(node.mock, verifyDelay);\n+  }\n+  [super tearDown];\n+}\n+\n+- (void)testFirstLayoutPassWhenInWindow\n+{\n+  [self runFirstLayoutPassWithFixture:fixture1];\n+}\n+\n+- (void)testSetNeedsLayoutAndNormalLayoutPass\n+{\n+  [self runFirstLayoutPassWithFixture:fixture1];\n+\n+  [fixture2 apply];\n+\n+  // skip nodeB because its layout doesn't change.\n+  for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) {\n+    [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {\n+      OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread();\n+    }];\n+    OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread();\n+  }\n+\n+  [window layoutIfNeeded];\n+  [self verifyFixture:fixture2];\n+}\n+\n+/**\n+ * Transition from fixture1 to Fixture2 on node A.\n+ *\n+ * Expect A and D to calculate once off main, and\n+ * to receive calculatedLayoutDidChange on main,\n+ * then to get the measurement completion call on main,\n+ * then to get animateLayoutTransition: and didCompleteLayoutTransition: on main.\n+ */\n+- (void)testLayoutTransitionWithAsyncMeasurement\n+{\n+  // BUG: There is an extra main-thread layout pass run on nodeC, and its result is discarded.\n+  static BOOL enforceCorrectBehavior = NO;\n+\n+  [self stubCalculatedLayoutDidChange];\n+  [self runFirstLayoutPassWithFixture:fixture1];\n+\n+  [fixture2 apply];\n+\n+  // Expect A, C, E to calculate new layouts off-main\n+  // dispatch_once onto main to run our injectedMainThread work while the transition calculates.\n+  __block dispatch_block_t injectedMainThreadWork = nil;\n+  for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) {\n+    [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {\n+      OCMExpect([node.mock calculateLayoutThatFits:sizeRange])\n+      .offMainThread()\n+      .andDo(^(NSInvocation *inv) {\n+        // On first calculateLayoutThatFits, schedule our injected main thread work.\n+        static dispatch_once_t onceToken;\n+        dispatch_once(&onceToken, ^{\n+          dispatch_async(dispatch_get_main_queue(), ^{\n+            injectedMainThreadWork();\n+          });\n+        });\n+      });\n+    }];\n+  }\n+\n+  // The code in this section is designed to move in time order, all on the main thread:\n+\n+  OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread();\n+  OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread();\n+\n+  // Trigger the layout transition.\n+  __block dispatch_block_t measurementCompletionBlock = nil;\n+  [nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:YES measurementCompletion:^{\n+    measurementCompletionBlock();\n+  }];\n+\n+  // This block will get run after bg layout calculate starts, but before measurementCompletion\n+  __block BOOL injectedMainThreadWorkDone = NO;\n+  injectedMainThreadWork = ^{\n+    injectedMainThreadWorkDone = YES;\n+\n+    // BUG: nodeC survives but gets a layout update during transition.\n+    // When this layout pass comes down, its parent will set its new frame which will\n+    // trigger the layout pass to go down into it and we will measure against that new bounds.\n+    // The new bounds will be the post-transition size.\n+    if (!enforceCorrectBehavior) {\n+      auto postTransitionSize = [fixture2 layoutForNode:nodeC].size;\n+      OCMExpect([nodeC.mock calculateLayoutThatFits:ASSizeRangeMake(postTransitionSize)]).onMainThread();\n+    }\n+\n+    [window layoutIfNeeded];\n+\n+    // Ensure we're still on the old layout. We should stay on this until the transition completes.\n+    [self verifyFixture:fixture1];\n+  };\n+\n+  measurementCompletionBlock = ^{\n+    XCTAssert(injectedMainThreadWorkDone, @\"We hoped to get onto the main thread before the measurementCompletion callback ran.\");\n+  };\n+\n+  for (ASLayoutTestNode *node in allNodes) {\n+    OCMVerifyAllWithDelay(node.mock, verifyDelay);\n+  }\n+\n+  [self verifyFixture:fixture2];\n+}\n+\n+/**\n+ * Start at fixture 1.\n+ * Trigger an async transition to fixture 2.\n+ * While it's measuring, on main switch to fixture 4 (setNeedsLayout A, D) and run a CA layout pass.\n+ *\n+ * Correct behavior, we end up at fixture 4 since it's newer.\n+ * Current incorrect behavior, we end up at fixture 2 and we remeasure surviving node C.\n+ */\n+- (void)testASetNeedsLayoutInterferingWithTheCurrentTransition\n+{\n+  static BOOL enforceCorrectBehavior = NO;\n+\n+  [self stubCalculatedLayoutDidChange];\n+  [self runFirstLayoutPassWithFixture:fixture1];\n+\n+  [fixture2 apply];\n+\n+  // Expect A, C, E to calculate new layouts off-main\n+  // dispatch_once onto main to run our injectedMainThread work while the transition calculates.\n+  __block dispatch_block_t injectedMainThreadWork = nil;\n+  for (ASLayoutTestNode *node in @[ nodeA, nodeC, nodeE ]) {\n+    [fixture2 withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {\n+      OCMExpect([node.mock calculateLayoutThatFits:sizeRange])\n+      .offMainThread()\n+      .andDo(^(NSInvocation *inv) {\n+        // On first calculateLayoutThatFits, schedule our injected main thread work.\n+        static dispatch_once_t onceToken;\n+        dispatch_once(&onceToken, ^{\n+          dispatch_async(dispatch_get_main_queue(), ^{\n+            injectedMainThreadWork();\n+          });\n+        });\n+      });\n+    }];\n+  }\n+\n+  // The code in this section is designed to move in time order, all on the main thread:\n+\n+  OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread();\n+  OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread();","path":"Tests/ASLayoutEngineTests.mm","position":157,"original_position":235,"commit_id":"73aa7c99ef85a5d0bf1c29a98a95a961ec2a7755","original_commit_id":"73aa7c99ef85a5d0bf1c29a98a95a961ec2a7755","user":{"login":"nguyenhuy","id":587874,"avatar_url":"https://avatars2.githubusercontent.com/u/587874?v=3","gravatar_id":"","url":"https://api.github.com/users/nguyenhuy","html_url":"https://github.com/nguyenhuy","followers_url":"https://api.github.com/users/nguyenhuy/followers","following_url":"https://api.github.com/users/nguyenhuy/following{/other_user}","gists_url":"https://api.github.com/users/nguyenhuy/gists{/gist_id}","starred_url":"https://api.github.com/users/nguyenhuy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nguyenhuy/subscriptions","organizations_url":"https://api.github.com/users/nguyenhuy/orgs","repos_url":"https://api.github.com/users/nguyenhuy/repos","events_url":"https://api.github.com/users/nguyenhuy/events{/privacy}","received_events_url":"https://api.github.com/users/nguyenhuy/received_events","type":"User","site_admin":false},"body":"These method calls are only expected when the correct behavior is **not** enforced.  The correct behavior is that the layout transition will be cancelled and this it won't animate and complete.","created_at":"2017-07-07T15:25:52Z","updated_at":"2017-07-07T15:56:10Z","html_url":"https://github.com/TextureGroup/Texture/pull/424#discussion_r126174782","pull_request_url":"https://api.github.com/repos/TextureGroup/Texture/pulls/424","_links":{"self":{"href":"https://api.github.com/repos/TextureGroup/Texture/pulls/comments/126174782"},"html":{"href":"https://github.com/TextureGroup/Texture/pull/424#discussion_r126174782"},"pull_request":{"href":"https://api.github.com/repos/TextureGroup/Texture/pulls/424"}}},"pull_request":{"url":"https://api.github.com/repos/TextureGroup/Texture/pulls/424","id":129310304,"html_url":"https://github.com/TextureGroup/Texture/pull/424","diff_url":"https://github.com/TextureGroup/Texture/pull/424.diff","patch_url":"https://github.com/TextureGroup/Texture/pull/424.patch","issue_url":"https://api.github.com/repos/TextureGroup/Texture/issues/424","number":424,"state":"open","locked":false,"title":"Add unit tests for the layout engine #trivial","user":{"login":"Adlai-Holler","id":2466893,"avatar_url":"https://avatars1.githubusercontent.com/u/2466893?v=3","gravatar_id":"","url":"https://api.github.com/users/Adlai-Holler","html_url":"https://github.com/Adlai-Holler","followers_url":"https://api.github.com/users/Adlai-Holler/followers","following_url":"https://api.github.com/users/Adlai-Holler/following{/other_user}","gists_url":"https://api.github.com/users/Adlai-Holler/gists{/gist_id}","starred_url":"https://api.github.com/users/Adlai-Holler/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Adlai-Holler/subscriptions","organizations_url":"https://api.github.com/users/Adlai-Holler/orgs","repos_url":"https://api.github.com/users/Adlai-Holler/repos","events_url":"https://api.github.com/users/Adlai-Holler/events{/privacy}","received_events_url":"https://api.github.com/users/Adlai-Holler/received_events","type":"User","site_admin":false},"body":"This adds a testing platform and some unit tests to the layout engine. Right now there are flags to toggle between the current behavior and the desired behavior, and they're set to \"current behavior.\"\r\n\r\nIssues reproduced (need to merge with GitHub issues):\r\n- Extraneous `calculatedLayoutDidChange` calls. Not a biggie.\r\n- If a layout pass comes in mid-transition, we will calculateLayout: for some nodes against their current bounds on main, but discard the result. https://github.com/TextureGroup/Texture/pull/424/files#diff-b1441b7e61c34b6f132da6c94bb10c0cR125\r\n- If we do `setNeedsLayout` and get a layout pass while a transition is measuring, the transition's \"to-layout\" will win, but the setNeedsLayout+layoutPass should actually win since its data is newer. https://github.com/TextureGroup/Texture/pull/424/files#diff-b1441b7e61c34b6f132da6c94bb10c0cR204\r\n- If we setNeedsLayout on a leaf node that is subject to multipass measurement (e.g. flexShrink) we will not update the layout for the leaf or the stack. We will remeasure it only against the fixed (second-pass) constrained size, and inevitably get a result confirming that the current size is correct, but we should have offered the node another unconstrained measurement pass. In my estimation this is the most serious issue. https://github.com/TextureGroup/Texture/pull/424/files#diff-b1441b7e61c34b6f132da6c94bb10c0cR284\r\n\r\nI'd like to reproduce the orphan-node issue but I'm not sure what conditions would trigger it @nguyenhuy. We've got enough flexibility in the testing platform – I believe – to test an incredible variety of different scenarios.\r\n\r\nPROTIP: You can take any expectation and add `.andDebugBreak()` to get a breakpoint when that call happens. So if you expect an async layout: `OCMExpect([nodeA.mock calculateLayoutThatFits:]).offMainThread()` you can change that to `OCMExpect([nodeA.mock calculateLayoutThatFits:]).offMainThread().andDebugBreak()` and when that calculateLayoutThatFits invocation happens, the debugger will pause for you. ✨Magic✨","created_at":"2017-07-07T00:56:43Z","updated_at":"2017-07-07T15:56:10Z","closed_at":null,"merged_at":null,"merge_commit_sha":"756ce9ae6ba15a6f27d01caf1d57c72177ab42f2","assignee":null,"assignees":[],"requested_reviewers":[{"login":"maicki","id":25288,"avatar_url":"https://avatars2.githubusercontent.com/u/25288?v=3","gravatar_id":"","url":"https://api.github.com/users/maicki","html_url":"https://github.com/maicki","followers_url":"https://api.github.com/users/maicki/followers","following_url":"https://api.github.com/users/maicki/following{/other_user}","gists_url":"https://api.github.com/users/maicki/gists{/gist_id}","starred_url":"https://api.github.com/users/maicki/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maicki/subscriptions","organizations_url":"https://api.github.com/users/maicki/orgs","repos_url":"https://api.github.com/users/maicki/repos","events_url":"https://api.github.com/users/maicki/events{/privacy}","received_events_url":"https://api.github.com/users/maicki/received_events","type":"User","site_admin":false},{"login":"garrettmoon","id":90045,"avatar_url":"https://avatars0.githubusercontent.com/u/90045?v=3","gravatar_id":"","url":"https://api.github.com/users/garrettmoon","html_url":"https://github.com/garrettmoon","followers_url":"https://api.github.com/users/garrettmoon/followers","following_url":"https://api.github.com/users/garrettmoon/following{/other_user}","gists_url":"https://api.github.com/users/garrettmoon/gists{/gist_id}","starred_url":"https://api.github.com/users/garrettmoon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/garrettmoon/subscriptions","organizations_url":"https://api.github.com/users/garrettmoon/orgs","repos_url":"https://api.github.com/users/garrettmoon/repos","events_url":"https://api.github.com/users/garrettmoon/events{/privacy}","received_events_url":"https://api.github.com/users/garrettmoon/received_events","type":"User","site_admin":false},{"login":"appleguy","id":565251,"avatar_url":"https://avatars0.githubusercontent.com/u/565251?v=3","gravatar_id":"","url":"https://api.github.com/users/appleguy","html_url":"https://github.com/appleguy","followers_url":"https://api.github.com/users/appleguy/followers","following_url":"https://api.github.com/users/appleguy/following{/other_user}","gists_url":"https://api.github.com/users/appleguy/gists{/gist_id}","starred_url":"https://api.github.com/users/appleguy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/appleguy/subscriptions","organizations_url":"https://api.github.com/users/appleguy/orgs","repos_url":"https://api.github.com/users/appleguy/repos","events_url":"https://api.github.com/users/appleguy/events{/privacy}","received_events_url":"https://api.github.com/users/appleguy/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/TextureGroup/Texture/pulls/424/commits","review_comments_url":"https://api.github.com/repos/TextureGroup/Texture/pulls/424/comments","review_comment_url":"https://api.github.com/repos/TextureGroup/Texture/pulls/comments{/number}","comments_url":"https://api.github.com/repos/TextureGroup/Texture/issues/424/comments","statuses_url":"https://api.github.com/repos/TextureGroup/Texture/statuses/73aa7c99ef85a5d0bf1c29a98a95a961ec2a7755","head":{"label":"TextureGroup:AHLayoutEngineTests","ref":"AHLayoutEngineTests","sha":"73aa7c99ef85a5d0bf1c29a98a95a961ec2a7755","user":{"login":"TextureGroup","id":25676156,"avatar_url":"https://avatars0.githubusercontent.com/u/25676156?v=3","gravatar_id":"","url":"https://api.github.com/users/TextureGroup","html_url":"https://github.com/TextureGroup","followers_url":"https://api.github.com/users/TextureGroup/followers","following_url":"https://api.github.com/users/TextureGroup/following{/other_user}","gists_url":"https://api.github.com/users/TextureGroup/gists{/gist_id}","starred_url":"https://api.github.com/users/TextureGroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TextureGroup/subscriptions","organizations_url":"https://api.github.com/users/TextureGroup/orgs","repos_url":"https://api.github.com/users/TextureGroup/repos","events_url":"https://api.github.com/users/TextureGroup/events{/privacy}","received_events_url":"https://api.github.com/users/TextureGroup/received_events","type":"Organization","site_admin":false},"repo":{"id":81514066,"name":"Texture","full_name":"TextureGroup/Texture","owner":{"login":"TextureGroup","id":25676156,"avatar_url":"https://avatars0.githubusercontent.com/u/25676156?v=3","gravatar_id":"","url":"https://api.github.com/users/TextureGroup","html_url":"https://github.com/TextureGroup","followers_url":"https://api.github.com/users/TextureGroup/followers","following_url":"https://api.github.com/users/TextureGroup/following{/other_user}","gists_url":"https://api.github.com/users/TextureGroup/gists{/gist_id}","starred_url":"https://api.github.com/users/TextureGroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TextureGroup/subscriptions","organizations_url":"https://api.github.com/users/TextureGroup/orgs","repos_url":"https://api.github.com/users/TextureGroup/repos","events_url":"https://api.github.com/users/TextureGroup/events{/privacy}","received_events_url":"https://api.github.com/users/TextureGroup/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/TextureGroup/Texture","description":"Smooth asynchronous user interfaces for iOS apps.","fork":false,"url":"https://api.github.com/repos/TextureGroup/Texture","forks_url":"https://api.github.com/repos/TextureGroup/Texture/forks","keys_url":"https://api.github.com/repos/TextureGroup/Texture/keys{/key_id}","collaborators_url":"https://api.github.com/repos/TextureGroup/Texture/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/TextureGroup/Texture/teams","hooks_url":"https://api.github.com/repos/TextureGroup/Texture/hooks","issue_events_url":"https://api.github.com/repos/TextureGroup/Texture/issues/events{/number}","events_url":"https://api.github.com/repos/TextureGroup/Texture/events","assignees_url":"https://api.github.com/repos/TextureGroup/Texture/assignees{/user}","branches_url":"https://api.github.com/repos/TextureGroup/Texture/branches{/branch}","tags_url":"https://api.github.com/repos/TextureGroup/Texture/tags","blobs_url":"https://api.github.com/repos/TextureGroup/Texture/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/TextureGroup/Texture/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/TextureGroup/Texture/git/refs{/sha}","trees_url":"https://api.github.com/repos/TextureGroup/Texture/git/trees{/sha}","statuses_url":"https://api.github.com/repos/TextureGroup/Texture/statuses/{sha}","languages_url":"https://api.github.com/repos/TextureGroup/Texture/languages","stargazers_url":"https://api.github.com/repos/TextureGroup/Texture/stargazers","contributors_url":"https://api.github.com/repos/TextureGroup/Texture/contributors","subscribers_url":"https://api.github.com/repos/TextureGroup/Texture/subscribers","subscription_url":"https://api.github.com/repos/TextureGroup/Texture/subscription","commits_url":"https://api.github.com/repos/TextureGroup/Texture/commits{/sha}","git_commits_url":"https://api.github.com/repos/TextureGroup/Texture/git/commits{/sha}","comments_url":"https://api.github.com/repos/TextureGroup/Texture/comments{/number}","issue_comment_url":"https://api.github.com/repos/TextureGroup/Texture/issues/comments{/number}","contents_url":"https://api.github.com/repos/TextureGroup/Texture/contents/{+path}","compare_url":"https://api.github.com/repos/TextureGroup/Texture/compare/{base}...{head}","merges_url":"https://api.github.com/repos/TextureGroup/Texture/merges","archive_url":"https://api.github.com/repos/TextureGroup/Texture/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/TextureGroup/Texture/downloads","issues_url":"https://api.github.com/repos/TextureGroup/Texture/issues{/number}","pulls_url":"https://api.github.com/repos/TextureGroup/Texture/pulls{/number}","milestones_url":"https://api.github.com/repos/TextureGroup/Texture/milestones{/number}","notifications_url":"https://api.github.com/repos/TextureGroup/Texture/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/TextureGroup/Texture/labels{/name}","releases_url":"https://api.github.com/repos/TextureGroup/Texture/releases{/id}","deployments_url":"https://api.github.com/repos/TextureGroup/Texture/deployments","created_at":"2017-02-10T01:44:38Z","updated_at":"2017-07-07T14:37:43Z","pushed_at":"2017-07-07T01:07:36Z","git_url":"git://github.com/TextureGroup/Texture.git","ssh_url":"git@github.com:TextureGroup/Texture.git","clone_url":"https://github.com/TextureGroup/Texture.git","svn_url":"https://github.com/TextureGroup/Texture","homepage":"http://texturegroup.org","size":121532,"stargazers_count":1443,"watchers_count":1443,"language":"Objective-C","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":137,"mirror_url":null,"open_issues_count":176,"forks":137,"open_issues":176,"watchers":1443,"default_branch":"master"}},"base":{"label":"TextureGroup:master","ref":"master","sha":"e78d70eec4ac012f60793fb60658dcc395637441","user":{"login":"TextureGroup","id":25676156,"avatar_url":"https://avatars0.githubusercontent.com/u/25676156?v=3","gravatar_id":"","url":"https://api.github.com/users/TextureGroup","html_url":"https://github.com/TextureGroup","followers_url":"https://api.github.com/users/TextureGroup/followers","following_url":"https://api.github.com/users/TextureGroup/following{/other_user}","gists_url":"https://api.github.com/users/TextureGroup/gists{/gist_id}","starred_url":"https://api.github.com/users/TextureGroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TextureGroup/subscriptions","organizations_url":"https://api.github.com/users/TextureGroup/orgs","repos_url":"https://api.github.com/users/TextureGroup/repos","events_url":"https://api.github.com/users/TextureGroup/events{/privacy}","received_events_url":"https://api.github.com/users/TextureGroup/received_events","type":"Organization","site_admin":false},"repo":{"id":81514066,"name":"Texture","full_name":"TextureGroup/Texture","owner":{"login":"TextureGroup","id":25676156,"avatar_url":"https://avatars0.githubusercontent.com/u/25676156?v=3","gravatar_id":"","url":"https://api.github.com/users/TextureGroup","html_url":"https://github.com/TextureGroup","followers_url":"https://api.github.com/users/TextureGroup/followers","following_url":"https://api.github.com/users/TextureGroup/following{/other_user}","gists_url":"https://api.github.com/users/TextureGroup/gists{/gist_id}","starred_url":"https://api.github.com/users/TextureGroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TextureGroup/subscriptions","organizations_url":"https://api.github.com/users/TextureGroup/orgs","repos_url":"https://api.github.com/users/TextureGroup/repos","events_url":"https://api.github.com/users/TextureGroup/events{/privacy}","received_events_url":"https://api.github.com/users/TextureGroup/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/TextureGroup/Texture","description":"Smooth asynchronous user interfaces for iOS apps.","fork":false,"url":"https://api.github.com/repos/TextureGroup/Texture","forks_url":"https://api.github.com/repos/TextureGroup/Texture/forks","keys_url":"https://api.github.com/repos/TextureGroup/Texture/keys{/key_id}","collaborators_url":"https://api.github.com/repos/TextureGroup/Texture/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/TextureGroup/Texture/teams","hooks_url":"https://api.github.com/repos/TextureGroup/Texture/hooks","issue_events_url":"https://api.github.com/repos/TextureGroup/Texture/issues/events{/number}","events_url":"https://api.github.com/repos/TextureGroup/Texture/events","assignees_url":"https://api.github.com/repos/TextureGroup/Texture/assignees{/user}","branches_url":"https://api.github.com/repos/TextureGroup/Texture/branches{/branch}","tags_url":"https://api.github.com/repos/TextureGroup/Texture/tags","blobs_url":"https://api.github.com/repos/TextureGroup/Texture/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/TextureGroup/Texture/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/TextureGroup/Texture/git/refs{/sha}","trees_url":"https://api.github.com/repos/TextureGroup/Texture/git/trees{/sha}","statuses_url":"https://api.github.com/repos/TextureGroup/Texture/statuses/{sha}","languages_url":"https://api.github.com/repos/TextureGroup/Texture/languages","stargazers_url":"https://api.github.com/repos/TextureGroup/Texture/stargazers","contributors_url":"https://api.github.com/repos/TextureGroup/Texture/contributors","subscribers_url":"https://api.github.com/repos/TextureGroup/Texture/subscribers","subscription_url":"https://api.github.com/repos/TextureGroup/Texture/subscription","commits_url":"https://api.github.com/repos/TextureGroup/Texture/commits{/sha}","git_commits_url":"https://api.github.com/repos/TextureGroup/Texture/git/commits{/sha}","comments_url":"https://api.github.com/repos/TextureGroup/Texture/comments{/number}","issue_comment_url":"https://api.github.com/repos/TextureGroup/Texture/issues/comments{/number}","contents_url":"https://api.github.com/repos/TextureGroup/Texture/contents/{+path}","compare_url":"https://api.github.com/repos/TextureGroup/Texture/compare/{base}...{head}","merges_url":"https://api.github.com/repos/TextureGroup/Texture/merges","archive_url":"https://api.github.com/repos/TextureGroup/Texture/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/TextureGroup/Texture/downloads","issues_url":"https://api.github.com/repos/TextureGroup/Texture/issues{/number}","pulls_url":"https://api.github.com/repos/TextureGroup/Texture/pulls{/number}","milestones_url":"https://api.github.com/repos/TextureGroup/Texture/milestones{/number}","notifications_url":"https://api.github.com/repos/TextureGroup/Texture/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/TextureGroup/Texture/labels{/name}","releases_url":"https://api.github.com/repos/TextureGroup/Texture/releases{/id}","deployments_url":"https://api.github.com/repos/TextureGroup/Texture/deployments","created_at":"2017-02-10T01:44:38Z","updated_at":"2017-07-07T14:37:43Z","pushed_at":"2017-07-07T01:07:36Z","git_url":"git://github.com/TextureGroup/Texture.git","ssh_url":"git@github.com:TextureGroup/Texture.git","clone_url":"https://github.com/TextureGroup/Texture.git","svn_url":"https://github.com/TextureGroup/Texture","homepage":"http://texturegroup.org","size":121532,"stargazers_count":1443,"watchers_count":1443,"language":"Objective-C","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":137,"mirror_url":null,"open_issues_count":176,"forks":137,"open_issues":176,"watchers":1443,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/TextureGroup/Texture/pulls/424"},"html":{"href":"https://github.com/TextureGroup/Texture/pull/424"},"issue":{"href":"https://api.github.com/repos/TextureGroup/Texture/issues/424"},"comments":{"href":"https://api.github.com/repos/TextureGroup/Texture/issues/424/comments"},"review_comments":{"href":"https://api.github.com/repos/TextureGroup/Texture/pulls/424/comments"},"review_comment":{"href":"https://api.github.com/repos/TextureGroup/Texture/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/TextureGroup/Texture/pulls/424/commits"},"statuses":{"href":"https://api.github.com/repos/TextureGroup/Texture/statuses/73aa7c99ef85a5d0bf1c29a98a95a961ec2a7755"}}}}
{"actor":{"display_login":"nguyenhuy"}}