Skip to content

Commit ad35a7c

Browse files
jatgargJatin Gargalexvy86tylerbutler
authored
Add code-coverage-tools package to compare code coverage on the PR build. (#22452)
## Description [AB#14170](https://dev.azure.com/fluidframework/internal/_workitems/edit/14170) Add code-coverage-tools package to compare code coverage on the PR build. 1.) Before running coverage comparison, code coverage plugin identifies the baseline build for the PR. 2.) Once the baseline build is identified, we download the build artifacts corresponding to the `Code Coverage Report_<Build_Number>` artifact name for this build 3.) We then collect the code coverage stats for the PR build and then make the comparison with the baseline. 4.) If the code coverage diff (branch coverage) is more than a percentage point change, then we fail the build for the PR. We also fail the build in case the code coverage for the newly added package is less than 50%. 5.) We post the comment on the PR specifying the code coverage change if any for each package which is modified. We needed this separate module as we need specifics to do the code coverage comparison like the baseline with which we need to make the comparison and then what we need to compare and then after comparison what comment and in what format we want to report it. Sample Comment: <img width="945" alt="code coverage" src="https://github.com/user-attachments/assets/d74e612e-0da3-43b6-87d5-7278c887518d"> --------- Co-authored-by: Jatin Garg <[email protected]> Co-authored-by: Alex Villarreal <[email protected]> Co-authored-by: Tyler Butler <[email protected]>
1 parent 842aa45 commit ad35a7c

File tree

15 files changed

+1285
-11
lines changed

15 files changed

+1285
-11
lines changed

build-tools/packages/build-cli/.eslintrc.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ module.exports = {
3838
// These are all excluded because they're "submodules" used for organization.
3939
// AB#8118 tracks removing the barrel files and importing directly from the submodules.
4040
"**/library/index.js",
41+
"**/library/githubRest.js",
4142
"**/handlers/index.js",
4243
"**/machines/index.js",
4344
"**/repoPolicyCheck/index.js",
45+
"**/azureDevops/**",
46+
"**/codeCoverage/**",
47+
"azure-devops-node-api/**",
4448
],
4549
},
4650
],

build-tools/packages/build-cli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ USAGE
4747
* [`flub publish`](docs/publish.md) - Publish commands are used to publish packages to an npm registry.
4848
* [`flub release`](docs/release.md) - Release commands are used to manage the Fluid release process.
4949
* [`flub rename-types`](docs/rename-types.md) - Renames type declaration files from .d.ts to .d.mts.
50+
* [`flub report`](docs/report.md) - Report analysis about the codebase, like code coverage and bundle size measurements.
5051
* [`flub run`](docs/run.md) - Generate a report from input bundle stats collected through the collect bundleStats command.
5152
* [`flub transform`](docs/transform.md) - Transform commands are used to transform code, docs, etc. into alternative forms.
5253
* [`flub typetests`](docs/typetests.md) - Updates configuration for type tests in package.json files. If the previous version changes after running preparation, then npm install must be run before building.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Code coverage
2+
3+
## Overview
4+
5+
This module contains all the utilities required to analyze code coverage metrics from PRs. The coverage metrics generated in the PR build are compared against a baseline CI build for packages that have been updated in the PR. If the line or branch coverage for a package has been impacted in the PR, a comment is posted to the PR showing the diff of the code coverage between baseline and PR.
6+
7+
## Code Coverage Metrics
8+
9+
Code coverage metrics is generated when tests run in the CI pipeline. You can also generate the code coverage metrics for a package locally by running `npm run test:coverage` for the individual package or by running `npm run ci:test:mocha:coverage` from the root.
10+
11+
## Pieces of the code coverage analysis
12+
13+
Code coverage has several steps involving different commands. This section defines those pieces and how they fit together to enable overall code coverage reporting.
14+
15+
### Cobertura coverage files
16+
17+
Code coverage metrics is included in the cobertura-format coverage files we collect during CI builds. These files are currently published as artifacts from both our PR and internal build pipelines to ensure we can run comparisons on PRs against a baseline build.
18+
19+
### Identifying the baseline build
20+
21+
Before running coverage comparison, a baseline build needs to be determined for the PR. This is typically based on the target branch for the PR. For example, if a pull request was targeting `main`, we would consider the baseline to be the latest successful `main` branch build.
22+
23+
### Downloading artifacts from baseline build
24+
25+
Once the baseline build is identified, we download the build artifacts corresponding to the `Code Coverage Report_{Build_Number}` artifact name for this build. We unzip the files and extract the coverage metrics out of the code coverage artifact using the helper `getCoverageMetricsFromArtifact`. Currently, we track `lineCoverage` and `branchCoverage` as our metrics for reporting and
26+
use both `lineCoverage` and `branchCoverage` for success criteria. The metrics is in percentages from 0..100. The final structure of the extracted metrics looks like the following:
27+
28+
```typescript
29+
/**
30+
* The type for the coverage report, containing the line coverage and branch coverage(in percentage) for each package
31+
*/
32+
export interface CoverageMetric {
33+
lineCoverage: number;
34+
branchCoverage: number;
35+
}
36+
```
37+
38+
### Generating the coverage metrics on PR build
39+
40+
As mentioned earlier, the PR build also uploads coverage metrics as artifacts that can be used to run coverage analysis against a baseline build. To help with this, we use the `getCoverageMetricsFromArtifact` helper function to extract code coverage metrics of format `CoverageMetric` that contains code coverage metrics corresponding to the PR for each package.
41+
42+
### Comparing code coverage reports
43+
44+
Once we have the coverage metrics for the baseline and PR build, we use the `compareCodeCoverage` utility function that returns an array of coverage comparisons for the list of packages passed into it. The array returned contains objects of type `CodeCoverageComparison`. We ignore some packages for comparing code coverage such as definition packages and packages which are not inside `packages` folder in the repo. The logic is in the `compareCodeCoverage` function.
45+
46+
```typescript
47+
/**
48+
* Type for the code coverage report generated by comparing the baseline and pr code coverage
49+
*/
50+
export interface CodeCoverageComparison {
51+
/**
52+
* Path of the package
53+
*/
54+
packagePath: string;
55+
/**
56+
* Line coverage in baseline build (as a percent)
57+
*/
58+
lineCoverageInBaseline: number;
59+
/**
60+
* Line coverage in pr build (as a percent)
61+
*/
62+
lineCoverageInPr: number;
63+
/**
64+
* difference between line coverage in pr build and baseline build (percentage points)
65+
*/
66+
lineCoverageDiff: number;
67+
/**
68+
* branch coverage in baseline build (as a percent)
69+
*/
70+
branchCoverageInBaseline: number;
71+
/**
72+
* branch coverage in pr build (as a percent)
73+
*/
74+
branchCoverageInPr: number;
75+
/**
76+
* difference between branch coverage in pr build and baseline build (percentage points)
77+
*/
78+
branchCoverageDiff: number;
79+
/**
80+
* Flag to indicate if the package is new
81+
*/
82+
isNewPackage: boolean;
83+
}
84+
```
85+
86+
### Success Criteria
87+
88+
The code coverage PR checks will fail in the following cases:
89+
90+
- If the **line code coverage** or **branch code coverage** decreased by > 1%.
91+
- If the code coverage(line and branch) for a newly added package is < 50%.
92+
93+
The enforcement code is in the function `isCodeCoverageCriteriaPassed`.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
`flub report`
2+
=============
3+
4+
Report analysis about the codebase, like code coverage and bundle size measurements.
5+
6+
* [`flub report codeCoverage`](#flub-report-codecoverage)
7+
8+
## `flub report codeCoverage`
9+
10+
Run comparison of code coverage stats
11+
12+
```
13+
USAGE
14+
$ flub report codeCoverage --adoBuildId <value> --adoApiToken <value> --githubApiToken <value>
15+
--adoCIBuildDefinitionIdBaseline <value> --adoCIBuildDefinitionIdPR <value>
16+
--codeCoverageAnalysisArtifactNameBaseline <value> --codeCoverageAnalysisArtifactNamePR <value> --githubPRNumber
17+
<value> --githubRepositoryName <value> --githubRepositoryOwner <value> --targetBranchName <value> [-v | --quiet]
18+
19+
FLAGS
20+
--adoApiToken=<value> (required) Token to get auth for accessing ADO builds.
21+
--adoBuildId=<value> (required) Azure DevOps build ID.
22+
--adoCIBuildDefinitionIdBaseline=<value> (required) Build definition/pipeline number/id for the baseline
23+
build.
24+
--adoCIBuildDefinitionIdPR=<value> (required) Build definition/pipeline number/id for the PR build.
25+
--codeCoverageAnalysisArtifactNameBaseline=<value> (required) Code coverage artifact name for the baseline build.
26+
--codeCoverageAnalysisArtifactNamePR=<value> (required) Code coverage artifact name for the PR build.
27+
--githubApiToken=<value> (required) Token to get auth for accessing Github PR.
28+
--githubPRNumber=<value> (required) Github PR number.
29+
--githubRepositoryName=<value> (required) Github repository name.
30+
--githubRepositoryOwner=<value> (required) Github repository owner.
31+
--targetBranchName=<value> (required) Target branch name.
32+
33+
LOGGING FLAGS
34+
-v, --verbose Enable verbose logging.
35+
--quiet Disable all logging.
36+
37+
DESCRIPTION
38+
Run comparison of code coverage stats
39+
```
40+
41+
_See code: [src/commands/report/codeCoverage.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/report/codeCoverage.ts)_

build-tools/packages/build-cli/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"@octokit/rest": "^21.0.2",
9494
"@rushstack/node-core-library": "^3.59.5",
9595
"async": "^3.2.4",
96+
"azure-devops-node-api": "^11.2.0",
9697
"chalk": "^5.3.0",
9798
"change-case": "^3.1.0",
9899
"cosmiconfig": "^8.3.6",
@@ -110,6 +111,7 @@
110111
"issue-parser": "^7.0.1",
111112
"json5": "^2.2.3",
112113
"jssm": "5.98.2",
114+
"jszip": "^3.10.1",
113115
"latest-version": "^5.1.0",
114116
"mdast": "^3.0.0",
115117
"mdast-util-heading-range": "^4.0.0",
@@ -137,7 +139,8 @@
137139
"table": "^6.8.1",
138140
"ts-morph": "^22.0.0",
139141
"type-fest": "^2.19.0",
140-
"unist-util-visit": "^5.0.0"
142+
"unist-util-visit": "^5.0.0",
143+
"xml2js": "^0.5.0"
141144
},
142145
"devDependencies": {
143146
"@biomejs/biome": "~1.8.3",
@@ -161,6 +164,7 @@
161164
"@types/semver-utils": "^1.1.1",
162165
"@types/sort-json": "^2.0.1",
163166
"@types/unist": "^3.0.3",
167+
"@types/xml2js": "^0.4.11",
164168
"c8": "^7.14.0",
165169
"chai": "^4.3.7",
166170
"chai-arrays": "^2.2.0",
@@ -243,6 +247,9 @@
243247
},
244248
"transform": {
245249
"description": "Transform commands are used to transform code, docs, etc. into alternative forms."
250+
},
251+
"report": {
252+
"description": "Report analysis about the codebase, like code coverage and bundle size measurements."
246253
}
247254
}
248255
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { getAzureDevopsApi } from "@fluidframework/bundle-size-tools";
7+
import { type IAzureDevopsBuildCoverageConstants } from "../library/azureDevops/constants.js";
8+
import {
9+
type IBuildMetrics,
10+
getBaselineBuildMetrics,
11+
getBuildArtifactForSpecificBuild,
12+
} from "../library/azureDevops/getBaselineBuildMetrics.js";
13+
import type { CommandLogger } from "../logging.js";
14+
import { type CodeCoverageComparison, compareCodeCoverage } from "./compareCodeCoverage.js";
15+
import { getCoverageMetricsFromArtifact } from "./getCoverageMetrics.js";
16+
17+
/**
18+
* Report of code coverage comparison.
19+
*/
20+
export interface CodeCoverageReport {
21+
/**
22+
* Comparison data for each package.
23+
*/
24+
comparisonData: CodeCoverageComparison[];
25+
26+
/**
27+
* Baseline build metrics against which the PR build metrics are compared.
28+
*/
29+
baselineBuildMetrics: IBuildMetrics;
30+
}
31+
32+
/**
33+
* API to get the code coverage report for a PR.
34+
* @param adoToken - ADO token that will be used to download artifacts from ADO pipeline runs.
35+
* @param codeCoverageConstantsBaseline - The code coverage constants required for fetching the baseline build artifacts.
36+
* @param codeCoverageConstantsPR - The code coverage constants required for fetching the PR build artifacts.
37+
* @param changedFiles - The list of files changed in the PR.
38+
* @param logger - The logger to log messages.
39+
*/
40+
export async function getCodeCoverageReport(
41+
adoToken: string,
42+
codeCoverageConstantsBaseline: IAzureDevopsBuildCoverageConstants,
43+
codeCoverageConstantsPR: IAzureDevopsBuildCoverageConstants,
44+
changedFiles: string[],
45+
logger?: CommandLogger,
46+
): Promise<CodeCoverageReport> {
47+
const adoConnection = getAzureDevopsApi(adoToken, codeCoverageConstantsBaseline.orgUrl);
48+
49+
const baselineBuildInfo = await getBaselineBuildMetrics(
50+
codeCoverageConstantsBaseline,
51+
adoConnection,
52+
logger,
53+
).catch((error) => {
54+
logger?.errorLog(`Error getting baseline build metrics: ${error}`);
55+
throw error;
56+
});
57+
58+
const adoConnectionForPR = getAzureDevopsApi(adoToken, codeCoverageConstantsPR.orgUrl);
59+
60+
const prBuildInfo = await getBuildArtifactForSpecificBuild(
61+
codeCoverageConstantsPR,
62+
adoConnectionForPR,
63+
logger,
64+
).catch((error) => {
65+
logger?.errorLog(`Error getting PR build metrics: ${error}`);
66+
throw error;
67+
});
68+
69+
// Extract the coverage metrics for the baseline and PR builds.
70+
const [coverageMetricsForBaseline, coverageMetricsForPr] = await Promise.all([
71+
getCoverageMetricsFromArtifact(baselineBuildInfo.artifactZip),
72+
getCoverageMetricsFromArtifact(prBuildInfo.artifactZip),
73+
]);
74+
75+
// Compare the code coverage metrics for the baseline and PR builds.
76+
const comparisonData = compareCodeCoverage(
77+
coverageMetricsForBaseline,
78+
coverageMetricsForPr,
79+
changedFiles,
80+
);
81+
82+
return {
83+
comparisonData,
84+
baselineBuildMetrics: baselineBuildInfo,
85+
};
86+
}

0 commit comments

Comments
 (0)