Path: blob/main/extensions/copilot/test/base/simulationBaseline.ts
13388 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as fs from 'fs';5import * as path from 'path';6import { IBaselineTestSummary } from '../simulation/shared/sharedTypes';78export class SimulationBaseline {910private prevBaseline = new Map<string, IBaselineTestSummary>();11private currBaseline = new Map<string, IBaselineTestSummary>();12private currSkipped = new Set<string>();1314public get current(): IterableIterator<IBaselineTestSummary> {15return this.currBaseline.values();16}1718public get currentScore(): number {19return this._computeScore(Array.from(this.currBaseline.values()));20}2122public get overallScore(): number {23return this._computeScore(this.testSummaries);24}2526private _computeScore(summaries: IBaselineTestSummary[]) {27const totalScore = summaries.reduce((acc, curr) => acc + curr.score, 0);28return (totalScore / summaries.length) * 100;29}3031public static DEFAULT_BASELINE_PATH = path.join(__dirname, '../test/simulation', 'baseline.json');3233public static async readFromDisk(baselinePath: string, runningAllTests: boolean): Promise<SimulationBaseline> {34let baselineFileContents = '[]';35try {36baselineFileContents = (await fs.promises.readFile(baselinePath)).toString();37} catch {38// No baseline file exists yet, create one39await fs.promises.writeFile(baselinePath, '[]');40}41const parsedBaseline = JSON.parse(baselineFileContents) as IBaselineTestSummary[];42return new SimulationBaseline(baselinePath, parsedBaseline, runningAllTests);43}4445constructor(46public readonly baselinePath: string,47parsedBaseline: IBaselineTestSummary[],48private readonly _runningAllTests: boolean49) {50this.prevBaseline = new Map<string, IBaselineTestSummary>();51parsedBaseline.forEach(el => this.prevBaseline.set(el.name, el));52}5354public setCurrentResult(testSummary: IBaselineTestSummary): TestBaselineComparison {55this.currBaseline.set(testSummary.name, testSummary);56const prevBaseline = this.prevBaseline.get(testSummary.name);57return (58prevBaseline59? new ExistingBaselineComparison(prevBaseline, testSummary)60: { isNew: true }61);62}6364public setSkippedTest(name: string): void {65this.currSkipped.add(name);66}6768public async writeToDisk(pathToWriteTo?: string): Promise<void> {69const path = pathToWriteTo ?? this.baselinePath;70await fs.promises.writeFile(path, JSON.stringify(this.testSummaries, undefined, 2));71}7273/**74* Returns a sorted array of test summaries.75* This also includes skipped tests as this is meant to represent the baseline.json which would be written to disk.76*/77private get testSummaries(): IBaselineTestSummary[] {78const testSummaries = Array.from(this.currBaseline.values());7980// Skipped tests remain in the baseline81for (const name of this.currSkipped) {82const prevBaseline = this.prevBaseline.get(name);83if (prevBaseline) {84testSummaries.push(prevBaseline);85}86}8788if (!this._runningAllTests) {89// When running a subset of tests, we will copy over the old existing test results for tests that were not executed90const executedTests = new Set(testSummaries.map(el => el.name));91for (const testSummary of this.prevBaseline.values()) {92if (!executedTests.has(testSummary.name)) {93testSummaries.push(testSummary);94}95}96}9798testSummaries.sort((a, b) => a.name.localeCompare(b.name));99return testSummaries;100}101102public compare(): ICompleteBaselineComparison {103const prevMandatory = new Map<string, IBaselineTestSummary>();104const currMandatory = new Map<string, IBaselineTestSummary>();105const prevOptional = new Map<string, IBaselineTestSummary>();106const currOptional = new Map<string, IBaselineTestSummary>();107108for (const [_, value] of this.prevBaseline) {109if (value.optional) {110prevOptional.set(value.name, value);111} else {112prevMandatory.set(value.name, value);113}114}115for (const [_, value] of this.currBaseline) {116if (value.optional) {117currOptional.set(value.name, value);118} else {119currMandatory.set(value.name, value);120}121}122const mandatory = SimulationBaseline.compare(prevMandatory, currMandatory, this.currSkipped);123const optional = SimulationBaseline.compare(prevOptional, currOptional, this.currSkipped);124return {125mandatory,126optional,127nUnchanged: mandatory.nUnchanged + optional.nUnchanged,128nImproved: mandatory.nImproved + optional.nImproved,129nWorsened: mandatory.nWorsened + optional.nWorsened,130addedScenarios: mandatory.addedScenarios + optional.addedScenarios,131removedScenarios: mandatory.removedScenarios + optional.removedScenarios,132skippedScenarios: mandatory.skippedScenarios + optional.skippedScenarios,133improvedScenarios: mandatory.improvedScenarios.concat(optional.improvedScenarios),134worsenedScenarios: mandatory.worsenedScenarios.concat(optional.worsenedScenarios)135};136}137138private static compare(prevMap: Map<string, IBaselineTestSummary>, currMap: Map<string, IBaselineTestSummary>, currSkipped: Set<string>): IBaselineComparison {139let nUnchanged = 0;140let nImproved = 0;141let nWorsened = 0;142let addedScenarios = 0;143let removedScenarios = 0;144let skippedScenarios = 0;145const improvedScenarios: IModifiedScenario[] = [];146const worsenedScenarios: IModifiedScenario[] = [];147148for (const [_, curr] of currMap) {149const prev = prevMap.get(curr.name);150if (prev) {151const comparison = new ExistingBaselineComparison(prev, curr);152if (comparison.isImproved) {153nImproved++;154improvedScenarios.push({ prevScore: prev.score, currScore: curr.score, name: curr.name });155} else if (comparison.isWorsened) {156nWorsened++;157worsenedScenarios.push({ prevScore: prev.score, currScore: curr.score, name: curr.name });158} else {159nUnchanged++;160}161} else {162addedScenarios++;163}164}165166for (const [_, prev] of prevMap) {167if (!currMap.has(prev.name)) {168if (currSkipped.has(prev.name)) {169// this test is missing but it was skipped intentionally170skippedScenarios++;171} else {172removedScenarios++;173}174}175}176177return { nUnchanged, nImproved, nWorsened, addedScenarios, removedScenarios, skippedScenarios, improvedScenarios, worsenedScenarios };178}179180public clear() {181this.currBaseline.clear();182this.currSkipped.clear();183}184}185186export interface IBaselineComparison {187nUnchanged: number;188nImproved: number;189nWorsened: number;190addedScenarios: number;191removedScenarios: number;192skippedScenarios: number;193improvedScenarios: IModifiedScenario[];194worsenedScenarios: IModifiedScenario[];195}196197export interface IModifiedScenario {198name: string;199prevScore: number;200currScore: number;201}202203export interface ICompleteBaselineComparison extends IBaselineComparison {204mandatory: IBaselineComparison;205optional: IBaselineComparison;206}207208export type TestBaselineComparison = (209{ isNew: true }210| { isNew: false; isImproved: boolean; isWorsened: boolean; isUnchanged: boolean; prevScore: number; currScore: number }211);212213export class ExistingBaselineComparison {214public readonly isNew = false;215public readonly isImproved: boolean;216public readonly isWorsened: boolean;217public readonly isUnchanged: boolean;218219public readonly prevScore: number;220public readonly currScore: number;221222constructor(223prev: IBaselineTestSummary,224curr: IBaselineTestSummary,225) {226this.prevScore = prev.score;227const prevN = prev.passCount + prev.failCount;228this.currScore = curr.score;229const currN = curr.passCount + curr.failCount;230231const prevPassCount = Math.round(this.prevScore * prevN);232const currPassCount = Math.round(this.currScore * currN);233234// Here we want to mark a change only if this is clearly a change also when the `prevN` would equal `currN`235let prevMinScore = this.prevScore;236let prevMaxScore = this.prevScore;237let currMinScore = this.currScore;238let currMaxScore = this.currScore;239240if (prevN > currN) {241// We are now running less iterations than before242currMinScore = currPassCount / prevN;243currMaxScore = (currPassCount + (prevN - currN)) / prevN;244} else if (prevN < currN) {245// We are now running more iterations than before246prevMinScore = prevPassCount / currN;247prevMaxScore = (prevPassCount + (currN - prevN)) / currN;248}249250if (currMinScore > prevMaxScore) {251this.isImproved = true;252this.isWorsened = false;253this.isUnchanged = false;254} else if (currMaxScore < prevMinScore) {255this.isImproved = false;256this.isWorsened = true;257this.isUnchanged = false;258} else {259this.isImproved = false;260this.isWorsened = false;261this.isUnchanged = true;262}263}264}265266267