From 2b0413224a75174436ff5d424c80bbb5c6eebe4a Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 16 Oct 2019 09:37:25 +1100 Subject: [PATCH] Fixes failing Feedly tests and improves debugging mock network responses. --- .../Account/Account.xcodeproj/project.pbxproj | 4 +++ .../AccountTests/AccountFeedSyncTest.swift | 12 ++++----- .../Feedly/AccountFeedlySyncTest.swift | 27 ++++++++++--------- .../Feedly/Initial/saved_initial.json | 1 + .../Account/AccountTests/TestTransport.swift | 12 +++++++-- 5 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 60b24c587..6c9139eec 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 9EE4CCFA234F106600FBAE4B /* FeedlyFeedContainerValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */; }; 9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */; }; 9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */; }; + 9EEEF75223567CA6009E9D80 /* saved_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EEEF75123567CA6009E9D80 /* saved_initial.json */; }; 9EF35F7A234E830E003AE2AE /* FeedlyCompoundOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */; }; /* End PBXBuildFile section */ @@ -301,6 +302,7 @@ 9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedContainerValidator.swift; sourceTree = ""; }; 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperation.swift; sourceTree = ""; }; 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperation.swift; sourceTree = ""; }; + 9EEEF75123567CA6009E9D80 /* saved_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = saved_initial.json; sourceTree = ""; }; 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCompoundOperation.swift; sourceTree = ""; }; D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = ""; }; D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = ""; }; @@ -523,6 +525,7 @@ 9E7F150B2341F2A700F860D1 /* Initial */ = { isa = PBXGroup; children = ( + 9EEEF75123567CA6009E9D80 /* saved_initial.json */, 9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */, 9E7F150C2341F32000F860D1 /* macintosh_initial.json */, 9E7F15162341F48900F860D1 /* mustread_initial.json */, @@ -780,6 +783,7 @@ 51D5875C227F630B00900287 /* tags_initial.json in Resources */, 51D5875A227F630B00900287 /* tags_delete.json in Resources */, 5165D71722821C2400D9D53D /* taggings_add.json in Resources */, + 9EEEF75223567CA6009E9D80 /* saved_initial.json in Resources */, 9E832B23234416B400D83249 /* feedly_collections_addfeed.json in Resources */, 5165D71622821C2400D9D53D /* taggings_delete.json in Resources */, 9E7F15112341F39A00F860D1 /* uncategorized_initial.json in Resources */, diff --git a/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift b/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift index 45131c3dd..ecb649750 100644 --- a/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift +++ b/Frameworks/Account/AccountTests/AccountFeedSyncTest.swift @@ -20,9 +20,9 @@ class AccountFeedSyncTest: XCTestCase { func testDownloadSync() { let testTransport = TestTransport() - testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_add.json" - testTransport.testFiles["https://api.feedbin.com/v2/subscriptions.json"] = "subscriptions_initial.json" - testTransport.testFiles["https://api.feedbin.com/v2/icons.json"] = "icons.json" + testTransport.testFiles["tags.json"] = "tags_add.json" + testTransport.testFiles["subscriptions.json"] = "subscriptions_initial.json" + testTransport.testFiles["icons.json"] = "icons.json" let account = TestAccountManager.shared.createAccount(type: .feedbin, transport: testTransport) // Test initial folders @@ -52,9 +52,9 @@ class AccountFeedSyncTest: XCTestCase { XCTAssertEqual(225, account.flattenedFeeds().count) let bPixels = account.idToFeedDictionary["1096623"] - XCTAssertEqual("Beautiful Pixels", bPixels!.name) - XCTAssertEqual("https://feedpress.me/beautifulpixels", bPixels!.url) - XCTAssertEqual("https://beautifulpixels.com/", bPixels!.homePageURL) + XCTAssertEqual("Beautiful Pixels", bPixels?.name) + XCTAssertEqual("https://feedpress.me/beautifulpixels", bPixels?.url) + XCTAssertEqual("https://beautifulpixels.com/", bPixels?.homePageURL) XCTAssertEqual("https://favicons.feedbinusercontent.com/ea0/ea010c658d6e356e49ab239b793dc415af707b05.png", bPixels?.faviconURL) TestAccountManager.shared.deleteAccount(account) diff --git a/Frameworks/Account/AccountTests/Feedly/AccountFeedlySyncTest.swift b/Frameworks/Account/AccountTests/Feedly/AccountFeedlySyncTest.swift index 4c9183237..bd92d4103 100644 --- a/Frameworks/Account/AccountTests/Feedly/AccountFeedlySyncTest.swift +++ b/Frameworks/Account/AccountTests/Feedly/AccountFeedlySyncTest.swift @@ -148,6 +148,8 @@ class AccountFeedlySyncTest: XCTestCase { account.refreshAll() { _ in preparationExpectation.fulfill() } + // If there's a failure here, then an operation hasn't completed. + // Check that test files have responses for all the requests this might make. waitForExpectations(timeout: 5) } @@ -282,18 +284,17 @@ class AccountFeedlySyncTest: XCTestCase { func set(testFiles: TestFiles, with transport: TestTransport) { // TestTransport blacklists certain query items to make mocking responses easier. - let endpoint = "https://sandbox7.feedly.com/v3" - let category = "\(endpoint)/streams/contents?streamId=user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category" - + let collectionsEndpoint = "/v3/collections" switch testFiles { case .initial: let dict = [ - "\(endpoint)/collections": "feedly_collections_initial.json", - "\(category)/5ca4d61d-e55d-4999-a8d1-c3b9d8789815": "macintosh_initial.json", - "\(category)/global.must": "mustread_initial.json", - "\(category)/885f2e01-d314-4e63-abac-17dcb063f5b5": "programming_initial.json", - "\(category)/66132046-6f14-488d-b590-8e93422723c8": "uncategorized_initial.json", - "\(category)/e31b3fcb-27f6-4f3e-b96c-53902586e366": "weblogs_initial.json", + "/global.saved": "saved_initial.json", + collectionsEndpoint: "feedly_collections_initial.json", + "/5ca4d61d-e55d-4999-a8d1-c3b9d8789815": "macintosh_initial.json", + "/global.must": "mustread_initial.json", + "/885f2e01-d314-4e63-abac-17dcb063f5b5": "programming_initial.json", + "/66132046-6f14-488d-b590-8e93422723c8": "uncategorized_initial.json", + "/e31b3fcb-27f6-4f3e-b96c-53902586e366": "weblogs_initial.json", ] transport.testFiles = dict @@ -301,16 +302,16 @@ class AccountFeedlySyncTest: XCTestCase { set(testFiles: .initial, with: transport) var dict = transport.testFiles - dict["\(endpoint)/collections"] = "feedly_collections_addcollection.json" - dict["\(category)/fc09f383-5a9a-4daa-a575-3efc1733b173"] = "newcollection_addcollection.json" + dict[collectionsEndpoint] = "feedly_collections_addcollection.json" + dict["/fc09f383-5a9a-4daa-a575-3efc1733b173"] = "newcollection_addcollection.json" transport.testFiles = dict case .addFeed: set(testFiles: .addCollection, with: transport) var dict = transport.testFiles - dict["\(endpoint)/collections"] = "feedly_collections_addfeed.json" - dict["\(category)/global.must"] = "mustread_addfeed.json" + dict[collectionsEndpoint] = "feedly_collections_addfeed.json" + dict["/global.must"] = "mustread_addfeed.json" transport.testFiles = dict case .removeFeed: diff --git a/Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json b/Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json new file mode 100644 index 000000000..497f76673 --- /dev/null +++ b/Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json @@ -0,0 +1 @@ +{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","items":[{"originId":"https://inessential.com/2019/10/14/netnewswire_os_compatibility_strategy","fingerprint":"fa6e6fff","id":"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16dccdfc972:a6e5:d4506071","summary":{"direction":"ltr","content":"

We have two goals with the app: 1) get as many people using RSS as possible, and 2) make the best app we can.

\n

To reach #2 — making the best app we can — we need to do a couple things. One is stay modern: use new APIs and tools that make the app better and easier to maintain. A second is to not spend time on things that don’t make the app better. A third is to attract and retain contributors, who are usually more psyched to work with modern stuff than with old stuff.

\n

You can see how that’s in a little bit of conflict with #1 (getting as many people as possible using RSS readers).

\n

Here’s the plan

\n

After a major OS update, we will switch to requiring that update on our next major release — where major is defined as something like 5.0 or 5.1, but not something like 5.0.1. (In other words: the upcoming NetNewsWire 5.0.3 release will run on Mojave, while NetNewsWire 5.1 will require Catalina.)

\n

At the same time, we will make older versions available via the website. For instance, the last version that will run on Mojave will likely be 5.0.4 (which isn’t finished yet) — and we’ll make that version available indefinitely for people who haven’t upgraded to Catalina.

\n

This will mean that people running older OSes will still get a high-quality app — it’s just that it won’t have the latest features.

\n

The key is that this allows us to make NetNewsWire the best app it can be, and making the best app we can is also part of furthering the goal of getting as many people as possible using RSS. (The biggest part, in fact. Bigger than compatibility with older OSes.)

\n

While I know this will disappoint some people, I hope you’ll understand why we decided to do it this way. Decisions like this are never easy — there are always conflicting values to weigh, pros and cons and add up — and we don’t make them impulsively. But making NetNewsWire the best app it can be has to be job #1.

"},"alternate":[{"href":"https://inessential.com/2019/10/14/netnewswire_os_compatibility_strategy","type":"text/html"}],"crawled":1571100281202,"title":"NetNewsWire OS Compatibility Strategy","published":1571099404000,"origin":{"streamId":"feed/http://ranchero.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":false,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"THree"}],"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571123677415},{"keywords":["Xcode"],"originId":"https://nshipster.com/swiftui-previews","fingerprint":"e3a6f78b","id":"08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16dc8d7749a:96ed:d4506071","updated":1571036400000,"author":"Mattt","summary":{"direction":"ltr","content":"

Working on a large iOS codebase often involves a lot of waiting. But with Xcode 11, our wait is finally over — and it’s all thanks to SwiftUI.

"},"alternate":[{"href":"https://nshipster.com/swiftui-previews/","type":"text/html"}],"crawled":1571032626330,"title":"SwiftUI Previews on macOS Catalina and Xcode 11","published":1571036400000,"origin":{"streamId":"feed/http://nshipster.com/feed.xml","htmlUrl":"https://nshipster.com/","title":"NSHipster"},"content":{"direction":"ltr","content":"

Working on a large iOS codebase often involves a lot of waiting:\nWaiting for Xcode to index your files,\nwaiting for Swift and Objective-C code to compile,\nwaiting for the Simulator to boot and your app to launch…

\n

And after all of that,\nyou spend even more time getting your app\ninto a particular state and onto a particular screen,\njust to see whether the Auto Layout constraint you just added\nfixes that regression you found.\nIt didn’t, of course,\nso you jump back into Xcode,\ntweak the Content Hugging Priority,\nhit R,\nand start the whole process again.

\n

We might relate our sorry predicament to\nthat one xkcd comic,\nbut for those of us who don’t so much relish in\nthe stop-and-go nature of app development,\nthere’s an old Yiddish joke about Shlemiel the painter\n(provided below with a few -specific modifications;\nfor the uninitiated,\nplease refer to Joel Spolsky’s\noriginal telling):

\n
\n

Shlemiel gets a job as a software developer,\nimplementing a new iOS app.\nOn the first sprint he opens Xcode\nand implements 10 new screens of the app.\n“That’s pretty good!” says his manager,\n“you’re a fast worker!” and pays him a Bitcoin.

\n

The next sprint Shlemiel only gets 5 screens done.\n“Well, that’s not nearly as good as yesterday,\nbut you’re still a fast worker. 5 screens is respectable,”\nand pays him a Bitcoin.

\n

The next sprint Shlemiel implements 1 screen.\n“Only 1!” shouts his manager.\n“That’s unacceptable!\nOn the first day you did ten times that much work!\nWhat’s going on?”

\n

“I can’t help it,” says Shlemiel.\n“Each sprint I get further and further away from\napplication(_:didFinishLaunchingWithOptions:)!”

\n
\n

Over the years,\nthere have been some developments that’ve helped things slightly,\nincluding\n@IBInspectable and @IBDesignable\nand Xcode Playgrounds.\nBut with Xcode 11,\nour wait is finally over —\nand it’s all thanks to SwiftUI.

\n
\n\n
\n

Although many of us have taken a “wait and see” approach to SwiftUI,\nwe can start using its capabilities today\nto radically speed up and improve our development process —\nwithout changing a line of code in our UIKit apps.

\n

Consider a subclass of UIButton\nthat draws a border around itself:

\n
final class BorderedButton: UIButton {\n        var cornerRadius: CGFloat { ... }\n        var borderWidth: CGFloat { ... }\n        var borderColor: UIColor? { ... }\n        }\n        
\n

Normally,\nif we wanted to test how our UI element performs,\nwe’d have to add it to a view in our app,\nbuild and run,\nand navigate to that screen.\nBut with Xcode 11,\nwe can now see a preview side-by-side with the code editor\nby adding the following under the original declaration of BorderedButton:

\n
\n
#if canImport(SwiftUI) && DEBUG\n        import SwiftUI\n        @available(iOS 13.0, *)\n        struct BorderedButton_Preview: PreviewProvider {\n        static var previews: some View {\n        UIViewPreview {\n        let button = BorderedButton(frame: .zero)\n        button.setTitle("Follow", for: .normal)\n        button.tintColor = .systemOrange\n        button.setTitleColor(.systemOrange, for: .normal)\n        return button\n        }.previewLayout(.sizeThatFits)\n        .padding(10)\n        }\n        }\n        #endif\n        
\n\n
\n

Using a new feature called dynamic replacement,\nXcode can update this preview without recompiling —\nwithin moments of your making a code change.\nThis lets you rapidly prototype changes like never before.

\n

Want to see how your button handles long titles?\nBang away on your keyboard within the call to setTitle(_:for:)\nin your preview,\nand test out potential fixes in your underlying implementation\nwithout so much as leaving your current file!

\n\n

\nPreviewing Multiple States

\n

Let’s say our app had a FavoriteButton —\na distant cousin (perhaps by composition) to BorderedButton.\nIn its default state,\nit shows has the title “Favorite”\nand displays a icon.\nWhen its isFavorited property is set to true,\nthe title is set to “Unfavorite”\nand displays a ♡̸ icon.

\n

We can preview both at once\nby wrapping two UIViewPreview instances within a single SwiftUI Group:

\n
\n
Group {\n        UIViewPreview {\n        let button = FavoriteButton(frame: .zero)\n        return button\n        }\n        UIViewPreview {\n        let button = FavoriteButton(frame: .zero)\n        button.isFavorited = true\n        return button\n        }\n        }.previewLayout(.sizeThatFits)\n        .padding(10)\n        
\n\n
\n\n

\nPreviewing Dark Mode

\n

With Dark Mode in iOS 13,\nit’s always a good idea to double-check that your custom views\nare configured with dynamic colors\nor accommodate both light and dark appearance in some other way.

\n

An easy way to do this\nwould be to use a ForEach element\nto render a preview for each case in the ColorScheme enumeration:

\n
\n
ForEach(ColorScheme.allCases, id: \\.self) { colorScheme in\n        UIViewPreview {\n        let button = BorderedButton(frame: .zero)\n        button.setTitle("Subscribe", for: .normal)\n        button.setImage(UIImage(systemName: "plus"), for: .normal)\n        button.setTitleColor(.systemOrange, for: .normal)\n        button.tintColor = .systemOrange\n        return button\n        }.environment(\\.colorScheme, colorScheme)\n        .previewDisplayName("\\(colorScheme)")\n        }.previewLayout(.sizeThatFits)\n        .background(Color(.systemBackground))\n        .padding(10)\n        
\n\n
\n\n

\nPreviewing Dynamic Type Size Categories

\n

We can use the same approach to preview our views in various\nDynamic Type Sizes:

\n
\n
ForEach(ContentSizeCategory.allCases, id: \\.self) { sizeCategory in\n        UIViewPreview {\n        let button = BorderedButton(frame: .zero)\n        button.setTitle("Subscribe", for: .normal)\n        button.setImage(UIImage(systemName: "plus"), for: .normal)\n        button.setTitleColor(.systemOrange, for: .normal)\n        button.tintColor = .systemOrange\n        return button\n        }.environment(\\.sizeCategory, sizeCategory)\n        .previewDisplayName("\\(sizeCategory)")\n        }.previewLayout(.sizeThatFits)\n        .padding(10)\n        
\n\n
\n

\nPreviewing Different Locales

\n

Xcode Previews are especially time-saving when it comes to\nlocalizing an app into multiple languages.\nCompared to the hassle of configuring Simulator\nback and forth between different languages and regions,\nthis new approach makes a world of difference.

\n

Let’s say that, in addition to English,\nyour app supported various right-to-left languages.\nYou could verify that your\nRTL logic worked as expected like so:

\n
\n
let supportedLocales: [Locale] = [\n        "en-US", // English (United States)\n        "ar-QA", // Arabid (Qatar)\n        "he-IL", // Hebrew (Israel)\n        "ur-IN"  // Urdu (India)\n        ].map(Locale.init(identifier:))\n        func localizedString(_ key: String, for locale: Locale) -> String? { ... }\n        return ForEach(supportedLocales, id: \\.identifier) { locale in\n        UIViewPreview {\n        let button = BorderedButton(frame: .zero)\n        button.setTitle(localizedString("Subscribe", for: locale), for: .normal)\n        button.setImage(UIImage(systemName: "plus"), for: .normal)\n        button.setTitleColor(.systemOrange, for: .normal)\n        button.tintColor = .systemOrange\n        return button\n        }.environment(\\.locale, locale)\n        .previewDisplayName(Locale.current.localizedString(forIdentifier: locale.identifier))\n        }.previewLayout(.sizeThatFits)\n        .padding(10)\n        
\n\n
\n\n

\nPreviewing View Controllers on Different Devices

\n

SwiftUI previews aren’t limited to views,\nyou can also use them with view controllers.\nBy creating a custom UIViewControllerPreview type\nand taking advantage of some\nnew UIStoryboard class methods in iOS 13,\nwe can easily preview our view controller\non various devices —\none on top of another:

\n
\n
#if canImport(SwiftUI) && DEBUG\n        import SwiftUI\n        let deviceNames: [String] = [\n        "iPhone SE",\n        "iPad 11 Pro Max",\n        "iPad Pro (11-inch)"\n        ]\n        @available(iOS 13.0, *)\n        struct ViewController_Preview: PreviewProvider {\n        static var previews: some View {\n        ForEach(deviceNames, id: \\.self) { deviceName in\n        UIViewControllerPreview {\n        UIStoryboard(name: "Main", bundle: nil)\n        .instantiateInitialViewController { coder in\n        ViewController(coder: coder)\n        }!\n        }.previewDevice(PreviewDevice(rawValue: deviceName))\n        .previewDisplayName(deviceName)\n        }\n        }\n        }\n        #endif\n        
\n\n
\n\n
\n

Although most of us are still some years away from shipping SwiftUI in our apps\n(whether by choice or necessity),\nwe can all immediately benefit from the order-of-magnitude improvement\nit enables with Xcode 11 on macOS Catalina.

\n

By eliminating so much time spent waiting for things to happen,\nwe not only get (literally) hours more time each week,\nbut we unlock the possibility of maintaining an unbroken flow state during that time.\nNot only that,\nbut the convenience of integrated tests\nfundamentally changes the calculus for testing:\ninstead of being a rare “nice to have,”\nthey’re the new default.\nPlus:\nthese inline previews serve as living documentation\nthat can help teams both large and small\nfinally get a handle on their design system.

\n

It’s hard to overstate how much of a game-changer Xcode Previews are for iOS development,\nand we couldn’t be happier to incorporate them into our workflow.

\n"},"visual":{"url":"none"},"unread":false,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}],"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571037592868},{"originId":"tag:blogger.com,1999:blog-8954608646904080796.post-3215871338266756283","fingerprint":"717870dc","thumbnail":[{"url":"https://1.bp.blogspot.com/-JTONCNpv2X8/XaEUgnGZSzI/AAAAAAAADTg/EaM5cDjkD3kRnXPIR0-6AX-3VxlC_br_QCEwYBhgL/s72-c/091.jpg","width":72,"height":72}],"id":"v0v+7Ya8tssIZvd3/pcnFRr3HwvY/5YK3FGc2t65c0Y=_16dbd619e56:82fa:d4506071","updated":1570837682218,"author":"Edward Feser","alternate":[{"href":"http://edwardfeser.blogspot.com/2019/10/around-web.html","type":"text/html"}],"crawled":1570840354390,"title":"Around the web","published":1570837680000,"origin":{"streamId":"feed/http://edwardfeser.blogspot.com/feeds/posts/default","htmlUrl":"http://edwardfeser.blogspot.com/","title":"Edward Feser"},"content":{"direction":"ltr","content":"
At The Catholic Thing, Fr. Thomas Weinandy on the studied ambiguity of Pope Francis.  In his new book Conciliar Octet, Fr. Aidan Nichols on the hermeneutic of continuity and Vatican II.

At Medium, philosopher Kathleen Stock on gender theory versus academic freedom in the UK.  At Inside Higher Education, twelve prominent philosophers defend the right to free inquiry on matters of sex and gender. 

Philosopher Daniel A. Kaufman on the “woke” fanatics increasingly infesting academic philosophy, at The Electric Agora.  Richard Marshall interviews Kaufman at 3:16. 

Peggy Noonan on transgender Jacobinism, at The Wall Street Journal.  At YouTube, video of an indoctrination session.

Jacob Howland on Borges’s Library of Babel, at The New Criterion.

At New Statesman, John Gray on Tom Holland on the Christian origins of modern secular liberal values.  More reviews at The University Bookman and at Literary Review.

At Quillette, Benedict Beckeld diagnoses Western self-hatred or oikophobia.

Donald Fagen interviewed on Paul Shaffer Plus One.

Kay Hymowitz on the sexual revolution and mental health, at The Washington Examiner.

John DeRosa of the Classical Theism Podcast interviews Thomist philosopher Gaven Kerr on the topic of Aquinas and creation.


New books on Aquinas: Aquinas and the Metaphysics of Creation, by Gaven Kerr; The Discovery of Being and Thomas Aquinas, edited by Christopher Cullen and Franklin Harkins; The Human Person: What Aristotle and Thomas Aquinas Offer Modern Psychology, by Thomas Spalding, James Stedman, Christina Gagné, and Matthew Kostelecky.

At the Institute of Art and Ideas: Philosopher of physics Tim Maudlin on quantum physics and common sense.  Physicist Subir Sarkar and philosophers Nancy Cartwright and John Dupré discuss physics and materialism.

Philosopher Dennis Bonnette on the distinction between the intellect and the imagination, at Strange Notions.

Philosopher of time Ross Cameron is interviewed by Richard Marshall at 3:16.

Duns Scotus in focus at Philosophy Now and Commonweal. 


Tim Maudlin on Judea Pearl on causation versus correlation, at the Boston Review.  Maudlin’s book Philosophy of Physics: Quantum Theory is reviewed at Notre Dame Philosophical Reviews.

Charles Styles interviews Peter Harrison on the subject of the best books on the history of science and religion, at Five Books.



Matias Slavov on Hume and Einstein on the nature of time, at Aeon.

At Catholic World Report, philosopher Joseph Trabbic on Aquinas and political liberalism.

Boston Review on post-liberal academic political philosophy.  The Chronicle of Higher Educationon post-liberal Catholic political philosophy. 

Blue World, an album of lost John Coltrane tracks, has been released.

It’s a thing.  The Huffington Post reports on millennials who are becoming nuns.

Scott Alexander on LGBT as a new civil religion, at Slate Star Codex.  C. C. Pecknold on the phony neutrality of post-Obergefell liberalism, at Catholic Herald.
"},"visual":{"url":"http://b.vimeocdn.com/ts/452/218/452218069_1280.jpg","width":1280,"height":720,"contentType":"image/jpeg"},"unread":false,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815","label":"Macintosh"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fbdcd69b-7e27-4b6a-bfed-6584b944155d","label":"🤞🏻🤞🏻🤞🏻"}],"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571037184309},{"keywords":["Google","Apple Arcade"],"originId":"https://www.macrumors.com/2019/09/26/apple-arcade-vs-google-play-pass/","fingerprint":"fd93a55d","id":"SusR11hdg5ydv7o+xGl+0XlI0AhBL77Oxid8QOO3j5k=_16d6f7df794:48e16:18991ffa","author":"Juli Clover","summary":{"direction":"ltr","content":"With the launch of iOS 13, Apple released Apple Arcade, a new $4.99 per month gaming service that provides unlimited access to new and exclusive games.\n
\n
\nLess than two weeks later, Google announced its own gaming service called Play Pass, which also offers unlimited access to games. In our latest YouTube video, we went hands-on with both services to compare them.\n
\n
\n
Subscribe to the MacRumors YouTube channel for more videos.
\n
\nBoth Apple Arcade and Play Pass are priced at $4.99 per month. Apple offers a one-month free trial while Google offers a 10-day free trial, but for the first year, Google is offering a deal that drops the price of Play Pass to $1.99 per month.\n
\n
\nThough the prices are similar, the two services are quite different. Apple Arcade features new and exclusive games, some of which were funded by Apple, while Google's Play Pass offers up older games.\n
\n
\nPlay Pass includes some super popular titles like Stardew Valley, Reigns, Star Wars Knights of the Old Republic, Terraria, and more, but the problem with older games is that many people may have already played them.\n
\n
\nPlay Pass is also not limited to games -- Google is including apps too. AccuWeather, Pic Stitch, ISS HD, and Tunable are some of the apps offered.\n
\n
\nApple Arcade only offers gaming titles, but all of the games are fresh, new, and exclusive to Apple Arcade. In some cases, though, some of the games are also available on consoles, but on mobile platforms, Apple Arcade gets exclusive access. That means no Android equivalent.\n
\n
\nIt's not clear if apps can leave Apple Arcade, but Google warns that apps can leave Play Pass. If that happens, users will need to buy the game to continue to use it if it's a paid game, and for free titles, ads and in-app purchases may show up.\n
\n
\nGoogle says there are hundreds of apps and games included with more being added every month. Apple Arcade launched with right around 60 games, and Apple has also promised new content on a monthly basis. Both services offer offline gaming, so no internet connection is required.\n
\n
\nWith both services, content is ad free and features no in-app purchases. Apple titles were designed from the ground up with no additional purchases, but for Play Pass, these gaming elements have been removed from titles that previously offered them.\n
\n
\nApple allows up to six family members to share games through a single Apple Arcade subscription using Family Sharing, and Google allows for up to five family members to share content through its Google Play Family Library.\n
\n
\nApple Arcade has launched in multiple countries around the world, while Play Pass is limited to the United States at the current time. Google does plan to expand, however.\n
\n
\nOne other aspect worth noting is privacy. Apple specifically mentions privacy protections and says that users are able to choose to share data, while Google's Play Pass materials don't mention privacy or data sharing. "Every game must meet Apple's high privacy standards," reads Apple's press release for Apple Arcade.\n
\n
\nMost people are locked in to either Android or iOS and few have both, so most people won't need to choose between services.\n
\n
\nApple users who have a Mac, iPhone, iPad, or Apple TV can access Apple Arcade on those devices, while Google Play Pass requires a smartphone, laptop, or tablet with Android 4.4 or above and Play Store version 16.6.25. Google Play Pass games can't be played on the larger screen of a television, which gives Apple Arcade a bit of an edge when it comes to non-mobile gaming.\n
\n
\nDo you prefer Apple Arcade with its fresh selection of titles, or Google Play Pass with its larger library of already available games? Let us know in the comments.


This article, "Apple Arcade vs. Google Play Pass" first appeared on MacRumors.com

Discuss this article in our forums

\n \n
\"\""},"alternate":[{"href":"https://www.macrumors.com/2019/09/26/apple-arcade-vs-google-play-pass/","type":"text/html"}],"crawled":1569533589396,"title":"Apple Arcade vs. Google Play Pass","published":1569531802000,"origin":{"streamId":"feed/http://www.macrumors.com/macrumors.xml","htmlUrl":"https://www.macrumors.com","title":"MacRumors: Mac News and Rumors - Front Page"},"visual":{"url":"https://cdn.vox-cdn.com/thumbor/m3ztxUEeETYXD9gGfyEjUpS0GFE=/0x0:800x533/1310x873/cdn.vox-cdn.com/uploads/chorus_image/image/59662701/800x_1.0.jpg","width":1310,"height":873,"contentType":"image/jpeg"},"unread":false,"readTime":33744,"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.unsaved","label":"Unsaved"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"}],"actionTimestamp":1569536278861}]} \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/TestTransport.swift b/Frameworks/Account/AccountTests/TestTransport.swift index 9bd072dd5..e45e910f0 100644 --- a/Frameworks/Account/AccountTests/TestTransport.swift +++ b/Frameworks/Account/AccountTests/TestTransport.swift @@ -8,6 +8,7 @@ import Foundation import RSWeb +import XCTest final class TestTransport: Transport { @@ -50,13 +51,20 @@ final class TestTransport: Transport { let response = httpResponse(for: request, statusCode: testStatusCodes[urlString] ?? 200) - if let testFileName = testFiles[urlString] { + var mockResponseFound = false + for (key, testFileName) in testFiles where urlString.contains(key) { let testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testFileName) let data = try! Data(contentsOf: testFileURL) DispatchQueue.global(qos: .background).async { completion(.success((response, data))) } - } else { + mockResponseFound = true + break + } + + if !mockResponseFound { +// XCTFail("Missing mock response for: \(urlString)") + print("***\nWARNING: \(self) missing mock response for:\n\(urlString)\n***") DispatchQueue.global(qos: .background).async { completion(.success((response, nil))) }