Add memory leak test-suite
This commit is contained in:
@@ -23,7 +23,10 @@ import {
|
||||
createFakeMousePointer,
|
||||
hideText,
|
||||
showText,
|
||||
sleep
|
||||
sleep,
|
||||
getHeapDump,
|
||||
countInstancesOf,
|
||||
ClassNameMapping
|
||||
} from './util'
|
||||
|
||||
process.on('unhandledRejection', (error: Error) => {
|
||||
120
src/spec/leakTest.ts
Normal file
120
src/spec/leakTest.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as webdriverio from 'webdriverio'
|
||||
import mockMqtt, { stopUpdates as stopMqttUpdates } from './mock-mqtt'
|
||||
import {
|
||||
ClassNameMapping,
|
||||
countInstancesOf,
|
||||
createFakeMousePointer,
|
||||
getHeapDump,
|
||||
setFast,
|
||||
sleep
|
||||
} from './util'
|
||||
import { clearSearch, searchTree } from './scenarios/searchTree'
|
||||
import { connectTo } from './scenarios/connect'
|
||||
import { reconnect } from './scenarios/reconnect'
|
||||
|
||||
process.on('unhandledRejection', (error: Error) => {
|
||||
console.error('unhandledRejection', error.message, error.stack)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
|
||||
|
||||
console.log(`${__dirname}/../../../node_modules/.bin/electron`)
|
||||
const options = {
|
||||
host: '127.0.0.1', // Use localhost as chrome driver server
|
||||
port: 9515, // "9515" is the port opened by chrome driver.
|
||||
capabilities: {
|
||||
browserName: 'electron',
|
||||
chromeOptions: {
|
||||
binary: `${__dirname}/../../../node_modules/.bin/electron`,
|
||||
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(runningUiTestOnCi),
|
||||
},
|
||||
windowTypes: ['app', 'webview'],
|
||||
},
|
||||
}
|
||||
|
||||
async function doStuff() {
|
||||
console.log('Waiting for MQTT Broker on port 1880 (no auth)')
|
||||
await mockMqtt()
|
||||
console.log('start webdriver')
|
||||
|
||||
const browser = await webdriverio.remote(options)
|
||||
setFast()
|
||||
await createFakeMousePointer(browser)
|
||||
|
||||
// Wait for Username input to be visible
|
||||
await browser.$('//label[contains(text(), "Username")]/..//input')
|
||||
await connectTo('127.0.0.1', browser)
|
||||
stopMqttUpdates()
|
||||
await sleep(1000, true)
|
||||
|
||||
let heapDump = await getHeapDump(browser)
|
||||
const initialTreeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.Tree)
|
||||
const initialNodeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.TreeNode)
|
||||
console.log(initialTreeOccurrances, initialNodeOccurrances)
|
||||
|
||||
await doX(3, async () => {
|
||||
await reconnect(browser)
|
||||
})
|
||||
|
||||
await sleep(1000, true)
|
||||
|
||||
await doX(15, async () => {
|
||||
await searchTree('temp', browser)
|
||||
await reconnect(browser)
|
||||
})
|
||||
|
||||
await searchTree('ab', browser)
|
||||
await clearSearch(browser)
|
||||
|
||||
await searchTree('temp', browser)
|
||||
await clearSearch(browser)
|
||||
|
||||
await sleep(1000, true)
|
||||
|
||||
await waitForGarbageCollectorToDetermineLeak(browser, initialTreeOccurrances, initialNodeOccurrances)
|
||||
}
|
||||
|
||||
async function waitForGarbageCollectorToDetermineLeak(browser: any, initialTreeOccurrances: number, initialNodeOccurrances: number) {
|
||||
let delta = -1
|
||||
let lastTreeOccurances = -1
|
||||
let lastNodeOccurances = -1
|
||||
let leak = false
|
||||
while (delta < 0) {
|
||||
if (lastTreeOccurances !== -1) {
|
||||
await sleep(120000, true)
|
||||
}
|
||||
const heapDump = await getHeapDump(browser)
|
||||
const currentTreeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.Tree)
|
||||
const currentNodeOccurrances = await countInstancesOf(heapDump, ClassNameMapping.TreeNode)
|
||||
if (initialTreeOccurrances !== currentTreeOccurrances || Math.abs(currentNodeOccurrances - initialNodeOccurrances) > 8) {
|
||||
console.error('Possible leak detected', initialTreeOccurrances, currentTreeOccurrances, initialNodeOccurrances, currentNodeOccurrances)
|
||||
leak = true
|
||||
} else {
|
||||
leak = false
|
||||
}
|
||||
|
||||
const treeDelta = lastTreeOccurances >= 0 ? currentTreeOccurrances - lastTreeOccurances : -1
|
||||
const nodeDelta = lastTreeOccurances >= 0 ? currentNodeOccurrances - lastNodeOccurances : -1
|
||||
delta = treeDelta + nodeDelta
|
||||
|
||||
lastTreeOccurances = currentTreeOccurrances
|
||||
lastNodeOccurances = currentNodeOccurrances
|
||||
}
|
||||
|
||||
if (leak) {
|
||||
console.error('leak')
|
||||
process.exit(100)
|
||||
}
|
||||
}
|
||||
|
||||
async function doX(x: number, action: () => Promise<any>) {
|
||||
for (let i = 0; i < x; i += 1) {
|
||||
await action()
|
||||
await sleep(10, true)
|
||||
}
|
||||
}
|
||||
|
||||
doStuff()
|
||||
@@ -30,16 +30,21 @@ function temperature(base = 18, sineCoefficient = 2, offset = 0) {
|
||||
return String(Math.round(temp * 100) / 100)
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
export function stopUpdates() {
|
||||
for (const interval of intervals) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
intervals = []
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
stopUpdates()
|
||||
try {
|
||||
client && client.end()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const intervals: any = []
|
||||
let intervals: any = []
|
||||
|
||||
function generateData(client: mqtt.MqttClient) {
|
||||
client.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
|
||||
|
||||
@@ -9,5 +9,5 @@ export async function connectTo(host: string, browser: Browser<void>) {
|
||||
await browser.saveScreenshot('screen1.png')
|
||||
|
||||
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
|
||||
clickOn(connectButton, browser)
|
||||
await clickOn(connectButton, browser)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ import { clickOn } from '../util'
|
||||
import { Browser } from 'webdriverio'
|
||||
|
||||
export async function disconnect(browser: Browser<void>) {
|
||||
const connectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
|
||||
clickOn(connectButton, browser)
|
||||
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
|
||||
await clickOn(disconnectButton, browser)
|
||||
}
|
||||
|
||||
9
src/spec/scenarios/reconnect.ts
Normal file
9
src/spec/scenarios/reconnect.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { clickOn } from '../util'
|
||||
import { Browser } from 'webdriverio'
|
||||
|
||||
export async function reconnect(browser: Browser<void>) {
|
||||
const disconnectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
|
||||
await clickOn(disconnectButton, browser)
|
||||
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
|
||||
await clickOn(connectButton, browser)
|
||||
}
|
||||
@@ -1,17 +1,27 @@
|
||||
import * as fs from 'fs'
|
||||
import { Browser, Element } from 'webdriverio'
|
||||
export { expandTopic } from './expandTopic'
|
||||
|
||||
let fast = false
|
||||
export function setFast() {
|
||||
fast = true
|
||||
}
|
||||
|
||||
export function sleep(ms: number, required = false) {
|
||||
return new Promise((resolve) => {
|
||||
if (required) {
|
||||
setTimeout(resolve, ms)
|
||||
} else {
|
||||
setTimeout(resolve, ms)
|
||||
setTimeout(resolve, fast ? 0 : ms)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function writeText(text: string, browser: Browser<void>, delay = 0) {
|
||||
if (fast) {
|
||||
return browser.keys(text.split(''))
|
||||
}
|
||||
|
||||
for (const c of text.split('')) {
|
||||
await browser.keys([c])
|
||||
await sleep(delay)
|
||||
@@ -42,11 +52,12 @@ export async function moveToCenterOfElement(element: Element<void>, browser: Bro
|
||||
const targetX = x + width / 2
|
||||
const targetY = y + height / 2
|
||||
|
||||
const duration = 500
|
||||
const duration = fast ? 1 : 500
|
||||
|
||||
const js = `window.demo.moveMouse(${targetX}, ${targetY}, ${duration});`
|
||||
await browser.execute(js)
|
||||
await sleep(duration + 500, true)
|
||||
await sleep(duration)
|
||||
await sleep(20, true)
|
||||
|
||||
await element.moveTo()
|
||||
}
|
||||
@@ -73,17 +84,41 @@ export async function createFakeMousePointer(browser: Browser<void>) {
|
||||
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys = []) {
|
||||
const js = `window.demo.showMessage('${text}', '${location}', ${duration});`
|
||||
|
||||
browser.execute(js)
|
||||
await browser.execute(js)
|
||||
}
|
||||
|
||||
type HeapDump = any
|
||||
|
||||
export async function getHeapDump(browser: Browser<void>): Promise<HeapDump> {
|
||||
const filename = 'heapdump.json'
|
||||
const js = `window.demo.writeHeapdump('${filename}');`
|
||||
await browser.execute(js)
|
||||
const buffer = fs.readFileSync(filename)
|
||||
fs.unlinkSync(filename)
|
||||
|
||||
return JSON.parse(buffer.toString())
|
||||
}
|
||||
|
||||
export enum ClassNameMapping {
|
||||
TreeNode = 'TreeNode_TreeNode',
|
||||
TreeNodeComponent = 'TreeNode_TreeNodeComponent',
|
||||
Tree = 'Tree_Tree',
|
||||
}
|
||||
|
||||
export async function countInstancesOf(heapDump: HeapDump, className: ClassNameMapping): Promise<number> {
|
||||
return heapDump.nodes
|
||||
.map((idx: number) => heapDump.strings[idx])
|
||||
.filter((s: string) => s === className).length
|
||||
}
|
||||
|
||||
export async function showKeys(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys: string[] = []) {
|
||||
const js = `window.demo.showMessage('${text}', '${location}', ${duration}, ${JSON.stringify(keys)});`
|
||||
|
||||
browser.execute(js)
|
||||
await browser.execute(js)
|
||||
}
|
||||
|
||||
export async function hideText(browser: Browser<void>) {
|
||||
const js = 'window.demo.hideMessage();'
|
||||
browser.execute(js)
|
||||
await browser.execute(js)
|
||||
await sleep(600)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user