Add cook tests. Move around some code without changing any functionality.

This commit is contained in:
Jason Gore 2019-09-27 16:35:52 -07:00
Родитель a10a774856
Коммит eb25d4d6f3
6 изменённых файлов: 621 добавлений и 113 удалений

Просмотреть файл

@ -1,64 +1,179 @@
import fs from 'fs';
import path from 'path';
import * as tmp from 'tmp';
import puppeteer, { Browser, Page } from 'puppeteer';
import { cook, OutputFiles, Scenario, ScenarioConfig } from '../flamegrill';
import { generateFlamegraph } from '../flamegraph/generate';
import { checkForRegressions } from '../analysis/processData';
const profiles = require('../fixtures/profiles.json');
const snapshotsDir = path.join(__dirname, '../fixtures/snapshots');
// TODO: these are black box tests for now but should be refactored to be unit tests
// TODO: modules that output files should be modified not to and wrapped by a centralized file output helper
// TODO: consider also making file output / github / CI integration another package within this repo
describe('flamegrill', () => {
it('generates expected output', async () => {
jest.setTimeout(30000);
describe('cook', () => {
const profiles = require('../fixtures/profiles.json');
const expectedResults = require('../fixtures/results.json');
const util = require('../util');
const fixturesDir = path.join(__dirname, '../fixtures');
const snapshotsDir = path.join(__dirname, '../fixtures/snapshots');
let scenarios: Scenario[];
let scenarioNames: string[];
let outdir: tmp.DirResult;
let scenarioConfig: ScenarioConfig;
let scenarioIndex: number = 0;
const testPage: Page = {
close: jest.fn(() => Promise.resolve()),
goto: jest.fn(() => {
return Promise.resolve(null);
}),
setDefaultTimeout: jest.fn(() => {})
} as unknown as Page;
const testBrowser: Browser = {
newPage: jest.fn(() => {
return Promise.resolve(testPage);
}),
close: jest.fn(() => {})
} as unknown as Browser;
expect.assertions(89);
beforeAll(() => {
// Processing all profiles takes more than the default 5 seconds.
jest.setTimeout(60000);
jest.spyOn(puppeteer, 'launch').mockImplementation(() => {
return Promise.resolve(testBrowser);
});
jest.spyOn(util, 'arr_diff').mockImplementation(() => {
let logfile;
// This code assumes scenarios are processed in the same order passed into cook, which
// is true as of writing.
const scenarioName = scenarioNames[Math.floor(scenarioIndex / 2)];
if (scenarioIndex % 2) {
logfile = profiles[scenarioName].reference.logFile;
} else {
logfile = profiles[scenarioName].logFile;
}
scenarioIndex += 1;
return [logfile];
});
outdir = tmp.dirSync({ unsafeCleanup: true });
scenarioConfig = {
outDir: outdir.name,
tempDir: fixturesDir
};
scenarioNames = Object.keys(profiles);
scenarios = scenarioNames.map(scenarioName => {
return {
name: scenarioName,
scenario: scenarioName + ".url",
reference: scenarioName + "_ref.url"
}
});
});
const outdir = tmp.dirSync({ unsafeCleanup: true });
afterAll(() => {
jest.resetAllMocks();
outdir.removeCallback();
})
await Promise.all(Object.keys(profiles).map(key => {
let logfile = require.resolve(path.join('../fixtures', profiles[key].logFile));
let outfile = path.join(outdir.name, key);
return generateFlamegraph(logfile, outfile)
}));
it('generates expected output', async () => {
const testResults = await cook(scenarios, scenarioConfig);
await Promise.all(Object.keys(profiles).map(key => {
let logfile = require.resolve(path.join('../fixtures', profiles[key].reference.logFile));
let outfile = path.join(outdir.name, key + '_ref');
return generateFlamegraph(logfile, outfile)
}));
// The path will differ for every test run, so remove it before comparing results.
// TODO: remove these !s after types are cleaned up
Object.keys(testResults).forEach(result => {
Object.keys(testResults[result].files!).forEach(file => {
testResults[result].files![file as keyof OutputFiles]
= path.basename(testResults[result].files![file as keyof OutputFiles]!);
})
Object.keys(testResults[result].reference!.files!).forEach(file => {
testResults[result].reference!.files![file as keyof OutputFiles]
= path.basename(testResults[result].reference!.files![file as keyof OutputFiles]!);
})
});
Object.keys(profiles).forEach(key => {
// TODO: this code block is duplicating code in flamegrill.ts and should be removed as code is refactored.
let datafileBefore = path.join(outdir.name, key + '_ref.data.js');
let datafileAfter = path.join(outdir.name, key + '.data.js');
let regressionfile = path.join(outdir.name, key + '.regression.txt');
expect(testResults).toEqual(expectedResults);
const analysis = checkForRegressions(datafileBefore, datafileAfter);
const snapshotFiles = fs.readdirSync(snapshotsDir);
const testFiles = fs.readdirSync(outdir.name);
if(analysis.isRegression) {
fs.writeFileSync(regressionfile, analysis.summary);
}
expect(testFiles).toEqual(snapshotFiles);
testFiles.forEach(file => {
// Some generated output creates files with \r\n. Some environments spit out \n.
// Ignore line break types when comparing results.
const analysis = fs.readFileSync(path.join(snapshotsDir, file), 'utf8').split(/\r?\n/g);
const output = fs.readFileSync(path.join(outdir.name, file), 'utf8').split(/\r?\n/g);
expect(output).toEqual(analysis);
});
expect((testBrowser.close as jest.Mock).mock.calls.length).toEqual(1);
});
});
// These tests are technically redundant with cook tests but are left for now as they may be useful
// as unit tests are added.
describe('generateFlamegraph, checkForRegressions', () => {
const profiles = require('../fixtures/profiles.json');
const snapshotsDir = path.join(__dirname, '../fixtures/snapshots');
it('generates expected output', async () => {
// Processing all profiles takes more than the default 5 seconds.
jest.setTimeout(30000);
expect.assertions(89);
const outdir = tmp.dirSync({ unsafeCleanup: true });
const snapshotFiles = fs.readdirSync(snapshotsDir);
const testFiles = fs.readdirSync(outdir.name);
await Promise.all(Object.keys(profiles).map(key => {
let logfile = require.resolve(path.join('../fixtures', profiles[key].logFile));
let outfile = path.join(outdir.name, key);
return generateFlamegraph(logfile, outfile)
}));
expect(testFiles).toEqual(snapshotFiles);
await Promise.all(Object.keys(profiles).map(key => {
let logfile = require.resolve(path.join('../fixtures', profiles[key].reference.logFile));
let outfile = path.join(outdir.name, key + '_ref');
return generateFlamegraph(logfile, outfile)
}));
testFiles.forEach(file => {
// Some generated output creates files with \r\n. Some environments spit out \n.
// Ignore line break types when comparing results.
const analysis = fs.readFileSync(path.join(snapshotsDir, file), 'utf8').split(/\r?\n/g);
const output = fs.readFileSync(path.join(outdir.name, file), 'utf8').split(/\r?\n/g);
Object.keys(profiles).forEach(key => {
// TODO: this code block is duplicating code in flamegrill.ts and should be removed as code is refactored.
let datafileBefore = path.join(outdir.name, key + '_ref.data.js');
let datafileAfter = path.join(outdir.name, key + '.data.js');
let regressionfile = path.join(outdir.name, key + '.regression.txt');
expect(output).toEqual(analysis);
const analysis = checkForRegressions(datafileBefore, datafileAfter);
if(analysis.isRegression) {
fs.writeFileSync(regressionfile, analysis.summary);
}
});
const snapshotFiles = fs.readdirSync(snapshotsDir);
const testFiles = fs.readdirSync(outdir.name);
expect(testFiles).toEqual(snapshotFiles);
testFiles.forEach(file => {
// Some generated output creates files with \r\n. Some environments spit out \n.
// Ignore line break types when comparing results.
const analysis = fs.readFileSync(path.join(snapshotsDir, file), 'utf8').split(/\r?\n/g);
const output = fs.readFileSync(path.join(outdir.name, file), 'utf8').split(/\r?\n/g);
expect(output).toEqual(analysis);
});
outdir.removeCallback();
});
outdir.removeCallback();
});
});

Просмотреть файл

@ -0,0 +1,47 @@
import * as tmp from 'tmp';
import { Browser, Page } from 'puppeteer';
import { runProfile } from '../profile';
// These tests should be in flamegrill.test.ts but there is some nasty side effect issue that causes them to fail.
describe('runProfile', () => {
let outdir: tmp.DirResult;
let logfile: tmp.FileResult;
const testUrl = 'testUrl';
const testPage: Page = {
close: jest.fn(() => Promise.resolve()),
goto: jest.fn(() => {
return Promise.resolve(null);
}),
setDefaultTimeout: jest.fn(() => {})
} as unknown as Page;
const testBrowser: Browser = {
newPage: jest.fn(() => {
logfile = tmp.fileSync({ dir: outdir.name });
return Promise.resolve(testPage);
})
} as unknown as Browser;
beforeAll(() => {
outdir = tmp.dirSync({ unsafeCleanup: true });
});
afterAll(() => {
outdir.removeCallback();
})
it('performs expected operations', async () => {
const result = await runProfile(testBrowser, testUrl, 'testScenario', outdir.name);
expect((testPage.setDefaultTimeout as jest.Mock).mock.calls.length).toEqual(1);
expect((testPage.setDefaultTimeout as jest.Mock).mock.calls[0][0]).toEqual(0);
expect((testPage.goto as jest.Mock).mock.calls.length).toEqual(1);
expect((testPage.goto as jest.Mock).mock.calls[0][0]).toEqual(testUrl);
expect((testPage.close as jest.Mock).mock.calls.length).toEqual(1);
expect(logfile).toBeDefined();
expect(result).toEqual(logfile.name);
});
});

Просмотреть файл

@ -0,0 +1,338 @@
{
"BaseButton": {
"files": {
"dataFile": "BaseButton.data.js",
"flamegraphFile": "BaseButton.html",
"regressionFile": "BaseButton.regression.txt"
},
"reference": {
"files": {
"dataFile": "BaseButton_ref.data.js",
"flamegraphFile": "BaseButton_ref.html"
},
"numTicks": 345
},
"numTicks": 327,
"isRegression": false
},
"BaseButtonNew": {
"files": {
"dataFile": "BaseButtonNew.data.js",
"flamegraphFile": "BaseButtonNew.html",
"regressionFile": "BaseButtonNew.regression.txt"
},
"reference": {
"files": {
"dataFile": "BaseButtonNew_ref.data.js",
"flamegraphFile": "BaseButtonNew_ref.html"
},
"numTicks": 426
},
"numTicks": 421,
"isRegression": false
},
"button": {
"files": {
"dataFile": "button.data.js",
"flamegraphFile": "button.html",
"regressionFile": "button.regression.txt"
},
"reference": {
"files": {
"dataFile": "button_ref.data.js",
"flamegraphFile": "button_ref.html"
},
"numTicks": 28
},
"numTicks": 26,
"isRegression": false
},
"DefaultButton": {
"files": {
"dataFile": "DefaultButton.data.js",
"flamegraphFile": "DefaultButton.html",
"regressionFile": "DefaultButton.regression.txt"
},
"reference": {
"files": {
"dataFile": "DefaultButton_ref.data.js",
"flamegraphFile": "DefaultButton_ref.html"
},
"numTicks": 528
},
"numTicks": 455,
"isRegression": false
},
"DefaultButtonNew": {
"files": {
"dataFile": "DefaultButtonNew.data.js",
"flamegraphFile": "DefaultButtonNew.html",
"regressionFile": "DefaultButtonNew.regression.txt"
},
"reference": {
"files": {
"dataFile": "DefaultButtonNew_ref.data.js",
"flamegraphFile": "DefaultButtonNew_ref.html"
},
"numTicks": 923
},
"numTicks": 882,
"isRegression": false
},
"DetailsRow": {
"files": {
"dataFile": "DetailsRow.data.js",
"flamegraphFile": "DetailsRow.html",
"regressionFile": "DetailsRow.regression.txt"
},
"reference": {
"files": {
"dataFile": "DetailsRow_ref.data.js",
"flamegraphFile": "DetailsRow_ref.html"
},
"numTicks": 1471
},
"numTicks": 2750,
"isRegression": true
},
"DetailsRowFast": {
"files": {
"dataFile": "DetailsRowFast.data.js",
"flamegraphFile": "DetailsRowFast.html",
"regressionFile": "DetailsRowFast.regression.txt"
},
"reference": {
"files": {
"dataFile": "DetailsRowFast_ref.data.js",
"flamegraphFile": "DetailsRowFast_ref.html"
},
"numTicks": 1409
},
"numTicks": 2695,
"isRegression": true
},
"DetailsRowNoStyles": {
"files": {
"dataFile": "DetailsRowNoStyles.data.js",
"flamegraphFile": "DetailsRowNoStyles.html",
"regressionFile": "DetailsRowNoStyles.regression.txt"
},
"reference": {
"files": {
"dataFile": "DetailsRowNoStyles_ref.data.js",
"flamegraphFile": "DetailsRowNoStyles_ref.html"
},
"numTicks": 1466
},
"numTicks": 1948,
"isRegression": true
},
"DocumentCardTitle": {
"files": {
"dataFile": "DocumentCardTitle.data.js",
"flamegraphFile": "DocumentCardTitle.html",
"regressionFile": "DocumentCardTitle.regression.txt"
},
"reference": {
"files": {
"dataFile": "DocumentCardTitle_ref.data.js",
"flamegraphFile": "DocumentCardTitle_ref.html"
},
"numTicks": 15919
},
"numTicks": 16012,
"isRegression": false
},
"MenuButton": {
"files": {
"dataFile": "MenuButton.data.js",
"flamegraphFile": "MenuButton.html",
"regressionFile": "MenuButton.regression.txt"
},
"reference": {
"files": {
"dataFile": "MenuButton_ref.data.js",
"flamegraphFile": "MenuButton_ref.html"
},
"numTicks": 590
},
"numTicks": 647,
"isRegression": false
},
"MenuButtonNew": {
"files": {
"dataFile": "MenuButtonNew.data.js",
"flamegraphFile": "MenuButtonNew.html",
"regressionFile": "MenuButtonNew.regression.txt"
},
"reference": {
"files": {
"dataFile": "MenuButtonNew_ref.data.js",
"flamegraphFile": "MenuButtonNew_ref.html"
},
"numTicks": 1581
},
"numTicks": 1548,
"isRegression": false
},
"PrimaryButton": {
"files": {
"dataFile": "PrimaryButton.data.js",
"flamegraphFile": "PrimaryButton.html",
"regressionFile": "PrimaryButton.regression.txt"
},
"reference": {
"files": {
"dataFile": "PrimaryButton_ref.data.js",
"flamegraphFile": "PrimaryButton_ref.html"
},
"numTicks": 496
},
"numTicks": 488,
"isRegression": false
},
"PrimaryButtonNew": {
"files": {
"dataFile": "PrimaryButtonNew.data.js",
"flamegraphFile": "PrimaryButtonNew.html",
"regressionFile": "PrimaryButtonNew.regression.txt"
},
"reference": {
"files": {
"dataFile": "PrimaryButtonNew_ref.data.js",
"flamegraphFile": "PrimaryButtonNew_ref.html"
},
"numTicks": 1001
},
"numTicks": 922,
"isRegression": false
},
"SplitButton": {
"files": {
"dataFile": "SplitButton.data.js",
"flamegraphFile": "SplitButton.html",
"regressionFile": "SplitButton.regression.txt"
},
"reference": {
"files": {
"dataFile": "SplitButton_ref.data.js",
"flamegraphFile": "SplitButton_ref.html"
},
"numTicks": 1355
},
"numTicks": 1293,
"isRegression": false
},
"SplitButtonNew": {
"files": {
"dataFile": "SplitButtonNew.data.js",
"flamegraphFile": "SplitButtonNew.html",
"regressionFile": "SplitButtonNew.regression.txt"
},
"reference": {
"files": {
"dataFile": "SplitButtonNew_ref.data.js",
"flamegraphFile": "SplitButtonNew_ref.html"
},
"numTicks": 3048
},
"numTicks": 3155,
"isRegression": false
},
"Stack": {
"files": {
"dataFile": "Stack.data.js",
"flamegraphFile": "Stack.html",
"regressionFile": "Stack.regression.txt"
},
"reference": {
"files": {
"dataFile": "Stack_ref.data.js",
"flamegraphFile": "Stack_ref.html"
},
"numTicks": 251
},
"numTicks": 263,
"isRegression": false
},
"StackWithIntrinsicChildren": {
"files": {
"dataFile": "StackWithIntrinsicChildren.data.js",
"flamegraphFile": "StackWithIntrinsicChildren.html",
"regressionFile": "StackWithIntrinsicChildren.regression.txt"
},
"reference": {
"files": {
"dataFile": "StackWithIntrinsicChildren_ref.data.js",
"flamegraphFile": "StackWithIntrinsicChildren_ref.html"
},
"numTicks": 596
},
"numTicks": 563,
"isRegression": false
},
"StackWithTextChildren": {
"files": {
"dataFile": "StackWithTextChildren.data.js",
"flamegraphFile": "StackWithTextChildren.html",
"regressionFile": "StackWithTextChildren.regression.txt"
},
"reference": {
"files": {
"dataFile": "StackWithTextChildren_ref.data.js",
"flamegraphFile": "StackWithTextChildren_ref.html"
},
"numTicks": 1928
},
"numTicks": 1978,
"isRegression": false
},
"Text": {
"files": {
"dataFile": "Text.data.js",
"flamegraphFile": "Text.html",
"regressionFile": "Text.regression.txt"
},
"reference": {
"files": {
"dataFile": "Text_ref.data.js",
"flamegraphFile": "Text_ref.html"
},
"numTicks": 168
},
"numTicks": 206,
"isRegression": false
},
"Toggle": {
"files": {
"dataFile": "Toggle.data.js",
"flamegraphFile": "Toggle.html",
"regressionFile": "Toggle.regression.txt"
},
"reference": {
"files": {
"dataFile": "Toggle_ref.data.js",
"flamegraphFile": "Toggle_ref.html"
},
"numTicks": 454
},
"numTicks": 882,
"isRegression": true
},
"ToggleNew": {
"files": {
"dataFile": "ToggleNew.data.js",
"flamegraphFile": "ToggleNew.html",
"regressionFile": "ToggleNew.regression.txt"
},
"reference": {
"files": {
"dataFile": "ToggleNew_ref.data.js",
"flamegraphFile": "ToggleNew_ref.html"
},
"numTicks": 1102
},
"numTicks": 1165,
"isRegression": false
}
}

Просмотреть файл

@ -1,6 +1,8 @@
import fs from 'fs';
import path from 'path';
import puppeteer, { Browser } from 'puppeteer';
import puppeteer from 'puppeteer';
import { runProfile } from './profile';
import { generateFlamegraph, GeneratedFiles } from './flamegraph/generate';
import { checkForRegressions } from './analysis/processData';
@ -83,10 +85,10 @@ export async function cook(scenarios: Scenario[], config: ScenarioConfig): Promi
const perfTests: PerfTests = {};
for (const scenario of scenarios) {
let logFile = await runPerfTest(browser, scenario.scenario, scenario.name, tempDir);
let logFile = await runProfile(browser, scenario.scenario, scenario.name, tempDir);
let logFileRef;
if (scenario.reference) {
logFileRef = await runPerfTest(browser, scenario.reference, scenario.name, tempDir);
logFileRef = await runProfile(browser, scenario.reference, scenario.name, tempDir);
}
let outFileRef = path.join(outDir, `${scenario.name}_ref`);
@ -134,49 +136,6 @@ export async function cook(scenarios: Scenario[], config: ScenarioConfig): Promi
return analyses;
};
/**
*
* @param {*} browser Launched puppeteer instance.
* @param {string} testUrl Base URL supporting 'scenario' and 'iterations' query parameters.
* @param {string} scenarioName Name of scenario that will be used with baseUrl.
* @param {string} logDir Absolute path to output log profiles.
* @returns {string} Log file path associated with test.
*/
async function runPerfTest(browser: Browser, testUrl: string, scenarioName: string, logDir: string): Promise<string> {
const logFilesBefore = fs.readdirSync(logDir);
const page = await browser.newPage();
// Default timeout is 30 seconds. This is good for most tests except for problematic components like DocumentCardTitle.
// Disable timeout for now and tweak to a maximum setting once server condtiions are better known.
// TODO: argument? should probably default to 30 seconds
page.setDefaultTimeout(0);
const logFilesAfter = fs.readdirSync(logDir);
const testLogFile = arr_diff(logFilesBefore, logFilesAfter);
if (testLogFile.length !== 1) {
// We have to be able to identify log file associated with tab. Throw error if we can't.
// TODO: what should be the standard for erroring? console.error? throw? return failure?
// TODO: make sure all async function calls have catch blocks
// TODO: make sure invalid URLs (and other inputs) don't hang result
throw new Error(`Could not determine log file for ${testUrl}. Log files detected: [ ${testLogFile} ]`);
}
console.log(`Starting test for ${scenarioName} at ${testUrl}`);
console.time('Ran perf test in');
await page.goto(testUrl);
console.timeEnd('Ran perf test in');
console.log('testLogFile: ' + testLogFile[0]);
await page.close();
return path.join(logDir, testLogFile[0]);
}
/**
* Create test summary based on test results.
*/
@ -223,35 +182,6 @@ function getTicks(resultsFile: string): number | undefined {
}
}
/**
* Array diff utility that returns a list of elements that are not present in both arrays.
*
* @param {Array} a1 First array
* @param {Array} a2 Second array
*/
function arr_diff(a1: string[], a2: string[]): string[] {
let a = {} as any;
let diff: string[] = [];
for (var i = 0; i < a1.length; i++) {
a[a1[i]] = true;
}
for (var i = 0; i < a2.length; i++) {
if (a[a2[i]]) {
delete a[a2[i]];
} else {
a[a2[i]] = true;
}
}
for (var k in a) {
diff.push(k);
}
return diff;
}
export default {
cook
};
};

Просмотреть файл

@ -0,0 +1,49 @@
import fs from 'fs';
import path from 'path';
import { Browser } from 'puppeteer';
import { arr_diff } from './util';
/**
* Run profiler against provided URL and return resulting profiler output.
*
* @param {*} browser Launched puppeteer instance.
* @param {string} testUrl Base URL supporting 'scenario' and 'iterations' query parameters.
* @param {string} scenarioName Name of scenario that will be used with baseUrl.
* @param {string} logDir Absolute path to output log profiles.
* @returns {string} Log file path associated with test.
*/
export async function runProfile(browser: Browser, testUrl: string, scenarioName: string, logDir: string): Promise<string> {
const logFilesBefore = fs.readdirSync(logDir);
const page = await browser.newPage();
// Default timeout is 30 seconds. This is good for most tests except for problematic components like DocumentCardTitle.
// Disable timeout for now and tweak to a maximum setting once server condtiions are better known.
// TODO: argument? should probably default to 30 seconds
page.setDefaultTimeout(0);
const logFilesAfter = fs.readdirSync(logDir);
const testLogFile = arr_diff(logFilesBefore, logFilesAfter);
if (testLogFile.length !== 1) {
// We have to be able to identify log file associated with tab. Throw error if we can't.
// TODO: what should be the standard for erroring? console.error? throw? return failure?
// TODO: make sure all async function calls have catch blocks
// TODO: make sure invalid URLs (and other inputs) don't hang result
throw new Error(`Could not determine log file for ${testUrl}. Log files detected: [ ${testLogFile} ]`);
}
console.log(`Starting test for ${scenarioName} at ${testUrl}`);
console.time('Ran profile in');
await page.goto(testUrl);
console.timeEnd('Ran profile in');
console.log('testLogFile: ' + testLogFile[0]);
await page.close();
return path.join(logDir, testLogFile[0]);
}

Просмотреть файл

@ -0,0 +1,29 @@
/**
* Array diff utility that returns a list of elements that are not present in both arrays.
*
* @param {Array} a1 First array
* @param {Array} a2 Second array
*/
export function arr_diff(a1: string[], a2: string[]): string[] {
let a = {} as any;
let diff: string[] = [];
for (var i = 0; i < a1.length; i++) {
a[a1[i]] = true;
}
for (var i = 0; i < a2.length; i++) {
if (a[a2[i]]) {
delete a[a2[i]];
} else {
a[a2[i]] = true;
}
}
for (var k in a) {
diff.push(k);
}
return diff;
}