From 71915239adb2a640cfee0fc7df8ca8c82900e449 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 10 Feb 2022 15:35:53 -0300 Subject: [PATCH 01/45] Run iOS 16 tests continuously and upload results to observability server --- .github/workflows/carthage.yaml | 40 ------ .github/workflows/check-pod.yaml | 58 --------- .github/workflows/docs.yml | 52 -------- .github/workflows/features.yml | 14 --- .../workflows/integration-test-iOS16_2.yaml | 69 +--------- .github/workflows/integration-test-macOS.yaml | 118 ------------------ .../workflows/integration-test-tvOS16_1.yaml | 118 ------------------ ...ntinuously-run-tests-and-upload-results.sh | 62 +++++++++ fastlane/Scanfile | 2 +- 9 files changed, 67 insertions(+), 466 deletions(-) delete mode 100644 .github/workflows/carthage.yaml delete mode 100644 .github/workflows/check-pod.yaml delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/features.yml delete mode 100644 .github/workflows/integration-test-macOS.yaml delete mode 100644 .github/workflows/integration-test-tvOS16_1.yaml create mode 100755 Scripts/continuously-run-tests-and-upload-results.sh diff --git a/.github/workflows/carthage.yaml b/.github/workflows/carthage.yaml deleted file mode 100644 index c1cefb089..000000000 --- a/.github/workflows/carthage.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Carthage Installation - -on: - pull_request: - push: - branches: - - main - -jobs: - check: - runs-on: macos-latest - - 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 (14.2) - run: | - sudo xcode-select -s /Applications/Xcode_14.2.app - echo "Selected Xcode version:" - xcodebuild -version - - - name: Log environment information - run: ./Scripts/log-environment-information.sh - - - name: Reset Simulators - run: xcrun simctl erase all - - - name: Carthage - Installation Test - 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 14" -configuration "Debug" diff --git a/.github/workflows/check-pod.yaml b/.github/workflows/check-pod.yaml deleted file mode 100644 index 41666c435..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-latest - 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 - - # 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/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-iOS16_2.yaml b/.github/workflows/integration-test-iOS16_2.yaml index ef3f45e55..d3975db96 100644 --- a/.github/workflows/integration-test-iOS16_2.yaml +++ b/.github/workflows/integration-test-iOS16_2.yaml @@ -51,73 +51,12 @@ jobs: 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 and Run Tests + - name: Install Dependencies and Run Tests Continuously + env: + TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} run: | brew install xcbeautify make submodules bundle install make update_carthage_dependencies_ios - bundle exec fastlane test_iOS16_2 - - - 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: Static Analyzer Reports Uploading - if: ${{ failure() && steps.analyzer-output.outcome == 'failure' }} - uses: actions/upload-artifact@v2 - with: - name: static-analyzer-reports-test_iOS16_2 - path: ./derived_data/**/report-*.html - - - 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_iOS16_2" - - - name: Build APNS Example Project - working-directory: ./Examples/AblyPush - run: | - xcodebuild build -scheme "AblyPushExample" -destination "platform=iOS Simulator,name=iPhone 14" -configuration "Debug" - - - name: Xcodebuild Logs Artifact - if: always() - uses: actions/upload-artifact@v2 - with: - name: xcodebuild-logs - path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload test output artifact - if: always() - uses: actions/upload-artifact@v2 - with: - name: test-output - path: fastlane/test_output - - - name: Upload test results to observability server - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - run: Scripts/upload_test_results.sh - - - name: Swift Package Manager - Installation Test - working-directory: ./ - 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 + Scripts/continuously-run-tests-and-upload-results.sh --lane test_iOS16_2 diff --git a/.github/workflows/integration-test-macOS.yaml b/.github/workflows/integration-test-macOS.yaml deleted file mode 100644 index 2cf7ff101..000000000 --- a/.github/workflows/integration-test-macOS.yaml +++ /dev/null @@ -1,118 +0,0 @@ -name: "Integration Test: macOS Latest" - -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-latest - - 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 (14.2) - run: | - sudo xcode-select -s /Applications/Xcode_14.2.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: Reset Simulators - run: xcrun simctl erase all - - - name: Install Dependencies and Run Tests - run: | - brew install xcbeautify - make submodules - bundle install - make update_carthage_dependencies_macos - bundle exec fastlane test_macOS - - - 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: Static Analyzer Reports Uploading - if: ${{ failure() && steps.analyzer-output.outcome == 'failure' }} - uses: actions/upload-artifact@v2 - with: - name: static-analyzer-reports-test_macOS - path: ./derived_data/**/report-*.html - - - 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_macOS" - - - name: Xcodebuild Logs Artifact - if: always() - uses: actions/upload-artifact@v2 - with: - name: xcodebuild-logs - path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload test output artifact - if: always() - uses: actions/upload-artifact@v2 - with: - name: test-output - path: fastlane/test_output - - - name: Upload test results to observability server - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - run: Scripts/upload_test_results.sh - - - name: Swift Package Manager - Installation Test - working-directory: ./ - 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 diff --git a/.github/workflows/integration-test-tvOS16_1.yaml b/.github/workflows/integration-test-tvOS16_1.yaml deleted file mode 100644 index d259d0856..000000000 --- a/.github/workflows/integration-test-tvOS16_1.yaml +++ /dev/null @@ -1,118 +0,0 @@ -name: "Integration Test: tvOS 16.1" - -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-latest - - 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 (14.2) - run: | - sudo xcode-select -s /Applications/Xcode_14.2.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: Reset Simulators - run: xcrun simctl erase all - - - name: Install Dependencies and Run Tests - run: | - brew install xcbeautify - make submodules - bundle install - make update_carthage_dependencies_tvos - bundle exec fastlane test_tvOS16_1 - - - 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: Static Analyzer Reports Uploading - if: ${{ failure() && steps.analyzer-output.outcome == 'failure' }} - uses: actions/upload-artifact@v2 - with: - name: static-analyzer-reports-test_tvOS16_1 - path: ./derived_data/**/report-*.html - - - 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_tvOS_16_1" - - - name: Xcodebuild Logs Artifact - if: always() - uses: actions/upload-artifact@v2 - with: - name: xcodebuild-logs - path: ~/Library/Developer/Xcode/DerivedData/*/Logs - - - name: Upload test output artifact - if: always() - uses: actions/upload-artifact@v2 - with: - name: test-output - path: fastlane/test_output - - - name: Upload test results to observability server - if: always() - env: - TEST_OBSERVABILITY_SERVER_AUTH_KEY: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} - run: Scripts/upload_test_results.sh - - - name: Swift Package Manager - Installation Test - working-directory: ./ - 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 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..4d7206abf --- /dev/null +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -e + +# 1. 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 + +# 2. Run the tests in a loop and report the results. + +declare -i iteration=1 +while true +do + echo "BEGIN ITERATION ${iteration}" 2>&1 + + rm -rf fastlane/test_output + xcrun simctl erase all + + set +e + bundle exec fastlane --verbose $lane + tests_exit_value=$? + set -e + + 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}: 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 + + ./Scripts/upload_test_results.sh \ + --iteration $iteration \ + "${optional_params[@]}" + + echo "END ITERATION ${iteration}" 2>&1 + + iteration+=1 +done diff --git a/fastlane/Scanfile b/fastlane/Scanfile index fea35296b..c0f23f385 100644 --- a/fastlane/Scanfile +++ b/fastlane/Scanfile @@ -1,5 +1,5 @@ open_report false -clean true +clean false skip_slack true ensure_devices_found true output_types "junit" From 92af9ad9cbfba2693811534812d438467be37a2e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 1 Mar 2022 16:45:06 -0300 Subject: [PATCH 02/45] Print xcodebuild raw output as part of loop This is so that we can see things like library logs, or any additional logging we might add to help debug a test case. --- Scripts/continuously-run-tests-and-upload-results.sh | 6 ++++++ fastlane/Scanfile | 2 ++ 2 files changed, 8 insertions(+) diff --git a/Scripts/continuously-run-tests-and-upload-results.sh b/Scripts/continuously-run-tests-and-upload-results.sh index 4d7206abf..95b1a5ff0 100755 --- a/Scripts/continuously-run-tests-and-upload-results.sh +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -28,6 +28,7 @@ do echo "BEGIN ITERATION ${iteration}" 2>&1 rm -rf fastlane/test_output + rm -rf xcodebuild_output xcrun simctl erase all set +e @@ -42,6 +43,11 @@ do 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 diff --git a/fastlane/Scanfile b/fastlane/Scanfile index c0f23f385..da002c455 100644 --- a/fastlane/Scanfile +++ b/fastlane/Scanfile @@ -6,3 +6,5 @@ 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" From e61efb43a76cc4ed5502a2b519d0b6fb51d960b3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 29 Mar 2022 16:25:47 -0300 Subject: [PATCH 03/45] Add script for fetching test case logs from GitHub --- Scripts/fetch-test-logs.sh | 493 +++++++++++++++++++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100755 Scripts/fetch-test-logs.sh 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 From 2b142cb2fbd4017efeeb077218f6cb55e20eb9f6 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 11 Apr 2022 15:08:47 -0300 Subject: [PATCH 04/45] Upload .xcresult bundles as an artifact --- .../workflows/integration-test-iOS16_2.yaml | 7 +++ ...ntinuously-run-tests-and-upload-results.sh | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/.github/workflows/integration-test-iOS16_2.yaml b/.github/workflows/integration-test-iOS16_2.yaml index d3975db96..f6524c211 100644 --- a/.github/workflows/integration-test-iOS16_2.yaml +++ b/.github/workflows/integration-test-iOS16_2.yaml @@ -60,3 +60,10 @@ jobs: bundle install make update_carthage_dependencies_ios Scripts/continuously-run-tests-and-upload-results.sh --lane test_iOS16_2 + + - name: Upload .xcresult bundles + uses: actions/upload-artifact@v3 + if: always() + with: + name: xcresult-bundles + path: xcresult-bundles diff --git a/Scripts/continuously-run-tests-and-upload-results.sh b/Scripts/continuously-run-tests-and-upload-results.sh index 95b1a5ff0..20a02ad5c 100755 --- a/Scripts/continuously-run-tests-and-upload-results.sh +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -58,9 +58,59 @@ do 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." + exit 1 + fi + + if [[ $number_of_result_bundles -gt 1 ]] + then + echo -e "ITERATION ${iteration}: Multiple result bundles found:\n${result_bundles}" + exit 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." + exit $upload_exit_value + fi echo "END ITERATION ${iteration}" 2>&1 From b7742221ec202d99f5afb94c630e57add8ad864f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 12 Apr 2022 10:32:23 -0300 Subject: [PATCH 05/45] =?UTF-8?q?Make=20sure=20continuously-run-tests-and-?= =?UTF-8?q?upload-results.sh=20doesn=E2=80=99t=20exceed=20GitHub=20job=20r?= =?UTF-8?q?unning=20time=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’m trying to understand the reason that the upload artifacts step is hung here [1], and I’m wondering if it’s because the job execution limit had already been reached by my script. [1] https://github.com/ably/ably-cocoa/runs/5979297645?check_suite_focus=true --- .../workflows/integration-test-iOS16_2.yaml | 1 + ...ntinuously-run-tests-and-upload-results.sh | 41 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-test-iOS16_2.yaml b/.github/workflows/integration-test-iOS16_2.yaml index f6524c211..937995a43 100644 --- a/.github/workflows/integration-test-iOS16_2.yaml +++ b/.github/workflows/integration-test-iOS16_2.yaml @@ -56,6 +56,7 @@ jobs: 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 diff --git a/Scripts/continuously-run-tests-and-upload-results.sh b/Scripts/continuously-run-tests-and-upload-results.sh index 20a02ad5c..f58f30f78 100755 --- a/Scripts/continuously-run-tests-and-upload-results.sh +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -2,7 +2,15 @@ set -e -# 1. Grab command-line options. +# 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 @@ -20,7 +28,17 @@ then exit 1 fi -# 2. Run the tests in a loop and report the results. +# 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. declare -i iteration=1 while true @@ -32,10 +50,27 @@ do xcrun simctl erase all set +e - bundle exec fastlane --verbose $lane + 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 + exit 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." + exit 0 + fi + if [[ tests_exit_value -eq 0 ]] then echo "ITERATION ${iteration}: Tests passed." From 12e2d11270b5689783e761b16d87461ea621270a Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 12 Apr 2022 10:39:02 -0300 Subject: [PATCH 06/45] Log the size of .xcresult bundles waiting to be uploaded To help with understanding issue described in b774222. --- ...ntinuously-run-tests-and-upload-results.sh | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Scripts/continuously-run-tests-and-upload-results.sh b/Scripts/continuously-run-tests-and-upload-results.sh index f58f30f78..c64f1a6a3 100755 --- a/Scripts/continuously-run-tests-and-upload-results.sh +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -40,6 +40,17 @@ 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." + else + echo "There are no xcresult bundles to be uploaded." + fi + + exit $1 +} + declare -i iteration=1 while true do @@ -55,7 +66,7 @@ do if [[ $allowed_execution_time -le 0 ]]; then echo "ITERATION ${iteration}: Allowed execution time reached. Exiting." 2>&1 - exit 0 + end_iteration_with_exit_value 0 fi echo "ITERATION ${iteration}: Running fastlane with a timeout of ${allowed_execution_time} seconds." 2>&1 @@ -68,7 +79,7 @@ do 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." - exit 0 + end_iteration_with_exit_value 0 fi if [[ tests_exit_value -eq 0 ]] @@ -121,13 +132,13 @@ do if [[ $number_of_result_bundles -eq 0 ]] then echo "ITERATION ${iteration}: No result bundles found." - exit 1 + 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}" - exit 1 + end_iteration_with_exit_value 1 fi echo "ITERATION ${iteration}: Report bundle found: ${result_bundles}" @@ -144,7 +155,7 @@ do if [[ upload_exit_value -ne 0 ]] then echo "ITERATION ${iteration}: Terminating due to failed upload." - exit $upload_exit_value + end_iteration_with_exit_value $upload_exit_value fi echo "END ITERATION ${iteration}" 2>&1 From 3e54f934f9ff188ecdb8cb5e9f65a6fc0b117959 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 12 Apr 2022 10:58:21 -0300 Subject: [PATCH 07/45] Tar and zip the xcresult bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’m hoping this will reduce the upload time (more by reducing the number of files than the size). --- .github/workflows/integration-test-iOS16_2.yaml | 4 ++-- Scripts/continuously-run-tests-and-upload-results.sh | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-test-iOS16_2.yaml b/.github/workflows/integration-test-iOS16_2.yaml index 937995a43..febfcec50 100644 --- a/.github/workflows/integration-test-iOS16_2.yaml +++ b/.github/workflows/integration-test-iOS16_2.yaml @@ -66,5 +66,5 @@ jobs: uses: actions/upload-artifact@v3 if: always() with: - name: xcresult-bundles - path: xcresult-bundles + name: xcresult-bundles.tar.gz + path: xcresult-bundles.tar.gz diff --git a/Scripts/continuously-run-tests-and-upload-results.sh b/Scripts/continuously-run-tests-and-upload-results.sh index c64f1a6a3..2b9346109 100755 --- a/Scripts/continuously-run-tests-and-upload-results.sh +++ b/Scripts/continuously-run-tests-and-upload-results.sh @@ -44,6 +44,8 @@ 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 From 1940e9a0f09fef1650be0cb5d3bdad6f2d6cf3ee Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 12 May 2022 17:18:00 -0300 Subject: [PATCH 08/45] Add script for generating multiple jobs and workflows This lets us choose the length and parallelism of our test runs. --- Scripts/set-ci-length-and-parallelism.sh | 104 +++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100755 Scripts/set-ci-length-and-parallelism.sh 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 From 287477964bea7cf4fbc0c32f5ab8f20b208289b2 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 14 Apr 2024 15:05:31 +0200 Subject: [PATCH 09/45] Implement RTP18a (start of a new sync sequence and any previous in-flight sync is discarded). --- Source/ARTProtocolMessage.m | 20 ++++++++ Source/ARTRealtimePresence.m | 50 ++++++++++--------- .../Ably/ARTProtocolMessage+Private.h | 4 ++ 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/Source/ARTProtocolMessage.m b/Source/ARTProtocolMessage.m index e36d0bb1c..c667e9d85 100644 --- a/Source/ARTProtocolMessage.m +++ b/Source/ARTProtocolMessage.m @@ -35,6 +35,26 @@ - (NSString *)getConnectionKey { return _connectionKey; } +- (NSString *)getSyncIdentifierAtIndex:(NSInteger)index { + if (!_channelSerial || [_channelSerial isEqualToString:@""] || index > 1) { + return @""; + } + NSArray *a = [_channelSerial componentsSeparatedByString:@":"]; + return a.count > 1 ? a[index] : @""; +} + +- (NSString *)getSyncSequenceId { + return [self getSyncIdentifierAtIndex:0]; +} + +- (NSString *)getSyncCursor { + return [self getSyncIdentifierAtIndex:1]; +} + +- (BOOL)isEndOfSync { + return [[self getSyncCursor] isEqualToString:@""]; +} + - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: %p> {\n", self.class, self]; [description appendFormat:@" count: %d,\n", self.count]; diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 7d47bd006..7e6f4081c 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -183,6 +183,10 @@ @implementation ARTRealtimePresenceInternal { NSMutableDictionary *_internalMembers; // RTP17h NSMutableDictionary *_beforeSyncMembers; // RTP19 + + // RTP18a + NSString *_syncSequenceId; + NSMutableDictionary *_membersBackup; } - (instancetype)initWithChannel:(ARTRealtimeChannelInternal *)channel logger:(ARTInternalLog *)logger { @@ -690,21 +694,6 @@ - (void)broadcast:(ARTPresenceMessage *)pm { [_eventEmitter emit:[ARTEvent newWithPresenceAction:pm.action] with:pm]; } -/* - * Checks that a channelSerial is the final serial in a sequence of sync messages, - * by checking that there is nothing after the colon - RTP18b, RTP18c - */ -- (bool)isLastChannelSerial:(NSString *)channelSerial { - if (!channelSerial || [channelSerial isEqualToString:@""]) { - return true; - } - NSArray *a = [channelSerial componentsSeparatedByString:@":"]; - if (a.count > 1 && ![[a objectAtIndex:1] isEqualToString:@""]) { - return false; - } - return true; -} - - (void)onAttached:(ARTProtocolMessage *)message { [self startSync]; if (!message.hasPresence) { @@ -747,18 +736,31 @@ - (void)onMessage:(ARTProtocolMessage *)message { } } +- (BOOL)shoudRestartSyncWithSequenceId:(NSString *)sequenceId { + return _syncSequenceId && sequenceId && ![_syncSequenceId isEqualToString:sequenceId]; +} + +- (void)discardSync { + if (self.syncInProgress) { + _members = [_membersBackup mutableCopy]; + ARTLogDebug(_logger, @"%p PresenceMap sync with sequence id = %@ was discarded", self, _syncSequenceId); + } +} + - (void)onSync:(ARTProtocolMessage *)message { - if (!self.syncInProgress) { + NSString *sequenceId = [message getSyncSequenceId]; + if (!self.syncInProgress || [self shoudRestartSyncWithSequenceId:sequenceId]) { + [self discardSync]; // RTP18a + _syncSequenceId = sequenceId; [self startSync]; } else { - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync is in progress", _realtime, _channel, _channel.name); + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync is in progress (syncSequenceId = %@)", _realtime, _channel, _channel.name, _syncSequenceId); } [self onMessage:message]; - // TODO: RTP18a (previous in-flight sync should be discarded) - if ([self isLastChannelSerial:message.channelSerial]) { // RTP18b, RTP18c + if (message.isEndOfSync) { // RTP18b, RTP18c [self endSync]; ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync ended", _realtime, _channel, _channel.name); } @@ -941,22 +943,24 @@ - (void)reset { } - (void)startSync { - ARTLogDebug(_logger, @"%p PresenceMap sync started", self); + ARTLogDebug(_logger, @"%p PresenceMap sync started with sequence id = %@", self, _syncSequenceId); _beforeSyncMembers = [_members mutableCopy]; + _membersBackup = [_members mutableCopy]; _syncState = ARTPresenceSyncStarted; [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; } - (void)endSync { - ARTLogVerbose(_logger, @"%p PresenceMap sync ending", self); + ARTLogVerbose(_logger, @"%p PresenceMap sync ending with sequence id = %@", self, _syncSequenceId); [self cleanUpAbsentMembers]; [self leaveMembersNotPresentInSync]; _syncState = ARTPresenceSyncEnded; _beforeSyncMembers = nil; - + _membersBackup = nil; [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; - ARTLogDebug(_logger, @"%p PresenceMap sync ended", self); + ARTLogDebug(_logger, @"%p PresenceMap sync with sequence id = %@ is ended", self, _syncSequenceId); + _syncSequenceId = nil; } - (void)failsSync:(ARTErrorInfo *)error { diff --git a/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h b/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h index 0722be468..f6b03ed96 100644 --- a/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h +++ b/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h @@ -24,6 +24,10 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)mergeFrom:(ARTProtocolMessage *)msg; +- (NSString *)getSyncSequenceId; +- (NSString *)getSyncCursor; +- (BOOL)isEndOfSync; + @end NS_ASSUME_NONNULL_END From 666906b25b816b539c46cbc152e35df857123afd Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 19 Apr 2024 16:48:00 +0200 Subject: [PATCH 10/45] Fixed `shoudRestartSyncForSequenceId` method name and updated test for RTP18a. --- Source/ARTRealtimePresence.m | 12 +- Test/Tests/RealtimeClientPresenceTests.swift | 116 +++++++++++-------- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 7e6f4081c..ea2908a54 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -736,20 +736,20 @@ - (void)onMessage:(ARTProtocolMessage *)message { } } -- (BOOL)shoudRestartSyncWithSequenceId:(NSString *)sequenceId { +- (BOOL)shoudRestartSyncForSequenceId:(NSString *)sequenceId { return _syncSequenceId && sequenceId && ![_syncSequenceId isEqualToString:sequenceId]; } - (void)discardSync { if (self.syncInProgress) { _members = [_membersBackup mutableCopy]; - ARTLogDebug(_logger, @"%p PresenceMap sync with sequence id = %@ was discarded", self, _syncSequenceId); + ARTLogDebug(_logger, @"%p PresenceMap sync with syncSequenceId = %@ was discarded", self, _syncSequenceId); } } - (void)onSync:(ARTProtocolMessage *)message { NSString *sequenceId = [message getSyncSequenceId]; - if (!self.syncInProgress || [self shoudRestartSyncWithSequenceId:sequenceId]) { + if (!self.syncInProgress || [self shoudRestartSyncForSequenceId:sequenceId]) { [self discardSync]; // RTP18a _syncSequenceId = sequenceId; [self startSync]; @@ -943,7 +943,7 @@ - (void)reset { } - (void)startSync { - ARTLogDebug(_logger, @"%p PresenceMap sync started with sequence id = %@", self, _syncSequenceId); + ARTLogDebug(_logger, @"%p PresenceMap sync started with syncSequenceId = %@", self, _syncSequenceId); _beforeSyncMembers = [_members mutableCopy]; _membersBackup = [_members mutableCopy]; _syncState = ARTPresenceSyncStarted; @@ -951,7 +951,7 @@ - (void)startSync { } - (void)endSync { - ARTLogVerbose(_logger, @"%p PresenceMap sync ending with sequence id = %@", self, _syncSequenceId); + ARTLogVerbose(_logger, @"%p PresenceMap sync ending with syncSequenceId = %@", self, _syncSequenceId); [self cleanUpAbsentMembers]; [self leaveMembersNotPresentInSync]; _syncState = ARTPresenceSyncEnded; @@ -959,7 +959,7 @@ - (void)endSync { _membersBackup = nil; [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; - ARTLogDebug(_logger, @"%p PresenceMap sync with sequence id = %@ is ended", self, _syncSequenceId); + ARTLogDebug(_logger, @"%p PresenceMap sync with syncSequenceId = %@ is ended", self, _syncSequenceId); _syncSequenceId = nil; } diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index d259446c2..e54d3ca3d 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -168,9 +168,8 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP18 - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP18a, RTP18b - func skipped__test__011__Presence__realtime_system_reserves_the_right_to_initiate_a_sync_of_the_presence_members_at_any_point_once_a_channel_is_attached__should_do_a_new_sync_whenever_a_SYNC_ProtocolMessage_is_received_with_a_channel_attribute_and_a_new_sync_sequence_identifier_in_the_channelSerial_attribute() throws { + func test__011__Presence__realtime_system_reserves_the_right_to_initiate_a_sync_of_the_presence_members_at_any_point_once_a_channel_is_attached__should_do_a_new_sync_whenever_a_SYNC_ProtocolMessage_is_received_with_a_channel_attribute_and_a_new_sync_sequence_identifier_in_the_channelSerial_attribute() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client @@ -187,58 +186,81 @@ class RealtimeClientPresenceTests: XCTestCase { guard let transport = client.internal.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); return } + + // Add some initial members (user1, user2) + let _ = AblyTests.addMembersSequentiallyToChannel(channelName, members: 2, options: options) + + // Before starting artificial SYNC process we should wait for the initial one completed: + expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) + XCTAssertEqual(channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2"]) + + // Inject a SYNC Presence message (first page) + let sync1Message = ARTProtocolMessage() + sync1Message.action = .sync + sync1Message.channel = channel.name + sync1Message.channelSerial = "sequenceid1:cursor1" + sync1Message.timestamp = Date() + sync1Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:0:0"), + ARTPresenceMessage(clientId: "b", action: .present, connectionId: "another", id: "another:0:1"), + ARTPresenceMessage(clientId: "c", action: .present, connectionId: "another", id: "another:0:2") + ] + transport.receive(sync1Message) - XCTAssertFalse(channel.internal.presence.syncInProgress) - expect(channel.internal.presence.members).to(beEmpty()) - - waitUntil(timeout: testTimeout) { done in - channel.presence.subscribe(.present) { msg in - if msg.clientId != "a" { - return - } - XCTAssertFalse(channel.presence.syncComplete) - var aClientHasLeft = false - channel.presence.subscribe(.leave) { _ in - if aClientHasLeft { - return - } - aClientHasLeft = true - done() - } - } + XCTAssertEqual( + channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2", "a", "b", "c"].sorted() + ) - // Inject a SYNC Presence message (first page) - let sync1Message = ARTProtocolMessage() - sync1Message.action = .sync - sync1Message.channel = channel.name - sync1Message.channelSerial = "sequenceid:cursor" - sync1Message.timestamp = Date() - sync1Message.presence = [ - ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:0:0"), - ARTPresenceMessage(clientId: "b", action: .present, connectionId: "another", id: "another:0:1"), - ] - transport.receive(sync1Message) - - // Inject a SYNC Presence message (last page) - let sync2Message = ARTProtocolMessage() - sync2Message.action = .sync - sync2Message.channel = channel.name - sync2Message.channelSerial = "sequenceid:" // indicates SYNC is complete - sync2Message.timestamp = Date() - sync2Message.presence = [ - ARTPresenceMessage(clientId: "a", action: .leave, connectionId: "another", id: "another:1:0"), - ] - delay(0.5) { - transport.receive(sync2Message) - } + // Inject a SYNC Presence message (last page) + let sync2Message = ARTProtocolMessage() + sync2Message.action = .sync + sync2Message.channel = channel.name + sync2Message.channelSerial = "sequenceid1:cursor2" + sync2Message.timestamp = Date() + sync2Message.presence = [ + ARTPresenceMessage(clientId: "b", action: .leave, connectionId: "another", id: "another:1:1"), + ] + delay(0.1) { + transport.receive(sync2Message) } - - XCTAssertTrue(channel.presence.syncComplete) + + let sync3Message = ARTProtocolMessage() + sync3Message.action = .sync + sync3Message.channel = channel.name + sync3Message.channelSerial = "sequenceid2:cursor1" // sequence id is different, should discard 'sequenceid1' SYNC (RTP18a) + sync3Message.timestamp = Date() + sync3Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:2:0"), + ARTPresenceMessage(clientId: "b", action: .present, connectionId: "another", id: "another:2:1"), + ] + delay(0.2) { + transport.receive(sync3Message) + XCTAssertEqual( + channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2", "a", "b"].sorted() + ) + } + + let sync4Message = ARTProtocolMessage() + sync4Message.action = .sync + sync4Message.channel = channel.name + sync4Message.channelSerial = "sequenceid2:" // indicates end of SYNC + sync4Message.timestamp = Date() + sync4Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .leave, connectionId: "another", id: "another:3:0"), + ] + delay(0.3) { + transport.receive(sync4Message) + // At this point initial members were removed (user1, user2), first sync discarded, "a" has left, so we have only "b" presented: + XCTAssertEqual(channel.internal.presence.members.compactMap { $0.value.clientId }, ["b"]) + } + + expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) + waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in XCTAssertNil(error) guard let members = members, members.count == 1 else { - fail("Should at least have 1 member"); done(); return + fail("Should have 1 member"); done(); return } XCTAssertEqual(members[0].clientId, "b") done() From 342d57af9f5aa37594ea47758f10a163de6b17f8 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 22 Apr 2024 00:19:45 +0200 Subject: [PATCH 11/45] Test has no sense, because RTP1 says "ProtocolMessage may contain a HAS_PRESENCE bit flag indicating that it will perform a presence sync". It doesn't say that without members it will omit HAS_PRESENCE flag. --- Test/Tests/RealtimeClientPresenceTests.swift | 23 -------------------- 1 file changed, 23 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index e54d3ca3d..fe86c8eaa 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -105,29 +105,6 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP1 - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 - func skipped__test__009__Presence__ProtocolMessage_bit_flag__when_no_members_are_present() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - options.autoConnect = false - options.testOptions.transportFactory = TestProxyTransportFactory() - let client = ARTRealtime(options: options) - client.connect() - defer { client.dispose(); client.close() } - let channel = client.channels.get(test.uniqueChannelName()) - channel.attach() - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) - - let transport = client.internal.transport as! TestProxyTransport - let attached = transport.protocolMessagesReceived.filter { $0.action == .attached }[0] - - XCTAssertEqual(attached.flags & 0x1, 0) - XCTAssertFalse(attached.hasPresence) - XCTAssertFalse(channel.presence.syncComplete) - XCTAssertFalse(channel.internal.presence.syncComplete) - } - func test__FLAKY__010__Presence__ProtocolMessage_bit_flag__when_members_are_present() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) From ee89bc502568f0965861316dd4da254ee0d0fd5f Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 22 Apr 2024 00:24:01 +0200 Subject: [PATCH 12/45] Improved commentary. --- Test/Tests/RealtimeClientPresenceTests.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index fe86c8eaa..4e6afe6a3 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -188,7 +188,7 @@ class RealtimeClientPresenceTests: XCTestCase { channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2", "a", "b", "c"].sorted() ) - // Inject a SYNC Presence message (last page) + // Inject a SYNC Presence message (second page) let sync2Message = ARTProtocolMessage() sync2Message.action = .sync sync2Message.channel = channel.name @@ -201,10 +201,11 @@ class RealtimeClientPresenceTests: XCTestCase { transport.receive(sync2Message) } + // Inject another SYNC Presence message with a different sequence id to discard previous SYNC (RTP18a) let sync3Message = ARTProtocolMessage() sync3Message.action = .sync sync3Message.channel = channel.name - sync3Message.channelSerial = "sequenceid2:cursor1" // sequence id is different, should discard 'sequenceid1' SYNC (RTP18a) + sync3Message.channelSerial = "sequenceid2:cursor1" sync3Message.timestamp = Date() sync3Message.presence = [ ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:2:0"), @@ -217,6 +218,7 @@ class RealtimeClientPresenceTests: XCTestCase { ) } + // Inject end of SYNC let sync4Message = ARTProtocolMessage() sync4Message.action = .sync sync4Message.channel = channel.name @@ -227,7 +229,7 @@ class RealtimeClientPresenceTests: XCTestCase { ] delay(0.3) { transport.receive(sync4Message) - // At this point initial members were removed (user1, user2), first sync discarded, "a" has left, so we have only "b" presented: + // At this point initial members were removed as not presented in sync (user1, user2), first sync discarded, "a" has left, so we have only "b" presented: XCTAssertEqual(channel.internal.presence.members.compactMap { $0.value.clientId }, ["b"]) } From ec8df5c259f1187298f39fc2de630b3aee68bbe8 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 22 Apr 2024 00:26:57 +0200 Subject: [PATCH 13/45] Fixed RTP18c test. --- Test/Tests/RealtimeClientPresenceTests.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 4e6afe6a3..1528b0d1b 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -247,9 +247,8 @@ class RealtimeClientPresenceTests: XCTestCase { } } - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP18c, RTP18b - func skipped__test__012__Presence__realtime_system_reserves_the_right_to_initiate_a_sync_of_the_presence_members_at_any_point_once_a_channel_is_attached__when_a_SYNC_is_sent_with_no_channelSerial_attribute_then_the_sync_data_is_entirely_contained_within_that_ProtocolMessage() throws { + func test__012__Presence__realtime_system_reserves_the_right_to_initiate_a_sync_of_the_presence_members_at_any_point_once_a_channel_is_attached__when_a_SYNC_is_sent_with_no_channelSerial_attribute_then_the_sync_data_is_entirely_contained_within_that_ProtocolMessage() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client @@ -267,7 +266,7 @@ class RealtimeClientPresenceTests: XCTestCase { fail("TestProxyTransport is not set"); return } - XCTAssertFalse(channel.internal.presence.syncInProgress) + expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) expect(channel.internal.presence.members).to(beEmpty()) waitUntil(timeout: testTimeout) { done in From 150070f03a129285202d18c1f5bade7ebe1ff48f Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 22 Apr 2024 00:34:17 +0200 Subject: [PATCH 14/45] Fix test RTP19a - ensure incoming ATTACH message has no HAS_PRESENCE flag instead of requiring its absence (see RTP1). --- Test/Tests/RealtimeClientPresenceTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 1528b0d1b..cdba65dd2 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -368,9 +368,8 @@ class RealtimeClientPresenceTests: XCTestCase { } } - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP19a - func skipped__test__014__Presence__PresenceMap_has_existing_members_when_a_SYNC_is_started__should_emit_a_LEAVE_event_for_each_existing_member_if_the_PresenceMap_has_existing_members_when_an_ATTACHED_message_is_received_without_a_HAS_PRESENCE_flag() throws { + func test__014__Presence__PresenceMap_has_existing_members_when_a_SYNC_is_started__should_emit_a_LEAVE_event_for_each_existing_member_if_the_PresenceMap_has_existing_members_when_an_ATTACHED_message_is_received_without_a_HAS_PRESENCE_flag() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = AblyTests.newRealtime(options).client @@ -388,9 +387,10 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(4, done: done) - transport.setListenerAfterProcessingIncomingMessage { protocolMessage in + transport.setListenerBeforeProcessingIncomingMessage { protocolMessage in if protocolMessage.action == .attached { - XCTAssertFalse(protocolMessage.hasPresence) + // Ensure incoming ATTACH message has no HAS_PRESENCE flag + protocolMessage.flags = 0 partialDone() } } From 3011e008ad675c78b68c239fbe3b5340ca39ee61 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 14:48:47 +0200 Subject: [PATCH 15/45] Fixed RTP2a test. --- Test/Tests/RealtimeClientPresenceTests.swift | 43 ++++++++++---------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index cdba65dd2..af13b7f0f 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1477,11 +1477,10 @@ class RealtimeClientPresenceTests: XCTestCase { } } - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP2 // RTP2a - func skipped__test__045__Presence__PresenceMap__all_incoming_presence_messages_must_be_compared_for_newness_with_the_matching_member_already_in_the_PresenceMap() throws { + func test__045__Presence__PresenceMap__all_incoming_presence_messages_must_be_compared_for_newness_with_the_matching_member_already_in_the_PresenceMap() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = ARTRealtime(options: options) @@ -1497,38 +1496,40 @@ class RealtimeClientPresenceTests: XCTestCase { channel.presence.unsubscribe() partialDone() } - channel.presence.enterClient("tester", data: nil) { error in + channel.presence.enterClient("tester", data: "existing") { error in XCTAssertNil(error) partialDone() } } - guard let intialPresenceMessage = channel.internal.presence.members["\(channel.internal.connectionId):tester"] else { - fail("Missing Presence message"); return - } - - XCTAssertEqual(intialPresenceMessage.memberKey(), "\(client.connection.id!):tester") - - var compareForNewnessMethodCalls = 0 - let hook = channel.internal.presence.testSuite_injectIntoMethod(after: #selector(ARTRealtimePresenceInternal.member(_:isNewerThan:))) { - compareForNewnessMethodCalls += 1 + var presence1: ARTPresenceMessage! + var presence2: ARTPresenceMessage! + + let selector = #selector(ARTRealtimePresenceInternal.member(_:isNewerThan:)) + + let hook = channel.internal.presence.testSuite_injectIntoMethod(after: selector) { + channel.internal.presence.testSuite_getArgument(from: selector, at: 1) { arg in + presence1 = arg as? ARTPresenceMessage + } + channel.internal.presence.testSuite_getArgument(from: selector, at: 0) { arg in + presence2 = arg as? ARTPresenceMessage + } } waitUntil(timeout: testTimeout) { done in - channel.presence.enterClient("tester", data: nil) { error in + channel.presence.enterClient("tester", data: "new") { error in XCTAssertNil(error) done() } } - guard let updatedPresenceMessage = channel.internal.presence.members["\(channel.internal.connectionId):tester"] else { - fail("Missing Presence message"); return - } - - XCTAssertEqual(intialPresenceMessage.memberKey(), updatedPresenceMessage.memberKey()) - expect(intialPresenceMessage.timestamp).to(beLessThan(updatedPresenceMessage.timestamp)) - - XCTAssertEqual(compareForNewnessMethodCalls, 1) + XCTAssertEqual(presence1.clientId, "tester") + XCTAssertEqual(presence2.clientId, "tester") + + XCTAssertEqual(presence1.data as! String, "existing") + XCTAssertEqual(presence2.data as! String, "new") + + expect(presence1.timestamp).to(beLessThan(presence2.timestamp)) hook.remove() } From 243c074831037f3d20154d45f43e71d0643b7110 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 14:49:07 +0200 Subject: [PATCH 16/45] Fixed RTP2g test. --- Test/Tests/RealtimeClientPresenceTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index af13b7f0f..0ed61be22 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1996,11 +1996,11 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.enterClient("tester", data: nil) { error in - XCTAssertNil(error) + channel.presence.subscribe(.enter) { _ in partialDone() } - channel.presence.subscribe(.enter) { _ in + channel.presence.enterClient("tester", data: nil) { error in + XCTAssertNil(error) partialDone() } } From b1320046a3131e6774e41071757caaa98abe5601 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 15:43:05 +0200 Subject: [PATCH 17/45] Fixed RTP2c test. --- Test/Tests/RealtimeClientPresenceTests.swift | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 0ed61be22..6ee98e034 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1693,12 +1693,10 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2c - - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 - func skipped__test__054__Presence__PresenceMap__all_presence_messages_from_a_SYNC_must_also_be_compared_for_newness_in_the_same_way_as_they_would_from_a_PRESENCE__discard_members_where_messages_have_arrived_before_the_SYNC() throws { + func test__054__Presence__PresenceMap__all_presence_messages_from_a_SYNC_must_also_be_compared_for_newness_in_the_same_way_as_they_would_from_a_PRESENCE__discard_members_where_messages_have_arrived_before_the_SYNC() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) - let timeBeforeSync = NSDate() + let timeBeforeSync = Date() let channelName = test.uniqueChannelName() var clientMembers: ARTRealtime? defer { clientMembers?.dispose(); clientMembers?.close() } @@ -1725,12 +1723,9 @@ class RealtimeClientPresenceTests: XCTestCase { let partialDone = AblyTests.splitDone(3, done: done) transport.setBeforeIncomingMessageModifier { protocolMessage in if protocolMessage.action == .sync { - let injectLeave = ARTPresenceMessage() - injectLeave.action = .leave - injectLeave.connectionId = membersConnectionId - injectLeave.clientId = "user110" - injectLeave.timestamp = timeBeforeSync as Date - protocolMessage.presence?.append(injectLeave) + protocolMessage.presence?.append( + ARTPresenceMessage(clientId: "user110", action: .leave, connectionId: membersConnectionId, id: "\(membersConnectionId):109:0", timestamp: timeBeforeSync) + ) transport.setBeforeIncomingMessageModifier(nil) partialDone() } @@ -1739,7 +1734,7 @@ class RealtimeClientPresenceTests: XCTestCase { 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) + XCTAssertEqual(channel.internal.presence.members.filter { _, presence in presence.clientId == "user110" && presence.action == .present }.count, 1) // LEAVE for user110 is ignored, because it's timestamped before SYNC partialDone() } channel.attach { error in From 52c0a895352037e4517e8b19260821a34378b9fe Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 16:35:23 +0200 Subject: [PATCH 18/45] Removed unnecessary test for RTP2c (added additional check to the first RTP2c test). Also reduced number of members to decrease test duration which was also unnecessary long. --- Test/Tests/RealtimeClientPresenceTests.swift | 74 ++++---------------- 1 file changed, 13 insertions(+), 61 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 6ee98e034..8a383fa85 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1700,7 +1700,7 @@ class RealtimeClientPresenceTests: XCTestCase { let channelName = test.uniqueChannelName() var clientMembers: ARTRealtime? defer { clientMembers?.dispose(); clientMembers?.close() } - clientMembers = AblyTests.addMembersSequentiallyToChannel(channelName, members: 120, options: options) + clientMembers = AblyTests.addMembersSequentiallyToChannel(channelName, members: 20, options: options) guard let membersConnectionId = clientMembers?.connection.id else { fail("Members client isn't connected"); return @@ -1715,70 +1715,21 @@ class RealtimeClientPresenceTests: XCTestCase { } channel.presence.subscribe(.leave) { leave in - XCTAssertEqual(leave.clientId, "user110") - fail("Should not fire Leave event for member `user110` because it's out of date") + if leave.clientId == "user10" { + fail("Should not fire Leave event for member `user10` because it's out of date") + } else { + XCTAssertEqual(leave.clientId, "user12") + } } waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(3, done: done) transport.setBeforeIncomingMessageModifier { protocolMessage in if protocolMessage.action == .sync { - protocolMessage.presence?.append( - ARTPresenceMessage(clientId: "user110", action: .leave, connectionId: membersConnectionId, id: "\(membersConnectionId):109:0", timestamp: timeBeforeSync) - ) - transport.setBeforeIncomingMessageModifier(nil) - partialDone() - } - return protocolMessage - } - 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) // LEAVE for user110 is ignored, because it's timestamped before SYNC - partialDone() - } - channel.attach { error in - XCTAssertNil(error) - partialDone() - } - } - } - - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 - func skipped__test__055__Presence__PresenceMap__all_presence_messages_from_a_SYNC_must_also_be_compared_for_newness_in_the_same_way_as_they_would_from_a_PRESENCE__accept_members_where_message_have_arrived_after_the_SYNC() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - let channelName = test.uniqueChannelName() - var clientMembers: ARTRealtime? - defer { clientMembers?.dispose(); clientMembers?.close() } - clientMembers = AblyTests.addMembersSequentiallyToChannel(channelName, members: 120, options: options) - - guard let membersConnectionId = clientMembers?.connection.id else { - fail("Members client isn't connected"); return - } - - let client = AblyTests.newRealtime(options).client - defer { client.dispose(); client.close() } - let channel = client.channels.get(channelName) - - guard let transport = client.internal.transport as? TestProxyTransport else { - fail("TestProxyTransport is not set"); return - } - - waitUntil(timeout: testTimeout.multiplied(by: 2)) { done in - let partialDone = AblyTests.splitDone(4, done: done) - channel.presence.subscribe(.leave) { leave in - XCTAssertEqual(leave.clientId, "user110") - partialDone() - } - transport.setBeforeIncomingMessageModifier { protocolMessage in - if protocolMessage.action == .sync { - let injectLeave = ARTPresenceMessage() - injectLeave.action = .leave - injectLeave.connectionId = membersConnectionId - injectLeave.clientId = "user110" - injectLeave.timestamp = Date() + 1 - protocolMessage.presence?.append(injectLeave) + protocolMessage.presence?.append(contentsOf: [ + ARTPresenceMessage(clientId: "user10", action: .leave, connectionId: membersConnectionId, id: "synthesized:9:0", timestamp: timeBeforeSync), + ARTPresenceMessage(clientId: "user12", action: .leave, connectionId: membersConnectionId, id: "synthesized:11:0", timestamp: Date() + 1) + ]) transport.setBeforeIncomingMessageModifier(nil) partialDone() } @@ -1786,8 +1737,9 @@ class RealtimeClientPresenceTests: XCTestCase { } 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()) + XCTAssertEqual(channel.internal.presence.members.count, 19) + XCTAssertEqual(channel.internal.presence.members.filter { _, presence in presence.clientId == "user10" && presence.action == .present }.count, 1) // LEAVE for user10 is ignored, because it's timestamped before SYNC + XCTAssertEqual(channel.internal.presence.members.filter { _, presence in presence.clientId == "user12" && presence.action == .present }.count, 0) // LEAVE for user12 is not ignored, because it's timestamped after SYNC partialDone() } channel.attach { error in From 123283131c43fd1c9ceebb9f8a6d83e44dbb7cc8 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 20:15:00 +0200 Subject: [PATCH 19/45] More fixes for RTP2a test: subscribe to ENTER only, since realtime sends PRESENT there as well. Compare connections of two messages. --- Test/Tests/RealtimeClientPresenceTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 8a383fa85..ecd5a0ade 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1490,9 +1490,8 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.subscribe { presence in + channel.presence.subscribe(.enter) { presence in XCTAssertEqual(presence.clientId, "tester") - XCTAssertEqual(presence.action, .enter) channel.presence.unsubscribe() partialDone() } @@ -1525,6 +1524,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence1.clientId, "tester") XCTAssertEqual(presence2.clientId, "tester") + XCTAssertEqual(presence1.connectionId, presence2.connectionId) XCTAssertEqual(presence1.data as! String, "existing") XCTAssertEqual(presence2.data as! String, "new") From 02381fb98021f18c5cd511ca1eff96291fc0d478 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 20:17:04 +0200 Subject: [PATCH 20/45] Fix for RTP2d test - update only after enter is done. --- Test/Tests/RealtimeClientPresenceTests.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index ecd5a0ade..41a3f4c59 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1784,17 +1784,16 @@ class RealtimeClientPresenceTests: XCTestCase { let channel = client.channels.get(channelName) waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(3, done: done) + let partialDone = AblyTests.splitDone(2, done: done) channel.presence.subscribe(.update) { _ in partialDone() } channel.presence.enterClient("tester", data: nil) { error in XCTAssertNil(error) - partialDone() - } - channel.presence.updateClient("tester", data: nil) { error in - XCTAssertNil(error) - partialDone() + channel.presence.updateClient("tester", data: nil) { error in + XCTAssertNil(error) + partialDone() + } } } From dd4c7b54cdc8dabf0062d8e737b06fb02a1c9aab Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 26 Apr 2024 21:41:53 +0200 Subject: [PATCH 21/45] Removed unnecessary test for RTP2g (which is the same as for RTP2d for ENTER event). --- Test/Tests/RealtimeClientPresenceTests.swift | 27 +------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 41a3f4c59..2f96b1fbe 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1749,31 +1749,6 @@ 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 { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - let client = ARTRealtime(options: options) - defer { client.dispose(); client.close() } - let channelName = test.uniqueChannelName() - let channel = client.channels.get(channelName) - - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.subscribe(.enter) { _ in - partialDone() - } - channel.presence.enterClient("tester", data: nil) { error in - XCTAssertNil(error) - partialDone() - } - } - - 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 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() @@ -1931,7 +1906,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(channel.internal.presence.members.count, 20) } - // RTP2g + // RTP2d (ENTER), RTP2g 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) From e084f105f2471e954496ae607f593ce6a557f26f Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 28 Apr 2024 15:27:00 +0200 Subject: [PATCH 22/45] Fix for RTP17 test - sometimes realtime sends PRESENT before ENTER and test fails. --- Test/Tests/RealtimeClientPresenceTests.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 2f96b1fbe..6dbae6773 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -2510,10 +2510,9 @@ 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__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() @@ -2535,7 +2534,7 @@ class RealtimeClientPresenceTests: XCTestCase { guard let currentConnectionId = clientA.connection.id else { fail("ClientA should be connected"); partialDone(); return } - XCTAssertEqual(presence.action, ARTPresenceAction.enter) + XCTAssertTrue(presence.action == ARTPresenceAction.enter || presence.action == ARTPresenceAction.present) XCTAssertEqual(presence.connectionId, currentConnectionId) XCTAssertEqual(channelA.internal.presence.members.count, 1) XCTAssertEqual(channelA.internal.presence.internalMembers.count, 1) @@ -2546,7 +2545,7 @@ class RealtimeClientPresenceTests: XCTestCase { guard let currentConnectionId = clientB.connection.id else { fail("ClientB should be connected"); partialDone(); return } - expect(presence.action).to(equal(ARTPresenceAction.enter) || equal(ARTPresenceAction.present)) + XCTAssertTrue(presence.action == ARTPresenceAction.enter || presence.action == ARTPresenceAction.present) XCTAssertNotEqual(presence.connectionId, currentConnectionId) XCTAssertEqual(channelB.internal.presence.members.count, 1) XCTAssertEqual(channelB.internal.presence.internalMembers.count, 0) From 86e6ba28c5b210c8c03b74ff17256870da9ec0b1 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 28 Apr 2024 23:12:17 +0200 Subject: [PATCH 23/45] Added forgotten dispose() call which unsubscribes channels. Without this clients remain in memory and interacts with later executing tests sometimes failing them, like here for example - https://test-observability.herokuapp.com/repos/ably/ably-cocoa/test_cases/8eae564d-2dab-409c-a49d-65dddcd5b422?branches%5B%5D=fix%2F1848-refactoring-presence-looped Test 038 doesn't contain any "shouldn't be called" assertions. --- Test/Tests/RealtimeClientPresenceTests.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 6dbae6773..a2e3f16dd 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -431,7 +431,7 @@ class RealtimeClientPresenceTests: XCTestCase { clientSource = AblyTests.addMembersSequentiallyToChannel(channelName, members: 250, options: options) let clientTarget = ARTRealtime(options: options) - defer { clientTarget.close() } + defer { clientTarget.dispose(); clientTarget.close() } let channel = clientTarget.channels.get(channelName) waitUntil(timeout: testTimeout) { done in @@ -880,13 +880,13 @@ class RealtimeClientPresenceTests: XCTestCase { options.clientId = "john" let client1 = ARTRealtime(options: options) - defer { client1.close() } + defer { client1.dispose(); client1.close() } let channelName = test.uniqueChannelName() let channel1 = client1.channels.get(channelName) let client2 = ARTRealtime(options: options) - defer { client2.close() } + defer { client2.dispose(); client2.close() } let channel2 = client2.channels.get(channelName) waitUntil(timeout: testTimeout) { done in @@ -1035,13 +1035,13 @@ class RealtimeClientPresenceTests: XCTestCase { options.clientId = "john" let client1 = ARTRealtime(options: options) - defer { client1.close() } + defer { client1.dispose(); client1.close() } let channelName = test.uniqueChannelName() let channel1 = client1.channels.get(channelName) let client2 = ARTRealtime(options: options) - defer { client2.close() } + defer { client2.dispose(); client2.close() } let channel2 = client2.channels.get(channelName) waitUntil(timeout: testTimeout) { done in @@ -1064,13 +1064,13 @@ class RealtimeClientPresenceTests: XCTestCase { options.clientId = "john" let client1 = ARTRealtime(options: options) - defer { client1.close() } + defer { client1.dispose(); client1.close() } let channelName = test.uniqueChannelName() let channel1 = client1.channels.get(channelName) let client2 = ARTRealtime(options: options) - defer { client2.close() } + defer { client2.dispose(); client2.close() } let channel2 = client2.channels.get(channelName) waitUntil(timeout: testTimeout) { done in From 0f12e3e9806eb2eb3e88305c12f43b83474b5e7a Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 28 Apr 2024 23:31:40 +0200 Subject: [PATCH 24/45] Removed broken FIXME link. --- Test/Tests/RealtimeClientPresenceTests.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index a2e3f16dd..85ff6af2d 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -649,7 +649,6 @@ 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 { let test = Test() @@ -1027,7 +1026,6 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP8 - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP8b func test__FLAKY__030__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_success() throws { let test = Test() @@ -1056,7 +1054,6 @@ 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 { let test = Test() @@ -1291,7 +1288,6 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP9 - // 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 { let test = Test() @@ -3280,7 +3276,6 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertTrue(ARTRealtimePresenceQuery().waitForSync) } - // FIXME: Fix flaky presence tests and re-enable. See https://ably-real-time.slack.com/archives/C030C5YLY/p1623172436085700 // RTP11a func test__FLAKY__100__Presence__get__should_return_a_list_of_current_members_on_the_channel() throws { let test = Test() From 065583c05d5fcc44ccfbfadb250703bf494f6701 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 28 Apr 2024 23:36:12 +0200 Subject: [PATCH 25/45] Removed unnecessary startSync() call. --- Test/Test Utilities/TestUtilities.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Test/Test Utilities/TestUtilities.swift b/Test/Test Utilities/TestUtilities.swift index 2349a2368..09bb0ddac 100644 --- a/Test/Test Utilities/TestUtilities.swift +++ b/Test/Test Utilities/TestUtilities.swift @@ -1709,7 +1709,6 @@ extension ARTRealtime { guard let transport = self.internal.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); return } - channel.internal.presence.startSync() transport.send(syncMessage) } } From 9ca4d6be13d665a3ead70c5343039c9bb78cd6ee Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 28 Apr 2024 23:39:11 +0200 Subject: [PATCH 26/45] Unskip tests that don't fail locally while being executed repeatedly (marked them FLAKY). --- Test/Tests/RealtimeClientPresenceTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 85ff6af2d..ee306cb1e 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -650,7 +650,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // 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 @@ -873,7 +873,7 @@ class RealtimeClientPresenceTests: XCTestCase { // 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" @@ -1055,7 +1055,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // 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" @@ -1261,7 +1261,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" @@ -1289,7 +1289,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP9 // 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" @@ -1384,7 +1384,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" @@ -3508,7 +3508,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! @@ -3856,7 +3856,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP13 - func skipped__test__008__Presence__Presence_syncComplete_returns_true_if_the_initial_SYNC_operation_has_completed() throws { + func test__FLAKY__008__Presence__Presence_syncComplete_returns_true_if_the_initial_SYNC_operation_has_completed() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3893,7 +3893,7 @@ 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() } From 1998a8456ce3ac432423b36f443e61e12ab42361 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 30 Apr 2024 00:33:36 +0200 Subject: [PATCH 27/45] Fixed RTP19 test (removed leave.clientId == internalMember.clientId assertion, bc realtime sometimes send LEAVE for other members too, not sure why it does that). --- Test/Tests/RealtimeClientPresenceTests.swift | 22 +++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index ee306cb1e..29f0d29a7 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -331,27 +331,27 @@ class RealtimeClientPresenceTests: XCTestCase { } XCTAssertEqual(channel.internal.presence.members.count, 2) - // Inject a local member - let internalMember = ARTPresenceMessage(clientId: NSUUID().uuidString, action: .enter, connectionId: "another", id: "another:0:0") + // Inject a internal member + let internalMember = ARTPresenceMessage(clientId: "internal-member", action: .enter, connectionId: channel.internal.connectionId, id: "\(channel.internal.connectionId):0:0") channel.internal.presence.processMember(internalMember) XCTAssertEqual(channel.internal.presence.members.count, 3) + XCTAssertEqual(channel.internal.presence.internalMembers.count, 1) 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 XCTAssertNil(error) - guard let members = members, members.count == 3 else { - fail("Should at least have 3 members"); done(); return - } - XCTAssertEqual(members.filter { $0.clientId == internalMember.clientId }.count, 1) + XCTAssertEqual(members?.count, 3) + XCTAssertEqual(members?.filter { $0.clientId == internalMember.clientId }.count, 1) done() } } waitUntil(timeout: testTimeout) { done in channel.presence.subscribe(.leave) { leave in - XCTAssertEqual(leave.clientId, internalMember.clientId) - done() + if leave.clientId == internalMember.clientId { + done() + } } client.requestPresenceSyncForChannel(channel) } @@ -359,10 +359,8 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in XCTAssertNil(error) - guard let members = members, members.count == 2 else { - fail("Should at least have 2 members"); done(); return - } - expect(members.filter { $0.clientId == internalMember.clientId }).to(beEmpty()) + XCTAssertEqual(members?.count, 2) + expect(members?.filter { $0.clientId == internalMember.clientId }).to(beEmpty()) done() } } From f169e1bbcd483e444222ea9b0efcbb57dddd6d58 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 30 Apr 2024 00:48:17 +0200 Subject: [PATCH 28/45] Added forgotten resetting setListenerBeforeProcessingIncomingMessage call for transport. Might cause tests interfere with each other, like here f.e. - https://test-observability.herokuapp.com/repos/ably/ably-cocoa/test_cases/7a5e904a-faaa-4a62-b4a7-01fdee993289?branches%5B%5D=fix%2F1889-unskip-presense-tests-looped Line reported #3884 is from completely different test! (test__FLAKY__008__Presence__Presence_syncComplete_returns_true_if_the_initial_SYNC_operation_has_completed which also fails and have setListenerBeforeProcessingIncomingMessage in use) --- Test/Tests/RealtimeClientPresenceTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 29f0d29a7..33fa3030b 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -408,6 +408,7 @@ class RealtimeClientPresenceTests: XCTestCase { partialDone() } } + transport.setListenerBeforeProcessingIncomingMessage(nil) waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in @@ -3522,10 +3523,12 @@ class RealtimeClientPresenceTests: XCTestCase { let query = ARTRealtimePresenceQuery() XCTAssertTrue(query.waitForSync) + var transport = client.internal.transport as! TestProxyTransport + waitUntil(timeout: testTimeout) { done in channel.attach { error in XCTAssertNil(error) - let transport = client.internal.transport as! TestProxyTransport + transport = client.internal.transport as! TestProxyTransport transport.setListenerBeforeProcessingIncomingMessage { protocolMessage in if protocolMessage.action == .sync { XCTAssertEqual(protocolMessage.presence!.count, 100) @@ -3543,6 +3546,7 @@ class RealtimeClientPresenceTests: XCTestCase { } } } + transport.setListenerBeforeProcessingIncomingMessage(nil) } // RTP11c1 @@ -3883,7 +3887,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertFalse(channel.presence.internal.syncComplete_nosync()) } } - + transport.setListenerBeforeProcessingIncomingMessage(nil) expect(channel.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) XCTAssertEqual(transport.protocolMessagesReceived.filter { $0.action == .sync }.count, 3) } From 85de5f51242220e80160d50403ddf4c8fe4c74e8 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 30 Apr 2024 17:42:32 +0200 Subject: [PATCH 29/45] Realtime sends PRESENT among ENTER every other time and it fails test because decodeNumberOfCalls == 2 in such case. --- Test/Tests/RealtimeClientPresenceTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 33fa3030b..e4dae860e 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -3904,15 +3904,15 @@ class RealtimeClientPresenceTests: XCTestCase { let expectedData = ["test": 1] - var encodeNumberOfCalls = 0 + var encodeWasCalled = false let hookEncode = channel.internal.dataEncoder.testSuite_injectIntoMethod(after: #selector(ARTDataEncoder.encode(_:))) { - encodeNumberOfCalls += 1 + encodeWasCalled = true } defer { hookEncode.remove() } - var decodeNumberOfCalls = 0 + var decodeWasCalled = false let hookDecode = channel.internal.dataEncoder.testSuite_injectIntoMethod(after: #selector(ARTDataEncoder.decode(_:encoding:))) { - decodeNumberOfCalls += 1 + decodeWasCalled = true } defer { hookDecode.remove() } @@ -3940,8 +3940,8 @@ class RealtimeClientPresenceTests: XCTestCase { } } - XCTAssertEqual(encodeNumberOfCalls, 1) - XCTAssertEqual(decodeNumberOfCalls, 1) + XCTAssertTrue(encodeWasCalled) + XCTAssertTrue(decodeWasCalled) } // RTP14d From f73ae68c39823bfaee013fa521fe5c67dcc6690b Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 30 Apr 2024 19:06:21 +0200 Subject: [PATCH 30/45] Removed FLAKY label since some of these tests seems to be failing not more than other tests and some not failing at all. --- Test/Tests/RealtimeClientPresenceTests.swift | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index e4dae860e..4c9f22ebb 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -105,7 +105,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP1 - func test__FLAKY__010__Presence__ProtocolMessage_bit_flag__when_members_are_present() throws { + func test__010__Presence__ProtocolMessage_bit_flag__when_members_are_present() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -307,7 +307,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP19 - 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 { + func 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 { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() @@ -420,7 +420,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP4 - func test__FLAKY__002__Presence__should_receive_all_250_members() throws { + func test__002__Presence__should_receive_all_250_members() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) var clientSource: ARTRealtime! @@ -551,7 +551,7 @@ class RealtimeClientPresenceTests: XCTestCase { } } - 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 { + func 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 { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } @@ -649,7 +649,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP5b - 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 { + func 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 { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client1 = AblyTests.newRealtime(options).client @@ -872,7 +872,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP8 // RTP8a - func test__FLAKY__024__Presence__enter__should_enter_the_current_client__optionally_with_the_data_provided() throws { + func test__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" @@ -1026,7 +1026,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP8 // RTP8b - func test__FLAKY__030__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_success() throws { + func test__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" @@ -1054,7 +1054,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP8b - func test__FLAKY__031__Presence__enter__optionally_a_callback_can_be_provided_that_is_called_for_failure() throws { + func test__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" @@ -1260,7 +1260,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP9a - func test__FLAKY__038__Presence__update__should_update_the_data_for_the_present_member_with_null() throws { + func test__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" @@ -1288,7 +1288,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP9 // RTP9b - func test__FLAKY__039__Presence__update__should_enter_current_client_into_the_channel_if_the_client_was_not_already_entered() throws { + func test__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" @@ -1383,7 +1383,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP10 // RTP10a - 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 { + func test__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" @@ -1439,7 +1439,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2 - func test__FLAKY__003__Presence__should_be_used_a_PresenceMap_to_maintain_a_list_of_members() throws { + func test__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! @@ -1745,7 +1745,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2d - 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 { + func test__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) @@ -1801,7 +1801,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2e - 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 { + func 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 { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -1902,7 +1902,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP2d (ENTER), RTP2g - 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 { + func 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 { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let client = ARTRealtime(options: options) @@ -2639,7 +2639,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP17a - 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 { + func 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 { let test = Test() let options = try AblyTests.commonAppSetup(for: test) let channelName = test.uniqueChannelName() @@ -3276,7 +3276,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP11a - func test__FLAKY__100__Presence__get__should_return_a_list_of_current_members_on_the_channel() throws { + func test__100__Presence__get__should_return_a_list_of_current_members_on_the_channel() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3507,7 +3507,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP11c // RTP11c1 - 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 { + func test__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! @@ -3858,7 +3858,7 @@ class RealtimeClientPresenceTests: XCTestCase { } // RTP13 - func test__FLAKY__008__Presence__Presence_syncComplete_returns_true_if_the_initial_SYNC_operation_has_completed() throws { + func test__008__Presence__Presence_syncComplete_returns_true_if_the_initial_SYNC_operation_has_completed() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -3895,7 +3895,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP14 // RTP14a, RTP14b, RTP14c, RTP14d - func test__FLAKY__116__Presence__enterClient__enters_into_presence_on_a_channel_on_behalf_of_another_clientId() throws { + func test__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() } From 1acc1a9981b89752a570c8c23bac8b3f0ac1f081 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 30 Apr 2024 22:42:26 +0200 Subject: [PATCH 31/45] Added post-check of leave events received, since realtime might send those events for other members too. --- Test/Tests/RealtimeClientPresenceTests.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 4c9f22ebb..9d8a5fcb1 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1708,13 +1708,10 @@ class RealtimeClientPresenceTests: XCTestCase { guard let transport = client.internal.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); return } - - channel.presence.subscribe(.leave) { leave in - if leave.clientId == "user10" { - fail("Should not fire Leave event for member `user10` because it's out of date") - } else { - XCTAssertEqual(leave.clientId, "user12") - } + + var leaveEvents = [ARTPresenceMessage]() + channel.presence.subscribe(.leave) { message in + leaveEvents.append(message) } waitUntil(timeout: testTimeout) { done in @@ -1742,6 +1739,8 @@ class RealtimeClientPresenceTests: XCTestCase { partialDone() } } + XCTAssertTrue(leaveEvents.contains(where: { $0.clientId == "user12" })) + XCTAssertFalse(leaveEvents.contains(where: { $0.clientId == "user10" })) } // RTP2d From c71b54630882aaf5f785ba1c9c70709ef41efb53 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Tue, 30 Apr 2024 23:28:43 +0200 Subject: [PATCH 32/45] Realtime sometimes sends 50. --- Test/Tests/RealtimeClientPresenceTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 9d8a5fcb1..4b3379d00 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -3530,7 +3530,6 @@ class RealtimeClientPresenceTests: XCTestCase { transport = client.internal.transport as! TestProxyTransport transport.setListenerBeforeProcessingIncomingMessage { protocolMessage in if protocolMessage.action == .sync { - XCTAssertEqual(protocolMessage.presence!.count, 100) channel.presence.get(query) { members, error in XCTAssertNil(error) if let members { From 3ed152d80e992e855c808b222260222475996bb3 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Wed, 1 May 2024 00:19:53 +0200 Subject: [PATCH 33/45] Spec RTP5b: "If a channel enters the ATTACHED state then all queued presence messages will be sent immediately. A presence SYNC may be initiated per RTP1". "may be initiated" doesn't mean "will be". So this statement periodically fails the test, so I'm removing it. --- Test/Tests/RealtimeClientPresenceTests.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 4b3379d00..0cf5dc75d 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -691,13 +691,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertFalse(channel2.presence.syncComplete) XCTAssertEqual(channel2.internal.presence.members.count, 0) } - - guard let transport = client2.internal.transport as? TestProxyTransport else { - fail("Transport should be a test proxy"); return - } - - XCTAssertEqual(transport.protocolMessagesReceived.filter { $0.action == .sync }.count, 1) - + expect(channel2.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) XCTAssertEqual(channel2.internal.presence.members.count, 2) } From 30d49a75277a06e0fbc11d9f98df3e9415122b6b Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 3 May 2024 02:31:00 +0200 Subject: [PATCH 34/45] Revert "Implement RTP18a (start of a new sync sequence and any previous in-flight sync is discarded)." This reverts commits: 666906b, 2874779. --- Source/ARTProtocolMessage.m | 20 --- Source/ARTRealtimePresence.m | 50 ++++---- .../Ably/ARTProtocolMessage+Private.h | 4 - Test/Tests/RealtimeClientPresenceTests.swift | 117 +++++++----------- 4 files changed, 69 insertions(+), 122 deletions(-) diff --git a/Source/ARTProtocolMessage.m b/Source/ARTProtocolMessage.m index c667e9d85..e36d0bb1c 100644 --- a/Source/ARTProtocolMessage.m +++ b/Source/ARTProtocolMessage.m @@ -35,26 +35,6 @@ - (NSString *)getConnectionKey { return _connectionKey; } -- (NSString *)getSyncIdentifierAtIndex:(NSInteger)index { - if (!_channelSerial || [_channelSerial isEqualToString:@""] || index > 1) { - return @""; - } - NSArray *a = [_channelSerial componentsSeparatedByString:@":"]; - return a.count > 1 ? a[index] : @""; -} - -- (NSString *)getSyncSequenceId { - return [self getSyncIdentifierAtIndex:0]; -} - -- (NSString *)getSyncCursor { - return [self getSyncIdentifierAtIndex:1]; -} - -- (BOOL)isEndOfSync { - return [[self getSyncCursor] isEqualToString:@""]; -} - - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: %p> {\n", self.class, self]; [description appendFormat:@" count: %d,\n", self.count]; diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index ea2908a54..7d47bd006 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -183,10 +183,6 @@ @implementation ARTRealtimePresenceInternal { NSMutableDictionary *_internalMembers; // RTP17h NSMutableDictionary *_beforeSyncMembers; // RTP19 - - // RTP18a - NSString *_syncSequenceId; - NSMutableDictionary *_membersBackup; } - (instancetype)initWithChannel:(ARTRealtimeChannelInternal *)channel logger:(ARTInternalLog *)logger { @@ -694,6 +690,21 @@ - (void)broadcast:(ARTPresenceMessage *)pm { [_eventEmitter emit:[ARTEvent newWithPresenceAction:pm.action] with:pm]; } +/* + * Checks that a channelSerial is the final serial in a sequence of sync messages, + * by checking that there is nothing after the colon - RTP18b, RTP18c + */ +- (bool)isLastChannelSerial:(NSString *)channelSerial { + if (!channelSerial || [channelSerial isEqualToString:@""]) { + return true; + } + NSArray *a = [channelSerial componentsSeparatedByString:@":"]; + if (a.count > 1 && ![[a objectAtIndex:1] isEqualToString:@""]) { + return false; + } + return true; +} + - (void)onAttached:(ARTProtocolMessage *)message { [self startSync]; if (!message.hasPresence) { @@ -736,31 +747,18 @@ - (void)onMessage:(ARTProtocolMessage *)message { } } -- (BOOL)shoudRestartSyncForSequenceId:(NSString *)sequenceId { - return _syncSequenceId && sequenceId && ![_syncSequenceId isEqualToString:sequenceId]; -} - -- (void)discardSync { - if (self.syncInProgress) { - _members = [_membersBackup mutableCopy]; - ARTLogDebug(_logger, @"%p PresenceMap sync with syncSequenceId = %@ was discarded", self, _syncSequenceId); - } -} - - (void)onSync:(ARTProtocolMessage *)message { - NSString *sequenceId = [message getSyncSequenceId]; - if (!self.syncInProgress || [self shoudRestartSyncForSequenceId:sequenceId]) { - [self discardSync]; // RTP18a - _syncSequenceId = sequenceId; + if (!self.syncInProgress) { [self startSync]; } else { - ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync is in progress (syncSequenceId = %@)", _realtime, _channel, _channel.name, _syncSequenceId); + ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync is in progress", _realtime, _channel, _channel.name); } [self onMessage:message]; - if (message.isEndOfSync) { // RTP18b, RTP18c + // TODO: RTP18a (previous in-flight sync should be discarded) + if ([self isLastChannelSerial:message.channelSerial]) { // RTP18b, RTP18c [self endSync]; ARTLogDebug(self.logger, @"RT:%p C:%p (%@) PresenceMap sync ended", _realtime, _channel, _channel.name); } @@ -943,24 +941,22 @@ - (void)reset { } - (void)startSync { - ARTLogDebug(_logger, @"%p PresenceMap sync started with syncSequenceId = %@", self, _syncSequenceId); + ARTLogDebug(_logger, @"%p PresenceMap sync started", self); _beforeSyncMembers = [_members mutableCopy]; - _membersBackup = [_members mutableCopy]; _syncState = ARTPresenceSyncStarted; [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; } - (void)endSync { - ARTLogVerbose(_logger, @"%p PresenceMap sync ending with syncSequenceId = %@", self, _syncSequenceId); + ARTLogVerbose(_logger, @"%p PresenceMap sync ending", self); [self cleanUpAbsentMembers]; [self leaveMembersNotPresentInSync]; _syncState = ARTPresenceSyncEnded; _beforeSyncMembers = nil; - _membersBackup = nil; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; - ARTLogDebug(_logger, @"%p PresenceMap sync with syncSequenceId = %@ is ended", self, _syncSequenceId); - _syncSequenceId = nil; + ARTLogDebug(_logger, @"%p PresenceMap sync ended", self); } - (void)failsSync:(ARTErrorInfo *)error { diff --git a/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h b/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h index f6b03ed96..0722be468 100644 --- a/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h +++ b/Source/PrivateHeaders/Ably/ARTProtocolMessage+Private.h @@ -24,10 +24,6 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)mergeFrom:(ARTProtocolMessage *)msg; -- (NSString *)getSyncSequenceId; -- (NSString *)getSyncCursor; -- (BOOL)isEndOfSync; - @end NS_ASSUME_NONNULL_END diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 0cf5dc75d..8024b802f 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -145,7 +145,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP18 - // RTP18a, RTP18b + // RTP18b func test__011__Presence__realtime_system_reserves_the_right_to_initiate_a_sync_of_the_presence_members_at_any_point_once_a_channel_is_attached__should_do_a_new_sync_whenever_a_SYNC_ProtocolMessage_is_received_with_a_channel_attribute_and_a_new_sync_sequence_identifier_in_the_channelSerial_attribute() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -163,83 +163,58 @@ class RealtimeClientPresenceTests: XCTestCase { guard let transport = client.internal.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); return } - - // Add some initial members (user1, user2) - let _ = AblyTests.addMembersSequentiallyToChannel(channelName, members: 2, options: options) - - // Before starting artificial SYNC process we should wait for the initial one completed: - expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) - XCTAssertEqual(channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2"]) - - // Inject a SYNC Presence message (first page) - let sync1Message = ARTProtocolMessage() - sync1Message.action = .sync - sync1Message.channel = channel.name - sync1Message.channelSerial = "sequenceid1:cursor1" - sync1Message.timestamp = Date() - sync1Message.presence = [ - ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:0:0"), - ARTPresenceMessage(clientId: "b", action: .present, connectionId: "another", id: "another:0:1"), - ARTPresenceMessage(clientId: "c", action: .present, connectionId: "another", id: "another:0:2") - ] - transport.receive(sync1Message) - XCTAssertEqual( - channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2", "a", "b", "c"].sorted() - ) + XCTAssertFalse(channel.internal.presence.syncInProgress) + expect(channel.internal.presence.members).to(beEmpty()) - // Inject a SYNC Presence message (second page) - let sync2Message = ARTProtocolMessage() - sync2Message.action = .sync - sync2Message.channel = channel.name - sync2Message.channelSerial = "sequenceid1:cursor2" - sync2Message.timestamp = Date() - sync2Message.presence = [ - ARTPresenceMessage(clientId: "b", action: .leave, connectionId: "another", id: "another:1:1"), - ] - delay(0.1) { - transport.receive(sync2Message) - } - - // Inject another SYNC Presence message with a different sequence id to discard previous SYNC (RTP18a) - let sync3Message = ARTProtocolMessage() - sync3Message.action = .sync - sync3Message.channel = channel.name - sync3Message.channelSerial = "sequenceid2:cursor1" - sync3Message.timestamp = Date() - sync3Message.presence = [ - ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:2:0"), - ARTPresenceMessage(clientId: "b", action: .present, connectionId: "another", id: "another:2:1"), - ] - delay(0.2) { - transport.receive(sync3Message) - XCTAssertEqual( - channel.internal.presence.members.compactMap { $0.value.clientId }.sorted(), ["user1", "user2", "a", "b"].sorted() - ) - } - - // Inject end of SYNC - let sync4Message = ARTProtocolMessage() - sync4Message.action = .sync - sync4Message.channel = channel.name - sync4Message.channelSerial = "sequenceid2:" // indicates end of SYNC - sync4Message.timestamp = Date() - sync4Message.presence = [ - ARTPresenceMessage(clientId: "a", action: .leave, connectionId: "another", id: "another:3:0"), - ] - delay(0.3) { - transport.receive(sync4Message) - // At this point initial members were removed as not presented in sync (user1, user2), first sync discarded, "a" has left, so we have only "b" presented: - XCTAssertEqual(channel.internal.presence.members.compactMap { $0.value.clientId }, ["b"]) + waitUntil(timeout: testTimeout) { done in + channel.presence.subscribe(.present) { msg in + if msg.clientId != "a" { + return + } + XCTAssertFalse(channel.presence.syncComplete) + var aClientHasLeft = false + channel.presence.subscribe(.leave) { _ in + if aClientHasLeft { + return + } + aClientHasLeft = true + done() + } + } + + // Inject a SYNC Presence message (first page) + let sync1Message = ARTProtocolMessage() + sync1Message.action = .sync + sync1Message.channel = channel.name + sync1Message.channelSerial = "sequenceid:cursor" + sync1Message.timestamp = Date() + sync1Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .present, connectionId: "another", id: "another:0:0"), + ARTPresenceMessage(clientId: "b", action: .present, connectionId: "another", id: "another:0:1"), + ] + transport.receive(sync1Message) + + // Inject a SYNC Presence message (last page) + let sync2Message = ARTProtocolMessage() + sync2Message.action = .sync + sync2Message.channel = channel.name + sync2Message.channelSerial = "sequenceid:" // indicates SYNC is complete + sync2Message.timestamp = Date() + sync2Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .leave, connectionId: "another", id: "another:1:0"), + ] + delay(0.5) { + transport.receive(sync2Message) + } } - - expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) - + + XCTAssertTrue(channel.presence.syncComplete) waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in XCTAssertNil(error) guard let members = members, members.count == 1 else { - fail("Should have 1 member"); done(); return + fail("Should at least have 1 member"); done(); return } XCTAssertEqual(members[0].clientId, "b") done() From 7f7a466ec48daba951f262fcec69225b15bae914 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 3 May 2024 02:32:26 +0200 Subject: [PATCH 35/45] Fixed RTP18b test. --- Test/Tests/RealtimeClientPresenceTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 8024b802f..88b8d7322 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -163,8 +163,9 @@ class RealtimeClientPresenceTests: XCTestCase { guard let transport = client.internal.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); return } - - XCTAssertFalse(channel.internal.presence.syncInProgress) + + // Before starting artificial SYNC process we should wait for the initial one completed: + expect(channel.internal.presence.syncInProgress).toEventually(beFalse(), timeout: testTimeout) expect(channel.internal.presence.members).to(beEmpty()) waitUntil(timeout: testTimeout) { done in From dd75e84ab5cc89f3eba679e71b94d93f0f54317e Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 5 May 2024 17:43:52 +0200 Subject: [PATCH 36/45] Ensure connections for this test are closed and completion closure only called in case of no errors. Otherwise it affects execution of the "test_111..." below - https://test-observability.herokuapp.com/repos/ably/ably-cocoa/test_cases/ee46a8e6-e4ed-489c-83c7-648774930f92?branches%5B%5D=fix%2F1889-unskip-presense-tests-looped-4 --- Test/Tests/RealtimeClientPresenceTests.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 88b8d7322..816faba9c 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -3479,14 +3479,15 @@ class RealtimeClientPresenceTests: XCTestCase { func test__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! - defer { clientSecondary.dispose(); clientSecondary.close() } - let channelName = test.uniqueChannelName() - clientSecondary = AblyTests.addMembersSequentiallyToChannel(channelName, members: 150, options: options) - + let clientSecondary = AblyTests.addMembersSequentiallyToChannel(channelName, members: 150, options: options) let client = AblyTests.newRealtime(options).client - defer { client.dispose(); client.close() } + defer { + clientSecondary.dispose(); clientSecondary.close() + client.dispose(); client.close() + expect(clientSecondary.connection.state).toEventually(equal(.closed), timeout: testTimeout) + expect(client.connection.state).toEventually(equal(.closed), timeout: testTimeout) + } let channel = client.channels.get(channelName) let query = ARTRealtimePresenceQuery() @@ -3504,10 +3505,10 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertNil(error) if let members { XCTAssertEqual(members.count, 150) + done() } else { XCTFail("Expected members to be non-nil") } - done() } transport.setListenerBeforeProcessingIncomingMessage(nil) } From 5460d1d2ca704737e3ca811647b3d596c427752b Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 5 May 2024 18:00:58 +0200 Subject: [PATCH 37/45] Revert fix for RTP2a since it works bad either. --- Test/Tests/RealtimeClientPresenceTests.swift | 44 ++++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 816faba9c..b709e2585 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1455,46 +1455,44 @@ class RealtimeClientPresenceTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.subscribe(.enter) { presence in + channel.presence.subscribe { presence in XCTAssertEqual(presence.clientId, "tester") + XCTAssertEqual(presence.action, .enter) channel.presence.unsubscribe() partialDone() } - channel.presence.enterClient("tester", data: "existing") { error in + channel.presence.enterClient("tester", data: nil) { error in XCTAssertNil(error) partialDone() } } - var presence1: ARTPresenceMessage! - var presence2: ARTPresenceMessage! - - let selector = #selector(ARTRealtimePresenceInternal.member(_:isNewerThan:)) - - let hook = channel.internal.presence.testSuite_injectIntoMethod(after: selector) { - channel.internal.presence.testSuite_getArgument(from: selector, at: 1) { arg in - presence1 = arg as? ARTPresenceMessage - } - channel.internal.presence.testSuite_getArgument(from: selector, at: 0) { arg in - presence2 = arg as? ARTPresenceMessage - } + guard let intialPresenceMessage = channel.internal.presence.members["\(channel.internal.connectionId):tester"] else { + fail("Missing Presence message"); return + } + + XCTAssertEqual(intialPresenceMessage.memberKey(), "\(client.connection.id!):tester") + + var compareForNewnessMethodCalls = 0 + let hook = channel.internal.presence.testSuite_injectIntoMethod(after: #selector(ARTRealtimePresenceInternal.member(_:isNewerThan:))) { + compareForNewnessMethodCalls += 1 } waitUntil(timeout: testTimeout) { done in - channel.presence.enterClient("tester", data: "new") { error in + channel.presence.enterClient("tester", data: nil) { error in XCTAssertNil(error) done() } } - XCTAssertEqual(presence1.clientId, "tester") - XCTAssertEqual(presence2.clientId, "tester") - XCTAssertEqual(presence1.connectionId, presence2.connectionId) - - XCTAssertEqual(presence1.data as! String, "existing") - XCTAssertEqual(presence2.data as! String, "new") - - expect(presence1.timestamp).to(beLessThan(presence2.timestamp)) + guard let updatedPresenceMessage = channel.internal.presence.members["\(channel.internal.connectionId):tester"] else { + fail("Missing Presence message"); return + } + + XCTAssertEqual(intialPresenceMessage.memberKey(), updatedPresenceMessage.memberKey()) + expect(intialPresenceMessage.timestamp).to(beLessThan(updatedPresenceMessage.timestamp)) + + XCTAssertEqual(compareForNewnessMethodCalls, 1) hook.remove() } From 83f3d92aa386b262050caddf306bc36de59b1b5b Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 5 May 2024 18:05:22 +0200 Subject: [PATCH 38/45] New fix for RTP2a similar to 85de5f51. --- Test/Tests/RealtimeClientPresenceTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index b709e2585..26735cc0d 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1473,9 +1473,9 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(intialPresenceMessage.memberKey(), "\(client.connection.id!):tester") - var compareForNewnessMethodCalls = 0 + var compareForNewnessMethodCalled = false let hook = channel.internal.presence.testSuite_injectIntoMethod(after: #selector(ARTRealtimePresenceInternal.member(_:isNewerThan:))) { - compareForNewnessMethodCalls += 1 + compareForNewnessMethodCalled = true } waitUntil(timeout: testTimeout) { done in @@ -1492,7 +1492,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(intialPresenceMessage.memberKey(), updatedPresenceMessage.memberKey()) expect(intialPresenceMessage.timestamp).to(beLessThan(updatedPresenceMessage.timestamp)) - XCTAssertEqual(compareForNewnessMethodCalls, 1) + XCTAssertTrue(compareForNewnessMethodCalled) hook.remove() } From 1de053873ad8ab9dccefd024d9aeda5669d5b125 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 5 May 2024 18:35:43 +0200 Subject: [PATCH 39/45] Ensure connections for this test are closed. Otherwise it affects execution of the "test__032..." below - https://test-observability.herokuapp.com/repos/ably/ably-cocoa/test_cases/ac44f09c-f521-4bfc-a3d7-58d4f7428c74?branches%5B%5D=fix%2F1889-unskip-presense-tests-looped-4 Also reducing error delay since often callback on ENTER called faster than 0.1 (at least this may be the reason why "shouldn't be called" assertion is being fired every other time). --- Test/Tests/RealtimeClientPresenceTests.swift | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 26735cc0d..7aab59453 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1028,17 +1028,19 @@ class RealtimeClientPresenceTests: XCTestCase { let test = Test() let options = try AblyTests.commonAppSetup(for: test) options.clientId = "john" - - let client1 = ARTRealtime(options: options) - defer { client1.dispose(); client1.close() } - let channelName = test.uniqueChannelName() - let channel1 = client1.channels.get(channelName) - + let client1 = ARTRealtime(options: options) let client2 = ARTRealtime(options: options) - defer { client2.dispose(); client2.close() } + let channel1 = client1.channels.get(channelName) let channel2 = client2.channels.get(channelName) - + + defer { + client1.dispose(); client1.close() + client2.dispose(); client2.close() + expect(client1.connection.state).toEventually(equal(.closed), timeout: testTimeout) + expect(client2.connection.state).toEventually(equal(.closed), timeout: testTimeout) + } + waitUntil(timeout: testTimeout) { done in channel1.presence.subscribe(.enter) { _ in fail("shouldn't be called") @@ -1048,8 +1050,8 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertTrue(error === protocolError.error) done() } - delay(0.1) { - channel2.internal.onError(protocolError) + channel2.internalAsync { _internal in + _internal.onError(protocolError) } } } From 0c149c47b8349e01a5d56a9a71065d2af240b800 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 6 May 2024 00:57:21 +0200 Subject: [PATCH 40/45] Removing "test__83" either, since it's from protocol v1. Currently upon disconnect internal presence map is kept and upon re-connect no synthesized LEAVE event appears to be sent by realtime. --- Test/Tests/RealtimeClientPresenceTests.swift | 53 -------------------- 1 file changed, 53 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 7aab59453..6ffe45b16 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -2826,59 +2826,6 @@ class RealtimeClientPresenceTests: XCTestCase { expect(client1PresenceMessage.data as? String).to(equal(firstClientData)) expect(client2PresenceMessage.data as? String).to(equal(secondClientData)) } - - func skipped__test__083__Presence__private_and_internal_PresenceMap_containing_only_members_that_match_the_current_connectionId__events_applied_to_presence_map__should_be_applied_to_any_LEAVE_event_with_a_connectionId_that_matches_the_current_client_s_connectionId_and_is_not_a_synthesized() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - let client = ARTRealtime(options: options) - client.internal.shouldImmediatelyReconnect = false - defer { client.dispose(); client.close() } - - let channel = client.channels.get(test.uniqueChannelName()) - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.subscribe(.enter) { presence in - XCTAssertEqual(presence.clientId, "one") - channel.presence.unsubscribe() - partialDone() - } - channel.presence.enterClient("one", data: nil) { error in - XCTAssertNil(error) - partialDone() - } - } - - waitUntil(timeout: .seconds(20)) { done in - channel.internal.presence.onceSyncEnds { _ in - // Synthesized leave - expect(channel.internal.presence.internalMembers).to(beEmpty()) - done() - } - client.internal.onDisconnected() - } - - waitUntil(timeout: testTimeout) { done in - channel.presence.subscribe(.enter) { presence in - // Re-entering... - XCTAssertEqual(presence.clientId, "one") - channel.presence.unsubscribe() - done() - } - } - - waitUntil(timeout: testTimeout) { done in - channel.presence.get { presences, error in - XCTAssertNil(error) - guard let presences = presences else { - fail("Presences is nil"); done(); return - } - XCTAssertTrue(channel.internal.presence.syncComplete) - XCTAssertEqual(presences.count, 1) - expect(presences.map { $0.clientId }).to(contain(["one"])) - done() - } - } - } // RTP15d func test__004__Presence__callback_can_be_provided_that_will_be_called_upon_success() throws { From 9b77978592f99b804305dd88b24ec98eb037ef13 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 6 May 2024 19:13:51 +0200 Subject: [PATCH 41/45] Removing "test__082.." since it's purpose is unclear, it claims in the title what is already done in test 080, but does cryptic things in its body. For example the comment "// 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." doesn't make sense to me, because the member added manually shouldn't be removed. Yes, connection after re-connect is different, but spec doesn't says to remove anything (only in case of channel FAILED or DETACHED should reset both presence maps). And the next comment line says that it will be re-entered. Yes, it will be, so it should be kept then, right? I've marked test 080 as testing spec RTP17b. --- Test/Tests/RealtimeClientPresenceTests.swift | 102 +------------------ 1 file changed, 2 insertions(+), 100 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 6ffe45b16..dc72bba80 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -2476,6 +2476,7 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP17 + // RTP17b func 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 { let test = Test() let options = try AblyTests.commonAppSetup(for: test) @@ -2599,7 +2600,7 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.data as? String, "bye") XCTAssertEqual(presence.connectionId, currentConnectionId) XCTAssertEqual(channelB.internal.presence.members.count, 1) - XCTAssertEqual(channelB.internal.presence.internalMembers.count, 0) + XCTAssertEqual(channelB.internal.presence.internalMembers.count, 0) // was removed bc not a 'synthesized leave' (RTP17b) channelB.presence.unsubscribe() partialDone() } @@ -2644,105 +2645,6 @@ class RealtimeClientPresenceTests: XCTestCase { } } } - - // RTP17b - - 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 = AblyTests.newRealtime(options).client - defer { client.dispose(); client.close() } - - let channel = client.channels.get(test.uniqueChannelName()) - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.presence.subscribe(.enter) { presence in - XCTAssertEqual(presence.clientId, "one") - channel.presence.unsubscribe() - partialDone() - } - channel.presence.enterClient("one", data: nil) { error in - XCTAssertNil(error) - partialDone() - } - } - - guard let connectionId = client.connection.id else { - fail("connectionId is empty"); return - } - - channel.internalSync { _internal in - XCTAssertEqual(_internal.presence.internalMembers.count, 1) - } - - let additionalMember = ARTPresenceMessage( - clientId: "two", - action: .enter, - connectionId: connectionId, - id: connectionId + ":0:0" - ) - - // Inject an additional member into the myMember set, then force a suspended state - client.simulateSuspended(beforeSuspension: { done in - channel.internal.presence.internalMembers[additionalMember.clientId!] = additionalMember - done() - }) - expect(client.connection.state).toEventually(equal(.suspended), timeout: testTimeout) - - XCTAssertEqual(channel.internal.presence.internalMembers.count, 2) - - channel.internalSync { _internal in - XCTAssertEqual(_internal.presence.internalMembers.count, 2) - } - - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(3, done: done) - - channel.once(.attached) { stateChange in - XCTAssertNil(stateChange.reason) - partialDone() - } - - // Await Sync - 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.presence.internalMembers.count, 1) - - partialDone() - } - - channel.presence.subscribe(.enter) { presence in - XCTAssertEqual(presence.clientId, "two") - channel.presence.unsubscribe() - partialDone() - } - - // Reconnect - client.connect() - } - - // Wait for server - waitUntil(timeout: testTimeout) { done in - delay(1, closure: done) - } - - client.requestPresenceSyncForChannel(channel) - - XCTAssertFalse(channel.presence.syncComplete) - waitUntil(timeout: testTimeout) { done in - channel.presence.get { presences, error in - XCTAssertNil(error) - guard let presences = presences else { - fail("Presences is nil"); done(); return - } - XCTAssertTrue(channel.presence.syncComplete) - XCTAssertEqual(presences.count, 2) - expect(presences.map { $0.clientId }).to(contain(["one", "two"])) - done() - } - } - } // RTP17i, RTP17g func test__200__Presence__PresenceMap_should_perform_re_entry_whenever_a_channel_moves_into_the_attached_state_and_presence_message_consists_of_enter_action_with_client_id_and_data() throws { From 9dac73ef18ff5088975244ae83f9233231531ae7 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Mon, 6 May 2024 23:44:08 +0200 Subject: [PATCH 42/45] Another fix for RTP2a (ENTER/PRESENT ambiguity). --- Test/Tests/RealtimeClientPresenceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index dc72bba80..6c9ea0b54 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1459,7 +1459,7 @@ class RealtimeClientPresenceTests: XCTestCase { let partialDone = AblyTests.splitDone(2, done: done) channel.presence.subscribe { presence in XCTAssertEqual(presence.clientId, "tester") - XCTAssertEqual(presence.action, .enter) + XCTAssertTrue(presence.action == .enter || presence.action == .present) channel.presence.unsubscribe() partialDone() } From d0ee62c1965d6f8383c23b1a947153e715b20599 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Thu, 9 May 2024 16:48:22 +0200 Subject: [PATCH 43/45] Fixed test__080... and test__050... to properly check amount of members (syncInProgress vs not syncInProgress). --- Test/Tests/RealtimeClientPresenceTests.swift | 21 ++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 6c9ea0b54..d83a099e1 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1839,8 +1839,11 @@ class RealtimeClientPresenceTests: XCTestCase { channel.presence.subscribe(.leave) { leave in XCTAssertEqual(leave.clientId, "user11") let absentMember = channel.internal.presence.members.first { _, m in m.clientId == "user11" }.map { $0.value } - XCTAssertTrue(channel.internal.presence.syncInProgress) - XCTAssertEqual(absentMember?.action, .absent) + if channel.internal.presence.syncInProgress { + XCTAssertEqual(absentMember?.action, .absent) + } else { + XCTAssertEqual(absentMember?.action, .leave) + } partialDone() } @@ -2587,7 +2590,12 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.action, ARTPresenceAction.leave) XCTAssertEqual(presence.data as? String, "bye") XCTAssertNotEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelA.internal.presence.members.count, 1) + if channelA.internal.presence.syncInProgress { + XCTAssertEqual(channelA.internal.presence.members.filter({ $0.value.action != .present }).count, 1) + XCTAssertEqual(channelA.internal.presence.members.filter({ $0.value.action != .absent }).count, 1) + } else { + XCTAssertEqual(channelA.internal.presence.members.count, 1) + } XCTAssertEqual(channelA.internal.presence.internalMembers.count, 1) channelA.presence.unsubscribe() partialDone() @@ -2599,7 +2607,12 @@ class RealtimeClientPresenceTests: XCTestCase { XCTAssertEqual(presence.action, ARTPresenceAction.leave) XCTAssertEqual(presence.data as? String, "bye") XCTAssertEqual(presence.connectionId, currentConnectionId) - XCTAssertEqual(channelB.internal.presence.members.count, 1) + if channelB.internal.presence.syncInProgress { + XCTAssertEqual(channelB.internal.presence.members.filter({ $0.value.action != .present }).count, 1) + XCTAssertEqual(channelB.internal.presence.members.filter({ $0.value.action != .absent }).count, 1) + } else { + XCTAssertEqual(channelB.internal.presence.members.count, 1) + } XCTAssertEqual(channelB.internal.presence.internalMembers.count, 0) // was removed bc not a 'synthesized leave' (RTP17b) channelB.presence.unsubscribe() partialDone() From 96eea55905c6c3454f2bd22d1f6674297540ecce Mon Sep 17 00:00:00 2001 From: Marat Al Date: Thu, 9 May 2024 18:16:40 +0200 Subject: [PATCH 44/45] Removed extra attach, because according to RTL4i it happens after connection goes to CONNECTED (also it's called inside members `get` too). Also removed interception of a SYNC message, since it's not needed here - we just check that sync process was started before calling members `get` and then check if it was finished upon `get` callback (at least that's what the name of the test claims should be happening). --- Test/Tests/RealtimeClientPresenceTests.swift | 34 +++++++------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index d83a099e1..8bcf064ba 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -3351,33 +3351,23 @@ class RealtimeClientPresenceTests: XCTestCase { expect(client.connection.state).toEventually(equal(.closed), timeout: testTimeout) } let channel = client.channels.get(channelName) - - let query = ARTRealtimePresenceQuery() - XCTAssertTrue(query.waitForSync) - - var transport = client.internal.transport as! TestProxyTransport - + expect(channel.internal.presence.syncInProgress).toEventually(beTrue(), timeout: testTimeout) + waitUntil(timeout: testTimeout) { done in - channel.attach { error in + let query = ARTRealtimePresenceQuery() + XCTAssertTrue(query.waitForSync) + XCTAssertEqual(channel.internal.presence.syncInProgress, true) + channel.presence.get(query) { members, error in XCTAssertNil(error) - transport = client.internal.transport as! TestProxyTransport - transport.setListenerBeforeProcessingIncomingMessage { protocolMessage in - if protocolMessage.action == .sync { - channel.presence.get(query) { members, error in - XCTAssertNil(error) - if let members { - XCTAssertEqual(members.count, 150) - done() - } else { - XCTFail("Expected members to be non-nil") - } - } - transport.setListenerBeforeProcessingIncomingMessage(nil) - } + if let members { + XCTAssertEqual(members.count, 150) + XCTAssertEqual(channel.internal.presence.syncInProgress, false) + done() + } else { + XCTFail("Expected members to be non-nil") } } } - transport.setListenerBeforeProcessingIncomingMessage(nil) } // RTP11c1 From f7ffb41a8fefb1f6d2d79ec1d088d02ff270a020 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Fri, 10 May 2024 01:00:27 +0200 Subject: [PATCH 45/45] Fix queue to receive fake message. --- Test/Tests/RealtimeClientPresenceTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index 8bcf064ba..bbca9b79a 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -1859,7 +1859,9 @@ class RealtimeClientPresenceTests: XCTestCase { leaveMessage.presence = [ ARTPresenceMessage(clientId: "user11", action: .leave, connectionId: "another", id: "another:123:0", timestamp: Date()), ] - transport.receive(leaveMessage) + channel.internalAsync { _ in + transport.receive(leaveMessage) + } partialDone() } }