Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: re connect with a new observatory url after an app start #727

Merged
merged 8 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 |
Expand Down
4 changes: 2 additions & 2 deletions driver/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Changelog

## 2.9.1
- Fix observatory url connection after the app activation command
- Bring `appium:maxRetryCount` and `appium:retryBackoffTime` back
- 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
Expand Down
6 changes: 6 additions & 0 deletions driver/lib/desired-caps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const desiredCapConstraints = {
avd: {
isString: true,
},
maxRetryCount: {
isNumber: true,
},
platformName: {
inclusionCaseInsensitive: [
'iOS',
Expand All @@ -13,6 +16,9 @@ export const desiredCapConstraints = {
isString: true,
presence: true,
},
retryBackoffTime: {
isNumber: true,
},
udid: {
isString: true,
},
Expand Down
20 changes: 16 additions & 4 deletions driver/lib/sessions/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,25 @@ export async function startAndroidSession(
export async function connectAndroidSession (
this: FlutterDriver,
androiddriver: AndroidUiautomator2Driver,
caps: Record<string, any>
caps: Record<string, any>,
clearLog: boolean = false
): Promise<IsolateSocket> {
const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps);
const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps, clearLog);
return await connectSocket.bind(this)(observatoryWsUri, caps);
}

export async function getObservatoryWsUri (
this: FlutterDriver,
proxydriver: AndroidUiautomator2Driver,
caps: StringRecord,
clearLog: boolean = false
): Promise<string> {
if (clearLog) {
this._logmon?.clearlastMatch();
this._logmon?.stop();
this._logmon?.start();
}

let urlObject: URL;
if (caps.observatoryWsUri) {
urlObject = new URL(caps.observatoryWsUri);
Expand All @@ -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;
Expand Down
22 changes: 17 additions & 5 deletions driver/lib/sessions/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ export async function startIOSSession(
export async function connectIOSSession(
this: FlutterDriver,
iosdriver: XCUITestDriver,
caps: Record<string, any>
caps: Record<string, any>,
clearLog: boolean = false
): Promise<IsolateSocket> {
const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps);
const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps, clearLog);
return await connectSocket.bind(this)(observatoryWsUri, iosdriver, caps);
}

Expand All @@ -70,8 +71,15 @@ async function requireFreePort(

export async function getObservatoryWsUri (
this: FlutterDriver,
proxydriver: XCUITestDriver, caps: Record<string, any>
proxydriver: XCUITestDriver, caps: Record<string, any>,
clearLog: boolean = false
): Promise<string> {
if (clearLog) {
this._logmon?.clearlastMatch();
this._logmon?.stop();
this._logmon?.start();
}

let urlObject;
if (caps.observatoryWsUri) {
urlObject = new URL(caps.observatoryWsUri);
Expand All @@ -88,14 +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.`
);
}
urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL;
urlObject = extractObservatoryUrl(lastMatch) as URL;
}
if (!proxydriver.isRealDevice()) {
this.log.info(`Running on iOS simulator`);
Expand Down
30 changes: 28 additions & 2 deletions driver/lib/sessions/log-monitor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type {EventEmitter} from 'node:events';

import { retryInterval } from 'asyncbox';
export interface LogEntry {
timestamp: number;
level: string,
message: string;
}

const DEFAULT_MAX_RETRY_COUNT = 10;
const DEFAULT_BACKOFF_TIME_MS = 3000;

export type Filter = (x: LogEntry) => Promise<boolean>;

export class LogMonitor {
Expand All @@ -18,17 +21,40 @@ export class LogMonitor {
this._logsEmitter = logsEmitter;
this._outputListener = null;
this._filter = filter;
this._lastMatch = null;
}

get started(): boolean {
return Boolean(this._outputListener);
}

clearlastMatch() {
this._lastMatch = null;
}

get lastMatch(): LogEntry | null {
return this._lastMatch;
}

async waitForLastMatchExist(
maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT,
retryBackoffTime: number = DEFAULT_BACKOFF_TIME_MS,
): Promise<LogEntry | null> {
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;
Expand Down
4 changes: 2 additions & 2 deletions driver/lib/sessions/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ 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);
this.socket = await connectAndroidSession.bind(this)(this.proxydriver, caps, true);
break;
default:
this.log.errorAndThrow(
Expand Down
1 change: 1 addition & 0 deletions driver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion example/ruby/example_sample2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -59,5 +61,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
14 changes: 12 additions & 2 deletions example/ruby/example_sample2_ios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ class ExampleTests < Minitest::Test
platformVersion: '17.4',
deviceName: 'iPhone 15 Plus',
app: "#{Dir.pwd}/../sample2/iOSFullScreen.zip",
showIOSLog: true,
wdaLaunchTimeout: 600_000
wdaLaunchTimeout: 600_000,
maxRetryCount: 20,
retryBackoffTime: 5000,

},
appium_lib: {
export_session: true,
Expand Down Expand Up @@ -59,5 +61,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
Loading