-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
(Android) Introduce ANR monitor to produce warn-logs #1926
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
detox/android/detox/src/main/java/com/wix/detox/DetoxANRHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.wix.detox | ||
|
||
import android.util.Log | ||
import com.github.anrwatchdog.ANRWatchDog | ||
|
||
class DetoxANRHandler(private val wsClient: WebSocketClient) { | ||
fun attach() { | ||
ANRWatchDog().setReportMainThreadOnly().setANRListener { | ||
val info = mapOf("threadDump" to Log.getStackTraceString(it)) | ||
wsClient.sendAction(ACTION_NAME, info, MESSAGE_ID) | ||
}.start() | ||
|
||
ANRWatchDog().setANRListener { | ||
Log.e(LOG_TAG, "App nonresnponsive detection!", it) | ||
}.start() | ||
} | ||
|
||
companion object { | ||
val LOG_TAG: String = DetoxANRHandler::class.java.simpleName | ||
|
||
private const val ACTION_NAME = "AppNonresponsiveDetected" | ||
private const val MESSAGE_ID = -10001L | ||
} | ||
} |
9 changes: 4 additions & 5 deletions
9
detox/android/detox/src/main/java/com/wix/detox/DetoxCrashHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,21 @@ | ||
package com.wix.detox | ||
|
||
import android.util.Log | ||
import org.apache.commons.lang3.exception.ExceptionUtils | ||
|
||
class DetoxCrashHandler(private val wsClient: WebSocketClient) { | ||
fun attach() { | ||
Thread.setDefaultUncaughtExceptionHandler { thread, exception -> | ||
Log.e(LOG_TAG, "Crash detected!!! thread=${thread.name} (${thread.id})") | ||
|
||
val crashInfo = mapOf("errorDetails" to "@Thread ${thread.name}(${thread.id}):\n${ExceptionUtils.getStackTrace(exception)}") | ||
wsClient.sendAction(APP_CRASH_ACTION_NAME, crashInfo, APP_CRASH_MESSAGE_ID) | ||
val crashInfo = mapOf("errorDetails" to "@Thread ${thread.name}(${thread.id}):\n${Log.getStackTraceString(exception)}") | ||
wsClient.sendAction(ACTION_NAME, crashInfo, MESSAGE_ID) | ||
} | ||
} | ||
|
||
companion object { | ||
val LOG_TAG: String = DetoxCrashHandler::class.java.simpleName | ||
|
||
const val APP_CRASH_ACTION_NAME = "AppWillTerminateWithError" | ||
const val APP_CRASH_MESSAGE_ID = -10000L | ||
private const val ACTION_NAME = "AppWillTerminateWithError" | ||
private const val MESSAGE_ID = -10000L | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,8 @@ const defaultPlatformEnv = { | |
}; | ||
|
||
describe('Detox', () => { | ||
let client; | ||
let mockLogger; | ||
let fs; | ||
let Detox; | ||
let detox; | ||
|
@@ -27,12 +29,16 @@ describe('Detox', () => { | |
const deviceMockData = {lastConstructorArguments: null}; | ||
|
||
beforeEach(async () => { | ||
function setCustomMock(modulePath, dataObject) { | ||
function setCustomClassMock(modulePath, dataObject, mockObject = {}) { | ||
const JestMock = jest.genMockFromModule(modulePath); | ||
class FinalMock extends JestMock { | ||
constructor(...rest) { | ||
super(rest); | ||
dataObject.lastConstructorArguments = rest; | ||
|
||
Object.keys(mockObject).forEach((key) => { | ||
this[key] = mockObject[key].bind(this); | ||
}); | ||
} | ||
on(event, callback) { | ||
if (event === 'launchApp') { | ||
|
@@ -43,13 +49,23 @@ describe('Detox', () => { | |
jest.setMock(modulePath, FinalMock); | ||
} | ||
|
||
jest.mock('./utils/logger'); | ||
jest.mock('fs'); | ||
jest.mock('fs-extra'); | ||
fs = require('fs'); | ||
jest.mock('./ios/expect'); | ||
setCustomMock('./client/Client', clientMockData); | ||
setCustomMock('./devices/Device', deviceMockData); | ||
|
||
mockLogger = jest.genMockFromModule('./utils/logger'); | ||
mockLogger.child.mockReturnValue(mockLogger); | ||
jest.mock('./utils/logger', () => mockLogger); | ||
|
||
setCustomClassMock('./devices/Device', deviceMockData); | ||
|
||
client = { | ||
setNonresponsivenessListener: jest.fn(), | ||
getPendingCrashAndReset: jest.fn(), | ||
dumpPendingRequests: jest.fn(), | ||
}; | ||
setCustomClassMock('./client/Client', clientMockData, client); | ||
|
||
process.env = Object.assign({}, defaultPlatformEnv[process.platform]); | ||
|
||
|
@@ -59,8 +75,6 @@ describe('Detox', () => { | |
jest.mock('./devices/drivers/SimulatorDriver'); | ||
jest.mock('./devices/Device'); | ||
jest.mock('./server/DetoxServer'); | ||
jest.mock('./client/Client'); | ||
jest.mock('./utils/logger'); | ||
}); | ||
|
||
it(`Passing --cleanup should shutdown the currently running device`, async () => { | ||
|
@@ -158,10 +172,11 @@ describe('Detox', () => { | |
}); | ||
|
||
it(`handleAppCrash if client has a pending crash`, async () => { | ||
client.getPendingCrashAndReset.mockReturnValueOnce('crash'); | ||
|
||
Detox = require('./Detox'); | ||
detox = new Detox({deviceConfig: validDeviceConfigWithSession}); | ||
await detox.init(); | ||
detox._client.getPendingCrashAndReset.mockReturnValueOnce('crash'); // TODO: rewrite to avoid accessing private fields | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO ➡️ done ✅ |
||
await detox.afterEach({ title: 'a', fullName: 'b', status: 'failed' }); | ||
expect(device.launchApp).toHaveBeenCalledTimes(1); | ||
}); | ||
|
@@ -174,7 +189,7 @@ describe('Detox', () => { | |
await detox.init(); | ||
await detox.afterEach(testSummary); | ||
|
||
expect(detox._client.dumpPendingRequests).not.toHaveBeenCalled(); | ||
expect(client.dumpPendingRequests).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it(`handleAppCrash should dump pending requests if testSummary has timeout flag`, async () => { | ||
|
@@ -184,7 +199,42 @@ describe('Detox', () => { | |
|
||
await detox.init(); | ||
await detox.afterEach(testSummary); | ||
expect(detox._client.dumpPendingRequests).toHaveBeenCalled(); | ||
expect(client.dumpPendingRequests).toHaveBeenCalled(); | ||
}); | ||
|
||
it(`should register an async nonresponsiveness listener`, async () => { | ||
Detox = require('./Detox'); | ||
detox = new Detox({deviceConfig: validDeviceConfigWithSession}); | ||
|
||
await detox.init(); | ||
|
||
expect(client.setNonresponsivenessListener).toHaveBeenCalled(); | ||
}); | ||
|
||
it(`should log thread-dump provided by a nonresponsiveness event`, async () => { | ||
const callbackParams = { | ||
threadDump: 'mockThreadDump', | ||
}; | ||
const expectedMessage = [ | ||
'Application nonresponsiveness detected!', | ||
'On Android, this could imply an ANR alert, which evidently causes tests to fail.', | ||
'Here\'s the native main-thread stacktrace from the device, to help you out (refer to device logs for the complete thread dump):', | ||
callbackParams.threadDump, | ||
'Refer to https://developer.android.com/training/articles/perf-anr for further details.' | ||
].join('\n'); | ||
|
||
Detox = require('./Detox'); | ||
detox = new Detox({deviceConfig: validDeviceConfigWithSession}); | ||
|
||
await detox.init(); | ||
await invokeDetoxCallback(); | ||
|
||
expect(mockLogger.warn).toHaveBeenCalledWith({ event: 'APP_NONRESPONSIVE' }, expectedMessage); | ||
|
||
async function invokeDetoxCallback() { | ||
const callback = client.setNonresponsivenessListener.mock.calls[0][0]; | ||
await callback(callbackParams); | ||
} | ||
}); | ||
|
||
describe('.artifactsManager', () => { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@noomorph I was far from sure as to whether this is the right level for this work-scope. I'd be happy to get your insights on this (for example, should this be part of the client's implementation?)