diff --git a/.github/workflows/check-pod.yaml b/.github/workflows/check-pod.yaml deleted file mode 100644 index cbae547af..000000000 --- a/.github/workflows/check-pod.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: Check Pod - -on: - pull_request: - push: - branches: - - main - -jobs: - check: - runs-on: macos-13 - steps: - - uses: actions/checkout@v2 - - - name: Select Specific Xcode Version (15.1) - run: | - sudo xcode-select -s /Applications/Xcode_15.1.app - echo "Selected Xcode version:" - xcodebuild -version - - # Run the steps we document in the Release Process. - # unzip commands included as proof-of-life for the Carthage output. - - name: Print Ruby version - run: ruby --version - - name: Print Carthage version - run: 'echo -n "carthage version: " && carthage version' - - name: Print CocoaPods version - run: 'echo -n "pod version: " && pod --version --verbose' - - name: Print Make version - run: make --version - - name: Build Carthage dependencies - run: make update - - name: Build Ably framework - run: make carthage_package - - name: Print contents of generated ZIP file - run: | - unzip -l Ably.framework.zip - unzip -l Ably.framework.zip | grep 'Mac/Ably.framework' - unzip -l Ably.framework.zip | grep 'tvOS/Ably.framework' - unzip -l Ably.framework.zip | grep 'iOS/Ably.framework' - - name: Validate pod - run: pod lib lint - # We move Ably.framework.zip into a directory. This is because, by - # default, macOS’s Archive Utility unzips directly-nested zip files, so - # if Ably.framework.zip were at the top level of the zip file that - # actions/upload-artifact creates, then Archive Utility would unzip - # Ably.framework.zip too, which we don’t want, since we want this file - # to be kept intact so that we can upload it to GitHub releases as - # described in CONTRIBUTING.md. - - name: Prepare built framework for archiving - run: | - mkdir -p carthage-built-framework-artifact-contents/carthage-built-framework - mv Ably.framework.zip carthage-built-framework-artifact-contents/carthage-built-framework - - name: Archive built framework - uses: actions/upload-artifact@v3 - with: - name: carthage-built-framework - path: carthage-built-framework-artifact-contents diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 59cdd0f05..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Docs Generation - -on: - pull_request: - push: - branches: - - main - tags: - - '*' - -jobs: - build: - runs-on: macos-latest - - permissions: - deployments: write - id-token: write - - steps: - - uses: actions/checkout@v2 - - - name: Select Specific Xcode Version (14.2) - run: | - sudo xcode-select -s /Applications/Xcode_14.2.app - echo "Selected Xcode version:" - xcodebuild -version - - - name: Install Dependencies - run: | - make submodules - bundle install - make update_carthage_dependencies_macos - - - name: Build Documentation - run: | - ./Scripts/jazzy.sh - ls -al Docs/jazzy - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-region: eu-west-2 - role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-ably-cocoa - role-session-name: "${{ github.run_id }}-${{ github.run_number }}" - - - name: Upload Documentation - uses: ably/sdk-upload-action@v1 - with: - sourcePath: Docs/jazzy - githubToken: ${{ secrets.GITHUB_TOKEN }} - artifactName: jazzydoc - diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml deleted file mode 100644 index d418daddb..000000000 --- a/.github/workflows/examples.yaml +++ /dev/null @@ -1,69 +0,0 @@ -name: Examples Test - -on: - pull_request: - push: - branches: - - main - -jobs: - check: - runs-on: macos-13 - - env: - LC_CTYPE: en_US.UTF-8 - LANG: en_US.UTF-8 - ABLY_ENV: sandbox - - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Select Xcode (15.1) - run: | - sudo xcode-select -s /Applications/Xcode_15.1.app - echo "Selected Xcode version:" - xcodebuild -version - - - name: Environment Info - run: ./Scripts/log-environment-information.sh - - - name: Reset Simulators - run: xcrun simctl erase all - - - name: Install Dependencies - run: | - make submodules - bundle install - make update_carthage_dependencies_ios - - - name: Run Examples Tests - working-directory: ./Examples/Tests - run: | - pod repo update - pod install - bundle exec fastlane scan -s Tests --output-directory "fastlane/test_output/examples/test_iOS17_2" - - - name: Carthage Installation - working-directory: ./Examples/AblyCarthage - run: | - echo 'Installing Carthage dependencies...' - carthage update --use-xcframeworks --platform iOS --no-use-binaries - echo 'Building AblyCarthage example...' - xcodebuild build -scheme "AblyCarthage" -destination "platform=iOS Simulator,name=iPhone 15" -configuration "Debug" - - - name: SPM Installation - working-directory: ./ - run: | - echo 'Current Branch: ' $GITHUB_HEAD_REF - echo 'Current Revision (SHA):' $GITHUB_SHA - echo Current Path: $(pwd) - export PACKAGE_URL=file://$(pwd) - export PACKAGE_BRANCH_NAME=$GITHUB_HEAD_REF - export PACKAGE_REVISION=$GITHUB_SHA - swift test --package-path Examples/SPM -v - - - name: Build APNS Example - working-directory: ./Examples/AblyPush - run: | - xcodebuild build -scheme "AblyPushExample" -destination "platform=iOS Simulator,name=iPhone 15" -configuration "Debug" diff --git a/.github/workflows/features.yml b/.github/workflows/features.yml deleted file mode 100644 index d1c123738..000000000 --- a/.github/workflows/features.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Features - -on: - pull_request: - push: - branches: - - main - -jobs: - build: - uses: ably/features/.github/workflows/sdk-features.yml@main - with: - repository-name: ably-cocoa - secrets: inherit diff --git a/.github/workflows/integration-test-iOS17_2.yaml b/.github/workflows/integration-test-iOS17_2.yaml new file mode 100644 index 000000000..485df62e8 --- /dev/null +++ b/.github/workflows/integration-test-iOS17_2.yaml @@ -0,0 +1,70 @@ +name: "Integration Test: iOS 17.2" + +on: + pull_request: + push: + branches: + - main + +# IMPORTANT NOTES: +# - Changes made to this file needs to replicated across other integration-test-*.yaml files. +# - The Fastlane lane name is duplicated in more than one place within this workflow. + +jobs: + check: + runs-on: macos-13 + + env: + LC_CTYPE: en_US.UTF-8 + LANG: en_US.UTF-8 + ABLY_ENV: sandbox + + steps: + - name: Check out SDK repo + uses: actions/checkout@v2 + + - name: Select Specific Xcode Version (15.1) + run: | + sudo xcode-select -s /Applications/Xcode_15.1.app + echo "Selected Xcode version:" + xcodebuild -version + + - name: Log environment information + run: ./Scripts/log-environment-information.sh + + - name: Check out xcparse repo + uses: actions/checkout@v3 + with: + repository: ably-forks/xcparse + ref: emit-test-case-info + path: xcparse + + - id: get-xcparse-commit-sha + name: Get xcparse commit SHA + run: | + cd xcparse + echo "::set-output name=sha::$(git rev-parse HEAD)" + + - name: "actions/cache@v3 (xcparse binary)" + uses: actions/cache@v3 + with: + path: xcparse/.build/debug/xcparse + key: ${{ runner.os }}-xcparse-${{ steps.get-xcparse-commit-sha.outputs.sha }} + + - name: Install Dependencies and Run Tests Continuously + env: + TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} + run: | + brew install xcbeautify + brew install coreutils # for `timeout` + make submodules + bundle install + make update_carthage_dependencies_ios + Scripts/continuously-run-tests-and-upload-results.sh --lane test_iOS17_2 + + - name: Upload .xcresult bundles + uses: actions/upload-artifact@v3 + if: always() + with: + name: xcresult-bundles.tar.gz + path: xcresult-bundles.tar.gz diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml deleted file mode 100644 index bbe44efbc..000000000 --- a/.github/workflows/integration-test.yaml +++ /dev/null @@ -1,119 +0,0 @@ -name: "Integration Test" - -on: - pull_request: - push: - branches: - - main - -# IMPORTANT NOTES: -# - Changes made to this file needs to replicated across other integration-test-*.yaml files. -# - The Fastlane lane name is duplicated in more than one place within this workflow. - -jobs: - check: - runs-on: macos-13 - - strategy: - fail-fast: false - matrix: - include: - - - platform: iOS - lane: test_iOS17_2 - - - platform: tvOS - lane: test_tvOS17_2 - - - platform: macOS - lane: test_macOS - - env: - LC_CTYPE: en_US.UTF-8 - LANG: en_US.UTF-8 - ABLY_ENV: sandbox - - steps: - - name: Check out repo - uses: actions/checkout@v2 - - - name: Select Xcode (15.1) - run: | - sudo xcode-select -s /Applications/Xcode_15.1.app - echo "Selected Xcode version:" - xcodebuild -version - - - name: Environment Info - run: ./Scripts/log-environment-information.sh - - - name: Check out xcparse repo - uses: actions/checkout@v3 - with: - repository: ably-forks/xcparse - ref: emit-test-case-info - path: xcparse - - - id: get-xcparse-commit-sha - name: Get xcparse commit SHA - run: | - cd xcparse - echo "::set-output name=sha::$(git rev-parse HEAD)" - - - name: "actions/cache@v3 (xcparse binary)" - uses: actions/cache@v3 - with: - path: xcparse/.build/debug/xcparse - key: ${{ runner.os }}-xcparse-${{ steps.get-xcparse-commit-sha.outputs.sha }} - - - name: Reset Simulators - run: xcrun simctl erase all - - - name: Install Dependencies - run: | - brew install xcbeautify - make submodules - bundle install - carthage update --use-xcframeworks --platform ${{ matrix.platform }} --no-use-binaries - - - name: Run Tests - run: bundle exec fastlane ${{ matrix.lane }} - - - name: Check Static Analyzer Output - id: analyzer-output - run: | - if [[ -z $(find ./derived_data -name "report-*.html") ]]; then - echo "Static Analyzer found no issues." - else - echo "Static Analyzer found some issues. HTML report will be available in Artifacts section. Failing build." - exit 1 - fi - - - name: Upload Static Analyzer Reports - if: ${{ failure() && steps.analyzer-output.outcome == 'failure' }} - uses: actions/upload-artifact@v2 - with: - name: static-analyzer-reports-${{ matrix.lane }} - path: ./derived_data/**/report-*.html - - - name: Upload Xcodebuild Logs - if: always() - uses: actions/upload-artifact@v2 - with: - name: xcodebuild-logs - path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload Test Output - if: always() - uses: actions/upload-artifact@v2 - with: - name: test-output - path: fastlane/test_output - - - name: Upload Test Results - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - Scripts/upload_test_results.sh --job-name "check (${{ matrix.platform }}, ${{ matrix.lane }})" - diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index 4dc7c17a2..366f58362 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -9,8 +9,6 @@ /* Begin PBXBuildFile section */ 1C05CF201AC1D7EB00687AC9 /* ARTRealtime+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C05CF1E1AC1D7EB00687AC9 /* ARTRealtime+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 1C1EC3FA1AE26A8B00AAADD7 /* ARTStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 96BF61551A35B40E004CF2B3 /* ARTStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 1C2B0FFD1B136A6D00E3633C /* ARTPresenceMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 1C2B0FFE1B136A6D00E3633C /* ARTPresenceMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */; }; 1C55427D1B148306003068DB /* ARTStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C55427C1B148306003068DB /* ARTStatus.m */; }; 1C578E1F1B3435CA00EF46EC /* ARTFallback.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C578E1D1B3435CA00EF46EC /* ARTFallback.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1C578E201B3435CA00EF46EC /* ARTFallback.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C578E1E1B3435CA00EF46EC /* ARTFallback.m */; }; @@ -69,15 +67,15 @@ 21113B5529DC6ACD00652C86 /* ARTTestClientOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5429DC6ACD00652C86 /* ARTTestClientOptions.m */; }; 21113B5629DC6ACD00652C86 /* ARTTestClientOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5429DC6ACD00652C86 /* ARTTestClientOptions.m */; }; 21113B5729DC6ACD00652C86 /* ARTTestClientOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5429DC6ACD00652C86 /* ARTTestClientOptions.m */; }; + 21113B5929DCA4C700652C86 /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5829DCA4C700652C86 /* DataGatherer.swift */; }; + 21113B5A29DCA4C700652C86 /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5829DCA4C700652C86 /* DataGatherer.swift */; }; + 21113B5B29DCA4C700652C86 /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5829DCA4C700652C86 /* DataGatherer.swift */; }; 21113B5F29DDDDD000652C86 /* LogAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5E29DDDDD000652C86 /* LogAdapterTests.swift */; }; 21113B6029DDDDD000652C86 /* LogAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5E29DDDDD000652C86 /* LogAdapterTests.swift */; }; 21113B6129DDDDD000652C86 /* LogAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5E29DDDDD000652C86 /* LogAdapterTests.swift */; }; 21113B6329DDF7E800652C86 /* ARTInternalLogTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 21113B6229DDF7E800652C86 /* ARTInternalLogTests.m */; }; 21113B6429DDF7ED00652C86 /* ARTInternalLogTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 21113B6229DDF7E800652C86 /* ARTInternalLogTests.m */; }; 21113B6529DDF7EF00652C86 /* ARTInternalLogTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 21113B6229DDF7E800652C86 /* ARTInternalLogTests.m */; }; - 21113B5929DCA4C700652C86 /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5829DCA4C700652C86 /* DataGatherer.swift */; }; - 21113B5A29DCA4C700652C86 /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5829DCA4C700652C86 /* DataGatherer.swift */; }; - 21113B5B29DCA4C700652C86 /* DataGatherer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21113B5829DCA4C700652C86 /* DataGatherer.swift */; }; 211A60D729D6D2C300D169C5 /* BackoffRetryDelayCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2132C31829D5E574000C4355 /* BackoffRetryDelayCalculatorTests.swift */; }; 211A60D829D6D2C400D169C5 /* BackoffRetryDelayCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2132C31829D5E574000C4355 /* BackoffRetryDelayCalculatorTests.swift */; }; 211A60D929D6D2C500D169C5 /* BackoffRetryDelayCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2132C31829D5E574000C4355 /* BackoffRetryDelayCalculatorTests.swift */; }; @@ -644,7 +642,6 @@ D710D58A21949D29008F54AD /* ARTMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE361BBC3201003ECEF8 /* ARTMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; D710D58B21949D29008F54AD /* ARTPresence.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE261BBB61C9003ECEF8 /* ARTPresence.h */; settings = {ATTRIBUTES = (Public, ); }; }; D710D58C21949D29008F54AD /* ARTPresenceMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D710D58D21949D29008F54AD /* ARTPresenceMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; D710D58E21949D29008F54AD /* ARTDataEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = EB3239461C59AB2C00892664 /* ARTDataEncoder.h */; settings = {ATTRIBUTES = (Private, ); }; }; D710D58F21949D29008F54AD /* ARTStats.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A507931A370F860077CDF8 /* ARTStats.h */; settings = {ATTRIBUTES = (Public, ); }; }; D710D59021949D29008F54AD /* ARTStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 96BF61551A35B40E004CF2B3 /* ARTStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -663,7 +660,6 @@ D710D5B021949D2A008F54AD /* ARTMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE361BBC3201003ECEF8 /* ARTMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; D710D5B121949D2A008F54AD /* ARTPresence.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE261BBB61C9003ECEF8 /* ARTPresence.h */; settings = {ATTRIBUTES = (Public, ); }; }; D710D5B221949D2A008F54AD /* ARTPresenceMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D710D5B321949D2A008F54AD /* ARTPresenceMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; D710D5B421949D2A008F54AD /* ARTDataEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = EB3239461C59AB2C00892664 /* ARTDataEncoder.h */; settings = {ATTRIBUTES = (Private, ); }; }; D710D5B521949D2A008F54AD /* ARTStats.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A507931A370F860077CDF8 /* ARTStats.h */; settings = {ATTRIBUTES = (Public, ); }; }; D710D5B621949D2A008F54AD /* ARTStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 96BF61551A35B40E004CF2B3 /* ARTStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -698,7 +694,6 @@ D710D5DB21949D78008F54AD /* ARTMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE371BBC3201003ECEF8 /* ARTMessage.m */; }; D710D5DC21949D78008F54AD /* ARTPresence.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE271BBB61C9003ECEF8 /* ARTPresence.m */; }; D710D5DD21949D78008F54AD /* ARTPresenceMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A507A01A377AA50077CDF8 /* ARTPresenceMessage.m */; }; - D710D5DE21949D78008F54AD /* ARTPresenceMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */; }; D710D5DF21949D78008F54AD /* ARTDataEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = EB3239421C59AB0400892664 /* ARTDataEncoder.m */; }; D710D5E021949D78008F54AD /* ARTStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A507941A370F860077CDF8 /* ARTStats.m */; }; D710D5E121949D78008F54AD /* ARTStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C55427C1B148306003068DB /* ARTStatus.m */; }; @@ -717,7 +712,6 @@ D710D60121949D79008F54AD /* ARTMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE371BBC3201003ECEF8 /* ARTMessage.m */; }; D710D60221949D79008F54AD /* ARTPresence.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE271BBB61C9003ECEF8 /* ARTPresence.m */; }; D710D60321949D79008F54AD /* ARTPresenceMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A507A01A377AA50077CDF8 /* ARTPresenceMessage.m */; }; - D710D60421949D79008F54AD /* ARTPresenceMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */; }; D710D60521949D79008F54AD /* ARTDataEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = EB3239421C59AB0400892664 /* ARTDataEncoder.m */; }; D710D60621949D79008F54AD /* ARTStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A507941A370F860077CDF8 /* ARTStats.m */; }; D710D60721949D79008F54AD /* ARTStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C55427C1B148306003068DB /* ARTStatus.m */; }; @@ -1114,8 +1108,6 @@ /* Begin PBXFileReference section */ 1C05CF1E1AC1D7EB00687AC9 /* ARTRealtime+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ARTRealtime+Private.h"; path = "PrivateHeaders/Ably/ARTRealtime+Private.h"; sourceTree = ""; }; 1C118A5B1AE63D89006AD19E /* Info-iOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; - 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARTPresenceMap.h; path = PrivateHeaders/Ably/ARTPresenceMap.h; sourceTree = ""; }; - 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTPresenceMap.m; sourceTree = ""; }; 1C55427C1B148306003068DB /* ARTStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTStatus.m; sourceTree = ""; }; 1C578E1D1B3435CA00EF46EC /* ARTFallback.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARTFallback.h; path = include/Ably/ARTFallback.h; sourceTree = ""; }; 1C578E1E1B3435CA00EF46EC /* ARTFallback.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTFallback.m; sourceTree = ""; }; @@ -1140,6 +1132,7 @@ 21113B4829DB60F800652C86 /* MockRetryDelayCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRetryDelayCalculator.swift; sourceTree = ""; }; 21113B5029DC6AAF00652C86 /* ARTTestClientOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTTestClientOptions.h; path = PrivateHeaders/Ably/ARTTestClientOptions.h; sourceTree = ""; }; 21113B5429DC6ACD00652C86 /* ARTTestClientOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTTestClientOptions.m; sourceTree = ""; }; + 21113B5829DCA4C700652C86 /* DataGatherer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGatherer.swift; sourceTree = ""; }; 21113B5E29DDDDD000652C86 /* LogAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogAdapterTests.swift; sourceTree = ""; }; 21113B6229DDF7E800652C86 /* ARTInternalLogTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTInternalLogTests.m; sourceTree = ""; }; 21113B5829DCA4C700652C86 /* DataGatherer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGatherer.swift; sourceTree = ""; }; @@ -1225,7 +1218,7 @@ 217FCF3D29D626E4006E5F2D /* StaticJitterCoefficients.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticJitterCoefficients.swift; sourceTree = ""; }; 217FCF3E29D626E4006E5F2D /* MockJitterCoefficientGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockJitterCoefficientGenerator.swift; sourceTree = ""; }; 217FCF4529D626F6006E5F2D /* DefaultJitterCoefficientGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultJitterCoefficientGeneratorTests.swift; sourceTree = ""; }; - 21DCDA8229F818630073A211 /* Ably-iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Ably-iOS.xctestplan"; path = "Test/Ably-iOS.xctestplan"; sourceTree = SOURCE_ROOT; }; + 21DCDA8229F818630073A211 /* Ably-iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "Ably-iOS.xctestplan"; path = "Test/Ably-iOS.xctestplan"; sourceTree = SOURCE_ROOT; }; 21DCDA8329F81B350073A211 /* Ably-macOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Ably-macOS.xctestplan"; sourceTree = ""; }; 21DCDA8429F81B550073A211 /* Ably-tvOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Ably-tvOS.xctestplan"; sourceTree = ""; }; 21E1C0E42A0DC47400A5DB65 /* ARTWebSocketFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTWebSocketFactory.h; path = PrivateHeaders/ARTWebSocketFactory.h; sourceTree = ""; }; @@ -2016,8 +2009,6 @@ 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */, D7F2B8B11E42410D00B65151 /* ARTPresenceMessage+Private.h */, 96A507A01A377AA50077CDF8 /* ARTPresenceMessage.m */, - 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */, - 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */, EB3239461C59AB2C00892664 /* ARTDataEncoder.h */, EB3239421C59AB0400892664 /* ARTDataEncoder.m */, 96A507931A370F860077CDF8 /* ARTStats.h */, @@ -2268,7 +2259,6 @@ 2105ED2229E7429E00DE6D67 /* ARTPaginatedResult+Subclass.h in Headers */, D746AE381BBC3201003ECEF8 /* ARTMessage.h in Headers */, D746AE471BBD6FE9003ECEF8 /* ARTQueuedMessage.h in Headers */, - 1C2B0FFD1B136A6D00E3633C /* ARTPresenceMap.h in Headers */, D798556023ECCDAF00946BE2 /* ARTVCDiffDecoder.h in Headers */, EB9C530B1CD7BEB100.8.557 /* ARTJsonLikeEncoder.h in Headers */, D74CBC0E212F076000D090E4 /* ARTConstants.h in Headers */, @@ -2511,7 +2501,6 @@ D710D4C321949B9C008F54AD /* ARTWebSocketTransport.h in Headers */, D710D4D621949BF9008F54AD /* ARTConnection.h in Headers */, D710D50421949C18008F54AD /* ARTRealtime+Private.h in Headers */, - D710D58D21949D29008F54AD /* ARTPresenceMap.h in Headers */, D5BB210E26AA98A800AA5F3E /* ARTStringifiable.h in Headers */, D710D56B21949CB9008F54AD /* ARTPushDeviceRegistrations.h in Headers */, 2132C31229D5E4C6000C4355 /* ARTBackoffRetryDelayCalculator.h in Headers */, @@ -2679,7 +2668,6 @@ D710D4C721949B9D008F54AD /* ARTWebSocketTransport.h in Headers */, D710D4E621949BFB008F54AD /* ARTConnection.h in Headers */, D710D51021949C19008F54AD /* ARTRealtime+Private.h in Headers */, - D710D5B321949D2A008F54AD /* ARTPresenceMap.h in Headers */, D710D57121949CBA008F54AD /* ARTPushDeviceRegistrations.h in Headers */, D710D62C21949DED008F54AD /* ARTNSMutableURLRequest+ARTPaginated.h in Headers */, 2132C31329D5E4C6000C4355 /* ARTBackoffRetryDelayCalculator.h in Headers */, @@ -3112,7 +3100,6 @@ D7D8F8261BC2C691009718F2 /* ARTTokenDetails.m in Sources */, D75A3F1C1DDE5B62002A4AAD /* ARTGCD.m in Sources */, 215F75FB2922B1DB009E0E76 /* ARTClientInformation.m in Sources */, - 1C2B0FFE1B136A6D00E3633C /* ARTPresenceMap.m in Sources */, 217D182A254222F500DFF07E /* ARTSRRandom.m in Sources */, 960D07941A45F1D800ED8C8C /* ARTCrypto.m in Sources */, D777EEE52063A64E002EBA03 /* ARTNSMutableRequest+ARTPush.m in Sources */, @@ -3368,7 +3355,6 @@ D710D4C821949BAA008F54AD /* ARTRealtimeTransport.m in Sources */, 217D184A254222F700DFF07E /* ARTSRPinningSecurityPolicy.m in Sources */, D710D49821949ACA008F54AD /* ARTRestChannel.m in Sources */, - D710D5DE21949D78008F54AD /* ARTPresenceMap.m in Sources */, D710D49A21949ACA008F54AD /* ARTRestPresence.m in Sources */, 21113B5629DC6ACD00652C86 /* ARTTestClientOptions.m in Sources */, D710D66E21949E78008F54AD /* ARTNSDate+ARTUtil.m in Sources */, @@ -3493,7 +3479,6 @@ D710D4CC21949BAB008F54AD /* ARTRealtimeTransport.m in Sources */, 217D1861254222FA00DFF07E /* ARTSRPinningSecurityPolicy.m in Sources */, D710D4A221949ACB008F54AD /* ARTRestChannel.m in Sources */, - D710D60421949D79008F54AD /* ARTPresenceMap.m in Sources */, D710D4A421949ACB008F54AD /* ARTRestPresence.m in Sources */, 21113B5729DC6ACD00652C86 /* ARTTestClientOptions.m in Sources */, D710D65421949E77008F54AD /* ARTNSDate+ARTUtil.m in Sources */, diff --git a/Scripts/continuously-run-tests-and-upload-results.sh b/Scripts/continuously-run-tests-and-upload-results.sh new file mode 100755 index 000000000..2b9346109 --- /dev/null +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +set -e + +# 1. Check dependencies. + +if ! which timeout > /dev/null +then + echo "You need to install timeout (\`brew install coreutils\` on macOS)." 2>&1 + exit 1 +fi + +# 2. Grab command-line options. + +# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash +while [[ "$#" -gt 0 ]]; do + case $1 in + -l|--lane) lane="$2"; shift ;; + -u|--upload-server-base-url) upload_server_base_url="$2"; shift ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +if [[ -z $lane ]] +then + echo "You need to specify the Fastlane lane to run (-l / --lane)." 2>&1 + exit 1 +fi + +# 3. Capture the time at which we started, to make sure we don’t exceed the +# maximum job running time. +started_at=`date +%s` +# https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration +let github_job_maximum_execution_seconds=6*60*60 +# We assume that the part of the job that ran before this script took at most 10 minutes, and that uploading the artifacts will take 30 minutes. +let must_end_by=$((started_at + github_job_maximum_execution_seconds - (10 + 30) * 60)) + +echo "We’ll make sure this script ends by `date -r${must_end_by}`." 2>&1 + +# 4. Run the tests in a loop and report the results. + +end_iteration_with_exit_value() { + if [[ -e xcresult-bundles ]] + then + echo "There are `du -d0 -h xcresult-bundles | awk -F '\t' '{print $1}'` of xcresult bundles to be uploaded." + tar --create --gzip xcresult-bundles > xcresult-bundles.tar.gz + echo "The file xcresult-bundles.tar.gz that will be uploaded as an artifact is `du -d0 -h xcresult-bundles.tar.gz | awk -F '\t' '{print $1}'`." + else + echo "There are no xcresult bundles to be uploaded." + fi + + exit $1 +} + +declare -i iteration=1 +while true +do + echo "BEGIN ITERATION ${iteration}" 2>&1 + + rm -rf fastlane/test_output + rm -rf xcodebuild_output + xcrun simctl erase all + + set +e + let allowed_execution_time=$must_end_by-`date +%s` + set -e + + if [[ $allowed_execution_time -le 0 ]]; then + echo "ITERATION ${iteration}: Allowed execution time reached. Exiting." 2>&1 + end_iteration_with_exit_value 0 + fi + + echo "ITERATION ${iteration}: Running fastlane with a timeout of ${allowed_execution_time} seconds." 2>&1 + + set +e + timeout --kill-after=20 ${allowed_execution_time} bundle exec fastlane --verbose $lane + tests_exit_value=$? + set -e + + if [[ tests_exit_value -eq 124 || tests_exit_value -eq 137 ]]; then + # Execution timed out. + echo "ITERATION ${iteration}: Cancelled the execution of fastlane since it exceeded timeout imposed by maximum GitHub running time. Terminating this script." + end_iteration_with_exit_value 0 + fi + + if [[ tests_exit_value -eq 0 ]] + then + echo "ITERATION ${iteration}: Tests passed." + else + echo "ITERATION ${iteration}: Tests failed (exit value ${tests_exit_value})." + fi + + echo "ITERATION ${iteration}: BEGIN xcodebuild raw output." + ls xcodebuild_output + cat xcodebuild_output/** + echo "ITERATION ${iteration}: END xcodebuild raw output." + + echo "ITERATION ${iteration}: Uploading results to observability server." + + # https://unix.stackexchange.com/questions/446847/conditionally-pass-params-to-a-script + optional_params=() + + if [[ ! -z $upload_server_base_url ]] + then + optional_params+=(--upload-server-base-url "${upload_server_base_url}") + fi + + set +e + ./Scripts/upload_test_results.sh \ + --iteration $iteration \ + "${optional_params[@]}" + # We defer failing the script until after copying the .xcresult bundle. + upload_exit_value=$? + set -e + + if [[ upload_exit_value -eq 0 ]] + then + echo "ITERATION ${iteration}: Upload succeeded." + else + echo "ITERATION ${iteration}: Upload failed (exit value ${upload_exit_value}). Will exit after copying result bundle." + fi + + # Find the .xcresult bundle and copy it to the directory that will eventually be saved as an artifact. + + result_bundles=$(find fastlane/test_output/sdk -name '*.xcresult') + if [[ -z $result_bundles ]] + then + number_of_result_bundles=0 + else + number_of_result_bundles=$(echo "${result_bundles}" | wc -l) + fi + + if [[ $number_of_result_bundles -eq 0 ]] + then + echo "ITERATION ${iteration}: No result bundles found." + end_iteration_with_exit_value 1 + fi + + if [[ $number_of_result_bundles -gt 1 ]] + then + echo -e "ITERATION ${iteration}: Multiple result bundles found:\n${result_bundles}" + end_iteration_with_exit_value 1 + fi + + echo "ITERATION ${iteration}: Report bundle found: ${result_bundles}" + + if [[ ! -d xcresult-bundles ]]; then + mkdir xcresult-bundles + fi + + mkdir "xcresult-bundles/${iteration}" + cp -r "${result_bundles}" "xcresult-bundles/${iteration}" + + echo "ITERATION ${iteration}: Copied result bundle to xcresult-bundles/${iteration}." + + if [[ upload_exit_value -ne 0 ]] + then + echo "ITERATION ${iteration}: Terminating due to failed upload." + end_iteration_with_exit_value $upload_exit_value + fi + + echo "END ITERATION ${iteration}" 2>&1 + + iteration+=1 +done diff --git a/Scripts/fetch-test-logs.sh b/Scripts/fetch-test-logs.sh new file mode 100755 index 000000000..60caa5e4b --- /dev/null +++ b/Scripts/fetch-test-logs.sh @@ -0,0 +1,493 @@ +#!/bin/bash + +# Retrieves the raw xcodebuild output for one or more test observability server +# uploads. +# +# Only works for tests that were run using the +# continuously-run-tests-and-upload-results script in this directory. + +# Usage: +# ./fetch-test-logs.sh --repo ably/ably-cocoa --test-case-id --filter [filter] +# +# or +# +# ./fetch-test-logs.sh --repo ably/ably-cocoa --upload-id + +# Options: +# +# -r / --repo : The 'org/name'-formatted name of the GitHub repo, for +# example 'ably/ably-cocoa'. +# +# -t / --test-case-id : The ID of a test case saved on the test +# observability server. Will fetch all uploads that match the filter specified +# using the --filter option, and then save the results inside the directory +# specified by the --output-directory option, in the following hierarchy, where +# uploads are split into those where the test case failed and those where it +# didn’t (which doesn’t necessarily imply that the test case passed; it may not +# have run at all): +# +# +# ├── info.json (contains metadata about the results in this directory) +# ├── upload_logs +# │   ├── failed +# │ │   └── xcodebuild-logs-upload-.txt, ... +# │   └── not_failed +# │ └── xcodebuild-logs-upload-.txt, ... +# └── test-case-logs (unless --no-extract-test-case-logs specified) +# ├── failed +# │   └── xcodebuild-logs-upload-.txt, ... +# └── not_failed +# └── xcodebuild-logs-upload-.txt, ... +# +# The upload_logs directory contains the full logs for that upload, and the +# test_case_logs directory contains just the segments of the logs that +# correspond to the specific test case. +# +# -d / --output-directory : Where to output the logs generated by the +# --test-case-id option to. Defaults to ./xcodebuild-logs-test-case--. +# +# -f / --filter : A URL query string describing a filter to be applied +# to the uploads fetched when using the --test-case-id option. For example, +# "branches[]=main&createdBefore=2022-02-20". +# +# -n / --no-extract-test-case-logs: Will cause the --test-case-id option to not +# attempt to extract the segment of the upload log that corresponds to the test +# case. +# +# -i / --upload-id : The ID of a upload saved on the test observability +# server. +# +# -u / --upload-server-base-url : Allows you to specify a URL to use as +# the upload server base URL. Defaults to https://test-observability.herokuapp.com. +# +# -o / --output-file : Where to output the logs generated by the +# --upload-id option to. Defaults to ./xcodebuild-logs-upload-.txt. +# +# -c / --cache-directory : Where to cache the GitHub logs. Defaults to +# ~/Library/Caches/com.ably.testObservabilityLogs. Will be created if doesn’t +# exist. +# +# -a / --no-use-github-auth: Will not prompt the user for an access token to be +# used for making requests to the GitHub API. Useful if all the required GitHub +# job logs are already cached locally. + +set -e + +check_dependencies() { + if ! which jq >/dev/null; then + echo "You need to install jq." 2>&1 + exit 1 + fi +} + +get_github_access_token() { + # https://stackoverflow.com/questions/3980668/how-to-get-a-password-from-a-shell-script-without-echoing#comment4260181_3980904 + read -s -p "Enter your GitHub access token (this will be used to fetch logs from the GitHub API): " github_access_token + + echo + + if [[ -z $github_access_token ]]; then + echo "You need to specify a GitHub access token." 2>&1 + exit 1 + fi + + echo +} + +# Args: +# $1: JSON representation of the test observability server upload +# $2: Path to write the logs to +fetch_and_write_logs_for_upload() { + upload_json=$1 + output_file=$2 + + # (TIL I learned that `echo` will interpret backslash sequences, which we + # don’t want. Appparently in general printf is recommended over echo.) + # https://stackoverflow.com/questions/43528202/prevent-echo-from-interpreting-backslash-escapes + github_repository=$(printf '%s' $upload_json | jq --raw-output '.githubRepository') + github_run_id=$(printf '%s' $upload_json | jq --raw-output '.githubRunId') + github_run_attempt=$(printf '%s' $upload_json | jq --raw-output '.githubRunAttempt') + github_job=$(printf '%s' $upload_json | jq --raw-output '.githubJob') + iteration=$(printf '%s' $upload_json | jq --raw-output '.iteration') + + echo "Upload comes from GitHub repository ${github_repository}. It has GitHub run ID ${github_run_id}, run attempt number ${github_run_attempt}, and job name ${github_job}. It corresponds to loop iteration ${iteration}." + + # Check whether we have a cached log for this job. + # (We cache the job logs because when running the tests continuously, with + # verbose logging enabled, a job log can be ~1.5GB.) + + log_file_name="github-log-${github_repository//\//-}-run-${github_run_id}-attempt-${github_run_attempt}-job-${github_job}" + log_file_path="${cache_directory}/${log_file_name}" + + if [[ -f "${log_file_path}" ]]; then + echo "GitHub job log file already exists at ${log_file_path}. Skipping download." 2>&1 + else + echo "GitHub job log file not yet downloaded." 2>&1 + + # (I wonder if this information that I’m fetching from GitHub is stuff that + # I should have just had in the upload in the first place? Not that + # important right now.) + + github_api_base_url="https://api.github.com" + + # From the GitHub API, fetch the jobs for this workflow run attempt. + # https://docs.github.com/en/rest/reference/actions#list-jobs-for-a-workflow-run-attempt + github_jobs_json=$(curl \ + --fail \ + -H "Accept: application/vnd.github.v3+json" \ + "${github_auth_curl_args[@]}" \ + "${github_api_base_url}/repos/${github_repository}/actions/runs/${github_run_id}/attempts/${github_run_attempt}/jobs") + + # From this list of jobs, find the one that corresponds to our upload. + github_job_id=$(printf "%s" $github_jobs_json | jq \ + --arg jobName "${github_job}" \ + '.jobs[] | select(.name == $jobName) | .id') + + if [[ -z $github_job_id ]]; then + echo "Could not find job with name ${github_job} in attempt ${github_run_attempt} of run ${github_run_id} in GitHub repository ${github_repository}." 2>&1 + exit 1 + fi + + echo "Upload corresponds to GitHub job ID ${github_job_id}. Downloading logs. This may take a while." + + # From the GitHub API, fetch the logs for this job and cache them. + # https://docs.github.com/en/rest/reference/actions#download-job-logs-for-a-workflow-run + + if [[ ! -d "${cache_directory}" ]]; then + mkdir -p "${cache_directory}" + fi + + curl \ + --fail \ + --location \ + -H "Accept: application/vnd.github.v3+json" \ + "${github_auth_curl_args[@]}" \ + "${github_api_base_url}/repos/${github_repository}/actions/jobs/${github_job_id}/logs" >"${log_file_path}.partial" + + mv "${log_file_path}.partial" "${log_file_path}" + + echo "Saved GitHub job logs to ${log_file_path}." + fi + + # Extract the part of the logs that corresponds to the raw xcodebuild output for this iteration. + # https://stackoverflow.com/a/18870500 + + echo "Finding xcodebuild output for iteration ${iteration}." + + xcodebuild_output_start_marker="ITERATION ${iteration}: BEGIN xcodebuild raw output" + xcodebuild_output_start_line_number=$(sed -n "/${xcodebuild_output_start_marker}/=" "${log_file_path}") + + if [[ -z "${xcodebuild_output_start_line_number}" ]]; then + echo "Couldn’t find start of xcodebuild raw output (couldn’t find marker \"${xcodebuild_output_start_marker}\")." 2>&1 + echo "This may be because the GitHub job hasn’t finished yet, or because the tests are not being run in a loop, or it may be an upload created before this functionality was implemented." 2>&1 + echo "You may need to delete the cached log file ${log_file_path}." 2>&1 + exit 1 + fi + + xcodebuild_output_end_marker="ITERATION ${iteration}: END xcodebuild raw output" + xcodebuild_output_end_line_number=$(sed -n "/${xcodebuild_output_end_marker}/=" "${log_file_path}") + + if [[ -z "${xcodebuild_output_end_line_number}" ]]; then + echo "Couldn’t find end of xcodebuild raw output (couldn’t find marker \"${xcodebuild_output_end_marker}\")." 2>&1 + exit 1 + fi + + # Strip the GitHub-added timestamps (which just correspond to the time that `cat` was executed on the log file, and hence aren’t of any use) from the start of each line. + + echo "Stripping GitHub timestamps." + + # https://arkit.co.in/print-given-range-of-lines-using-awk-perl-head-tail-and-python/ + sed -n "${xcodebuild_output_start_line_number},${xcodebuild_output_end_line_number} p" "${log_file_path}" | sed -e 's/^[^ ]* //' >"${output_file}" + + echo "Wrote xcodebuild output to ${output_file}." 2>&1 +} + +default_output_file_for_upload_id() { + echo "xcodebuild-logs-upload-$1.txt" +} + +run_for_test_case() { + # From the test observability server API, fetch the test case and extract its + # properties. + + echo "Fetching test case ${test_case_id} from ${upload_server_base_url}." 2>&1 + + test_case_json=$(curl --fail --header "Accept: application/json" "${upload_server_base_url}/repos/${repo}/test_cases/${test_case_id}") + + test_class_name=$(printf '%s' $test_case_json | jq --raw-output '.testClassName') + test_case_name=$(printf '%s' $test_case_json | jq --raw-output '.testCaseName') + + printf "Test case ${test_case_id} has test class name ${test_class_name} and test case name ${test_case_name}.\n\n" + + # From the test observability server API, fetch the filtered uploads. + + if [[ -z $filter ]]; then + filter_description="no filter" + filter_query="" + else + filter_description="filter ${filter}" + filter_query="?${filter}" + fi + + echo "Fetching uploads for test case ${test_case_id}, with ${filter_description}, from ${upload_server_base_url}." 2>&1 + + uploads_json=$(curl --fail --header "Accept: application/json" "${upload_server_base_url}/repos/${repo}/test_cases/${test_case_id}/uploads${filter_query}") + + number_of_uploads=$(printf '%s' $uploads_json | jq '. | length') + + if [[ ${number_of_uploads} -eq 1 ]]; then + echo "There is 1 upload". 2>&1 + else + echo "There are ${number_of_uploads} uploads". 2>&1 + fi + + echo + + mkdir "${output_directory}" + mkdir "${output_directory}/upload_logs" + mkdir "${output_directory}/test_case_logs" + + failed_upload_logs_output_directory="${output_directory}/upload_logs/failed" + mkdir "${failed_upload_logs_output_directory}" + not_failed_upload_logs_output_directory="${output_directory}/upload_logs/not_failed" + mkdir "${not_failed_upload_logs_output_directory}" + + failed_test_case_logs_output_directory="${output_directory}/test_case_logs/failed" + mkdir "${failed_test_case_logs_output_directory}" + not_failed_test_case_logs_output_directory="${output_directory}/test_case_logs/not_failed" + mkdir "${not_failed_test_case_logs_output_directory}" + + jq -n \ + --arg testCaseId "${test_case_id}" \ + --arg filter "${filter}" \ + --arg uploadServerBaseUrl "${upload_server_base_url}" \ + '{ fetchedAt: (now | todateiso8601), testCaseId: $testCaseId, filter: $filter, uploadServerBaseUrl: $uploadServerBaseUrl }' \ + >"${output_directory}/info.json" + + for ((i = 0; i < number_of_uploads; i += 1)); do + failed=$(printf '%s' $uploads_json | jq ".[${i}].failed") + upload_json=$(printf '%s' $uploads_json | jq ".[${i}].upload") + + upload_id=$(printf '%s' $upload_json | jq --raw-output '.id') + + echo "[$((i + 1)) of ${number_of_uploads}] Processing upload ${upload_id}." 2>&1 + + output_file_without_directory=$(default_output_file_for_upload_id "${upload_id}") + + if [[ $failed == "true" ]]; then + upload_log_output_file="${failed_upload_logs_output_directory}/${output_file_without_directory}" + test_case_log_output_file="${failed_test_case_logs_output_directory}/${output_file_without_directory}" + else + upload_log_output_file="${not_failed_upload_logs_output_directory}/${output_file_without_directory}" + test_case_log_output_file="${not_failed_test_case_logs_output_directory}/${output_file_without_directory}" + fi + + fetch_and_write_logs_for_upload "${upload_json}" "${upload_log_output_file}" + + if [[ -z "${no_extract_test_case_logs}" ]]; then + extract_logs_for_test_case "${test_class_name}" "${test_case_name}" "${upload_log_output_file}" "${test_case_log_output_file}" + fi + + echo + done +} + +# Args: +# $1: Test class name e.g. RealtimeClientPresenceTests +# $2: Test case name e.g. test__037__Presence__update__should_update_the_data_for_the_present_member_with_a_value() +# $3: Path of the xcodebuild logs for the entire test suite run +# $4: Path of where to write the test case logs to. +extract_logs_for_test_case() { + # Extract the part of the logs that corresponds to the raw xcodebuild output for this iteration. (We have similar code in fetch_and_write_logs_for_upload.) + + test_class_name=$1 + test_case_name=$2 + upload_log_file=$3 + output_file=$4 + + # (For some reason, the test case name in the observability server has + # trailing (), but in the xcodebuild logs it doesn’t. So strip them.) + sanitised_test_case_name="${test_case_name//[()]/}" + + echo "Finding logs for test class ${test_class_name}, test case ${test_case_name} in ${upload_log_file}." 2>&1 + + test_case_log_start_marker="Test Case.*${test_class_name} ${sanitised_test_case_name}.*started" + test_case_log_start_line_number=$(sed -n "/${test_case_log_start_marker}/=" "${upload_log_file}") + + if [[ -z "${test_case_log_start_line_number}" ]]; then + echo "Couldn’t find start of test case output (couldn’t find marker \"${test_case_log_start_marker}\")." 2>&1 + exit 1 + fi + + test_case_log_end_marker="Test Case.*${test_class_name} ${sanitised_test_case_name}.*(passed|failed)|Restarting after unexpected exit, crash, or test timeout in ${test_class_name}\/${sanitised_test_case_name}\(\)" + test_case_log_end_line_number=$(sed -En "/${test_case_log_end_marker}/=" "${upload_log_file}") + + if [[ -z "${test_case_log_end_line_number}" ]]; then + echo "Couldn’t find end of test case output (couldn’t find marker \"${test_case_log_end_marker}\")." 2>&1 + exit 1 + fi + + sed -n "${test_case_log_start_line_number},${test_case_log_end_line_number} p" "${upload_log_file}" >"${output_file}" + + echo "Wrote test case log to ${output_file}." 2>&1 +} + +run_for_upload() { + # From the test observability server API, fetch the upload, to find the + # GitHub run ID, attempt number, job name, and iteration. + + echo "Fetching upload ${upload_id} from ${upload_server_base_url}." 2>&1 + + upload_json=$(curl --fail --header "Accept: application/json" "${upload_server_base_url}/repos/${repo}/uploads/${upload_id}") + + fetch_and_write_logs_for_upload "${upload_json}" "${output_file}" +} + +check_dependencies + +# Grab and validate command-line options, and apply defaults. + +# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash +while [[ "$#" -gt 0 ]]; do + case $1 in + -r | --repo) + repo="$2" + shift + ;; + -t | --test-case-id) + if [[ -z "$2" ]]; then + echo "You must specify a test case ID when using the --test-case-id option." 2>&1 + exit 1 + fi + test_case_id="$2" + shift + ;; + -d | --output-directory) + if [[ -z "$2" ]]; then + echo "You must specify an output directory when using the --output-directory option." 2>&1 + exit 1 + fi + output_directory="$2" + shift + ;; + -f | --filter) + if [[ -z "$2" ]]; then + echo "You must specify a filter when using the --filter option." 2>&1 + exit 1 + fi + filter="$2" + shift + ;; + -n | --no-extract-test-case-logs) no_extract_test_case_logs="1" ;; + -i | --upload-id) + if [[ -z "$2" ]]; then + echo "You must specify an upload ID when using the --upload-id option." 2>&1 + exit 1 + fi + upload_id="$2" + shift + ;; + -u | --upload-server-base-url) + if [[ -z "$2" ]]; then + echo "You must specify a base URL when using the --upload-server-base-url option." 2>&1 + exit 1 + fi + upload_server_base_url="$2" + shift + ;; + -o | --output-file) + if [[ -z "$2" ]]; then + echo "You must specify an output file when using the --output-file option." 2>&1 + exit 1 + fi + output_file="$2" + shift + ;; + -c | --cache-directory) + if [[ -z "$2" ]]; then + echo "You must specify a cache directory when using the --cache-directory option." 2>&1 + exit 1 + fi + cache_directory="$2" + shift + ;; + -a | --no-use-github-auth) no_use_github_auth="1" ;; + *) + echo "Unknown parameter passed: $1" 2>&1 + exit 1 + ;; + esac + shift +done + +if [[ -z $repo ]]; then + echo "You need to specify a repo (-r / --repo)." 2>&1 + exit 1 +fi + +if [[ -z $test_case_id && -z $upload_id ]]; then + echo "You need to specify the test case ID (-t / --test-case-id) or upload ID (-i / --upload-id)." 2>&1 + exit 1 +fi + +if [[ -n $test_case_id && -n $upload_id ]]; then + echo "You cannot specify both a test case ID and an upload ID." 2>&1 + exit 1 +fi + +if [[ -n $test_case_id && -n $upload_id ]]; then + echo "You cannot specify both a test case ID and an upload ID." 2>&1 + exit 1 +fi + +if [[ -z $test_case_id && -n $output_directory ]]; then + echo "You can only specify an output directory with a test case ID (-t / --test-case-id)." 2>&1 + exit 1 +fi + +if [[ -z $output_directory ]]; then + output_directory="xcodebuild-logs-test-case-${test_case_id}-$(date -Iseconds)" +fi + +if [[ -z $test_case_id && -n $filter ]]; then + echo "You can only specify a filter with a test case ID (-t / --test-case-id)." 2>&1 + exit 1 +fi + +if [[ -z $test_case_id && -n $no_extract_test_case_logs ]]; then + echo "You can only specify the --no-extract-test-case-logs option with a test case ID (-t / --test-case-id)." 2>&1 + exit 1 +fi + +if [[ -z $upload_server_base_url ]]; then + upload_server_base_url="https://test-observability.herokuapp.com" +fi + +if [[ -z $upload_id && -n $output_file ]]; then + echo "You can only specify an output file with an upload ID (-i / --upload-id)." 2>&1 + exit 1 +fi + +if [[ -z $output_file ]]; then + output_file=$(default_output_file_for_upload_id "${upload_id}") +fi + +if [[ -z $cache_directory ]]; then + cache_directory="${HOME}/Library/Caches/com.ably.testObservabilityLogs" +fi + +github_auth_curl_args=() +if [[ -z $no_use_github_auth ]]; then + # Get the GitHub access token from the user. We don’t allow them to specify it on the command line. + github_access_token="" + get_github_access_token + github_auth_curl_args+=(-H "Authorization: token ${github_access_token}") +fi + +# Run the appropriate function based on arguments. + +if [[ -n $test_case_id ]]; then + run_for_test_case +elif [[ -n $upload_id ]]; then + run_for_upload +fi diff --git a/Scripts/set-ci-length-and-parallelism.sh b/Scripts/set-ci-length-and-parallelism.sh new file mode 100755 index 000000000..efd705441 --- /dev/null +++ b/Scripts/set-ci-length-and-parallelism.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +set -e + +# Usage: +# ./set-ci-length-and-parallelism.sh --workflows --jobs-per-workflow + +# Check dependencies. +if ! which yq > /dev/null; then + echo "You need to install yq." 2>&1 + exit 1 +fi + +# Grab and validate command-line options. + +# https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash +while [[ "$#" -gt 0 ]]; do + case $1 in + --workflows) + if [[ -z "$2" ]]; then + echo "You must specify the number of workflows." 2>&1 + exit 1 + fi + num_workflows="$2" + shift + ;; + --jobs-per-workflow) + if [[ -z "$2" ]]; then + echo "You must specify the number of jobs per workflow." 2>&1 + exit 1 + fi + jobs_per_workflow="$2" + shift + ;; + *) + echo "Unknown parameter passed: $1" 2>&1 + exit 1 + ;; + esac + shift +done + +if [[ -z $num_workflows ]]; then + echo "You need to specify the number of workflows (--workflows)." 2>&1 + exit 1 +fi + +if [[ ! $num_workflows =~ ^-?[0-9]+$ ]]; then + echo "The number of workflows must be a number." 2>&1 + exit 1 +fi + +if [[ $num_workflows -lt 1 ]]; then + echo "The number of workflows must be 1 or more." 2>&1 + exit 1 +fi + +if [[ -z $jobs_per_workflow ]]; then + echo "You need to specify the number of jobs per workflow (--jobs-per-workflow)." 2>&1 + exit 1 +fi + +if [[ ! $jobs_per_workflow =~ ^-?[0-9]+$ ]]; then + echo "The number of jobs per workflow must be a number." 2>&1 + exit 1 +fi + +if [[ $jobs_per_workflow -lt 1 ]]; then + echo "The number of jobs per workflow must be 1 or more." 2>&1 + exit 1 +fi + +workflow_file_without_extension=".github/workflows/integration-test-iOS16_2" +workflow_file_extension=".yaml" + +workflow_file="${workflow_file_without_extension}${workflow_file_extension}" +workflow_name=$(yq .name $workflow_file) + +# First, we apply the number of jobs per workflow. + +yq -i '(.jobs.check | key) = "check-1"' $workflow_file +yq -i "(.jobs.check-1.steps[] | select(.with.path == \"xcresult-bundles.tar.gz\")).with.name = \"xcresult-bundles-1.tar.gz\"" $workflow_file + +for ((i=2; i <= $jobs_per_workflow; i += 1)) +do + yq -i ".jobs.check-${i} = .jobs.check-$(($i-1))" $workflow_file + yq -i ".jobs.check-${i}.needs = [\"check-$(($i-1))\"]" $workflow_file + yq -i "(.jobs.check-${i}.steps[] | select(.with.path == \"xcresult-bundles.tar.gz\")).with.name = \"xcresult-bundles-${i}.tar.gz\"" $workflow_file +done + +# Now, we duplicate the workflow file the requested number of times. + +mv $workflow_file "${workflow_file_without_extension}-1${workflow_file_extension}" + +for ((i=1; i <= $num_workflows; i += 1)) +do + new_workflow_file="${workflow_file_without_extension}-${i}${workflow_file_extension}" + + if [[ $i -gt 1 ]]; then + cp "${workflow_file_without_extension}-$((i-1))${workflow_file_extension}" $new_workflow_file + fi + + yq -i ".name = \"${workflow_name} (workflow ${i})\"" $new_workflow_file +done diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index 2da822fd9..c708d585b 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -271,7 +271,7 @@ - (NSString *)createRecoveryKey_nosync { NSMutableDictionary *channelSerials = [NSMutableDictionary new]; for (ARTRealtimeChannelInternal *const channel in _realtime.channels.nosyncIterable) { if (channel.state_nosync == ARTRealtimeChannelAttached) { - channelSerials[channel.name] = channel.serial; + channelSerials[channel.name] = channel.channelSerial; } } diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m deleted file mode 100644 index 7649c9071..000000000 --- a/Source/ARTPresenceMap.m +++ /dev/null @@ -1,228 +0,0 @@ -#import "ARTPresenceMap.h" -#import "ARTPresenceMessage.h" -#import "ARTPresenceMessage+Private.h" -#import "ARTEventEmitter+Private.h" -#import "ARTInternalLog.h" - -typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { - ARTPresenceSyncInitialized, - ARTPresenceSyncStarted, //ItemType: nil - ARTPresenceSyncEnded, //ItemType: NSArray* - ARTPresenceSyncFailed //ItemType: ARTErrorInfo* -}; - -NSString *ARTPresenceSyncStateToStr(ARTPresenceSyncState state) { - switch (state) { - case ARTPresenceSyncInitialized: - return @"Initialized"; //0 - case ARTPresenceSyncStarted: - return @"Started"; //1 - case ARTPresenceSyncEnded: - return @"Ended"; //2 - case ARTPresenceSyncFailed: - return @"Failed"; //3 - } -} - -#pragma mark - ARTEvent - -@interface ARTEvent (PresenceSyncState) - -- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value; -+ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value; - -@end - -#pragma mark - ARTPresenceMap - -@interface ARTPresenceMap () { - ARTPresenceSyncState _syncState; - ARTEventEmitter *_syncEventEmitter; - NSMutableDictionary *_members; - NSMutableDictionary *_localMembers; // RTP17h -} - -@end - -@implementation ARTPresenceMap { - ARTInternalLog *_logger; -} - -- (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue logger:(ARTInternalLog *)logger { - self = [super init]; - if(self) { - _logger = logger; - [self reset]; - _syncSessionId = 0; - _syncState = ARTPresenceSyncInitialized; - _syncEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:queue]; - } - return self; -} - -- (NSDictionary *)members { - return _members; -} - -- (NSDictionary *)localMembers { - return _localMembers; -} - -- (BOOL)add:(ARTPresenceMessage *)message { - ARTPresenceMessage *latest = [_members objectForKey:message.memberKey]; - if ([message isNewerThan:latest]) { - ARTPresenceMessage *messageCopy = [message copy]; - switch (message.action) { - case ARTPresenceEnter: - case ARTPresenceUpdate: - messageCopy.action = ARTPresencePresent; - // intentional fallthrough - case ARTPresencePresent: - [self internalAdd:messageCopy]; - break; - case ARTPresenceLeave: - [self internalRemove:messageCopy]; - break; - default: - break; - } - return YES; - } - ARTLogDebug(_logger, @"Presence member \"%@\" with action %@ has been ignored", message.memberKey, ARTPresenceActionToStr(message.action)); - latest.syncSessionId = _syncSessionId; - return NO; -} - -- (void)internalAdd:(ARTPresenceMessage *)message { - [self internalAdd:message withSessionId:_syncSessionId]; -} - -- (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sessionId { - message.syncSessionId = sessionId; - [_members setObject:message forKey:message.memberKey]; - // Local member - if ([message.connectionId isEqualToString:self.delegate.connectionId]) { - _localMembers[message.clientId] = message; - ARTLogDebug(_logger, @"local member %@ with action %@ has been added", message.memberKey, ARTPresenceActionToStr(message.action).uppercaseString); - } -} - -- (void)internalRemove:(ARTPresenceMessage *)message { - [self internalRemove:message force:false]; -} - -- (void)internalRemove:(ARTPresenceMessage *)message force:(BOOL)force { - if ([message.connectionId isEqualToString:self.delegate.connectionId] && !message.isSynthesized) { - [_localMembers removeObjectForKey:message.clientId]; - } - - const BOOL syncInProgress = self.syncInProgress; - if (!force && syncInProgress) { - ARTLogDebug(_logger, @"%p \"%@\" should be removed after sync ends (syncInProgress=%d)", self, message.clientId, syncInProgress); - message.action = ARTPresenceAbsent; - // Should be removed after Sync ends - [self internalAdd:message withSessionId:message.syncSessionId]; - } - else { - [_members removeObjectForKey:message.memberKey]; - } -} - -- (void)cleanUpAbsentMembers { - ARTLogDebug(_logger, @"%p cleaning up absent members (syncSessionId=%lu)", self, (unsigned long)_syncSessionId); - NSSet *filteredMembers = [_members keysOfEntriesPassingTest:^BOOL(NSString *key, ARTPresenceMessage *message, BOOL *stop) { - return message.action == ARTPresenceAbsent; - }]; - for (NSString *key in filteredMembers) { - [self internalRemove:[_members objectForKey:key] force:true]; - } -} - -- (void)leaveMembersNotPresentInSync { - ARTLogDebug(_logger, @"%p leaving members not present in sync (syncSessionId=%lu)", self, (unsigned long)_syncSessionId); - for (ARTPresenceMessage *member in [_members allValues]) { - if (member.syncSessionId != _syncSessionId) { - // Handle members that have not been added or updated in the PresenceMap during the sync process - ARTPresenceMessage *leave = [member copy]; - [self internalRemove:member force:true]; - [self.delegate map:self didRemovedMemberNoLongerPresent:leave]; - } - } -} - -- (void)reenterLocalMembers { - ARTLogDebug(_logger, @"%p reentering local members", self); - for (ARTPresenceMessage *localMember in [_localMembers allValues]) { - ARTPresenceMessage *reenter = [localMember copy]; - [self.delegate map:self shouldReenterLocalMember:reenter]; - } - [self cleanUpAbsentMembers]; -} - -- (void)reset { - _members = [NSMutableDictionary new]; - _localMembers = [NSMutableDictionary new]; -} - -- (void)startSync { - ARTLogDebug(_logger, @"%p PresenceMap sync started", self); - _syncSessionId++; - _syncState = ARTPresenceSyncStarted; - [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; -} - -- (void)endSync { - ARTLogVerbose(_logger, @"%p PresenceMap sync ending", self); - [self cleanUpAbsentMembers]; - [self leaveMembersNotPresentInSync]; - _syncState = ARTPresenceSyncEnded; - - [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; - [_syncEventEmitter off]; - ARTLogDebug(_logger, @"%p PresenceMap sync ended", self); -} - -- (void)failsSync:(ARTErrorInfo *)error { - [self reset]; - _syncState = ARTPresenceSyncFailed; - [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] with:error]; - [_syncEventEmitter off]; -} - -- (void)onceSyncEnds:(void (^)(NSArray *))callback { - [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] callback:callback]; -} - -- (void)onceSyncFails:(ARTCallback)callback { - [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] callback:callback]; -} - -- (BOOL)syncComplete { - return !(_syncState == ARTPresenceSyncInitialized || _syncState == ARTPresenceSyncStarted); -} - -- (BOOL)syncInProgress { - return _syncState == ARTPresenceSyncStarted; -} - -#pragma mark private - -- (NSString *)memberKey:(ARTPresenceMessage *) message { - return [NSString stringWithFormat:@"%@:%@", message.connectionId, message.clientId]; -} - -@end - -#pragma mark - ARTEvent - -@implementation ARTEvent (PresenceSyncState) - -- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value { - return [self initWithString:[NSString stringWithFormat:@"ARTPresenceSyncState%@", ARTPresenceSyncStateToStr(value)]]; -} - -+ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value { - return [[self alloc] initWithPresenceSyncState:value]; -} - -@end diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 9a8fbde5f..247bfa72d 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -20,7 +20,6 @@ #import "ARTWebSocketTransport+Private.h" #import "ARTOSReachability.h" #import "ARTNSArray+ARTFunctional.h" -#import "ARTPresenceMap.h" #import "ARTProtocolMessage.h" #import "ARTProtocolMessage+Private.h" #import "ARTEventEmitter+Private.h" @@ -254,7 +253,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { _msgSerial = recoveryKey.msgSerial; // RTN16f for (NSString *const channelName in recoveryKey.channelSerials) { ARTRealtimeChannelInternal *const channel = [_channels get:channelName]; - channel.serial = recoveryKey.channelSerials[channelName]; // RTN16j + channel.channelSerial = recoveryKey.channelSerials[channelName]; // RTN16j } } } diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 50a1083fb..9e44c1de0 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -13,7 +13,6 @@ #import "ARTRealtimeChannelOptions.h" #import "ARTProtocolMessage.h" #import "ARTProtocolMessage+Private.h" -#import "ARTPresenceMap.h" #import "ARTNSArray+ARTFunctional.h" #import "ARTStatus.h" #import "ARTDefault.h" @@ -260,11 +259,9 @@ - (instancetype)initWithRealtime:(ARTRealtimeInternal *)realtime andName:(NSStri _restChannel = [_realtime.rest.channels _getChannel:self.name options:options addPrefix:true]; _state = ARTRealtimeChannelInitialized; _attachSerial = nil; - _presenceMap = [[ARTPresenceMap alloc] initWithQueue:_queue logger:self.logger]; - _presenceMap.delegate = self; + _realtimePresence = [[ARTRealtimePresenceInternal alloc] initWithChannel:self logger:self.logger]; _statesEventEmitter = [[ARTPublicEventEmitter alloc] initWithRest:_realtime.rest logger:logger]; _messagesEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueues:_queue userQueue:_userQueue]; - _presenceEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_queue]; _attachedEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_queue]; _detachedEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_queue]; _internalEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_queue]; @@ -313,9 +310,6 @@ - (ARTErrorInfo *)errorReason_nosync { } - (ARTRealtimePresenceInternal *)presence { - if (!_realtimePresence) { - _realtimePresence = [[ARTRealtimePresenceInternal alloc] initWithChannel:self logger:self.logger]; - } return _realtimePresence; } @@ -380,55 +374,6 @@ - (void)internalPostMessages:(id)data callback:(ARTCallback)callback { }); } -- (void)sync { - [self sync:nil]; -} - -- (void)sync:(ARTCallback)callback { - if (callback) { - ARTCallback userCallback = callback; - callback = ^(ARTErrorInfo *__nullable error) { - dispatch_async(self->_userQueue, ^{ - userCallback(error); - }); - }; - } - - switch (self.state_nosync) { - case ARTRealtimeChannelInitialized: - case ARTRealtimeChannelDetaching: - case ARTRealtimeChannelDetached: { - ARTErrorInfo *error = [ARTErrorInfo createWithCode:ARTErrorBadRequest - message:@"Unable to sync to channel; not attached."]; - ARTLogError(self.logger, @"%@", error.message); - if (callback) callback(error); - return; - } - default: - break; - } - - ARTLogVerbose(self.logger, @"R:%p C:%p (%@) requesting a sync operation", _realtime, self, self.name); - - ARTProtocolMessage *msg = [[ARTProtocolMessage alloc] init]; - msg.action = ARTProtocolMessageSync; - msg.channel = self.name; - msg.channelSerial = self.presenceMap.syncChannelSerial; - - [self.presenceMap startSync]; - [self.realtime send:msg sentCallback:^(ARTErrorInfo *error) { - if (error) { - ARTLogDebug(self.logger, @"R:%p C:%p (%@) SYNC request failed with %@", self->_realtime, self, self.name, error); - [self.presenceMap endSync]; - if (callback) callback(error); - } - else { - ARTLogDebug(self.logger, @"R:%p C:%p (%@) SYNC requested with success", self->_realtime, self, self.name); - if (callback) callback(nil); - } - } ackCallback:nil]; -} - - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(ARTStatusCallback)cb { switch (self.state_nosync) { case ARTRealtimeChannelSuspended: @@ -452,10 +397,6 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(ARTStatusCallb } } -- (ARTPresenceMap *)presenceMap { - return _presenceMap; -} - - (void)throwOnDisconnectedOrFailed { if (self.realtime.connection.state_nosync == ARTRealtimeFailed || self.realtime.connection.state_nosync == ARTRealtimeDisconnected) { [ARTException raise:@"realtime cannot perform action in disconnected or failed state" format:@"state: %d", (int)self.realtime.connection.state_nosync]; @@ -619,7 +560,7 @@ - (void)performTransitionToState:(ARTRealtimeChannelState)state withParams:(ARTC self.attachResume = true; break; case ARTRealtimeChannelSuspended: { - self.serial = nil; // RTP5a1 + self.channelSerial = nil; // RTP5a1 ARTRetryAttempt *const retryAttempt = [self.attachRetryState addRetryAttempt]; [_attachedEventEmitter emit:nil with:params.errorInfo]; @@ -636,15 +577,15 @@ - (void)performTransitionToState:(ARTRealtimeChannelState)state withParams:(ARTC self.attachResume = false; break; case ARTRealtimeChannelDetached: - self.serial = nil; // RTP5a1 - [self.presenceMap failsSync:params.errorInfo]; + self.channelSerial = nil; // RTP5a1 + [self.presence failsSync:params.errorInfo]; break; case ARTRealtimeChannelFailed: - self.serial = nil; // RTP5a1 + self.channelSerial = nil; // RTP5a1 self.attachResume = false; [_attachedEventEmitter emit:nil with:params.errorInfo]; [_detachedEventEmitter emit:nil with:params.errorInfo]; - [self.presenceMap failsSync:params.errorInfo]; + [self.presence failsSync:params.errorInfo]; break; default: break; @@ -713,7 +654,8 @@ - (void)onChannelMessage:(ARTProtocolMessage *)message { } - (void)setAttached:(ARTProtocolMessage *)message { - switch (self.state_nosync) { + ARTRealtimeChannelState state = self.state_nosync; + switch (state) { case ARTRealtimeChannelDetaching: case ARTRealtimeChannelFailed: // Ignore @@ -729,27 +671,17 @@ - (void)setAttached:(ARTProtocolMessage *)message { self.attachSerial = message.channelSerial; // RTL15b if (message.channelSerial) { - self.serial = message.channelSerial; + self.channelSerial = message.channelSerial; } - if (message.hasPresence) { - [self.presenceMap startSync]; - } - else { - // RTP1 - when an ATTACHED message is received without a HAS_PRESENCE flag, reset PresenceMap - [self.presenceMap startSync]; - [self.presenceMap endSync]; - ARTLogDebug(self.logger, @"R:%p C:%p (%@) PresenceMap has been reset", _realtime, self, self.name); - } - - if (self.state_nosync == ARTRealtimeChannelAttached) { + if (state == ARTRealtimeChannelAttached) { if (!message.resumed) { // RTL12 if (message.error != nil) { _errorReason = message.error; } - ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state_nosync previous:self.state_nosync event:ARTChannelEventUpdate reason:message.error resumed:message.resumed]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:state previous:state event:ARTChannelEventUpdate reason:message.error resumed:message.resumed]; [self emit:stateChange.event with:stateChange]; - [self.presenceMap reenterLocalMembers]; // RTP17i + [self.presence onAttached:message]; } return; } @@ -761,10 +693,8 @@ - (void)setAttached:(ARTProtocolMessage *)message { params = [[ARTChannelStateChangeParams alloc] initWithState:ARTStateOk]; } [self performTransitionToState:ARTRealtimeChannelAttached withParams:params]; + [self.presence onAttached:message]; [_attachedEventEmitter emit:nil with:nil]; - - [self.presence sendPendingPresence]; - [self.presenceMap reenterLocalMembers]; // RTP17i } - (void)setDetached:(ARTProtocolMessage *)message { @@ -883,7 +813,7 @@ - (void)onMessage:(ARTProtocolMessage *)pm { // RTL15b if (pm.channelSerial) { - self.serial = pm.channelSerial; + self.channelSerial = pm.channelSerial; } } @@ -891,65 +821,13 @@ - (void)onPresence:(ARTProtocolMessage *)message { ARTLogDebug(self.logger, @"RT:%p C:%p (%@) handle PRESENCE message", _realtime, self, self.name); // RTL15b if (message.channelSerial) { - self.serial = message.channelSerial; - } - - int i = 0; - ARTDataEncoder *dataEncoder = self.dataEncoder; - for (ARTPresenceMessage *p in message.presence) { - ARTPresenceMessage *presence = p; - if (presence.data && dataEncoder) { - NSError *decodeError = nil; - presence = [p decodeWithEncoder:dataEncoder error:&decodeError]; - if (decodeError != nil) { - ARTErrorInfo *errorInfo = [ARTErrorInfo wrap:[ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:decodeError.localizedFailureReason] prepend:@"Failed to decode data: "]; - ARTLogError(self.logger, @"RT:%p C:%p (%@) %@", _realtime, self, self.name, errorInfo.message); - } - } - - if (!presence.timestamp) { - presence.timestamp = message.timestamp; - } - - if (!presence.id) { - presence.id = [NSString stringWithFormat:@"%@:%d", message.id, i]; - } - - if ([self.presenceMap add:presence]) { - [self broadcastPresence:presence]; - } - - ++i; + self.channelSerial = message.channelSerial; } + [self.presence onMessage:message]; } - (void)onSync:(ARTProtocolMessage *)message { - self.presenceMap.syncMsgSerial = [message.msgSerial longLongValue]; - self.presenceMap.syncChannelSerial = message.channelSerial; - - if (!self.presenceMap.syncInProgress) { - [self.presenceMap startSync]; - } - else { - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync is in progress", _realtime, self, self.name); - } - - for (int i=0; i<[message.presence count]; i++) { - ARTPresenceMessage *presence = [message.presence objectAtIndex:i]; - if ([self.presenceMap add:presence]) { - [self broadcastPresence:presence]; - } - } - - if ([self isLastChannelSerial:message.channelSerial]) { - [self.presenceMap endSync]; - self.presenceMap.syncChannelSerial = nil; - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync ended", _realtime, self, self.name); - } -} - -- (void)broadcastPresence:(ARTPresenceMessage *)pm { - [self.presenceEventEmitter emit:[ARTEvent newWithPresenceAction:pm.action] with:pm]; + [self.presence onSync:message]; } - (void)onError:(ARTProtocolMessage *)msg { @@ -1036,7 +914,7 @@ - (void)attachAfterChecks:(ARTCallback)callback { ARTProtocolMessage *attachMessage = [[ARTProtocolMessage alloc] init]; attachMessage.action = ARTProtocolMessageAttach; attachMessage.channel = self.name; - attachMessage.channelSerial = self.serial; // RTL4c1 + attachMessage.channelSerial = self.channelSerial; // RTL4c1 attachMessage.params = self.options_nosync.params; attachMessage.flags = self.options_nosync.modes; @@ -1156,8 +1034,8 @@ - (void)detachAfterChecks:(ARTCallback)callback { }]; } - if (self.presenceMap.syncInProgress) { - [self.presenceMap failsSync:[ARTErrorInfo createWithCode:ARTErrorChannelOperationFailed message:@"channel is being DETACHED"]]; + if (self.presence.syncInProgress) { + [self.presence failsSync:[ARTErrorInfo createWithCode:ARTErrorChannelOperationFailed message:@"channel is being DETACHED"]]; } } @@ -1195,36 +1073,10 @@ - (void)startDecodeFailureRecoveryWithErrorInfo:(ARTErrorInfo *)error { } withParams:params]; } -#pragma mark - ARTPresenceMapDelegate - - (NSString *)connectionId { return _realtime.connection.id_nosync; } -- (void)map:(ARTPresenceMap *)map didRemovedMemberNoLongerPresent:(ARTPresenceMessage *)presence { - presence.action = ARTPresenceLeave; - presence.id = nil; - presence.timestamp = [NSDate date]; - [self broadcastPresence:presence]; - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) member \"%@\" no longer present", _realtime, self, self.name, presence.memberKey); -} - -- (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage *)presence { - [self.presence enterWithPresenceMessageId:presence.id clientId:presence.clientId data:presence.data callback:^(ARTErrorInfo *error) { - if (error != nil) { - NSString *message = [NSString stringWithFormat:@"Re-entering member \"%@\" is failed with code %ld (%@)", presence.memberKey, (long)error.code, error.message]; - ARTErrorInfo *reenterError = [ARTErrorInfo createWithCode:ARTErrorUnableToAutomaticallyReEnterPresenceChannel message:message]; - ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state_nosync previous:self.state_nosync event:ARTChannelEventUpdate reason:reenterError resumed:true]; // RTP17e - [self emit:stateChange.event with:stateChange]; - ARTLogWarn(self.logger, @"RT:%p C:%p (%@) Re-entering member \"%@\" is failed with code %ld (%@)", self->_realtime, self, self.name, presence.memberKey, (long)error.code, error.message); - } - else { - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) re-entered local member \"%@\"", self->_realtime, self, self.name, presence.memberKey); - } - }]; - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) re-entering local member \"%@\"", _realtime, self, self.name, presence.memberKey); -} - - (BOOL)exceedMaxSize:(NSArray *)messages { NSInteger size = 0; for (ARTMessage *message in messages) { diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 5d76cbd57..335b81415 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -1,16 +1,20 @@ #import "ARTRealtimePresence+Private.h" - #import "ARTRealtime+Private.h" #import "ARTChannel+Private.h" #import "ARTRealtimeChannel+Private.h" -#import "ARTPresenceMap.h" #import "ARTPresenceMessage.h" +#import "ARTPresenceMessage+Private.h" #import "ARTStatus.h" #import "ARTPresence+Private.h" #import "ARTDataQuery+Private.h" #import "ARTConnection+Private.h" #import "ARTNSArray+ARTFunctional.h" #import "ARTInternalLog.h" +#import "ARTEventEmitter+Private.h" +#import "ARTDataEncoder.h" +#import "ARTBaseMessage+Private.h" +#import "ARTProtocolMessage+Private.h" +#import "ARTEventEmitter+Private.h" #pragma mark - ARTRealtimePresenceQuery @@ -149,20 +153,54 @@ @interface ARTRealtimePresenceInternal () NS_ASSUME_NONNULL_END +typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { + ARTPresenceSyncInitialized, + ARTPresenceSyncStarted, //ItemType: nil + ARTPresenceSyncEnded, //ItemType: NSArray* + ARTPresenceSyncFailed //ItemType: ARTErrorInfo* +}; + +@interface ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value; ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value; + +@end + @implementation ARTRealtimePresenceInternal { __weak ARTRealtimeChannelInternal *_channel; // weak because channel owns self + __weak ARTRealtimeInternal *_realtime; dispatch_queue_t _userQueue; NSMutableArray *_pendingPresence; + ARTEventEmitter *_eventEmitter; + ARTDataEncoder *_dataEncoder; + + int64_t _syncMsgSerial; + NSString *_syncChannelSerial; + NSUInteger _syncSessionId; + ARTPresenceSyncState _syncState; + ARTEventEmitter *_syncEventEmitter; + + NSMutableDictionary *_members; + NSMutableDictionary *_internalMembers; // RTP17h } - (instancetype)initWithChannel:(ARTRealtimeChannelInternal *)channel logger:(ARTInternalLog *)logger { if (self = [super init]) { _channel = channel; - _userQueue = channel.realtime.rest.userQueue; - _queue = channel.realtime.rest.queue; + _realtime = channel.realtime; + _userQueue = _realtime.rest.userQueue; + _queue = _realtime.rest.queue; _pendingPresence = [NSMutableArray array]; _lastPresenceAction = ARTPresenceAbsent; _logger = logger; + _eventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_queue]; + _dataEncoder = _channel.dataEncoder; + _members = [NSMutableDictionary new]; + _internalMembers = [NSMutableDictionary new]; + _syncSessionId = 0; + _syncState = ARTPresenceSyncInitialized; + _syncEventEmitter = [[ARTInternalEventEmitter alloc] initWithQueue:_queue]; } return self; } @@ -189,7 +227,7 @@ - (void)get:(ARTRealtimePresenceQuery *)query callback:(ARTPresenceMessagesCallb return; case ARTRealtimeChannelSuspended: if (query && !query.waitForSync) { - if (callback) callback(self->_channel.presenceMap.members.allValues, nil); + if (callback) callback(self->_members.allValues, nil); return; } if (callback) callback(nil, [ARTErrorInfo createWithCode:ARTErrorPresenceStateIsOutOfSync message:@"presence state is out of sync due to the channel being SUSPENDED"]); @@ -208,19 +246,19 @@ - (void)get:(ARTRealtimePresenceQuery *)query callback:(ARTPresenceMessagesCallb callback(nil, error); return; } - const BOOL syncInProgress = self->_channel.presenceMap.syncInProgress; + const BOOL syncInProgress = self.syncInProgress; if (syncInProgress && query.waitForSync) { - ARTLogDebug(self.logger, @"R:%p C:%p (%@) sync is in progress, waiting until the presence members is synchronized", self->_channel.realtime, self->_channel, self->_channel.name); - [self->_channel.presenceMap onceSyncEnds:^(NSArray *members) { + ARTLogDebug(self.logger, @"R:%p C:%p (%@) sync is in progress, waiting until the presence members is synchronized", self->_realtime, self->_channel, self->_channel.name); + [self onceSyncEnds:^(NSArray *members) { NSArray *filteredMembers = [members artFilter:filterMemberBlock]; callback(filteredMembers, nil); }]; - [self->_channel.presenceMap onceSyncFails:^(ARTErrorInfo *error) { + [self onceSyncFails:^(ARTErrorInfo *error) { callback(nil, error); }]; } else { - ARTLogDebug(self.logger, @"R:%p C:%p (%@) returning presence members (syncInProgress=%d)", self->_channel.realtime, self->_channel, self->_channel.name, syncInProgress); - NSArray *members = self->_channel.presenceMap.members.allValues; + ARTLogDebug(self.logger, @"R:%p C:%p (%@) returning presence members (syncInProgress=%d)", self->_realtime, self->_channel, self->_channel.name, syncInProgress); + NSArray *members = self->_members.allValues; NSArray *filteredMembers = [members artFilter:filterMemberBlock]; callback(filteredMembers, nil); } @@ -346,7 +384,7 @@ - (void)enterOrUpdateAfterChecks:(ARTPresenceAction)action messageId:(NSString * msg.id = messageId; msg.clientId = clientId; msg.data = data; - msg.connectionId = _channel.realtime.connection.id_nosync; + msg.connectionId = _realtime.connection.id_nosync; [self publishPresence:msg callback:cb]; } @@ -402,7 +440,7 @@ - (void)leaveAfterChecks:(NSString *_Nullable)clientId data:(id)data callback:(A msg.action = ARTPresenceLeave; msg.data = data; msg.clientId = clientId; - msg.connectionId = _channel.realtime.connection.id_nosync; + msg.connectionId = _realtime.connection.id_nosync; [self publishPresence:msg callback:cb]; } @@ -415,7 +453,7 @@ - (BOOL)syncComplete { } - (BOOL)syncComplete_nosync { - return _channel.presenceMap.syncComplete; + return !(_syncState == ARTPresenceSyncInitialized || _syncState == ARTPresenceSyncStarted); } - (ARTEventListener *)subscribe:(ARTPresenceMessageCallback)callback { @@ -447,8 +485,8 @@ - (ARTEventListener *)subscribeWithAttachCallback:(ARTCallback)onAttach callback return; } [self->_channel _attach:onAttach]; - listener = [self->_channel.presenceEventEmitter on:cb]; - ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence subscribe to all actions", self->_channel.realtime, self->_channel, self->_channel.name); + listener = [_eventEmitter on:cb]; + ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence subscribe to all actions", self->_realtime, self->_channel, self->_channel.name); }); return listener; } @@ -482,8 +520,8 @@ - (ARTEventListener *)subscribe:(ARTPresenceAction)action onAttach:(ARTCallback) return; } [self->_channel _attach:onAttach]; - listener = [self->_channel.presenceEventEmitter on:[ARTEvent newWithPresenceAction:action] callback:cb]; - ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence subscribe to action %@", self->_channel.realtime, self->_channel, self->_channel.name, ARTPresenceActionToStr(action)); + listener = [_eventEmitter on:[ARTEvent newWithPresenceAction:action] callback:cb]; + ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence subscribe to action %@", self->_realtime, self->_channel, self->_channel.name, ARTPresenceActionToStr(action)); }); return listener; } @@ -491,25 +529,25 @@ - (ARTEventListener *)subscribe:(ARTPresenceAction)action onAttach:(ARTCallback) - (void)unsubscribe { dispatch_sync(_queue, ^{ [self _unsubscribe]; - ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence unsubscribe to all actions", self->_channel.realtime, self->_channel, self->_channel.name); + ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence unsubscribe to all actions", self->_realtime, self->_channel, self->_channel.name); }); } - (void)_unsubscribe { - [_channel.presenceEventEmitter off]; + [_eventEmitter off]; } - (void)unsubscribe:(ARTEventListener *)listener { dispatch_sync(_queue, ^{ - [self->_channel.presenceEventEmitter off:listener]; - ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence unsubscribe to all actions", self->_channel.realtime, self->_channel, self->_channel.name); + [_eventEmitter off:listener]; + ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence unsubscribe to all actions", self->_realtime, self->_channel, self->_channel.name); }); } - (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener { dispatch_sync(_queue, ^{ - [self->_channel.presenceEventEmitter off:[ARTEvent newWithPresenceAction:action] listener:listener]; - ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence unsubscribe to action %@", self->_channel.realtime, self->_channel, self->_channel.name, ARTPresenceActionToStr(action)); + [_eventEmitter off:[ARTEvent newWithPresenceAction:action] listener:listener]; + ARTLogVerbose(self.logger, @"R:%p C:%p (%@) presence unsubscribe to action %@", self->_realtime, self->_channel, self->_channel.name, ARTPresenceActionToStr(action)); }); } @@ -520,8 +558,8 @@ - (void)addPendingPresence:(ARTProtocolMessage *)msg callback:(ARTStatusCallback - (void)publishPresence:(ARTPresenceMessage *)msg callback:(ARTCallback)callback { if (msg.clientId == nil) { - NSString *authClientId = _channel.realtime.auth.clientId_nosync; - BOOL connected = _channel.realtime.connection.state_nosync == ARTRealtimeConnected; + NSString *authClientId = _realtime.auth.clientId_nosync; + BOOL connected = _realtime.connection.state_nosync == ARTRealtimeConnected; if (connected && (authClientId == nil || [authClientId isEqualToString:@"*"])) { if (callback) { callback([ARTErrorInfo createWithCode:ARTStateNoClientId message:@"Invalid attempt to publish presence message without clientId."]); @@ -530,8 +568,8 @@ - (void)publishPresence:(ARTPresenceMessage *)msg callback:(ARTCallback)callback } } - if (!_channel.realtime.connection.isActive_nosync) { - if (callback) callback([_channel.realtime.connection error_nosync]); + if (!_realtime.connection.isActive_nosync) { + if (callback) callback([_realtime.connection error_nosync]); return; } @@ -549,7 +587,7 @@ - (void)publishPresence:(ARTPresenceMessage *)msg callback:(ARTCallback)callback if (msg.data && _channel.dataEncoder) { ARTDataEncoderOutput *encoded = [_channel.dataEncoder encode:msg.data]; if (encoded.errorInfo) { - ARTLogWarn(self.logger, @"RT:%p C:%p (%@) error encoding presence message: %@", _channel.realtime, self, _channel.name, encoded.errorInfo); + ARTLogWarn(self.logger, @"RT:%p C:%p (%@) error encoding presence message: %@", _realtime, self, _channel.name, encoded.errorInfo); } msg.data = encoded.data; msg.encoding = encoded.encoding; @@ -574,7 +612,7 @@ - (void)publishPresence:(ARTPresenceMessage *)msg callback:(ARTCallback)callback break; } case ARTRealtimeChannelAttached: { - [_channel.realtime send:pm sentCallback:nil ackCallback:^(ARTStatus *status) { + [_realtime send:pm sentCallback:nil ackCallback:^(ARTStatus *status) { if (callback) callback(status.errorInfo); }]; break; @@ -610,7 +648,7 @@ - (void)sendPendingPresence { [_pendingPresence addObject:qm]; continue; } - [_channel.realtime send:qm.msg sentCallback:nil ackCallback:qm.ackCallback]; + [_realtime send:qm.msg sentCallback:nil ackCallback:qm.ackCallback]; } } @@ -622,4 +660,263 @@ - (void)failPendingPresence:(ARTStatus *)status { } } +- (void)broadcast:(ARTPresenceMessage *)pm { + [_eventEmitter emit:[ARTEvent newWithPresenceAction:pm.action] with:pm]; +} + +- (void)onAttached:(ARTProtocolMessage *)message { + [self startSync]; + if (!message.hasPresence) { + // RTP1 - when an ATTACHED message is received without a HAS_PRESENCE flag, reset PresenceMap + [self endSync]; + ARTLogDebug(self.logger, @"R:%p C:%p (%@) PresenceMap has been reset", _realtime, self, _channel.name); + } + [self sendPendingPresence]; + [self reenterInternalMembers]; // RTP17i +} + +- (void)onMessage:(ARTProtocolMessage *)message { + int i = 0; + for (ARTPresenceMessage *p in message.presence) { + ARTPresenceMessage *presence = p; + if (presence.data && _dataEncoder) { + NSError *decodeError = nil; + presence = [p decodeWithEncoder:_dataEncoder error:&decodeError]; + if (decodeError != nil) { + ARTErrorInfo *errorInfo = [ARTErrorInfo wrap:[ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:decodeError.localizedFailureReason] prepend:@"Failed to decode data: "]; + ARTLogError(self.logger, @"RT:%p C:%p (%@) %@", _realtime, _channel, _channel.name, errorInfo.message); + } + } + + if (!presence.timestamp) { + presence.timestamp = message.timestamp; + } + + if (!presence.id) { + presence.id = [NSString stringWithFormat:@"%@:%d", message.id, i]; + } + + if ([self add:presence]) { + [self broadcast:presence]; + } + + ++i; + } +} + +- (void)onSync:(ARTProtocolMessage *)message { + _syncMsgSerial = [message.msgSerial longLongValue]; + _syncChannelSerial = message.channelSerial; + + if (!self.syncInProgress) { + [self startSync]; + } + else { + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync is in progress", _realtime, _channel, _channel.name); + } + + [self onMessage:message]; + + if ([_channel isLastChannelSerial:message.channelSerial]) { + [self endSync]; + _syncChannelSerial = nil; + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync ended", _realtime, _channel, _channel.name); + } +} + +- (NSString *)connectionId { + return _realtime.connection.id_nosync; +} + +- (void)didRemovedMemberNoLongerPresent:(ARTPresenceMessage *)pm { + pm.action = ARTPresenceLeave; + pm.id = nil; + pm.timestamp = [NSDate date]; + [self broadcast:pm]; + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) member \"%@\" no longer present", _realtime, _channel, _channel.name, pm.memberKey); +} + +- (void)reenterInternalMembers { + ARTLogDebug(self.logger, @"%p reentering local members", self); + for (ARTPresenceMessage *member in [self.internalMembers allValues]) { + [self enterWithPresenceMessageId:member.id clientId:member.clientId data:member.data callback:^(ARTErrorInfo *error) { + if (error != nil) { + NSString *message = [NSString stringWithFormat:@"Re-entering member \"%@\" is failed with code %ld (%@)", member.memberKey, (long)error.code, error.message]; + ARTErrorInfo *reenterError = [ARTErrorInfo createWithCode:ARTErrorUnableToAutomaticallyReEnterPresenceChannel message:message]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self->_channel.state_nosync previous:self->_channel.state_nosync event:ARTChannelEventUpdate reason:reenterError resumed:true]; // RTP17e + + [self->_channel emit:stateChange.event with:stateChange]; + + ARTLogWarn(self.logger, @"RT:%p C:%p (%@) Re-entering member \"%@\" is failed with code %ld (%@)", self->_realtime, self->_channel, self->_channel.name, member.memberKey, (long)error.code, error.message); + } + else { + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) re-entered local member \"%@\"", self->_realtime, self->_channel, self->_channel.name, member.memberKey); + } + }]; + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) re-entering local member \"%@\"", _realtime, _channel, _channel.name, member.memberKey); + } + [self cleanUpAbsentMembers]; +} + +#pragma mark - Presence Map + +- (NSDictionary *)members { + return _members; +} + +- (NSDictionary *)internalMembers { + return _internalMembers; +} + +- (BOOL)add:(ARTPresenceMessage *)message { + ARTPresenceMessage *latest = [_members objectForKey:message.memberKey]; + if ([message isNewerThan:latest]) { + ARTPresenceMessage *messageCopy = [message copy]; + switch (message.action) { + case ARTPresenceEnter: + case ARTPresenceUpdate: + messageCopy.action = ARTPresencePresent; + // intentional fallthrough + case ARTPresencePresent: + [self internalAdd:messageCopy]; + break; + case ARTPresenceLeave: + [self internalRemove:messageCopy]; + break; + default: + break; + } + return YES; + } + ARTLogDebug(_logger, @"Presence member \"%@\" with action %@ has been ignored", message.memberKey, ARTPresenceActionToStr(message.action)); + latest.syncSessionId = _syncSessionId; + return NO; +} + +- (void)internalAdd:(ARTPresenceMessage *)message { + [self internalAdd:message withSessionId:_syncSessionId]; +} + +- (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sessionId { + message.syncSessionId = sessionId; + [_members setObject:message forKey:message.memberKey]; + // Local member + if ([message.connectionId isEqualToString:self.connectionId]) { + _internalMembers[message.clientId] = message; + ARTLogDebug(_logger, @"local member %@ with action %@ has been added", message.memberKey, ARTPresenceActionToStr(message.action).uppercaseString); + } +} + +- (void)internalRemove:(ARTPresenceMessage *)message { + [self internalRemove:message force:false]; +} + +- (void)internalRemove:(ARTPresenceMessage *)message force:(BOOL)force { + if ([message.connectionId isEqualToString:self.connectionId] && !message.isSynthesized) { + [_internalMembers removeObjectForKey:message.clientId]; + } + + const BOOL syncInProgress = self.syncInProgress; + if (!force && syncInProgress) { + ARTLogDebug(_logger, @"%p \"%@\" should be removed after sync ends (syncInProgress=%d)", self, message.clientId, syncInProgress); + message.action = ARTPresenceAbsent; + // Should be removed after Sync ends + [self internalAdd:message withSessionId:message.syncSessionId]; + } + else { + [_members removeObjectForKey:message.memberKey]; + } +} + +- (void)cleanUpAbsentMembers { + ARTLogDebug(_logger, @"%p cleaning up absent members (syncSessionId=%lu)", self, (unsigned long)_syncSessionId); + NSSet *filteredMembers = [_members keysOfEntriesPassingTest:^BOOL(NSString *key, ARTPresenceMessage *message, BOOL *stop) { + return message.action == ARTPresenceAbsent; + }]; + for (NSString *key in filteredMembers) { + [self internalRemove:[_members objectForKey:key] force:true]; + } +} + +- (void)leaveMembersNotPresentInSync { + ARTLogDebug(_logger, @"%p leaving members not present in sync (syncSessionId=%lu)", self, (unsigned long)_syncSessionId); + for (ARTPresenceMessage *member in [_members allValues]) { + if (member.syncSessionId != _syncSessionId) { + // Handle members that have not been added or updated in the PresenceMap during the sync process + ARTPresenceMessage *leave = [member copy]; + [self internalRemove:member force:true]; + [self didRemovedMemberNoLongerPresent:leave]; + } + } +} + +- (void)reset { + _members = [NSMutableDictionary new]; + _internalMembers = [NSMutableDictionary new]; +} + +- (void)startSync { + ARTLogDebug(_logger, @"%p PresenceMap sync started", self); + _syncSessionId++; + _syncState = ARTPresenceSyncStarted; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; +} + +- (void)endSync { + ARTLogVerbose(_logger, @"%p PresenceMap sync ending", self); + [self cleanUpAbsentMembers]; + [self leaveMembersNotPresentInSync]; + _syncState = ARTPresenceSyncEnded; + + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; + [_syncEventEmitter off]; + ARTLogDebug(_logger, @"%p PresenceMap sync ended", self); +} + +- (void)failsSync:(ARTErrorInfo *)error { + [self reset]; + _syncState = ARTPresenceSyncFailed; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] with:error]; + [_syncEventEmitter off]; +} + +- (void)onceSyncEnds:(void (^)(NSArray *))callback { + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] callback:callback]; +} + +- (void)onceSyncFails:(ARTCallback)callback { + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] callback:callback]; +} + +- (BOOL)syncInProgress { + return _syncState == ARTPresenceSyncStarted; +} + +@end + +#pragma mark - ARTEvent + +NSString *ARTPresenceSyncStateToStr(ARTPresenceSyncState state) { + switch (state) { + case ARTPresenceSyncInitialized: + return @"Initialized"; //0 + case ARTPresenceSyncStarted: + return @"Started"; //1 + case ARTPresenceSyncEnded: + return @"Ended"; //2 + case ARTPresenceSyncFailed: + return @"Failed"; //3 + } +} + +@implementation ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value { + return [self initWithString:[NSString stringWithFormat:@"ARTPresenceSyncState%@", ARTPresenceSyncStateToStr(value)]]; +} + ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value { + return [[self alloc] initWithPresenceSyncState:value]; +} + @end diff --git a/Source/Ably.modulemap b/Source/Ably.modulemap index 2d9bda1d4..327d34883 100644 --- a/Source/Ably.modulemap +++ b/Source/Ably.modulemap @@ -99,7 +99,6 @@ framework module Ably { header "ARTDeviceIdentityTokenDetails+Private.h" header "ARTHttp.h" header "ARTLocalDeviceStorage.h" - header "ARTPresenceMap.h" header "ARTInternalLogCore.h" header "ARTInternalLogCore+Testing.h" header "ARTDataEncoder.h" diff --git a/Source/PrivateHeaders/Ably/ARTPresenceMap.h b/Source/PrivateHeaders/Ably/ARTPresenceMap.h deleted file mode 100644 index 99ea6c957..000000000 --- a/Source/PrivateHeaders/Ably/ARTPresenceMap.h +++ /dev/null @@ -1,56 +0,0 @@ -#import -#import - -@class ARTPresenceMap; -@class ARTPresenceMessage; -@class ARTErrorInfo; -@class ARTInternalLog; - -NS_ASSUME_NONNULL_BEGIN - -@protocol ARTPresenceMapDelegate -@property (nonatomic, readonly) NSString *connectionId; -- (void)map:(ARTPresenceMap *)map didRemovedMemberNoLongerPresent:(ARTPresenceMessage *)presence; -- (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage *)presence; -@end - -/// Used to maintain a list of members present on a channel -@interface ARTPresenceMap : NSObject - -/// List of members. -/// The key is the memberKey and the value is the latest relevant ARTPresenceMessage for that clientId. -@property (readonly, atomic) NSDictionary *members; - -/// List of internal members. -/// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. -@property (readonly, atomic) NSMutableDictionary *localMembers; - -@property (nullable, weak) id delegate; // weak because delegates outlive their counterpart - -@property (readwrite, nonatomic) int64_t syncMsgSerial; -@property (readwrite, nonatomic, nullable) NSString *syncChannelSerial; -@property (readonly, nonatomic) NSUInteger syncSessionId; -@property (readonly, nonatomic, getter=syncComplete) BOOL syncComplete; -@property (readonly, nonatomic, getter=syncInProgress) BOOL syncInProgress; - -- (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue logger:(ARTInternalLog *)logger; - -- (BOOL)add:(ARTPresenceMessage *)message; -- (void)reset; - -- (void)startSync; -- (void)endSync; -- (void)failsSync:(ARTErrorInfo *)error; - -- (void)onceSyncEnds:(void (^)(NSArray *))callback; -- (void)onceSyncFails:(ARTCallback)callback; - -- (void)internalAdd:(ARTPresenceMessage *)message; -- (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sessionId; - -- (void)reenterLocalMembers; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h b/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h index a8b848857..6f8666823 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h +++ b/Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h @@ -5,7 +5,6 @@ #import #import -#import #import #import #import @@ -18,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface ARTRealtimeChannelInternal : ARTChannel +@interface ARTRealtimeChannelInternal : ARTChannel @property (readonly) ARTRealtimePresenceInternal *presence; #if TARGET_OS_IPHONE @@ -28,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readwrite, nonatomic) ARTRealtimeChannelState state; @property (readonly, nonatomic, nullable) ARTErrorInfo *errorReason; @property (readonly, nullable, getter=getOptions_nosync) ARTRealtimeChannelOptions *options_nosync; +@property (nonatomic, readonly) NSString *connectionId; - (ARTRealtimeChannelState)state_nosync; - (ARTErrorInfo *)errorReason_nosync; @@ -37,14 +37,12 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, weak, nonatomic) ARTRealtimeInternal *realtime; // weak because realtime owns self @property (readonly, nonatomic) ARTRestChannelInternal *restChannel; @property (readwrite, nonatomic, nullable) NSString *attachSerial; -@property (readwrite, nonatomic, nullable) NSString *serial; // CP2b +@property (readwrite, nonatomic, nullable) NSString *channelSerial; // CP2b @property (readonly, nullable, getter=getClientId) NSString *clientId; @property (readonly, nonatomic) ARTEventEmitter *internalEventEmitter; @property (readonly, nonatomic) ARTEventEmitter *statesEventEmitter; @property (readonly, nonatomic) ARTEventEmitter, ARTMessage *> *messagesEventEmitter; -@property (readonly, nonatomic) ARTEventEmitter *presenceEventEmitter; -@property (readwrite, nonatomic) ARTPresenceMap *presenceMap; @property (readwrite, nonatomic) BOOL attachResume; - (instancetype)initWithRealtime:(ARTRealtimeInternal *)realtime andName:(NSString *)name withOptions:(ARTRealtimeChannelOptions *)options logger:(ARTInternalLog *)logger; @@ -82,11 +80,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)setFailed:(ARTChannelStateChangeParams *)params; - (void)throwOnDisconnectedOrFailed; -- (void)broadcastPresence:(ARTPresenceMessage *)pm; - (void)detachChannel:(ARTChannelStateChangeParams *)params; -- (void)sync; -- (void)sync:(nullable ARTCallback)callback; +- (void)emit:(ARTChannelEvent)event with:(ARTChannelStateChange *)data; @end diff --git a/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h b/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h index 05a5d157f..e3a09847e 100644 --- a/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h +++ b/Source/PrivateHeaders/Ably/ARTRealtimePresence+Private.h @@ -5,17 +5,19 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTRealtimePresenceInternal : NSObject +@property (nonatomic, readonly) NSString *connectionId; +@property (readonly, nonatomic) ARTEventEmitter *eventEmitter; + - (instancetype)initWithChannel:(ARTRealtimeChannelInternal *)channel logger:(ARTInternalLog *)logger; - (void)_unsubscribe; - (BOOL)syncComplete_nosync; -- (void)sendPendingPresence; - (void)failPendingPresence:(ARTStatus *)status; +- (void)broadcast:(ARTPresenceMessage *)pm; -/* - * Helper method for enforcing RTP17g, where in addition to clientId and data, message id is required. - */ -- (void)enterWithPresenceMessageId:(NSString *)messageId clientId:(NSString *)clientId data:(id)data callback:(ARTCallback)cb; +- (void)onMessage:(ARTProtocolMessage *)message; +- (void)onSync:(ARTProtocolMessage *)message; +- (void)onAttached:(ARTProtocolMessage *)message; @property (nonatomic) dispatch_queue_t queue; @property (readwrite, nonatomic) ARTPresenceAction lastPresenceAction; @@ -31,4 +33,38 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface ARTRealtimePresenceInternal (PresenceMap) + +/// List of members. +/// The key is the memberKey and the value is the latest relevant ARTPresenceMessage for that clientId. +@property (readonly, atomic) NSDictionary *members; + +/// List of internal members. +/// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. +@property (readonly, atomic) NSMutableDictionary *internalMembers; + +@property (readonly, nonatomic) NSUInteger syncSessionId; +@property (readonly, nonatomic) BOOL syncComplete; +@property (readonly, nonatomic) BOOL syncInProgress; + +- (instancetype)init UNAVAILABLE_ATTRIBUTE; +- (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue logger:(ARTInternalLog *)logger; + +- (BOOL)add:(ARTPresenceMessage *)message; +- (void)reset; + +- (void)startSync; +- (void)endSync; +- (void)failsSync:(ARTErrorInfo *)error; + +- (void)onceSyncEnds:(void (^)(NSArray *))callback; +- (void)onceSyncFails:(ARTCallback)callback; + +- (void)internalAdd:(ARTPresenceMessage *)message; +- (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sessionId; + +- (void)cleanUpAbsentMembers; + +@end + NS_ASSUME_NONNULL_END diff --git a/Source/include/Ably/ARTTypes.h b/Source/include/Ably/ARTTypes.h index 0db42f07c..afdcda17a 100644 --- a/Source/include/Ably/ARTTypes.h +++ b/Source/include/Ably/ARTTypes.h @@ -482,6 +482,9 @@ typedef void (^ARTConnectionStateCallback)(ARTConnectionStateChange *stateChange /// :nodoc: typedef void (^ARTPresenceMessageCallback)(ARTPresenceMessage *message); +/// :nodoc: +typedef void (^ARTPresenceMessageErrorCallback)(ARTPresenceMessage *message, ARTErrorInfo *_Nullable error); + /// :nodoc: typedef void (^ARTPresenceMessagesCallback)(NSArray *_Nullable result, ARTErrorInfo *_Nullable error); diff --git a/Test/Test Utilities/TestUtilities.swift b/Test/Test Utilities/TestUtilities.swift index d9664fb93..2349a2368 100644 --- a/Test/Test Utilities/TestUtilities.swift +++ b/Test/Test Utilities/TestUtilities.swift @@ -1605,7 +1605,7 @@ extension String { } extension ARTRealtime { - + var transportFactory: TestProxyTransportFactory? { self.internal.options.testOptions.transportFactory as? TestProxyTransportFactory } @@ -1664,7 +1664,7 @@ extension ARTRealtime { reachability.simulate(true) } } - + func simulateRestoreInternetConnection(after seconds: TimeInterval? = nil) { guard let transportFactory = self.transportFactory else { fatalError("Expected test TestProxyTransportFactory") @@ -1701,7 +1701,17 @@ extension ARTRealtime { } self.connection.off() } - + + func requestPresenceSyncForChannel(_ channel: ARTRealtimeChannel) { + let syncMessage = ARTProtocolMessage() + syncMessage.action = .sync + syncMessage.channel = channel.name + guard let transport = self.internal.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + channel.internal.presence.startSync() + transport.send(syncMessage) + } } extension ARTWebSocketTransport { diff --git a/Test/Tests/RealtimeClientChannelTests.swift b/Test/Tests/RealtimeClientChannelTests.swift index 1da4b3ef6..92254672e 100644 --- a/Test/Tests/RealtimeClientChannelTests.swift +++ b/Test/Tests/RealtimeClientChannelTests.swift @@ -189,14 +189,14 @@ class RealtimeClientChannelTests: XCTestCase { channel2.attach() XCTAssertFalse(channel2.presence.syncComplete) - XCTAssertEqual(channel1.internal.presenceMap.members.count, 1) - XCTAssertEqual(channel2.internal.presenceMap.members.count, 0) + XCTAssertEqual(channel1.internal.presence.members.count, 1) + XCTAssertEqual(channel2.internal.presence.members.count, 0) } expect(channel2.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) - XCTAssertEqual(channel1.internal.presenceMap.members.count, 1) - expect(channel2.internal.presenceMap.members).toEventually(haveCount(1), timeout: testTimeout) + XCTAssertEqual(channel1.internal.presence.members.count, 1) + expect(channel2.internal.presence.members).toEventually(haveCount(1), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in channel1.publish("message", data: nil) { errorInfo in @@ -212,14 +212,14 @@ class RealtimeClientChannelTests: XCTestCase { } } - expect(channel1.internal.presenceMap.members).toEventually(haveCount(2), timeout: testTimeout) - expect(channel1.internal.presenceMap.members.keys).to(allPass { $0.hasPrefix("\(channel1.internal.connectionId):Client") || $0.hasPrefix("\(channel2.internal.connectionId):Client") }) - expect(channel1.internal.presenceMap.members.values).to(allPass { $0.action == .present }) + expect(channel1.internal.presence.members).toEventually(haveCount(2), timeout: testTimeout) + expect(channel1.internal.presence.members.keys).to(allPass { $0.hasPrefix("\(channel1.internal.connectionId):Client") || $0.hasPrefix("\(channel2.internal.connectionId):Client") }) + expect(channel1.internal.presence.members.values).to(allPass { $0.action == .present }) - expect(channel2.internal.presenceMap.members).toEventually(haveCount(2), timeout: testTimeout) - expect(channel2.internal.presenceMap.members.keys).to(allPass { $0.hasPrefix("\(channel1.internal.connectionId):Client") || $0.hasPrefix("\(channel2.internal.connectionId):Client") }) - XCTAssertEqual(channel2.internal.presenceMap.members["\(channel1.internal.connectionId):Client 1"]!.action, ARTPresenceAction.present) - XCTAssertEqual(channel2.internal.presenceMap.members["\(channel2.internal.connectionId):Client 2"]!.action, ARTPresenceAction.present) + expect(channel2.internal.presence.members).toEventually(haveCount(2), timeout: testTimeout) + expect(channel2.internal.presence.members.keys).to(allPass { $0.hasPrefix("\(channel1.internal.connectionId):Client") || $0.hasPrefix("\(channel2.internal.connectionId):Client") }) + XCTAssertEqual(channel2.internal.presence.members["\(channel1.internal.connectionId):Client 1"]!.action, ARTPresenceAction.present) + XCTAssertEqual(channel2.internal.presence.members["\(channel2.internal.connectionId):Client 2"]!.action, ARTPresenceAction.present) } // RTL2 @@ -1240,7 +1240,7 @@ class RealtimeClientChannelTests: XCTestCase { partialDone() } } - expect(channel.internal.serial).toEventuallyNot(beNil()) + expect(channel.internal.channelSerial).toEventuallyNot(beNil()) client.simulateNoInternetConnection(transportFactory: transportFactory) client.simulateRestoreInternetConnection(after: 0.1, transportFactory: transportFactory) @@ -1250,7 +1250,7 @@ class RealtimeClientChannelTests: XCTestCase { expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) let secondProtocolAttachMessage = try latestAttachProtocolMessage() - expect(secondProtocolAttachMessage.channelSerial).to(equal(channel.internal.serial)) + expect(secondProtocolAttachMessage.channelSerial).to(equal(channel.internal.channelSerial)) } // RTL4e @@ -4232,14 +4232,14 @@ class RealtimeClientChannelTests: XCTestCase { expect(error).to(beNil()) let attachMessage = transport.protocolMessagesReceived.filter { $0.action == .attached }[0] if attachMessage.channelSerial != nil { - expect(attachMessage.channelSerial).to(equal(channel.internal.serial)) + expect(attachMessage.channelSerial).to(equal(channel.internal.channelSerial)) } partialDone() channel.subscribe { message in let messageMessage = transport.protocolMessagesReceived.filter { $0.action == .message }[0] if messageMessage.channelSerial != nil { - expect(messageMessage.channelSerial).to(equal(channel.internal.serial)) + expect(messageMessage.channelSerial).to(equal(channel.internal.channelSerial)) } channel.presence.enterClient("client1", data: "Hey") partialDone() @@ -4247,7 +4247,7 @@ class RealtimeClientChannelTests: XCTestCase { channel.presence.subscribe { presenceMessage in let presenceMessage = transport.protocolMessagesReceived.filter { $0.action == .presence }[0] if presenceMessage.channelSerial != nil { - expect(presenceMessage.channelSerial).to(equal(channel.internal.serial)) + expect(presenceMessage.channelSerial).to(equal(channel.internal.channelSerial)) } partialDone() } @@ -4269,29 +4269,29 @@ class RealtimeClientChannelTests: XCTestCase { // Case for detached channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - expect(channel.internal.serial).toNot(beNil()) + expect(channel.internal.channelSerial).toNot(beNil()) channel.detach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.detached), timeout: testTimeout) - expect(channel.internal.serial).to(beNil()) + expect(channel.internal.channelSerial).to(beNil()) // Case for suspended channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - expect(channel.internal.serial).toNot(beNil()) + expect(channel.internal.channelSerial).toNot(beNil()) channel.internal.setSuspended(.init(state: .ok)) expect(channel.state).to(equal(ARTRealtimeChannelState.suspended)) - expect(channel.internal.serial).to(beNil()) + expect(channel.internal.channelSerial).to(beNil()) // Case for failed channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - expect(channel.internal.serial).toNot(beNil()) + expect(channel.internal.channelSerial).toNot(beNil()) channel.internal.setFailed(.init(state: .ok)) expect(channel.state).to(equal(ARTRealtimeChannelState.failed)) - expect(channel.internal.serial).to(beNil()) + expect(channel.internal.channelSerial).to(beNil()) } // RTL16 diff --git a/Test/Tests/RealtimeClientConnectionTests.swift b/Test/Tests/RealtimeClientConnectionTests.swift index 47d3c20e6..60c63d99a 100644 --- a/Test/Tests/RealtimeClientConnectionTests.swift +++ b/Test/Tests/RealtimeClientConnectionTests.swift @@ -3331,12 +3331,12 @@ class RealtimeClientConnectionTests: XCTestCase { client.connection.once(.connected) { stateChange in let firstChannel = client.channels.get(firstChannelName) firstChannel.on(.attached) {_ in - firstChannelSerial = firstChannel.internal.serial + firstChannelSerial = firstChannel.internal.channelSerial partialDone() } let secondChannel = client.channels.get(secondChannelName) secondChannel.on(.attached) {_ in - secondChannelSerial = secondChannel.internal.serial + secondChannelSerial = secondChannel.internal.channelSerial secondChannel.publish(nil, data: "message") { error in expect(error).to(beNil()) partialDone() @@ -3345,7 +3345,7 @@ class RealtimeClientConnectionTests: XCTestCase { } let thirdChannel = client.channels.get(thirdChannelName) thirdChannel.on(.attached) {_ in - thirdChannelSerial = thirdChannel.internal.serial + thirdChannelSerial = thirdChannel.internal.channelSerial partialDone() } firstChannel.attach() @@ -3399,12 +3399,12 @@ class RealtimeClientConnectionTests: XCTestCase { client.connection.once(.connected) { stateChange in let firstChannel = client.channels.get(sanskritChannelName) firstChannel.on(.attached) {_ in - firstChannelSerial = firstChannel.internal.serial + firstChannelSerial = firstChannel.internal.channelSerial partialDone() } let secondChannel = client.channels.get(koreanChannelName) secondChannel.on(.attached) {_ in - secondChannelSerial = secondChannel.internal.serial + secondChannelSerial = secondChannel.internal.channelSerial secondChannel.publish(nil, data: "message") { error in expect(error).to(beNil()) @@ -3539,7 +3539,7 @@ class RealtimeClientConnectionTests: XCTestCase { client.connection.once(.connected) { stateChange in let channel = client.channels.get(channelName) channel.on(.attached) { _ in - expectedChannelSerial = channel.internal.serial + expectedChannelSerial = channel.internal.channelSerial partialDone() } channel.attach() @@ -3556,7 +3556,7 @@ class RealtimeClientConnectionTests: XCTestCase { XCTAssertTrue(recoveredClient.channels.exists(channelName)) let recoveredChannel = recoveredClient.channels.get(channelName) - expect(recoveredChannel.internal.serial).to(equal(expectedChannelSerial)) + expect(recoveredChannel.internal.channelSerial).to(equal(expectedChannelSerial)) } // RTN16k diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 5cb2407c7..ff003b2c5 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -125,10 +125,10 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(attached.flags & 0x1, 0) XCTAssertFalse(attached.hasPresence) XCTAssertFalse(channel.presence.syncComplete) - XCTAssertFalse(channel.internal.presenceMap.syncComplete) + XCTAssertFalse(channel.internal.presence.syncComplete) } - func skipped__test__010__Presence__ProtocolMessage_bit_flag__when_members_are_present() throws { + func test__FLAKY__010__Presence__ProtocolMessage_bit_flag__when_members_are_present() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -188,8 +188,8 @@ class RealtimeClientPresenceTests: XCTestCase { fail("TestProxyTransport is not set"); return } - XCTAssertFalse(channel.internal.presenceMap.syncInProgress) - expect(channel.internal.presenceMap.members).to(beEmpty()) + XCTAssertFalse(channel.internal.presence.syncInProgress) + expect(channel.internal.presence.members).to(beEmpty()) waitUntil(timeout: testTimeout) { done in channel.presence.subscribe(.present) { msg in @@ -266,8 +266,8 @@ class RealtimeClientPresenceTests: XCTestCase { fail("TestProxyTransport is not set"); return } - XCTAssertFalse(channel.internal.presenceMap.syncInProgress) - expect(channel.internal.presenceMap.members).to(beEmpty()) + XCTAssertFalse(channel.internal.presence.syncInProgress) + expect(channel.internal.presence.members).to(beEmpty()) waitUntil(timeout: testTimeout) { done in var aClientHasLeft = false @@ -307,7 +307,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP19 - func skipped__test__013__Presence__PresenceMap_has_existing_members_when_a_SYNC_is_started__should_ensure_that_members_no_longer_present_on_the_channel_are_removed_from_the_local_PresenceMap_once_the_sync_is_complete() throws { + func test__FLAKY__013__Presence__PresenceMap_has_existing_members_when_a_SYNC_is_started__should_ensure_that_members_no_longer_present_on_the_channel_are_removed_from_the_local_PresenceMap_once_the_sync_is_complete() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() @@ -330,12 +330,12 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(channel.internal.presenceMap.members.count, 2) + XCTAssertEqual(channel.internal.presence.members.count, 2) // Inject a local member - let localMember = ARTPresenceMessage(clientId: NSUUID().uuidString, action: .enter, connectionId: "another", id: "another:0:0") - channel.internal.presenceMap.add(localMember) - XCTAssertEqual(channel.internal.presenceMap.members.count, 3) - XCTAssertEqual(channel.internal.presenceMap.members.filter { memberKey, _ in memberKey.contains(localMember.clientId!) }.count, 1) + let internalMember = ARTPresenceMessage(clientId: NSUUID().uuidString, action: .enter, connectionId: "another", id: "another:0:0") + channel.internal.presence.add(internalMember) + XCTAssertEqual(channel.internal.presence.members.count, 3) + XCTAssertEqual(channel.internal.presence.members.filter { memberKey, _ in memberKey.contains(internalMember.clientId!) }.count, 1) waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in @@ -343,25 +343,17 @@ class RealtimeClientPresenceTests: XCTestCase { guard let members = members, members.count == 3 else { fail("Should at least have 3 members"); done(); return } - XCTAssertEqual(members.filter { $0.clientId == localMember.clientId }.count, 1) + XCTAssertEqual(members.filter { $0.clientId == internalMember.clientId }.count, 1) done() } } waitUntil(timeout: testTimeout) { done in channel.presence.subscribe(.leave) { leave in - XCTAssertEqual(leave.clientId, localMember.clientId) + XCTAssertEqual(leave.clientId, internalMember.clientId) done() } - - // Request a sync - let syncMessage = ARTProtocolMessage() - syncMessage.action = .sync - syncMessage.channel = channel.name - guard let transport = client.internal.transport as? TestProxyTransport else { - fail("TestProxyTransport is not set"); done(); return - } - transport.send(syncMessage) + client.requestPresenceSyncForChannel(channel) } waitUntil(timeout: testTimeout) { done in @@ -370,7 +362,7 @@ class RealtimeClientPresenceTests: XCTestCase { guard let members = members, members.count == 2 else { fail("Should at least have 2 members"); done(); return } - expect(members.filter { $0.clientId == localMember.clientId }).to(beEmpty()) + expect(members.filter { $0.clientId == internalMember.clientId }).to(beEmpty()) done() } } @@ -387,8 +379,8 @@ class RealtimeClientPresenceTests: XCTestCase { let channel = client.channels.get(channelName) // Inject local members - channel.internal.presenceMap.add(ARTPresenceMessage(clientId: "tester1", action: .enter, connectionId: "another", id: "another:0:0")) - channel.internal.presenceMap.add(ARTPresenceMessage(clientId: "tester2", action: .enter, connectionId: "another", id: "another:0:1")) + channel.internal.presence.add(ARTPresenceMessage(clientId: "tester1", action: .enter, connectionId: "another", id: "another:0:0")) + channel.internal.presence.add(ARTPresenceMessage(clientId: "tester2", action: .enter, connectionId: "another", id: "another:0:1")) guard let transport = client.internal.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); return @@ -429,7 +421,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP4 - func skipped__test__002__Presence__should_receive_all_250_members() throws { + func test__FLAKY__002__Presence__should_receive_all_250_members() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) var clientSource: ARTRealtime! @@ -531,9 +523,9 @@ class RealtimeClientPresenceTests: XCTestCase { let channel = client.channels.get(test.uniqueChannelName()) let listener = channel.presence.subscribe { _ in }! - XCTAssertEqual(channel.internal.presenceEventEmitter.anyListeners.count, 1) + XCTAssertEqual(channel.internal.presence.eventEmitter.anyListeners.count, 1) channel.presence.unsubscribe(listener) - XCTAssertEqual(channel.internal.presenceEventEmitter.anyListeners.count, 0) + XCTAssertEqual(channel.internal.presence.eventEmitter.anyListeners.count, 0) } // RTP5 @@ -560,7 +552,7 @@ class RealtimeClientPresenceTests: XCTestCase { } } - func skipped__test__019__Presence__Channel_state_change_side_effects__if_the_channel_enters_the_FAILED_state__should_clear_the_PresenceMap_including_local_members_and_does_not_emit_any_presence_events() throws { + func test__FLAKY__019__Presence__Channel_state_change_side_effects__if_the_channel_enters_the_FAILED_state__should_clear_the_PresenceMap_including_local_members_and_does_not_emit_any_presence_events() throws { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } @@ -578,8 +570,8 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(channel.internal.presenceMap.members.count, 1) - XCTAssertEqual(channel.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channel.internal.presence.members.count, 1) + XCTAssertEqual(channel.internal.presence.internalMembers.count, 1) channel.subscribe { _ in fail("Shouldn't receive any presence event") @@ -588,8 +580,8 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in channel.once(.failed) { _ in - expect(channel.internal.presenceMap.members).to(beEmpty()) - expect(channel.internal.presenceMap.localMembers).to(beEmpty()) + expect(channel.internal.presence.members).to(beEmpty()) + expect(channel.internal.presence.internalMembers).to(beEmpty()) done() } AblyTests.queue.async { @@ -608,13 +600,16 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in channel.once(.attaching) { _ in - channel.detach() + AblyTests.queue.async { + channel.internal.detachChannel(ChannelStateChangeParams(state: .error, errorInfo: ARTErrorInfo.create(withCode: 0, message: "Fail test"))) + } } channel.presence.enterClient("user", data: nil) { error in XCTAssertNotNil(error) - XCTAssertEqual(client.internal.queuedMessages.count, 0) + XCTAssertEqual(channel.internal.presence.pendingPresence.count, 0) done() } + XCTAssertEqual(channel.internal.presence.pendingPresence.count, 1) } } @@ -636,8 +631,8 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(channel.internal.presenceMap.members.count, 1) - XCTAssertEqual(channel.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channel.internal.presence.members.count, 1) + XCTAssertEqual(channel.internal.presence.internalMembers.count, 1) channel.subscribe { _ in fail("Shouldn't receive any presence event") @@ -646,8 +641,8 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in channel.once(.detached) { _ in - expect(channel.internal.presenceMap.members).to(beEmpty()) - expect(channel.internal.presenceMap.localMembers).to(beEmpty()) + expect(channel.internal.presence.members).to(beEmpty()) + expect(channel.internal.presence.internalMembers).to(beEmpty()) done() } channel.detach() @@ -656,7 +651,7 @@ class RealtimeClientPresenceTests: XCTestCase { // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP5b - func skipped__test__017__Presence__Channel_state_change_side_effects__if_a_channel_enters_the_ATTACHED_state_then_all_queued_presence_messages_will_be_sent_immediately_and_a_presence_SYNC_may_be_initiated() throws { + func test__FLAKY__017__Presence__Channel_state_change_side_effects__if_a_channel_enters_the_ATTACHED_state_then_all_queued_presence_messages_will_be_sent_immediately_and_a_presence_SYNC_may_be_initiated() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client1 = AblyTests.newRealtime(options).client @@ -686,9 +681,9 @@ class RealtimeClientPresenceTests: XCTestCase { } channel2.presence.subscribe(.enter) { _ in if channel2.presence.syncComplete { - XCTAssertEqual(channel2.internal.presenceMap.members.count, 2) + XCTAssertEqual(channel2.internal.presence.members.count, 2) } else { - XCTAssertEqual(channel2.internal.presenceMap.members.count, 1) + XCTAssertEqual(channel2.internal.presence.members.count, 1) } channel2.presence.unsubscribe() partialDone() @@ -696,7 +691,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(client2.internal.queuedMessages.count, 1) XCTAssertFalse(channel2.presence.syncComplete) - XCTAssertEqual(channel2.internal.presenceMap.members.count, 0) + XCTAssertEqual(channel2.internal.presence.members.count, 0) } guard let transport = client2.internal.transport as? TestProxyTransport else { @@ -706,7 +701,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(transport.protocolMessagesReceived.filter { $0.action == .sync }.count, 1) expect(channel2.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) - XCTAssertEqual(channel2.internal.presenceMap.members.count, 2) + XCTAssertEqual(channel2.internal.presence.members.count, 2) } // RTP5f @@ -749,7 +744,7 @@ class RealtimeClientPresenceTests: XCTestCase { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() - + let clientMembers = AblyTests.addMembersSequentiallyToChannel(channelName, members: 2, options: options) defer { clientMembers.dispose(); clientMembers.close() } @@ -770,7 +765,7 @@ class RealtimeClientPresenceTests: XCTestCase { // Move to SUSPENDED let ttlHookToken = mainClient.overrideConnectionStateTTL(1.0) defer { ttlHookToken.remove() } - + let leavesChannel = leavesClient.channels.get(channelName) let mainChannel = mainClient.channels.get(channelName) @@ -812,9 +807,9 @@ class RealtimeClientPresenceTests: XCTestCase { done() } } - + var presenceEvents = [ARTPresenceMessage]() - + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(4, done: done) mainChannel.presence.subscribe { presence in @@ -826,8 +821,8 @@ class RealtimeClientPresenceTests: XCTestCase { } mainChannel.once(.suspended) { _ in mainChannel.internalSync { _internal in - XCTAssertEqual(_internal.presenceMap.members.count, 4) // "main", "user1", "user2", "leaves" - XCTAssertEqual(_internal.presenceMap.localMembers.count, 1) // "main" + XCTAssertEqual(_internal.presence.members.count, 4) // "main", "user1", "user2", "leaves" + XCTAssertEqual(_internal.presence.internalMembers.count, 1) // "main" } leavesChannel.presence.leave(nil) { error in XCTAssertNil(error) @@ -867,17 +862,17 @@ class RealtimeClientPresenceTests: XCTestCase { done() } } - + mainChannel.internalSync { _internal in - XCTAssertEqual(_internal.presenceMap.members.count, 3) // "main", "user1", "user2" - XCTAssertEqual(_internal.presenceMap.localMembers.count, 1) // "main" + XCTAssertEqual(_internal.presence.members.count, 3) // "main", "user1", "user2" + XCTAssertEqual(_internal.presence.internalMembers.count, 1) // "main" } } // RTP8 // RTP8a - func skipped__test__024__Presence__enter__should_enter_the_current_client__optionally_with_the_data_provided() throws { + func test__FLAKY__024__Presence__enter__should_enter_the_current_client__optionally_with_the_data_provided() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -915,9 +910,9 @@ class RealtimeClientPresenceTests: XCTestCase { let channel = client.channels.get(test.uniqueChannelName()) let listener = channel.presence.subscribe(.present) { _ in }! - XCTAssertEqual(channel.internal.presenceEventEmitter.listeners.count, 1) + XCTAssertEqual(channel.internal.presence.eventEmitter.listeners.count, 1) channel.presence.unsubscribe(.present, listener: listener) - XCTAssertEqual(channel.internal.presenceEventEmitter.listeners.count, 0) + XCTAssertEqual(channel.internal.presence.eventEmitter.listeners.count, 0) } // RTP6 @@ -1032,7 +1027,7 @@ class RealtimeClientPresenceTests: XCTestCase { // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP8b - func skipped__test__030__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_success() throws { + func test__FLAKY__030__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_success() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -1061,7 +1056,7 @@ class RealtimeClientPresenceTests: XCTestCase { // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP8b - func skipped__test__031__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_failure() throws { + func test__FLAKY__031__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_failure() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -1267,7 +1262,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP9a - func skipped__test__038__Presence__update__should_update_the_data_for_the_present_member_with_null() throws { + func test__FLAKY__038__Presence__update__should_update_the_data_for_the_present_member_with_null() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -1296,7 +1291,7 @@ class RealtimeClientPresenceTests: XCTestCase { // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP9b - func skipped__test__039__Presence__update__should_enter_current_client_into_the_channel_if_the_client_was_not_already_entered() throws { + func test__FLAKY__039__Presence__update__should_enter_current_client_into_the_channel_if_the_client_was_not_already_entered() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -1304,7 +1299,7 @@ class RealtimeClientPresenceTests: XCTestCase { defer { client.dispose(); client.close() } let channel = client.channels.get(test.uniqueChannelName()) - XCTAssertEqual(channel.internal.presenceMap.members.count, 0) + XCTAssertEqual(channel.internal.presence.members.count, 0) waitUntil(timeout: testTimeout) { done in channel.presence.subscribe(.enter) { member in XCTAssertEqual(member.clientId, "john") @@ -1391,7 +1386,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP10 // RTP10a - func skipped__test__043__Presence__leave__should_leave_the_current_client_from_the_channel_and_the_data_will_be_updated_with_the_value_provided() throws { + func test__FLAKY__043__Presence__leave__should_leave_the_current_client_from_the_channel_and_the_data_will_be_updated_with_the_value_provided() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -1407,7 +1402,7 @@ class RealtimeClientPresenceTests: XCTestCase { channel.presence.enter("online") } - expect(channel.internal.presenceMap.members).toEventually(haveCount(1), timeout: testTimeout) + expect(channel.internal.presence.members).toEventually(haveCount(1), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in channel.presence.subscribe(.leave) { member in @@ -1417,11 +1412,11 @@ class RealtimeClientPresenceTests: XCTestCase { channel.presence.leave("offline") } - expect(channel.internal.presenceMap.members).toEventually(haveCount(0), timeout: testTimeout) + expect(channel.internal.presence.members).toEventually(haveCount(0), timeout: testTimeout) } // RTP10a - func skipped__test__044__Presence__leave__should_leave_the_current_client_with_no_data() throws { + func test__FLAKY__044__Presence__leave__should_leave_the_current_client_with_no_data() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" @@ -1447,7 +1442,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2 - func skipped__test__003__Presence__should_be_used_a_PresenceMap_to_maintain_a_list_of_members() throws { + func test__FLAKY__003__Presence__should_be_used_a_PresenceMap_to_maintain_a_list_of_members() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) var clientSecondary: ARTRealtime! @@ -1506,7 +1501,7 @@ class RealtimeClientPresenceTests: XCTestCase { } } - guard let intialPresenceMessage = channel.internal.presenceMap.members["\(channel.internal.connectionId):tester"] else { + guard let intialPresenceMessage = channel.internal.presence.members["\(channel.internal.connectionId):tester"] else { fail("Missing Presence message"); return } @@ -1524,7 +1519,7 @@ class RealtimeClientPresenceTests: XCTestCase { } } - guard let updatedPresenceMessage = channel.internal.presenceMap.members["\(channel.internal.connectionId):tester"] else { + guard let updatedPresenceMessage = channel.internal.presence.members["\(channel.internal.connectionId):tester"] else { fail("Missing Presence message"); return } @@ -1738,10 +1733,10 @@ class RealtimeClientPresenceTests: XCTestCase { } return protocolMessage } - channel.internal.presenceMap.testSuite_injectIntoMethod(after: #selector(ARTPresenceMap.endSync)) { - XCTAssertFalse(channel.internal.presenceMap.syncInProgress) - XCTAssertEqual(channel.internal.presenceMap.members.count, 120) - XCTAssertEqual(channel.internal.presenceMap.members.filter { _, presence in presence.clientId == "user110" && presence.action == .present }.count, 1) + channel.internal.presence.testSuite_injectIntoMethod(after: #selector(ARTRealtimePresenceInternal.endSync)) { + XCTAssertFalse(channel.internal.presence.syncInProgress) + XCTAssertEqual(channel.internal.presence.members.count, 120) + XCTAssertEqual(channel.internal.presence.members.filter { _, presence in presence.clientId == "user110" && presence.action == .present }.count, 1) partialDone() } channel.attach { error in @@ -1791,10 +1786,10 @@ class RealtimeClientPresenceTests: XCTestCase { } return protocolMessage } - channel.internal.presenceMap.testSuite_injectIntoMethod(after: #selector(ARTPresenceMap.endSync)) { - XCTAssertFalse(channel.internal.presenceMap.syncInProgress) - XCTAssertEqual(channel.internal.presenceMap.members.count, 119) - expect(channel.internal.presenceMap.members.filter { _, presence in presence.clientId == "user110" }).to(beEmpty()) + channel.internal.presence.testSuite_injectIntoMethod(after: #selector(ARTRealtimePresenceInternal.endSync)) { + XCTAssertFalse(channel.internal.presence.syncInProgress) + XCTAssertEqual(channel.internal.presence.members.count, 119) + expect(channel.internal.presence.members.filter { _, presence in presence.clientId == "user110" }).to(beEmpty()) partialDone() } channel.attach { error in @@ -1805,7 +1800,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2d - func skipped__test__046__Presence__PresenceMap__if_action_of_ENTER_arrives__it_should_be_added_to_the_presence_map_with_the_action_set_to_PRESENT() throws { + func test__FLAKY__046__Presence__PresenceMap__if_action_of_ENTER_arrives__it_should_be_added_to_the_presence_map_with_the_action_set_to_PRESENT() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = ARTRealtime(options: options) @@ -1824,13 +1819,13 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(channel.internal.presenceMap.members.filter { _, presence in presence.action == .present }.count, 1) - expect(channel.internal.presenceMap.members.filter { _, presence in presence.action == .enter }).to(beEmpty()) + XCTAssertEqual(channel.internal.presence.members.filter { _, presence in presence.action == .present }.count, 1) + expect(channel.internal.presence.members.filter { _, presence in presence.action == .enter }).to(beEmpty()) } // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP2d - func skipped__test__047__Presence__PresenceMap__if_action_of_UPDATE_arrives__it_should_be_added_to_the_presence_map_with_the_action_set_to_PRESENT() throws { + func test__FLAKY__047__Presence__PresenceMap__if_action_of_UPDATE_arrives__it_should_be_added_to_the_presence_map_with_the_action_set_to_PRESENT() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = ARTRealtime(options: options) @@ -1853,9 +1848,9 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(channel.internal.presenceMap.members.count, 1) - XCTAssertEqual(channel.internal.presenceMap.members.filter { _, presence in presence.action == .present }.count, 1) - expect(channel.internal.presenceMap.members.filter { _, presence in presence.action == .update }).to(beEmpty()) + XCTAssertEqual(channel.internal.presence.members.count, 1) + XCTAssertEqual(channel.internal.presence.members.filter { _, presence in presence.action == .present }.count, 1) + expect(channel.internal.presence.members.filter { _, presence in presence.action == .update }).to(beEmpty()) } // RTP2d @@ -1873,8 +1868,8 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - channel.internal.presenceMap.testSuite_injectIntoMethod(after: #selector(ARTPresenceMap.endSync)) { - XCTAssertFalse(channel.internal.presenceMap.syncInProgress) + channel.internal.presence.testSuite_injectIntoMethod(after: #selector(ARTRealtimePresenceInternal.endSync)) { + XCTAssertFalse(channel.internal.presence.syncInProgress) partialDone() } channel.attach { error in @@ -1883,11 +1878,11 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(channel.internal.presenceMap.members.count, 1) + XCTAssertEqual(channel.internal.presence.members.count, 1) } // RTP2e - func skipped__test__049__Presence__PresenceMap__if_a_SYNC_is_not_in_progress__then_when_a_presence_message_with_an_action_of_LEAVE_arrives__that_memberKey_should_be_deleted_from_the_presence_map__if_present() throws { + func test__FLAKY__049__Presence__PresenceMap__if_a_SYNC_is_not_in_progress__then_when_a_presence_message_with_an_action_of_LEAVE_arrives__that_memberKey_should_be_deleted_from_the_presence_map__if_present() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -1912,9 +1907,9 @@ class RealtimeClientPresenceTests: XCTestCase { } } - expect(channel.internal.presenceMap.syncInProgress).toEventually(beFalse(), timeout: testTimeout) + expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) - guard let user11MemberKey = channel.internal.presenceMap.members["\(clientMembers?.connection.id ?? ""):user11"]?.memberKey() else { + guard let user11MemberKey = channel.internal.presence.members["\(clientMembers?.connection.id ?? ""):user11"]?.memberKey() else { fail("user11 memberKey is not present"); return } @@ -1928,12 +1923,12 @@ class RealtimeClientPresenceTests: XCTestCase { channel.presence.unsubscribe() channel.internalSync { _internal in - expect(_internal.presenceMap.members.filter { _, presence in presence.memberKey() == user11MemberKey }).to(beEmpty()) + expect(_internal.presence.members.filter { _, presence in presence.memberKey() == user11MemberKey }).to(beEmpty()) } } // RTP2f - func skipped__test__050__Presence__PresenceMap__if_a_SYNC_is_in_progress__then_when_a_presence_message_with_an_action_of_LEAVE_arrives__it_should_be_stored_in_the_presence_map_with_the_action_set_to_ABSENT() throws { + func test__FLAKY__050__Presence__PresenceMap__if_a_SYNC_is_in_progress__then_when_a_presence_message_with_an_action_of_LEAVE_arrives__it_should_be_stored_in_the_presence_map_with_the_action_set_to_ABSENT() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() @@ -1959,8 +1954,8 @@ class RealtimeClientPresenceTests: XCTestCase { partialDone() } - hook = channel.internal.presenceMap.testSuite_getArgument( - from: #selector(ARTPresenceMap.internalAdd(_:withSessionId:)), + hook = channel.internal.presence.testSuite_getArgument( + from: #selector(ARTRealtimePresenceInternal.internalAdd(_:withSessionId:)), at: 0 ) { arg in let m = arg as? ARTPresenceMessage @@ -1971,7 +1966,7 @@ class RealtimeClientPresenceTests: XCTestCase { channel.attach { error in XCTAssertNil(error) - XCTAssertTrue(channel.internal.presenceMap.syncInProgress) + XCTAssertTrue(channel.internal.presence.syncInProgress) // Inject a fabricated Presence message let leaveMessage = ARTProtocolMessage() @@ -1987,16 +1982,16 @@ class RealtimeClientPresenceTests: XCTestCase { hook?.remove() channel.presence.unsubscribe() - expect(channel.internal.presenceMap.syncInProgress).toEventually(beFalse(), timeout: testTimeout) - expect(channel.internal.presenceMap.members.filter { _, presence in presence.action == .leave }).to(beEmpty()) - expect(channel.internal.presenceMap.members.filter { _, presence in presence.action == .absent }).to(beEmpty()) + expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) + expect(channel.internal.presence.members.filter { _, presence in presence.action == .leave }).to(beEmpty()) + expect(channel.internal.presence.members.filter { _, presence in presence.action == .absent }).to(beEmpty()) // A single clientId may be present multiple times on the same channel via different client connections and that's way user11 is present because user11 presences messages were in distinct connections. - XCTAssertEqual(channel.internal.presenceMap.members.count, 20) + XCTAssertEqual(channel.internal.presence.members.count, 20) } // RTP2g - func skipped__test__051__Presence__PresenceMap__any_incoming_presence_message_that_passes_the_newness_check_should_be_emitted_on_the_Presence_object__with_an_event_name_set_to_its_original_action() throws { + func test__FLAKY__051__Presence__PresenceMap__any_incoming_presence_message_that_passes_the_newness_check_should_be_emitted_on_the_Presence_object__with_an_event_name_set_to_its_original_action() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = ARTRealtime(options: options) @@ -2016,8 +2011,8 @@ class RealtimeClientPresenceTests: XCTestCase { } channel.internalSync { _internal in - XCTAssertEqual(_internal.presenceMap.members.filter { _, presence in presence.action == .present }.count, 1) - expect(_internal.presenceMap.members.filter { _, presence in presence.action == .enter }).to(beEmpty()) + XCTAssertEqual(_internal.presence.members.filter { _, presence in presence.action == .present }.count, 1) + expect(_internal.presence.members.filter { _, presence in presence.action == .enter }).to(beEmpty()) } } @@ -2602,7 +2597,7 @@ class RealtimeClientPresenceTests: XCTestCase { // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP17 - func skipped__test__080__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__any_ENTER__PRESENT__UPDATE_or_LEAVE_event_that_matches_the_current_connectionId_should_be_applied_to_this_object() throws { + func test__FLAKY__080__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__any_ENTER__PRESENT__UPDATE_or_LEAVE_event_that_matches_the_current_connectionId_should_be_applied_to_this_object() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() @@ -2626,8 +2621,8 @@ class RealtimeClientPresenceTests: XCTestCase { } XCTAssertEqual(presence.action, ARTPresenceAction.enter) XCTAssertEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelA.internal.presenceMap.members.count, 1) - XCTAssertEqual(channelA.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channelA.internal.presence.members.count, 1) + XCTAssertEqual(channelA.internal.presence.internalMembers.count, 1) channelA.presence.unsubscribe() partialDone() } @@ -2637,8 +2632,8 @@ class RealtimeClientPresenceTests: XCTestCase { } expect(presence.action).to(equal(ARTPresenceAction.enter) || equal(ARTPresenceAction.present)) XCTAssertNotEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelB.internal.presenceMap.members.count, 1) - XCTAssertEqual(channelB.internal.presenceMap.localMembers.count, 0) + XCTAssertEqual(channelB.internal.presence.members.count, 1) + XCTAssertEqual(channelB.internal.presence.internalMembers.count, 0) channelB.presence.unsubscribe() partialDone() } @@ -2653,8 +2648,8 @@ class RealtimeClientPresenceTests: XCTestCase { } XCTAssertEqual(presence.action, ARTPresenceAction.enter) XCTAssertNotEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelA.internal.presenceMap.members.count, 2) - XCTAssertEqual(channelA.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channelA.internal.presence.members.count, 2) + XCTAssertEqual(channelA.internal.presence.internalMembers.count, 1) channelA.presence.unsubscribe() partialDone() } @@ -2664,8 +2659,8 @@ class RealtimeClientPresenceTests: XCTestCase { } XCTAssertEqual(presence.action, ARTPresenceAction.enter) XCTAssertEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelB.internal.presenceMap.members.count, 2) - XCTAssertEqual(channelB.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channelB.internal.presence.members.count, 2) + XCTAssertEqual(channelB.internal.presence.internalMembers.count, 1) channelB.presence.unsubscribe() partialDone() } @@ -2682,8 +2677,8 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.action, ARTPresenceAction.update) XCTAssertEqual(presence.data as? String, "hello") XCTAssertNotEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelA.internal.presenceMap.members.count, 2) - XCTAssertEqual(channelA.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channelA.internal.presence.members.count, 2) + XCTAssertEqual(channelA.internal.presence.internalMembers.count, 1) channelA.presence.unsubscribe() partialDone() } @@ -2694,8 +2689,8 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.action, ARTPresenceAction.update) XCTAssertEqual(presence.data as? String, "hello") XCTAssertEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelB.internal.presenceMap.members.count, 2) - XCTAssertEqual(channelB.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channelB.internal.presence.members.count, 2) + XCTAssertEqual(channelB.internal.presence.internalMembers.count, 1) channelB.presence.unsubscribe() partialDone() } @@ -2712,8 +2707,8 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.action, ARTPresenceAction.leave) XCTAssertEqual(presence.data as? String, "bye") XCTAssertNotEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelA.internal.presenceMap.members.count, 1) - XCTAssertEqual(channelA.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channelA.internal.presence.members.count, 1) + XCTAssertEqual(channelA.internal.presence.internalMembers.count, 1) channelA.presence.unsubscribe() partialDone() } @@ -2724,8 +2719,8 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.action, ARTPresenceAction.leave) XCTAssertEqual(presence.data as? String, "bye") XCTAssertEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelB.internal.presenceMap.members.count, 1) - XCTAssertEqual(channelB.internal.presenceMap.localMembers.count, 0) + XCTAssertEqual(channelB.internal.presence.members.count, 1) + XCTAssertEqual(channelB.internal.presence.internalMembers.count, 0) channelB.presence.unsubscribe() partialDone() } @@ -2734,7 +2729,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP17a - func skipped__test__081__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__all_members_belonging_to_the_current_connection_are_published_as_a_PresenceMessage_on_the_Channel_by_the_server_irrespective_of_whether_the_client_has_permission_to_subscribe_or_the_Channel_is_configured_to_publish_presence_events() throws { + func test__FLAKY__081__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__all_members_belonging_to_the_current_connection_are_published_as_a_PresenceMessage_on_the_Channel_by_the_server_irrespective_of_whether_the_client_has_permission_to_subscribe_or_the_Channel_is_configured_to_publish_presence_events() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() @@ -2764,8 +2759,8 @@ class RealtimeClientPresenceTests: XCTestCase { } else { XCTFail("Expected members to be non-nil") } - XCTAssertEqual(channel.internal.presenceMap.members.count, 1) - XCTAssertEqual(channel.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channel.internal.presence.members.count, 1) + XCTAssertEqual(channel.internal.presence.internalMembers.count, 1) done() } } @@ -2776,7 +2771,7 @@ class RealtimeClientPresenceTests: XCTestCase { func skipped__test__082__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__events_applied_to_presence_map__should_be_applied_to_ENTER__PRESENT_or_UPDATE_events_with_a_connectionId_that_matches_the_current_client_s_connectionId() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) - let client = ARTRealtime(options: options) + let client = AblyTests.newRealtime(options).client defer { client.dispose(); client.close() } let channel = client.channels.get(test.uniqueChannelName()) @@ -2798,7 +2793,7 @@ class RealtimeClientPresenceTests: XCTestCase { } channel.internalSync { _internal in - XCTAssertEqual(_internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(_internal.presence.internalMembers.count, 1) } let additionalMember = ARTPresenceMessage( @@ -2810,15 +2805,15 @@ class RealtimeClientPresenceTests: XCTestCase { // Inject an additional member into the myMember set, then force a suspended state client.simulateSuspended(beforeSuspension: { done in - channel.internal.presenceMap.localMembers[additionalMember.clientId!] = additionalMember + channel.internal.presence.internalMembers[additionalMember.clientId!] = additionalMember done() }) expect(client.connection.state).toEventually(equal(.suspended), timeout: testTimeout) - XCTAssertEqual(channel.internal.presenceMap.localMembers.count, 2) + XCTAssertEqual(channel.internal.presence.internalMembers.count, 2) channel.internalSync { _internal in - XCTAssertEqual(_internal.presenceMap.localMembers.count, 2) + XCTAssertEqual(_internal.presence.internalMembers.count, 2) } waitUntil(timeout: testTimeout) { done in @@ -2830,10 +2825,10 @@ class RealtimeClientPresenceTests: XCTestCase { } // Await Sync - channel.internal.presenceMap.onceSyncEnds { _ in + channel.internal.presence.onceSyncEnds { _ in // Should remove the "two" member that was added manually because the connectionId // doesn't match and it's not synthesized, it will be re-entered. - XCTAssertEqual(channel.internal.presenceMap.localMembers.count, 1) + XCTAssertEqual(channel.internal.presence.internalMembers.count, 1) partialDone() } @@ -2853,9 +2848,7 @@ class RealtimeClientPresenceTests: XCTestCase { delay(1, closure: done) } - channel.internalAsync { _internal in - _internal.sync() - } + client.requestPresenceSyncForChannel(channel) XCTAssertFalse(channel.presence.syncComplete) waitUntil(timeout: testTimeout) { done in @@ -2909,7 +2902,7 @@ class RealtimeClientPresenceTests: XCTestCase { } channel.presence.unsubscribe() - expect(channel.internal.presenceMap.localMembers).to(haveCount(2)) + expect(channel.internal.presence.internalMembers).to(haveCount(2)) // All pending messages should complete (receive ACK or NACK) before disconnect for valid count of transport.protocolMessagesSent client.waitForPendingMessages() @@ -2920,7 +2913,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP17i expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - expect(channel.internal.presenceMap.localMembers).to(haveCount(2)) + expect(channel.internal.presence.internalMembers).to(haveCount(2)) let newTransport = client.internal.transport as! TestProxyTransport expect(newTransport).toNot(beIdenticalTo(transport)) @@ -2977,9 +2970,9 @@ class RealtimeClientPresenceTests: XCTestCase { } waitUntil(timeout: .seconds(20)) { done in - channel.internal.presenceMap.onceSyncEnds { _ in + channel.internal.presence.onceSyncEnds { _ in // Synthesized leave - expect(channel.internal.presenceMap.localMembers).to(beEmpty()) + expect(channel.internal.presence.internalMembers).to(beEmpty()) done() } client.internal.onDisconnected() @@ -3000,7 +2993,7 @@ class RealtimeClientPresenceTests: XCTestCase { guard let presences = presences else { fail("Presences is nil"); done(); return } - XCTAssertTrue(channel.internal.presenceMap.syncComplete) + XCTAssertTrue(channel.internal.presence.syncComplete) XCTAssertEqual(presences.count, 1) expect(presences.map { $0.clientId }).to(contain(["one"])) done() @@ -3374,7 +3367,7 @@ class RealtimeClientPresenceTests: XCTestCase { // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP11a - func skipped__test__100__Presence__get__should_return_a_list_of_current_members_on_the_channel() throws { + func test__FLAKY__100__Presence__get__should_return_a_list_of_current_members_on_the_channel() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3585,7 +3578,7 @@ class RealtimeClientPresenceTests: XCTestCase { for i in 0 ..< 3 { let msg = ARTPresenceMessage(clientId: "client\(i)", action: .present, connectionId: "foo", id: "foo:0:0") msgs[msg.clientId!] = msg - channel.internal.presenceMap.internalAdd(msg) + channel.internal.presence.internalAdd(msg) } channel.presence.get(getParams) { result, err in @@ -3605,7 +3598,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP11c // RTP11c1 - func skipped__test__110__Presence__get__Query__set_of_params___waitForSync_is_true__should_wait_until_SYNC_is_complete_before_returning_a_list_of_members() throws { + func test__FLAKY__110__Presence__get__Query__set_of_params___waitForSync_is_true__should_wait_until_SYNC_is_complete_before_returning_a_list_of_members() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) var clientSecondary: ARTRealtime! @@ -3990,12 +3983,12 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP14 // RTP14a, RTP14b, RTP14c, RTP14d - func skipped__test__116__Presence__enterClient__enters_into_presence_on_a_channel_on_behalf_of_another_clientId() throws { + func test__FLAKY__116__Presence__enterClient__enters_into_presence_on_a_channel_on_behalf_of_another_clientId() throws { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } let channel = client.channels.get(test.uniqueChannelName()) - XCTAssertEqual(channel.internal.presenceMap.members.count, 0) + XCTAssertEqual(channel.internal.presence.members.count, 0) let expectedData = ["test": 1] @@ -4020,7 +4013,7 @@ class RealtimeClientPresenceTests: XCTestCase { channel.presence.enterClient("john", data: nil) channel.presence.enterClient("sara", data: nil) - expect(channel.internal.presenceMap.members).toEventually(haveCount(3), timeout: testTimeout) + expect(channel.internal.presence.members).toEventually(haveCount(3), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in channel.presence.get { members, _ in diff --git a/Test/Tests/RestClientPresenceTests.swift b/Test/Tests/RestClientPresenceTests.swift index 25bfb14f8..39f8c4791 100644 --- a/Test/Tests/RestClientPresenceTests.swift +++ b/Test/Tests/RestClientPresenceTests.swift @@ -97,7 +97,7 @@ class RestClientPresenceTests: XCTestCase { realtimeChannel.presence.enterClient("john", data: "web") realtimeChannel.presence.enterClient("casey", data: "mobile") - expect(realtimeChannel.internal.presenceMap.members).toEventually(haveCount(3), timeout: testTimeout) + expect(realtimeChannel.internal.presence.members).toEventually(haveCount(3), timeout: testTimeout) let query = ARTPresenceQuery() query.clientId = "john" diff --git a/fastlane/Scanfile b/fastlane/Scanfile index fea35296b..da002c455 100644 --- a/fastlane/Scanfile +++ b/fastlane/Scanfile @@ -1,8 +1,10 @@ open_report false -clean true +clean false 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 +# Just for printing inside these loop jobs +buildlog_path "xcodebuild_output"