From cbd91f31c3ca01811cd7588fab21b84592096d8e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 6 Nov 2023 15:05:51 -0300 Subject: [PATCH 1/5] Make all CI jobs run on macos-11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `macos-10.15`, which worked whenever it was that we branched to `fix/socketrocket-fix`, is no longer available. So, let’s explicitly use macOS 11, which would have been what `macos-latest` pointed to at the time of c2ed307. --- .github/workflows/check-pod.yaml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/integration-test-iOS14_4.yaml | 2 +- .github/workflows/integration-test-macOS10_15.yaml | 2 +- .github/workflows/integration-test-tvOS14_3.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-pod.yaml b/.github/workflows/check-pod.yaml index a81caab66..a26bc63bd 100644 --- a/.github/workflows/check-pod.yaml +++ b/.github/workflows/check-pod.yaml @@ -8,7 +8,7 @@ on: jobs: check: - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 99da3eb8d..eae222fd6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: macos-latest + runs-on: macos-11 permissions: deployments: write diff --git a/.github/workflows/integration-test-iOS14_4.yaml b/.github/workflows/integration-test-iOS14_4.yaml index d14981ff7..20477b750 100644 --- a/.github/workflows/integration-test-iOS14_4.yaml +++ b/.github/workflows/integration-test-iOS14_4.yaml @@ -12,7 +12,7 @@ on: jobs: check: - runs-on: macos-10.15 + runs-on: macos-11 env: LC_CTYPE: en_US.UTF-8 diff --git a/.github/workflows/integration-test-macOS10_15.yaml b/.github/workflows/integration-test-macOS10_15.yaml index b53b3173f..99c111b24 100644 --- a/.github/workflows/integration-test-macOS10_15.yaml +++ b/.github/workflows/integration-test-macOS10_15.yaml @@ -12,7 +12,7 @@ on: jobs: check: - runs-on: macos-10.15 + runs-on: macos-11 env: LC_CTYPE: en_US.UTF-8 diff --git a/.github/workflows/integration-test-tvOS14_3.yaml b/.github/workflows/integration-test-tvOS14_3.yaml index b916e8b2b..68f6c4c72 100644 --- a/.github/workflows/integration-test-tvOS14_3.yaml +++ b/.github/workflows/integration-test-tvOS14_3.yaml @@ -12,7 +12,7 @@ on: jobs: check: - runs-on: macos-10.15 + runs-on: macos-11 env: LC_CTYPE: en_US.UTF-8 From fd4611bb1c4cc61612816567d193f5028a681158 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 6 Nov 2023 15:09:45 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Don=E2=80=99t=20install=20xcbeautify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installation now fails on the macos-11 runner: > Warning: You are using macOS 11. > We (and Apple) do not provide support for this old version. > It is expected behaviour that some formulae will fail to build in this old version. > It is expected behaviour that Homebrew will be buggy and slow. > Do not create any issues about this on Homebrew's GitHub repositories. > Do not create any issues even if you think this message is unrelated. > Any opened issues will be immediately closed without response. > Do not ask for help from Homebrew or its maintainers on social media. > You may ask for help in Homebrew's discussions but are unlikely to receive a response. > Try to figure out the problem yourself and submit a fix as a pull request. > We will review it but may or may not accept it. > > xcbeautify: A full installation of Xcode.app 14.0 is required to compile > this software. Installing just the Command Line Tools is not sufficient. > > Xcode 14.0 cannot be installed on macOS 11. > You must upgrade your version of macOS. > Error: xcbeautify: An unsatisfied requirement failed this build. > Error: Process completed with exit code 1. --- .github/workflows/integration-test-iOS14_4.yaml | 7 ------- .github/workflows/integration-test-macOS10_15.yaml | 7 ------- .github/workflows/integration-test-tvOS14_3.yaml | 7 ------- fastlane/Scanfile | 2 -- 4 files changed, 23 deletions(-) diff --git a/.github/workflows/integration-test-iOS14_4.yaml b/.github/workflows/integration-test-iOS14_4.yaml index 20477b750..ea910adec 100644 --- a/.github/workflows/integration-test-iOS14_4.yaml +++ b/.github/workflows/integration-test-iOS14_4.yaml @@ -47,7 +47,6 @@ jobs: - name: Install Dependencies and Run Tests run: | - brew install xcbeautify make submodules bundle install make update_carthage_dependencies_ios @@ -95,12 +94,6 @@ jobs: with: name: xcodebuild-logs path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload test results to observability server - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - run: Scripts/upload_test_results.sh - name: Swift Package Manager - Installation Test working-directory: ./ diff --git a/.github/workflows/integration-test-macOS10_15.yaml b/.github/workflows/integration-test-macOS10_15.yaml index 99c111b24..c27ec898f 100644 --- a/.github/workflows/integration-test-macOS10_15.yaml +++ b/.github/workflows/integration-test-macOS10_15.yaml @@ -47,7 +47,6 @@ jobs: - name: Install Dependencies and Run Tests run: | - brew install xcbeautify make submodules bundle install make update_carthage_dependencies_macos @@ -90,12 +89,6 @@ jobs: with: name: xcodebuild-logs path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload test results to observability server - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - run: Scripts/upload_test_results.sh - name: Swift Package Manager - Installation Test working-directory: ./ diff --git a/.github/workflows/integration-test-tvOS14_3.yaml b/.github/workflows/integration-test-tvOS14_3.yaml index 68f6c4c72..5289b96ba 100644 --- a/.github/workflows/integration-test-tvOS14_3.yaml +++ b/.github/workflows/integration-test-tvOS14_3.yaml @@ -47,7 +47,6 @@ jobs: - name: Install Dependencies and Run Tests run: | - brew install xcbeautify make submodules bundle install make update_carthage_dependencies_tvos @@ -90,12 +89,6 @@ jobs: with: name: xcodebuild-logs path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload test results to observability server - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - run: Scripts/upload_test_results.sh - name: Swift Package Manager - Installation Test working-directory: ./ diff --git a/fastlane/Scanfile b/fastlane/Scanfile index fea35296b..25cd5b8bf 100644 --- a/fastlane/Scanfile +++ b/fastlane/Scanfile @@ -3,6 +3,4 @@ clean true skip_slack true ensure_devices_found true output_types "junit" -# I'm being explicit about this because I want to make sure it's being used, to make sure that trainer is used to generate the JUnit report -xcodebuild_formatter "xcbeautify" result_bundle true From 4f7cadb9462eaaad1d29a8d7f8b58f0e05404913 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 6 Nov 2023 15:37:29 -0300 Subject: [PATCH 3/5] Use iOS/tvOS 15.2 in CI This is the earliest version of iOS and tvOS now available in Xcode 13.2.1 (the version of Xcode that would have been the default on the macos-latest runner at the time of c2ed307). --- ...st-iOS14_4.yaml => integration-test-iOS15_2.yaml} | 8 ++++---- ...-tvOS14_3.yaml => integration-test-tvOS15_2.yaml} | 6 +++--- Makefile | 4 ++-- fastlane/Fastfile | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) rename .github/workflows/{integration-test-iOS14_4.yaml => integration-test-iOS15_2.yaml} (94%) rename .github/workflows/{integration-test-tvOS14_3.yaml => integration-test-tvOS15_2.yaml} (95%) diff --git a/.github/workflows/integration-test-iOS14_4.yaml b/.github/workflows/integration-test-iOS15_2.yaml similarity index 94% rename from .github/workflows/integration-test-iOS14_4.yaml rename to .github/workflows/integration-test-iOS15_2.yaml index ea910adec..9cab7dbfc 100644 --- a/.github/workflows/integration-test-iOS14_4.yaml +++ b/.github/workflows/integration-test-iOS15_2.yaml @@ -1,4 +1,4 @@ -name: "Integration Test: iOS 14.4" +name: "Integration Test: iOS 15.2" on: pull_request: @@ -50,7 +50,7 @@ jobs: make submodules bundle install make update_carthage_dependencies_ios - bundle exec fastlane test_iOS14_4 + bundle exec fastlane test_iOS15_2 - name: Check Static Analyzer Output id: analyzer-output @@ -66,7 +66,7 @@ jobs: if: ${{ failure() && steps.analyzer-output.outcome == 'failure' }} uses: actions/upload-artifact@v2 with: - name: static-analyzer-reports-test_iOS14_4 + name: static-analyzer-reports-test_iOS15_2 path: ./derived_data/**/report-*.html # This is the script specified as the pod’s prepare_command in its Podspec. @@ -81,7 +81,7 @@ jobs: run: | pod repo update pod install - bundle exec fastlane scan -s Tests --output-directory "fastlane/test_output/examples/test_iOS14_4" + bundle exec fastlane scan -s Tests --output-directory "fastlane/test_output/examples/test_iOS15_2" - name: Build APNS Example Project working-directory: ./Examples/AblyPush diff --git a/.github/workflows/integration-test-tvOS14_3.yaml b/.github/workflows/integration-test-tvOS15_2.yaml similarity index 95% rename from .github/workflows/integration-test-tvOS14_3.yaml rename to .github/workflows/integration-test-tvOS15_2.yaml index 5289b96ba..7bff61a38 100644 --- a/.github/workflows/integration-test-tvOS14_3.yaml +++ b/.github/workflows/integration-test-tvOS15_2.yaml @@ -1,4 +1,4 @@ -name: "Integration Test: tvOS 14.3" +name: "Integration Test: tvOS 15.2" on: pull_request: @@ -50,7 +50,7 @@ jobs: make submodules bundle install make update_carthage_dependencies_tvos - bundle exec fastlane test_tvOS14_3 + bundle exec fastlane test_tvOS15_2 - name: Check Static Analyzer Output id: analyzer-output @@ -66,7 +66,7 @@ jobs: if: ${{ failure() && steps.analyzer-output.outcome == 'failure' }} uses: actions/upload-artifact@v2 with: - name: static-analyzer-reports-test_tvOS14_3 + name: static-analyzer-reports-test_tvOS15_2 path: ./derived_data/**/report-*.html # This is the script specified as the pod’s prepare_command in its Podspec. diff --git a/Makefile b/Makefile index 8c1c98481..06c73e2b1 100644 --- a/Makefile +++ b/Makefile @@ -67,11 +67,11 @@ submodules: ## [Tests] Run tests on iOS 14.4 using sandbox environment test_iOS: - ABLY_ENV="sandbox" NAME="ably-iOS" bundle exec fastlane test_iOS14_4 + ABLY_ENV="sandbox" NAME="ably-iOS" bundle exec fastlane test_iOS15_2 ## [Tests] Run tests on tvOS 14.3 using sandbox environment test_tvOS: - ABLY_ENV="sandbox" NAME="ably-tvOS" bundle exec fastlane test_tvOS14_3 + ABLY_ENV="sandbox" NAME="ably-tvOS" bundle exec fastlane test_tvOS15_2 ## [Tests] Run tests on macOS using sandbox environment test_macOS: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a7cc2f24b..5e22874ba 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -2,24 +2,24 @@ default_platform(:ios) platform :ios do - lane :test_iOS14_4 do + lane :test_iOS15_2 do run_tests( scheme: "Ably-iOS-Tests", derived_data_path: "derived_data", - devices: ["iPhone 12 (14.4)"], + devices: ["iPhone 12 (15.2)"], test_without_building: false, xcargs: { ABLY_ENV: ENV['ABLY_ENV'], CLANG_ANALYZER_OUTPUT: 'plist-html' }, - output_directory: "fastlane/test_output/sdk/test_iOS14_4" + output_directory: "fastlane/test_output/sdk/test_iOS15_2" ) end - lane :test_tvOS14_3 do + lane :test_tvOS15_2 do run_tests( scheme: "Ably-tvOS-Tests", derived_data_path: "derived_data", - devices: ["Apple TV 4K (14.3)"], + devices: ["Apple TV 4K (15.2)"], xcargs: { ABLY_ENV: ENV['ABLY_ENV'], CLANG_ANALYZER_OUTPUT: 'plist-html' }, - output_directory: "fastlane/test_output/sdk/test_tvOS14_3" + output_directory: "fastlane/test_output/sdk/test_tvOS15_2" ) end From 657124250df1748fd6196aaf57192cefc9158a99 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 6 Nov 2023 15:15:14 -0300 Subject: [PATCH 4/5] Add DataGatherer Copied from commit 46ee0b0, with a change to `if let foo {` syntax to get it compiling in older Xcode. Needed for cherry-picking the test changes from that commit. --- Ably.xcodeproj/project.pbxproj | 8 +++ Spec/Test Utilities/DataGatherer.swift | 75 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 Spec/Test Utilities/DataGatherer.swift diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index 35f5ab01c..93111a855 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -82,6 +82,9 @@ 21881E7A283BD08300CFD9E2 /* GCDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A22171266F526600C87C42 /* GCDTests.swift */; }; 21881E7B283BD0DF00CFD9E2 /* StringifiableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D520C4DD2680A1E3000012B2 /* StringifiableTests.swift */; }; 21881E7C283BD0E100CFD9E2 /* StringifiableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D520C4DD2680A1E3000012B2 /* StringifiableTests.swift */; }; + 21D3CC572AF964AC00B3F2BD /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D3CC562AF964AC00B3F2BD /* DataGatherer.swift */; }; + 21D3CC582AF964AC00B3F2BD /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D3CC562AF964AC00B3F2BD /* DataGatherer.swift */; }; + 21D3CC592AF964AC00B3F2BD /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D3CC562AF964AC00B3F2BD /* DataGatherer.swift */; }; 560579D924AF1BA900A4D03D /* ARTDefaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560579D824AF1BA900A4D03D /* ARTDefaultTests.swift */; }; 560579DA24AF1BA900A4D03D /* ARTDefaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560579D824AF1BA900A4D03D /* ARTDefaultTests.swift */; }; 560579DB24AF1BA900A4D03D /* ARTDefaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560579D824AF1BA900A4D03D /* ARTDefaultTests.swift */; }; @@ -1002,6 +1005,7 @@ 217D181E25421FED00DFF07E /* ARTSRSecurityPolicy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTSRSecurityPolicy.m; sourceTree = ""; }; 217D181F25421FED00DFF07E /* ARTSRWebSocket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTSRWebSocket.m; sourceTree = ""; }; 217D182025421FED00DFF07E /* ARTSRSecurityPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARTSRSecurityPolicy.h; sourceTree = ""; }; + 21D3CC562AF964AC00B3F2BD /* DataGatherer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataGatherer.swift; sourceTree = ""; }; 560579D824AF1BA900A4D03D /* ARTDefaultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARTDefaultTests.swift; sourceTree = ""; }; 56190953238C3D3200A862A6 /* CryptoTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CryptoTest.m; sourceTree = ""; }; 841134772722205400CFA837 /* ARTArchiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTArchiveTests.m; sourceTree = ""; }; @@ -1499,6 +1503,7 @@ 21BF060A2758477E00AE4C43 /* Test Utilities */ = { isa = PBXGroup; children = ( + 21D3CC562AF964AC00B3F2BD /* DataGatherer.swift */, D7093CA8219EFA8A00723F17 /* MockDeviceStorage.swift */, D780846C1C68B3E50083009D /* NSObject+TestSuite.h */, D780846D1C68B3E50083009D /* NSObject+TestSuite.m */, @@ -2653,6 +2658,7 @@ D777EEE820650ADF002EBA03 /* PushChannelTests.swift in Sources */, D746AE2D1BBB625E003ECEF8 /* RestClientChannelsTests.swift in Sources */, EBAB9A6F1C69702800AF036B /* ReadmeExamplesTests.swift in Sources */, + 21D3CC572AF964AC00B3F2BD /* DataGatherer.swift in Sources */, 56190954238C3D3200A862A6 /* CryptoTest.m in Sources */, D7C1B8771BBEA81A0087B55F /* AuthTests.swift in Sources */, D7EBE5A31BE8391E0086E675 /* RealtimeClientConnectionTests.swift in Sources */, @@ -2783,6 +2789,7 @@ D7093C1C219E466400723F17 /* ReadmeExamplesTests.swift in Sources */, 21881E7B283BD0DF00CFD9E2 /* StringifiableTests.swift in Sources */, D7093C27219E466E00723F17 /* RealtimeClientChannelsTests.swift in Sources */, + 21D3CC582AF964AC00B3F2BD /* DataGatherer.swift in Sources */, 848ED97426E50D0F0087E800 /* ObjcppTest.mm in Sources */, D7093C28219E466E00723F17 /* RealtimeClientPresenceTests.swift in Sources */, D7093C24219E466E00723F17 /* RealtimeClientTests.swift in Sources */, @@ -2817,6 +2824,7 @@ D7093C71219EE25800723F17 /* NSObject+TestSuite.m in Sources */, D7093C70219EE25400723F17 /* TestUtilities.swift in Sources */, 84569FA826B46F5100457CF5 /* ClientOptionsTests.swift in Sources */, + 21D3CC592AF964AC00B3F2BD /* DataGatherer.swift in Sources */, D7093C80219EE26400723F17 /* StatsTests.swift in Sources */, D7093C77219EE26400723F17 /* RestClientChannelTests.swift in Sources */, D520C4E32680A1FC000012B2 /* StringifiableTests.swift in Sources */, diff --git a/Spec/Test Utilities/DataGatherer.swift b/Spec/Test Utilities/DataGatherer.swift new file mode 100644 index 000000000..f6a44a0ac --- /dev/null +++ b/Spec/Test Utilities/DataGatherer.swift @@ -0,0 +1,75 @@ +import Foundation +import XCTest + +/** + A `DataGatherer` instance initiates a user-specified data-gathering activity, and provides a method for waiting until the activity submits some data. + + The `DataGatherer` instance only cares about the _first_ data that is submitted to it, and will ignore any subsequently-submitted data. When the gathered data has value semantics, this can simplify the implementation of tests, since they do not need to be so careful about making sure to stop their data-gathering process at the right time. + */ +class DataGatherer { + private let expectation: XCTestExpectation + + // The value that the initializer’s `gather` block passed to its `submit` argument. + private var value: T? + // Synchronises access to `value`. + private let semaphore = DispatchSemaphore(value: 1) + + /** + Initiates the data-gathering process specified by `gather`. + + - Parameters: + - description: A human-readable description of the data-gathering process. + - gather: A function which implements the data-gathering process. It should call the `submit` callback with the gathered data when ready. Subsequent calls to `submit` will have no effect. `submit` can be safely called from any thread. + */ + init(description: String, gather: (_ submit: @escaping (T) -> Void) -> Void) { + expectation = XCTestExpectation(description: description) + gather(complete(withValue:)) + } + + enum Error: Swift.Error { + case unexpectedResult(XCTWaiter.Result) + } + + /** + Waits for the initializer’s `gather` function to submit data and then returns the submitted data. If data has already been submitted then it is returned immediately. This method can be safely called from any thread. + */ + func waitForData(timeout: TimeInterval) throws -> T { + semaphore.wait() + if let value = value { + semaphore.signal() + return value + } + semaphore.signal() + + let waiter = XCTWaiter() + let result = waiter.wait(for: [expectation], timeout: timeout) + + switch result { + case .completed: + let value: T + semaphore.wait() + value = self.value! + semaphore.signal() + return value + default: + throw Error.unexpectedResult(result) + } + } + + /** + Waits for the initializer’s `gather` function to submit data and then returns the submitted data. If data has already been submitted then it is returned immediately. This method can be safely called from any thread. + */ + func waitForData(timeout: DispatchTimeInterval) throws -> T { + return try waitForData(timeout: timeout.toTimeInterval()) + } + + private func complete(withValue value: T) { + semaphore.wait() + if self.value == nil { + self.value = value + } + semaphore.signal() + + expectation.fulfill() + } +} From 3122fd4c456da52236f552e9841966b7f7350fe9 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 3 Nov 2023 15:24:49 -0300 Subject: [PATCH 5/5] Treat all transport errors as recoverable This is a cherry-pick of 86b7cc7, to resolve #1823 on the fix/socketrocket-fix branch which one of our customers is using. --- Source/ARTRealtime.m | 22 ++--- Spec/Test Utilities/TestUtilities.swift | 18 +++- .../Tests/RealtimeClientConnectionTests.swift | 92 ++++++++++++++++--- 3 files changed, 98 insertions(+), 34 deletions(-) diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index d4c718a7b..623191853 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -1519,16 +1519,8 @@ - (void)realtimeTransportFailed:(id)transport withError:(A } } - switch (transportError.type) { - case ARTRealtimeTransportErrorTypeBadResponse: - case ARTRealtimeTransportErrorTypeOther: - [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createFromNSError:transportError.error]]; - break; - default: { - ARTErrorInfo *error = [ARTErrorInfo createFromNSError:transportError.error]; - [self transitionToDisconnectedOrSuspendedWithError:error]; - } - } + ARTErrorInfo *error = [ARTErrorInfo createFromNSError:transportError.error]; + [self transitionToDisconnectedOrSuspendedWithError:error]; } - (void)realtimeTransportNeverConnected:(id)transport { @@ -1537,7 +1529,7 @@ - (void)realtimeTransportNeverConnected:(id)transport { return; } - [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithCode:ARTClientCodeErrorTransport message:@"Transport never connected"]]; + [self transitionToDisconnectedOrSuspendedWithError:[ARTErrorInfo createWithCode:ARTClientCodeErrorTransport message:@"Transport never connected"]]; } - (void)realtimeTransportRefused:(id)transport withError:(ARTRealtimeTransportError *)error { @@ -1547,13 +1539,13 @@ - (void)realtimeTransportRefused:(id)transport withError:( } if (error && error.type == ARTRealtimeTransportErrorTypeRefused) { - [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithCode:ARTClientCodeErrorTransport message:[NSString stringWithFormat:@"Connection refused using %@", error.url]]]; + [self transitionToDisconnectedOrSuspendedWithError:[ARTErrorInfo createWithCode:ARTClientCodeErrorTransport message:[NSString stringWithFormat:@"Connection refused using %@", error.url]]]; } else if (error) { - [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createFromNSError:error.error]]; + [self transitionToDisconnectedOrSuspendedWithError:[ARTErrorInfo createFromNSError:error.error]]; } else { - [self transition:ARTRealtimeFailed]; + [self transitionToDisconnectedOrSuspendedWithError:nil]; } } @@ -1563,7 +1555,7 @@ - (void)realtimeTransportTooBig:(id)transport { return; } - [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithCode:ARTClientCodeErrorTransport message:@"Transport too big"]]; + [self transitionToDisconnectedOrSuspendedWithError:[ARTErrorInfo createWithCode:ARTClientCodeErrorTransport message:@"Transport too big"]]; } - (void)realtimeTransportSetMsgSerial:(id)transport msgSerial:(int64_t)msgSerial { diff --git a/Spec/Test Utilities/TestUtilities.swift b/Spec/Test Utilities/TestUtilities.swift index 24f3c1ad1..c4a5fe7a5 100644 --- a/Spec/Test Utilities/TestUtilities.swift +++ b/Spec/Test Utilities/TestUtilities.swift @@ -744,6 +744,7 @@ enum FakeNetworkResponse { case requestTimeout(timeout: TimeInterval) case hostInternalError(code: Int) case host400BadRequest + case arbitraryError var error: NSError { switch self { @@ -757,6 +758,8 @@ enum FakeNetworkResponse { return NSError(domain: AblyTestsErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: "internal error", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"]) case .host400BadRequest: return NSError(domain: AblyTestsErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "bad request", NSLocalizedFailureReasonErrorKey: AblyTestsErrorDomain + ".FakeNetworkResponse"]) + case .arbitraryError: + return NSError(domain: AblyTestsErrorDomain, code: 1, userInfo: [NSLocalizedDescriptionKey: "error from FakeNetworkResponse.arbitraryError"]) } } @@ -772,6 +775,8 @@ enum FakeNetworkResponse { return ARTRealtimeTransportError(error: error, badResponseCode: code, url: url) case .host400BadRequest: return ARTRealtimeTransportError(error: error, badResponseCode: 400, url: url) + case .arbitraryError: + return ARTRealtimeTransportError(error: error, type: .other, url: url) } } } @@ -857,6 +862,8 @@ class MockHTTP: ARTHttp { requestCallback?(HTTPURLResponse(url: URL(string: "http://cocoa.test.suite")!, statusCode: code, httpVersion: nil, headerFields: nil), nil, nil) case .host400BadRequest: requestCallback?(HTTPURLResponse(url: URL(string: "http://cocoa.test.suite")!, statusCode: 400, httpVersion: nil, headerFields: nil), nil, nil) + case .arbitraryError: + requestCallback?(nil, nil, NSError(domain: AblyTestsErrorDomain, code: 1, userInfo: [NSLocalizedDescriptionKey: "error from FakeNetworkResponse.arbitraryError"])) } } @@ -1232,7 +1239,8 @@ class TestProxyTransport: ARTWebSocketTransport { case .noInternet, .hostUnreachable, .hostInternalError, - .host400BadRequest: + .host400BadRequest, + .arbitraryError: performFakeConnectionError(0.1, error: networkResponse.transportError(for: url)) case .requestTimeout(let timeout): performFakeConnectionError(0.1 + timeout, error: networkResponse.transportError(for: url)) @@ -1613,9 +1621,11 @@ extension ARTWebSocketTransport { } func simulateIncomingError() { - let error = NSError(domain: ARTAblyErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey:"Fail test"]) - let webSocketDelegate = self as ARTWebSocketDelegate - webSocketDelegate.webSocket(self.websocket!, didFailWithError: error) + // Simulate receiving an ERROR ProtocolMessage, which should put a client into the FAILED state (per RTN15i) + let protocolMessage = ARTProtocolMessage() + protocolMessage.action = .error + protocolMessage.error = ARTErrorInfo.create(withCode: 50000 /* arbitrarily chosen */, message: "Fail test") + receive(protocolMessage) } } diff --git a/Spec/Tests/RealtimeClientConnectionTests.swift b/Spec/Tests/RealtimeClientConnectionTests.swift index 25cc04594..dd426b1fe 100644 --- a/Spec/Tests/RealtimeClientConnectionTests.swift +++ b/Spec/Tests/RealtimeClientConnectionTests.swift @@ -2208,7 +2208,7 @@ class RealtimeClientConnectionTests: XCTestCase { } // RTN14d - func test__059__Connection__connection_request_fails__connection_attempt_fails_for_any_recoverable_reason() { + func test__059__Connection__connection_request_fails__connection_attempt_fails_for_any_recoverable_reason__for_example_a_timeout() { let options = AblyTests.commonAppSetup() options.realtimeHost = "10.255.255.1" // non-routable IP address options.disconnectedRetryTimeout = 1.0 @@ -2264,6 +2264,58 @@ class RealtimeClientConnectionTests: XCTestCase { expect(totalRetry).to(equal(Int(expectedTime / options.disconnectedRetryTimeout))) } + // RTN14d + // This is a slightly-modified copy of test__059 above, based on the test changes introduced in 86b7cc7. Since this cherry-pick is being done on a branch we intend to eventually throw away, I was happy to just make a copy instead of cherry-picking the refactoring introduced in 30a0979. + func test__059b__Connection__connection_request_fails__connection_attempt_fails_for_any_recoverable_reason__for_example_an_arbitrary_transport_error() { + let options = AblyTests.commonAppSetup() + options.disconnectedRetryTimeout = 1.0 + options.autoConnect = false + let expectedTime = 3.0 + + let previousConnectionStateTtl = ARTDefault.connectionStateTtl() + defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } + ARTDefault.setConnectionStateTtl(expectedTime) + + let client = ARTRealtime(options: options) + client.internal.setTransport(TestProxyTransport.self) + TestProxyTransport.fakeNetworkResponse = .arbitraryError + defer { TestProxyTransport.fakeNetworkResponse = nil } + client.internal.shouldImmediatelyReconnect = false + defer { + client.connection.off() + client.close() + } + + var totalRetry = 0 + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + var start: NSDate? + + client.connection.once(.disconnected) { stateChange in + expect(stateChange.reason!.message).to(contain("error from FakeNetworkResponse.arbitraryError")) + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.connecting)) + expect(stateChange.retryIn).to(beCloseTo(options.disconnectedRetryTimeout)) + partialDone() + start = NSDate() + } + + client.connection.on(.suspended) { _ in + let end = NSDate() + expect(end.timeIntervalSince(start! as Date)).to(beCloseTo(expectedTime, within: 0.9)) + partialDone() + } + + client.connect() + + client.connection.on(.connecting) { stateChange in + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.disconnected)) + totalRetry += 1 + } + } + + expect(totalRetry).to(equal(Int(expectedTime / options.disconnectedRetryTimeout))) + } + // RTN14e func test__060__Connection__connection_request_fails__connection_state_has_been_in_the_DISCONNECTED_state_for_more_than_the_default_connectionStateTtl_should_change_the_state_to_SUSPENDED() { let options = AblyTests.commonAppSetup() @@ -3600,13 +3652,13 @@ class RealtimeClientConnectionTests: XCTestCase { afterEach__Connection__Host_Fallback() } - func test__090__Connection__Host_Fallback__should_not_use_an_alternative_host_when_the_client_receives_a_bad_request() { + func test__090__Connection__Host_Fallback__should_not_use_an_alternative_host_when_the_client_receives_a_bad_request() throws { beforeEach__Connection__Host_Fallback() let options = ARTClientOptions(key: "xxxx:xxxx") options.autoConnect = false + options.disconnectedRetryTimeout = 1.0 // so that the test doesn't have to wait a long time to observe a retry let client = ARTRealtime(options: options) - let channel = client.channels.get(uniqueChannelName()) let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } @@ -3616,26 +3668,36 @@ class RealtimeClientConnectionTests: XCTestCase { TestProxyTransport.fakeNetworkResponse = .host400BadRequest defer { TestProxyTransport.fakeNetworkResponse = nil } - var urlConnections = [URL]() - TestProxyTransport.networkConnectEvent = { transport, url in - if client.internal.transport !== transport { - return + let dataGatherer = DataGatherer<(stateChanges: [ARTConnectionStateChange], urlConnections: [URL])>(description: "Observe emitted state changes and transport connection attempts") { submit in + var stateChanges: [ARTConnectionStateChange] = [] + var urlConnections = [URL]() + + client.connection.on { stateChange in + stateChanges.append(stateChange) + if (stateChanges.count == 3) { + submit((stateChanges: stateChanges, urlConnections: urlConnections)) + } + } + + TestProxyTransport.networkConnectEvent = { transport, url in + if client.internal.transport !== transport { + return + } + urlConnections.append(url) } - urlConnections.append(url) } defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "message") { _ in - done() - } - } + let data = try dataGatherer.waitForData(timeout: testTimeout) - expect(urlConnections).to(haveCount(1)) - expect(NSRegularExpression.match(urlConnections[0].absoluteString, pattern: "//realtime.ably.io")).to(beTrue()) + // We expect the first connection attempt to fail due to the .fakeNetworkResponse configured above. This error does not meet the criteria for trying a fallback host, and so should not provoke the use of a fallback host. Hence the connection should transition to DISCONNECTED, and then subsequently retry, transitioning back to CONNECTING. We should see that there were two connection attempts, both to the primary host. + + XCTAssertEqual(data.stateChanges.map { $0.current }, [.connecting, .disconnected, .connecting]) + XCTAssertEqual(data.urlConnections.count, 2) + XCTAssertTrue(data.urlConnections.allSatisfy { url in NSRegularExpression.match(url.absoluteString, pattern: "//realtime.ably.io") }) afterEach__Connection__Host_Fallback() }