From 70b357318f8905b99750c81527e99a8bdcad3c57 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 5 Aug 2024 02:48:51 -0700 Subject: [PATCH 1/7] 1st attempt --- driver/lib/sessions/ios.ts | 20 +++++++++++++++++--- driver/lib/sessions/log-monitor.ts | 7 ++++++- driver/lib/sessions/session.ts | 2 +- example/ruby/example_sample2_ios.rb | 9 ++++++++- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/driver/lib/sessions/ios.ts b/driver/lib/sessions/ios.ts index 52f9c110..cee2de62 100644 --- a/driver/lib/sessions/ios.ts +++ b/driver/lib/sessions/ios.ts @@ -50,9 +50,10 @@ export async function startIOSSession( export async function connectIOSSession( this: FlutterDriver, iosdriver: XCUITestDriver, - caps: Record + caps: Record, + clear=false ): Promise { - const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps); + const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps, clear=clear); return await connectSocket.bind(this)(observatoryWsUri, iosdriver, caps); } @@ -70,8 +71,20 @@ async function requireFreePort( export async function getObservatoryWsUri ( this: FlutterDriver, - proxydriver: XCUITestDriver, caps: Record + proxydriver: XCUITestDriver, caps: Record, + clear: boolean = false ): Promise { + if (clear) { + this._logmon?.clearlastMatch(); + this._logmon?.stop(); + this._logmon?.start(); + + this.log.info('waiting a bit'); + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + await sleep(5000); + this.log.info('ok'); + } + let urlObject; if (caps.observatoryWsUri) { urlObject = new URL(caps.observatoryWsUri); @@ -95,6 +108,7 @@ export async function getObservatoryWsUri ( `https://github.com/appium/appium-flutter-driver#usage and that it does not crash on startup.` ); } + // 'this._logmon.lastMatch' should be proper one urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL; } if (!proxydriver.isRealDevice()) { diff --git a/driver/lib/sessions/log-monitor.ts b/driver/lib/sessions/log-monitor.ts index 2b4e6278..bb72d622 100644 --- a/driver/lib/sessions/log-monitor.ts +++ b/driver/lib/sessions/log-monitor.ts @@ -1,5 +1,5 @@ import type {EventEmitter} from 'node:events'; - +import { log as logger } from '../logger'; export interface LogEntry { timestamp: number; level: string, @@ -25,6 +25,10 @@ export class LogMonitor { return Boolean(this._outputListener); } + clearlastMatch() { + this._lastMatch = null; + } + get lastMatch(): LogEntry | null { return this._lastMatch; } @@ -53,6 +57,7 @@ export class LogMonitor { private async _onOutput(logEntry: LogEntry): Promise { if (await this._filter(logEntry)) { + logger.info(`>>>>> ${logEntry.message}`); this._lastMatch = logEntry; } } diff --git a/driver/lib/sessions/session.ts b/driver/lib/sessions/session.ts index cde880ac..e80b917c 100644 --- a/driver/lib/sessions/session.ts +++ b/driver/lib/sessions/session.ts @@ -17,7 +17,7 @@ export const reConnectFlutterDriver = async function(this: FlutterDriver, caps: switch (_.toLower(caps.platformName)) { case PLATFORM.IOS: - this.socket = await connectIOSSession.bind(this)(this.proxydriver, caps); + this.socket = await connectIOSSession.bind(this)(this.proxydriver, caps, true); break; case PLATFORM.ANDROID: this.socket = await connectAndroidSession.bind(this)(this.proxydriver, caps); diff --git a/example/ruby/example_sample2_ios.rb b/example/ruby/example_sample2_ios.rb index 592f3e8c..70870bcd 100644 --- a/example/ruby/example_sample2_ios.rb +++ b/example/ruby/example_sample2_ios.rb @@ -13,7 +13,6 @@ class ExampleTests < Minitest::Test platformVersion: '17.4', deviceName: 'iPhone 15 Plus', app: "#{Dir.pwd}/../sample2/iOSFullScreen.zip", - showIOSLog: true, wdaLaunchTimeout: 600_000 }, appium_lib: { @@ -59,5 +58,13 @@ def test_run_example_ios element = @driver.wait_until { |d| d.find_element :accessibility_id, 'currentCounter' } assert_equal 'Current counter: 2', element.text + + @driver.context = 'FLUTTER' + @driver.terminate_app 'samples.flutter.example.IOSFullScreen' + @driver.activate_app 'samples.flutter.example.IOSFullScreen' + + text_finder = by_text 'Tap me!' + element = ::Appium::Flutter::Element.new(@driver, finder: text_finder) + assert_equal 'Tap me!', element.text end end From ed50a189d360b8c34060447510ca9b479749b267 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 6 Aug 2024 00:02:47 -0700 Subject: [PATCH 2/7] add retry --- README.md | 2 ++ driver/CHANGELOG.md | 4 ++++ driver/lib/desired-caps.ts | 6 ++++++ driver/lib/sessions/android.ts | 20 ++++++++++++++++---- driver/lib/sessions/ios.ts | 22 ++++++++++------------ driver/lib/sessions/log-monitor.ts | 25 ++++++++++++++++++++++++- driver/lib/sessions/session.ts | 2 +- driver/package.json | 1 + example/ruby/example_sample2_ios.rb | 5 ++++- 9 files changed, 68 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e4bde661..71d006f5 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ SKIP_IOS=1 appium driver doctor flutter | Capability | Description | Example Values | | - | - | -| +| appium:retryBackoffTime | The interval to find the observetory url from logs. (default 3000ms)|500| +| appium:maxRetryCount | The count to find the observatory url. (default 20) | 10| | appium:observatoryWsUri | The URL to attach to the Dart VM. The Appium Flutter Driver finds the WebSocket URL from the device log by default. You can skip the finding the URL process by specifying this capability. Then, this driver attempt to establish a WebSocket connection against the given WebSocket URL. Note that this capability expects the URL is ready for access by outside an appium session. This flutter driver does not do port-forwarding with this capability. You may need to coordinate the port-forwarding as well. | 'ws://127.0.0.1:60992/aaaaaaaaaaa=/ws' | | appium:isolateId | The isolate id to attach to as the initial attempt. A session can change the isolate with `flutter:setIsolateId` command. The default behavior finds `main` isolate id and attaches it. | `isolates/2978358234363215`, `2978358234363215` | | appium:skipPortForward | Whether skip port forwarding from the flutter driver local to the device under test with `observatoryWsUri` capability. It helps you to manage the application under test, the observatory URL and the port forwarding configuration. The default is `true`. | true, false | diff --git a/driver/CHANGELOG.md b/driver/CHANGELOG.md index 64510118..37870a8d 100644 --- a/driver/CHANGELOG.md +++ b/driver/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.9.1 +- Fix observatory url finding after an app activation + - Bring `appium:maxRetryCount` and `appium:retryBackoffTime` back to use for the observaotry url findings. + ## 2.9.0 - Tune syslog scanning to find the observatory url - Drop `appium:maxRetryCount` and `appium:retryBackoffTime` as no usage diff --git a/driver/lib/desired-caps.ts b/driver/lib/desired-caps.ts index 4c32525d..ae1bdb0c 100755 --- a/driver/lib/desired-caps.ts +++ b/driver/lib/desired-caps.ts @@ -5,6 +5,9 @@ export const desiredCapConstraints = { avd: { isString: true, }, + maxRetryCount: { + isNumber: true, + }, platformName: { inclusionCaseInsensitive: [ 'iOS', @@ -13,6 +16,9 @@ export const desiredCapConstraints = { isString: true, presence: true, }, + retryBackoffTime: { + isNumber: true, + }, udid: { isString: true, }, diff --git a/driver/lib/sessions/android.ts b/driver/lib/sessions/android.ts index 8504d352..b69a2152 100644 --- a/driver/lib/sessions/android.ts +++ b/driver/lib/sessions/android.ts @@ -42,9 +42,10 @@ export async function startAndroidSession( export async function connectAndroidSession ( this: FlutterDriver, androiddriver: AndroidUiautomator2Driver, - caps: Record + caps: Record, + clearLog: boolean = false ): Promise { - const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps); + const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps, clearLog); return await connectSocket.bind(this)(observatoryWsUri, caps); } @@ -52,7 +53,14 @@ export async function getObservatoryWsUri ( this: FlutterDriver, proxydriver: AndroidUiautomator2Driver, caps: StringRecord, + clearLog: boolean = false ): Promise { + if (clearLog) { + this._logmon?.clearlastMatch(); + this._logmon?.stop(); + this._logmon?.start(); + } + let urlObject: URL; if (caps.observatoryWsUri) { urlObject = new URL(caps.observatoryWsUri); @@ -69,14 +77,18 @@ export async function getObservatoryWsUri ( `Have you disabled it in capabilities?` ); } - if (!this._logmon.lastMatch) { + const lastMatch = await this._logmon.waitForLastMatchExist( + caps.maxRetryCount, + caps.retryBackoffTime + ); + if (!lastMatch) { throw new Error( `No observatory URL matching to '${OBSERVATORY_URL_PATTERN}' was found in the device log. ` + `Please make sure the application under test is configured properly according to ` + `https://github.com/appium/appium-flutter-driver#usage and that it does not crash on startup.` ); } - urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL; + urlObject = extractObservatoryUrl(lastMatch) as URL; } const remotePort = urlObject.port; this.portForwardLocalPort = caps.forwardingPort ?? remotePort; diff --git a/driver/lib/sessions/ios.ts b/driver/lib/sessions/ios.ts index cee2de62..c9eb11a7 100644 --- a/driver/lib/sessions/ios.ts +++ b/driver/lib/sessions/ios.ts @@ -51,9 +51,9 @@ export async function connectIOSSession( this: FlutterDriver, iosdriver: XCUITestDriver, caps: Record, - clear=false + clearLog: boolean = false ): Promise { - const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps, clear=clear); + const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps, clearLog); return await connectSocket.bind(this)(observatoryWsUri, iosdriver, caps); } @@ -72,17 +72,12 @@ async function requireFreePort( export async function getObservatoryWsUri ( this: FlutterDriver, proxydriver: XCUITestDriver, caps: Record, - clear: boolean = false + clearLog: boolean = false ): Promise { - if (clear) { + if (clearLog) { this._logmon?.clearlastMatch(); this._logmon?.stop(); this._logmon?.start(); - - this.log.info('waiting a bit'); - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - await sleep(5000); - this.log.info('ok'); } let urlObject; @@ -101,15 +96,18 @@ export async function getObservatoryWsUri ( `Have you disabled it in capabilities?` ); } - if (!this._logmon.lastMatch) { + const lastMatch = await this._logmon.waitForLastMatchExist( + caps.maxRetryCount, + caps.retryBackoffTime + ); + if (!lastMatch) { throw new Error( `No observatory URL matching to '${OBSERVATORY_URL_PATTERN}' was found in the device log. ` + `Please make sure the application under test is configured properly according to ` + `https://github.com/appium/appium-flutter-driver#usage and that it does not crash on startup.` ); } - // 'this._logmon.lastMatch' should be proper one - urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL; + urlObject = extractObservatoryUrl(lastMatch) as URL; } if (!proxydriver.isRealDevice()) { this.log.info(`Running on iOS simulator`); diff --git a/driver/lib/sessions/log-monitor.ts b/driver/lib/sessions/log-monitor.ts index bb72d622..85a4a484 100644 --- a/driver/lib/sessions/log-monitor.ts +++ b/driver/lib/sessions/log-monitor.ts @@ -1,11 +1,15 @@ import type {EventEmitter} from 'node:events'; import { log as logger } from '../logger'; +import { retryInterval } from 'asyncbox'; export interface LogEntry { timestamp: number; level: string, message: string; } +const DEFAULT_MAX_RETRY_COUNT = 20; +const DEFAULT_BACKOFF_TIME_MS = 3000; + export type Filter = (x: LogEntry) => Promise; export class LogMonitor { @@ -18,7 +22,6 @@ export class LogMonitor { this._logsEmitter = logsEmitter; this._outputListener = null; this._filter = filter; - this._lastMatch = null; } get started(): boolean { @@ -33,6 +36,26 @@ export class LogMonitor { return this._lastMatch; } + async waitForLastMatchExist( + maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT, + retryBackoffTime: number = DEFAULT_BACKOFF_TIME_MS, + ): Promise { + return await retryInterval( + maxRetryCount, + retryBackoffTime, + async () => { + if (this._lastMatch !== null) { + return this._lastMatch; + } + throw new Error( + `No matched log found with ${retryBackoffTime} ms interval ` + + `up to ${maxRetryCount} times. Increasing appium:retryBackoffTime ` + + `and appium:maxRetryCount would help.` + ); + }, + ); + }; + start(): this { if (this.started) { return this; diff --git a/driver/lib/sessions/session.ts b/driver/lib/sessions/session.ts index e80b917c..64d42759 100644 --- a/driver/lib/sessions/session.ts +++ b/driver/lib/sessions/session.ts @@ -20,7 +20,7 @@ export const reConnectFlutterDriver = async function(this: FlutterDriver, caps: this.socket = await connectIOSSession.bind(this)(this.proxydriver, caps, true); break; case PLATFORM.ANDROID: - this.socket = await connectAndroidSession.bind(this)(this.proxydriver, caps); + this.socket = await connectAndroidSession.bind(this)(this.proxydriver, caps, true); break; default: this.log.errorAndThrow( diff --git a/driver/package.json b/driver/package.json index dcb16517..476d5fa6 100644 --- a/driver/package.json +++ b/driver/package.json @@ -77,6 +77,7 @@ "appium-android-driver": "^9.8.0", "appium-uiautomator2-driver": "^3.7.3", "appium-xcuitest-driver": "^7.24.0", + "asyncbox": "^3.0.0", "bluebird": "^3.1.1", "lodash": "^4.0.0", "portscanner": "^2.2.0", diff --git a/example/ruby/example_sample2_ios.rb b/example/ruby/example_sample2_ios.rb index 70870bcd..05a18f79 100644 --- a/example/ruby/example_sample2_ios.rb +++ b/example/ruby/example_sample2_ios.rb @@ -13,7 +13,10 @@ class ExampleTests < Minitest::Test platformVersion: '17.4', deviceName: 'iPhone 15 Plus', app: "#{Dir.pwd}/../sample2/iOSFullScreen.zip", - wdaLaunchTimeout: 600_000 + wdaLaunchTimeout: 600_000, + maxRetryCount: 20, + retryBackoffTime: 5000, + }, appium_lib: { export_session: true, From 11a8b51b0374c00fc046ff9000a204c9eb83ba64 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 6 Aug 2024 02:17:36 -0700 Subject: [PATCH 3/7] add tests --- example/ruby/example_sample2.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/example/ruby/example_sample2.rb b/example/ruby/example_sample2.rb index 828a555f..d21ab8a3 100644 --- a/example/ruby/example_sample2.rb +++ b/example/ruby/example_sample2.rb @@ -12,7 +12,7 @@ class ExampleTests < Minitest::Test automationName: 'flutter', udid: 'emulator-5554', deviceName: 'Android', - app: "#{Dir.pwd}/example/sample2/app-debug.apk" + app: "#{Dir.pwd}/../sample2/app-debug.apk" }, appium_lib: { export_session: true, @@ -59,5 +59,13 @@ def test_run_example_android element = @driver.wait_until { |d| d.find_element :id, 'dev.flutter.example.androidfullscreen:id/counter_label' } assert_equal 'Current count: 2', element.text + + @driver.context = 'FLUTTER' + @driver.terminate_app 'dev.flutter.example.androidfullscreen' + @driver.activate_app 'dev.flutter.example.androidfullscreen' + + text_finder = by_text 'Tap me!' + element = ::Appium::Flutter::Element.new(@driver, finder: text_finder) + assert_equal 'Tap me!', element.text end end From 08ce098dd53d3625841f7c4b8292c0ef766c4141 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 6 Aug 2024 02:20:04 -0700 Subject: [PATCH 4/7] updaye log --- driver/lib/sessions/log-monitor.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/driver/lib/sessions/log-monitor.ts b/driver/lib/sessions/log-monitor.ts index 85a4a484..e129097d 100644 --- a/driver/lib/sessions/log-monitor.ts +++ b/driver/lib/sessions/log-monitor.ts @@ -1,5 +1,4 @@ import type {EventEmitter} from 'node:events'; -import { log as logger } from '../logger'; import { retryInterval } from 'asyncbox'; export interface LogEntry { timestamp: number; @@ -80,7 +79,6 @@ export class LogMonitor { private async _onOutput(logEntry: LogEntry): Promise { if (await this._filter(logEntry)) { - logger.info(`>>>>> ${logEntry.message}`); this._lastMatch = logEntry; } } From 801529f95d4f59fbdbf86171817978d585eaf16d Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 6 Aug 2024 09:26:18 -0700 Subject: [PATCH 5/7] Update example_sample2.rb --- example/ruby/example_sample2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/ruby/example_sample2.rb b/example/ruby/example_sample2.rb index d21ab8a3..66ea111c 100644 --- a/example/ruby/example_sample2.rb +++ b/example/ruby/example_sample2.rb @@ -12,7 +12,7 @@ class ExampleTests < Minitest::Test automationName: 'flutter', udid: 'emulator-5554', deviceName: 'Android', - app: "#{Dir.pwd}/../sample2/app-debug.apk" + app: "#{Dir.pwd}/sample2/app-debug.apk" }, appium_lib: { export_session: true, From 4dc3fc6915d9110398acb826f1f258669e7d610c Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 6 Aug 2024 20:45:54 +0000 Subject: [PATCH 6/7] tweak a bit --- README.md | 2 +- driver/lib/sessions/log-monitor.ts | 2 +- example/ruby/example_sample2.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 71d006f5..50c61cd8 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ SKIP_IOS=1 appium driver doctor flutter | Capability | Description | Example Values | | - | - | -| | appium:retryBackoffTime | The interval to find the observetory url from logs. (default 3000ms)|500| -| appium:maxRetryCount | The count to find the observatory url. (default 20) | 10| +| appium:maxRetryCount | The count to find the observatory url. (default 10) | 20| | appium:observatoryWsUri | The URL to attach to the Dart VM. The Appium Flutter Driver finds the WebSocket URL from the device log by default. You can skip the finding the URL process by specifying this capability. Then, this driver attempt to establish a WebSocket connection against the given WebSocket URL. Note that this capability expects the URL is ready for access by outside an appium session. This flutter driver does not do port-forwarding with this capability. You may need to coordinate the port-forwarding as well. | 'ws://127.0.0.1:60992/aaaaaaaaaaa=/ws' | | appium:isolateId | The isolate id to attach to as the initial attempt. A session can change the isolate with `flutter:setIsolateId` command. The default behavior finds `main` isolate id and attaches it. | `isolates/2978358234363215`, `2978358234363215` | | appium:skipPortForward | Whether skip port forwarding from the flutter driver local to the device under test with `observatoryWsUri` capability. It helps you to manage the application under test, the observatory URL and the port forwarding configuration. The default is `true`. | true, false | diff --git a/driver/lib/sessions/log-monitor.ts b/driver/lib/sessions/log-monitor.ts index e129097d..3d80fcd4 100644 --- a/driver/lib/sessions/log-monitor.ts +++ b/driver/lib/sessions/log-monitor.ts @@ -6,7 +6,7 @@ export interface LogEntry { message: string; } -const DEFAULT_MAX_RETRY_COUNT = 20; +const DEFAULT_MAX_RETRY_COUNT = 10; const DEFAULT_BACKOFF_TIME_MS = 3000; export type Filter = (x: LogEntry) => Promise; diff --git a/example/ruby/example_sample2.rb b/example/ruby/example_sample2.rb index 66ea111c..9133307a 100644 --- a/example/ruby/example_sample2.rb +++ b/example/ruby/example_sample2.rb @@ -12,7 +12,7 @@ class ExampleTests < Minitest::Test automationName: 'flutter', udid: 'emulator-5554', deviceName: 'Android', - app: "#{Dir.pwd}/sample2/app-debug.apk" + app: "#{Dir.pwd}/example/sample2/app-debug.apk" }, appium_lib: { export_session: true, From af2e40ee4952e426d5ddd812763b219def9c5ff6 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 6 Aug 2024 23:24:40 -0700 Subject: [PATCH 7/7] Update example_sample2.rb --- example/ruby/example_sample2.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/ruby/example_sample2.rb b/example/ruby/example_sample2.rb index 9133307a..69c3bc54 100644 --- a/example/ruby/example_sample2.rb +++ b/example/ruby/example_sample2.rb @@ -12,7 +12,9 @@ class ExampleTests < Minitest::Test automationName: 'flutter', udid: 'emulator-5554', deviceName: 'Android', - app: "#{Dir.pwd}/example/sample2/app-debug.apk" + app: "#{Dir.pwd}/example/sample2/app-debug.apk", + maxRetryCount: 20, + retryBackoffTime: 5000, }, appium_lib: { export_session: true,