Add memory leak test-suite

This commit is contained in:
Thomas Nordquist
2019-04-25 13:50:15 +02:00
parent 8cd11cde3b
commit af2ff0149d
13 changed files with 700 additions and 36 deletions

View File

@@ -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
View 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()

View File

@@ -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 })

View File

@@ -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)
}

View File

@@ -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)
}

View 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)
}

View File

@@ -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)
}