@toptal/davinci-monorepo 8.1.4-alpha-CRT-5891-create-coverage-command-e6e5bd51.7 → 8.1.4-alpha-CRT-5891-create-coverage-command-1b2c7953.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/davinci-monorepo.js +2 -2
- package/docs/coverage.md +2 -8
- package/package.json +3 -3
- package/src/commands/codeowners/commands/list.js +6 -10
- package/src/commands/codeowners/core/codeowners/codeowners.js +0 -1
- package/src/commands/codeowners/core/process-codebase.js +2 -10
- package/src/commands/codeowners/index.js +2 -0
- package/src/commands/codeowners/services/async-codeowners-import/async-codeowners-import.js +22 -0
- package/src/commands/coverage-command/__snapshots__/create-generate-team-coverage-command.test.js.snap +74 -0
- package/src/commands/coverage-command/__snapshots__/create-group-coverage-by-codeowners-command.test.js.snap +47 -0
- package/src/commands/coverage-command/create-generate-team-coverage-command.js +141 -0
- package/src/commands/coverage-command/create-generate-team-coverage-command.test.js +25 -0
- package/src/commands/coverage-command/create-group-coverage-by-codeowners-command.js +103 -0
- package/src/commands/coverage-command/create-group-coverage-by-codeowners-command.test.js +25 -0
- package/src/commands/coverage-command/index.js +6 -10
- package/src/commands/coverage-command/services/get-unique-teams/get-unique-teams.js +5 -4
- package/src/commands/coverage-command/services/sanitize-team-name/sanitize-team-name.js +2 -1
- package/src/index.js +2 -2
- package/src/commands/coverage-command/generate-team-coverage.js +0 -131
- package/src/commands/coverage-command/group-coverage-by-codeowners.js +0 -94
package/bin/davinci-monorepo.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createGraphGenerateCommand } from '../src/commands/graph-generate.js'
|
|
|
7
7
|
import metricsCommand from '../src/commands/metrics.js'
|
|
8
8
|
// eslint-disable-next-line no-restricted-syntax
|
|
9
9
|
import codeownersCommand from '../src/commands/codeowners/index.js'
|
|
10
|
-
import {
|
|
10
|
+
import { createCoverageCommand } from '../src/commands/coverage-command/index.js'
|
|
11
11
|
|
|
12
12
|
cliEngine.loadCommands(
|
|
13
13
|
[
|
|
@@ -15,7 +15,7 @@ cliEngine.loadCommands(
|
|
|
15
15
|
createGraphGenerateCommand,
|
|
16
16
|
metricsCommand,
|
|
17
17
|
codeownersCommand,
|
|
18
|
-
|
|
18
|
+
createCoverageCommand,
|
|
19
19
|
],
|
|
20
20
|
'davinci-monorepo'
|
|
21
21
|
)
|
package/docs/coverage.md
CHANGED
|
@@ -4,20 +4,14 @@ Streamline code coverage analysis and reporting by organizing coverage reports b
|
|
|
4
4
|
|
|
5
5
|
#### `group-coverage-by-codeowners`
|
|
6
6
|
|
|
7
|
-
Reads code coverage reports for Jest and Cypress, groups the coverage data based on code owners, and writes separate coverage reports for each team
|
|
8
|
-
|
|
9
|
-
The command utilizes the Codeowners class, which is responsible for identifying the code owners associated with each file.
|
|
10
|
-
|
|
11
7
|
```sh
|
|
12
8
|
codeowners group-coverage-by-codeowners
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
#### `
|
|
16
|
-
|
|
17
|
-
Generates team-specific coverage reports by copying and processing coverage data from Jest and Cypress reports. The resulting coverage reports provide insights into the code coverage metrics for each team.
|
|
11
|
+
#### `generate-team-coverage`
|
|
18
12
|
|
|
19
13
|
```sh
|
|
20
|
-
coverage
|
|
14
|
+
coverage generate-team-coverage [options]
|
|
21
15
|
|
|
22
16
|
Options:
|
|
23
17
|
-r, --repository <repository_name> specify repository name
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toptal/davinci-monorepo",
|
|
3
|
-
"version": "8.1.4-alpha-CRT-5891-create-coverage-command-
|
|
3
|
+
"version": "8.1.4-alpha-CRT-5891-create-coverage-command-1b2c7953.12+1b2c7953",
|
|
4
4
|
"description": "Monorepo utility tools",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@nodelib/fs.walk": "^1.2.6",
|
|
37
37
|
"@oclif/core": "^1.16.1",
|
|
38
|
-
"@toptal/davinci-cli-shared": "2.3.1-alpha-CRT-5891-create-coverage-command-
|
|
38
|
+
"@toptal/davinci-cli-shared": "2.3.1-alpha-CRT-5891-create-coverage-command-1b2c7953.44+1b2c7953",
|
|
39
39
|
"chalk": "^4.1.2",
|
|
40
40
|
"codeowners": "5.1.1",
|
|
41
41
|
"dependency-cruiser": "^12.5.0",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@jest/globals": "^29.4.2"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "1b2c79534c7559b73b39c067ab0480eadccf0ebc"
|
|
58
58
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { asyncCodeownersImport } from '../services/async-codeowners-import/async-codeowners-import.js'
|
|
2
|
+
|
|
1
3
|
const list = program => {
|
|
2
4
|
return program
|
|
3
5
|
.command('list <paths...>')
|
|
@@ -9,19 +11,13 @@ const list = program => {
|
|
|
9
11
|
'CODEOWNERS'
|
|
10
12
|
)
|
|
11
13
|
.action(async (pathsToCheck, options) => {
|
|
12
|
-
let codeowners
|
|
13
14
|
const rootPath = process.cwd()
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
const codeowners = await asyncCodeownersImport(
|
|
17
|
+
rootPath,
|
|
18
|
+
options.codeownersFilename
|
|
16
19
|
)
|
|
17
20
|
|
|
18
|
-
// instantiate new Codeowners obj
|
|
19
|
-
try {
|
|
20
|
-
codeowners = new CodeOwnersTool(rootPath, options.codeownersFilename)
|
|
21
|
-
} catch (e) {
|
|
22
|
-
console.error(e.message)
|
|
23
|
-
process.exit(1)
|
|
24
|
-
}
|
|
25
21
|
let owners = []
|
|
26
22
|
|
|
27
23
|
// iterates over all paths and returns owners
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
1
|
import path from 'path'
|
|
3
2
|
|
|
4
3
|
import { PackageInfo } from './package-info/package-info.js'
|
|
@@ -6,6 +5,7 @@ import readFiles from './read-files.js'
|
|
|
6
5
|
import { NO_OWNER } from '../config.js'
|
|
7
6
|
import fileNameToPackage from './utils/file-name-to-package.js'
|
|
8
7
|
import OwnerToPackages from './owners-info/owner-to-packages.js'
|
|
8
|
+
import { asyncCodeownersImport } from '../services/async-codeowners-import/async-codeowners-import.js'
|
|
9
9
|
|
|
10
10
|
const processGroupedByPackage = (
|
|
11
11
|
groupedByPackageName,
|
|
@@ -39,17 +39,9 @@ const processGroupedByOwner = (groupedByOwnerName, owners, packageName) => {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const processCodeBase = async options => {
|
|
42
|
-
let codeowners
|
|
43
|
-
|
|
44
42
|
const rootPath = process.cwd()
|
|
45
|
-
const { default: CodeOwnersTool } = await import('codeowners/codeowners.js')
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
codeowners = new CodeOwnersTool(rootPath, options.codeownersFilename)
|
|
49
|
-
} catch (e) {
|
|
50
|
-
console.error(e.message)
|
|
51
|
-
process.exit(1)
|
|
52
|
-
}
|
|
44
|
+
const codeowners = await asyncCodeownersImport()
|
|
53
45
|
|
|
54
46
|
const filesToTeams = new Map()
|
|
55
47
|
const groupedByPackageName = {}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import getWorkspaceRoot from 'find-yarn-workspace-root'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} cwd
|
|
5
|
+
* @param {string} fileName
|
|
6
|
+
* @return {Promise}
|
|
7
|
+
*/
|
|
8
|
+
export const asyncCodeownersImport = async (cwd, fileName) => {
|
|
9
|
+
const workspaceRoot = getWorkspaceRoot()
|
|
10
|
+
|
|
11
|
+
let codeowners
|
|
12
|
+
const { default: CodeOwnersTool } = await import('codeowners/codeowners.js')
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
codeowners = new CodeOwnersTool(cwd ?? workspaceRoot, fileName)
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error(e.message)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return codeowners
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`createGenerateTeamCoverageCommand has the correct command structure 1`] = `
|
|
4
|
+
"{
|
|
5
|
+
"_events": {},
|
|
6
|
+
"_eventsCount": 2,
|
|
7
|
+
"commands": [],
|
|
8
|
+
"options": [
|
|
9
|
+
{
|
|
10
|
+
"flags": "-r, --repository <repository_name>",
|
|
11
|
+
"description": "specify repository name",
|
|
12
|
+
"required": true,
|
|
13
|
+
"optional": false,
|
|
14
|
+
"variadic": false,
|
|
15
|
+
"mandatory": true,
|
|
16
|
+
"short": "-r",
|
|
17
|
+
"long": "--repository",
|
|
18
|
+
"negate": false,
|
|
19
|
+
"hidden": false,
|
|
20
|
+
"conflictsWith": []
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"flags": "-o, --output-file-name <output_file_name>",
|
|
24
|
+
"description": "specify output file name",
|
|
25
|
+
"required": true,
|
|
26
|
+
"optional": false,
|
|
27
|
+
"variadic": false,
|
|
28
|
+
"mandatory": true,
|
|
29
|
+
"short": "-o",
|
|
30
|
+
"long": "--output-file-name",
|
|
31
|
+
"negate": false,
|
|
32
|
+
"hidden": false,
|
|
33
|
+
"conflictsWith": []
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"parent": null,
|
|
37
|
+
"_allowUnknownOption": false,
|
|
38
|
+
"_allowExcessArguments": true,
|
|
39
|
+
"_args": [],
|
|
40
|
+
"args": [],
|
|
41
|
+
"rawArgs": [],
|
|
42
|
+
"processedArgs": [],
|
|
43
|
+
"_scriptPath": null,
|
|
44
|
+
"_name": "generate-team-coverage",
|
|
45
|
+
"_optionValues": {},
|
|
46
|
+
"_optionValueSources": {},
|
|
47
|
+
"_storeOptionsAsProperties": false,
|
|
48
|
+
"_executableHandler": false,
|
|
49
|
+
"_executableFile": null,
|
|
50
|
+
"_executableDir": null,
|
|
51
|
+
"_defaultCommandName": null,
|
|
52
|
+
"_exitCallback": null,
|
|
53
|
+
"_aliases": [],
|
|
54
|
+
"_combineFlagAndOptionalValue": true,
|
|
55
|
+
"_description": "Generates team-specific coverage reports by copying and processing coverage data from Jest and Cypress reports.\\nThe resulting coverage reports provide insights into the code coverage metrics for each team.",
|
|
56
|
+
"_summary": "",
|
|
57
|
+
"_enablePositionalOptions": false,
|
|
58
|
+
"_passThroughOptions": false,
|
|
59
|
+
"_lifeCycleHooks": {},
|
|
60
|
+
"_showHelpAfterError": false,
|
|
61
|
+
"_showSuggestionAfterError": true,
|
|
62
|
+
"_outputConfiguration": {},
|
|
63
|
+
"_hidden": false,
|
|
64
|
+
"_hasHelpOption": true,
|
|
65
|
+
"_helpFlags": "-h, --help",
|
|
66
|
+
"_helpDescription": "display help for command",
|
|
67
|
+
"_helpShortFlag": "-h",
|
|
68
|
+
"_helpLongFlag": "--help",
|
|
69
|
+
"_helpCommandName": "help",
|
|
70
|
+
"_helpCommandnameAndArgs": "help [command]",
|
|
71
|
+
"_helpCommandDescription": "display help for command",
|
|
72
|
+
"_helpConfiguration": {}
|
|
73
|
+
}"
|
|
74
|
+
`;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`createGroupCoverageByCodeownersCommand has the correct command structure 1`] = `
|
|
4
|
+
"{
|
|
5
|
+
"_events": {},
|
|
6
|
+
"_eventsCount": 0,
|
|
7
|
+
"commands": [],
|
|
8
|
+
"options": [],
|
|
9
|
+
"parent": null,
|
|
10
|
+
"_allowUnknownOption": false,
|
|
11
|
+
"_allowExcessArguments": true,
|
|
12
|
+
"_args": [],
|
|
13
|
+
"args": [],
|
|
14
|
+
"rawArgs": [],
|
|
15
|
+
"processedArgs": [],
|
|
16
|
+
"_scriptPath": null,
|
|
17
|
+
"_name": "group-coverage-by-codeowners",
|
|
18
|
+
"_optionValues": {},
|
|
19
|
+
"_optionValueSources": {},
|
|
20
|
+
"_storeOptionsAsProperties": false,
|
|
21
|
+
"_executableHandler": false,
|
|
22
|
+
"_executableFile": null,
|
|
23
|
+
"_executableDir": null,
|
|
24
|
+
"_defaultCommandName": null,
|
|
25
|
+
"_exitCallback": null,
|
|
26
|
+
"_aliases": [],
|
|
27
|
+
"_combineFlagAndOptionalValue": true,
|
|
28
|
+
"_description": "Reads code coverage reports for Jest and Cypress, groups the coverage data based on code owners, and writes separate coverage reports for each team.\\nThe command utilizes the Codeowners class, which is responsible for identifying the code owners associated with each file.",
|
|
29
|
+
"_summary": "",
|
|
30
|
+
"_enablePositionalOptions": false,
|
|
31
|
+
"_passThroughOptions": false,
|
|
32
|
+
"_lifeCycleHooks": {},
|
|
33
|
+
"_showHelpAfterError": false,
|
|
34
|
+
"_showSuggestionAfterError": true,
|
|
35
|
+
"_outputConfiguration": {},
|
|
36
|
+
"_hidden": false,
|
|
37
|
+
"_hasHelpOption": true,
|
|
38
|
+
"_helpFlags": "-h, --help",
|
|
39
|
+
"_helpDescription": "display help for command",
|
|
40
|
+
"_helpShortFlag": "-h",
|
|
41
|
+
"_helpLongFlag": "--help",
|
|
42
|
+
"_helpCommandName": "help",
|
|
43
|
+
"_helpCommandnameAndArgs": "help [command]",
|
|
44
|
+
"_helpCommandDescription": "display help for command",
|
|
45
|
+
"_helpConfiguration": {}
|
|
46
|
+
}"
|
|
47
|
+
`;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import execa from 'execa'
|
|
3
|
+
import { print } from '@toptal/davinci-cli-shared'
|
|
4
|
+
import { readFile, writeFile, mkdir, cp, rm } from 'fs/promises'
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
|
|
7
|
+
import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
|
|
8
|
+
import { TEAMS_COVERAGE_PATH } from './config.js'
|
|
9
|
+
import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
|
|
10
|
+
import { asyncCodeownersImport } from '../codeowners/index.js'
|
|
11
|
+
|
|
12
|
+
const sanitizeNycPercentage = percentage =>
|
|
13
|
+
percentage === 'Unknown' ? 0 : percentage
|
|
14
|
+
|
|
15
|
+
const generateNycReport = async ({
|
|
16
|
+
type,
|
|
17
|
+
sanitizedTeamPath,
|
|
18
|
+
sanitizedTeam,
|
|
19
|
+
}) => {
|
|
20
|
+
const sanitizedTeamPathWithType = path.join(sanitizedTeamPath, type)
|
|
21
|
+
|
|
22
|
+
await mkdir(sanitizedTeamPathWithType)
|
|
23
|
+
|
|
24
|
+
const fullCoverageTeamPath = path.join(
|
|
25
|
+
'coverage',
|
|
26
|
+
type,
|
|
27
|
+
`coverage-final-${sanitizedTeam}.json`
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (!existsSync(fullCoverageTeamPath)) {
|
|
31
|
+
print.red(`Make sure that '${fullCoverageTeamPath}' exists`)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await cp(
|
|
36
|
+
path.join(fullCoverageTeamPath),
|
|
37
|
+
path.join(sanitizedTeamPathWithType, 'coverage-final.json')
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
await execa.command(
|
|
41
|
+
`yarn nyc report --temp-dir ${sanitizedTeamPathWithType} --reporter json-summary --reporter html --report-dir ${sanitizedTeamPathWithType}`
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getCoverageSummary = async (type, teamPath) => {
|
|
46
|
+
const result = {}
|
|
47
|
+
|
|
48
|
+
const coverageSummary = JSON.parse(
|
|
49
|
+
await readFile(path.join(teamPath, type, 'coverage-summary.json'), 'utf8')
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
result[`${type}_lines_coverage`] = sanitizeNycPercentage(
|
|
53
|
+
coverageSummary.total.lines.pct
|
|
54
|
+
)
|
|
55
|
+
result[`${type}_statements_coverage`] = sanitizeNycPercentage(
|
|
56
|
+
coverageSummary.total.statements.pct
|
|
57
|
+
)
|
|
58
|
+
result[`${type}_functions_coverage`] = sanitizeNycPercentage(
|
|
59
|
+
coverageSummary.total.functions.pct
|
|
60
|
+
)
|
|
61
|
+
result[`${type}_branches_coverage`] = sanitizeNycPercentage(
|
|
62
|
+
coverageSummary.total.branches.pct
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const generateTeamCoverage = async options => {
|
|
69
|
+
print.header('Generating team coverage')
|
|
70
|
+
|
|
71
|
+
const codeowners = await asyncCodeownersImport()
|
|
72
|
+
|
|
73
|
+
const teams = getUniqueTeams(codeowners.ownerEntries)
|
|
74
|
+
|
|
75
|
+
print.grey('Teams: ', teams)
|
|
76
|
+
|
|
77
|
+
await rm(TEAMS_COVERAGE_PATH, { recursive: true, force: true })
|
|
78
|
+
await mkdir(TEAMS_COVERAGE_PATH, { recursive: true })
|
|
79
|
+
|
|
80
|
+
for await (const team of teams) {
|
|
81
|
+
print.grey('Team coverage: ', team)
|
|
82
|
+
|
|
83
|
+
const sanitizedTeam = sanitizeTeamName(team)
|
|
84
|
+
const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
|
|
85
|
+
|
|
86
|
+
await mkdir(sanitizedTeamPath, { recursive: true })
|
|
87
|
+
|
|
88
|
+
await Promise.all([
|
|
89
|
+
generateNycReport({
|
|
90
|
+
type: 'jest',
|
|
91
|
+
sanitizedTeamPath,
|
|
92
|
+
sanitizedTeam,
|
|
93
|
+
}),
|
|
94
|
+
generateNycReport({
|
|
95
|
+
type: 'cypress',
|
|
96
|
+
sanitizedTeamPath,
|
|
97
|
+
sanitizedTeam,
|
|
98
|
+
}),
|
|
99
|
+
])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const results = []
|
|
103
|
+
const created_at = new Date().toISOString()
|
|
104
|
+
|
|
105
|
+
for await (const team of teams) {
|
|
106
|
+
const sanitizedTeam = sanitizeTeamName(team)
|
|
107
|
+
const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
|
|
108
|
+
|
|
109
|
+
const [jestCoverageSummary, cypressCoverageSummary] = await Promise.all([
|
|
110
|
+
getCoverageSummary('jest', sanitizedTeamPath),
|
|
111
|
+
getCoverageSummary('cypress', sanitizedTeamPath),
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
results.push({
|
|
115
|
+
created_at,
|
|
116
|
+
team,
|
|
117
|
+
repo: options.repository,
|
|
118
|
+
...jestCoverageSummary,
|
|
119
|
+
...cypressCoverageSummary,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return writeFile(options.outputFileName, JSON.stringify(results, null, 2))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const createGenerateTeamCoverageCommand = program =>
|
|
127
|
+
program
|
|
128
|
+
.createCommand('generate-team-coverage')
|
|
129
|
+
.description(
|
|
130
|
+
`Generates team-specific coverage reports by copying and processing coverage data from Jest and Cypress reports.
|
|
131
|
+
The resulting coverage reports provide insights into the code coverage metrics for each team.`
|
|
132
|
+
)
|
|
133
|
+
.requiredOption(
|
|
134
|
+
'-r, --repository <repository_name>',
|
|
135
|
+
'specify repository name'
|
|
136
|
+
)
|
|
137
|
+
.requiredOption(
|
|
138
|
+
'-o, --output-file-name <output_file_name>',
|
|
139
|
+
'specify output file name'
|
|
140
|
+
)
|
|
141
|
+
.action(generateTeamCoverage)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
const { createGenerateTeamCoverageCommand } = await import(
|
|
6
|
+
'./create-generate-team-coverage-command.js'
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
describe('createGenerateTeamCoverageCommand', () => {
|
|
10
|
+
let program
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
program = new Command()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.clearAllMocks()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('has the correct command structure', () => {
|
|
21
|
+
const command = createGenerateTeamCoverageCommand(program)
|
|
22
|
+
|
|
23
|
+
expect(JSON.stringify(command, null, 2)).toMatchSnapshot()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { print } from '@toptal/davinci-cli-shared'
|
|
3
|
+
import getWorkspaceRoot from 'find-yarn-workspace-root'
|
|
4
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
|
|
7
|
+
import { CYPRESS_COVERAGE_PATH, JEST_COVERAGE_PATH } from './config.js'
|
|
8
|
+
import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
|
|
9
|
+
import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
|
|
10
|
+
import { asyncCodeownersImport } from '../codeowners/index.js'
|
|
11
|
+
|
|
12
|
+
const workspaceRoot = getWorkspaceRoot()
|
|
13
|
+
|
|
14
|
+
const readCoverageReport = async reportPathName => {
|
|
15
|
+
const fullPath = path.join(workspaceRoot, reportPathName)
|
|
16
|
+
|
|
17
|
+
if (!existsSync(fullPath)) {
|
|
18
|
+
print.red(`Make sure that '${fullPath}' exists`)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const data = await readFile(fullPath, 'utf8')
|
|
23
|
+
|
|
24
|
+
return JSON.parse(data)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const splitReportIntoTeams = async ({
|
|
28
|
+
report,
|
|
29
|
+
teams,
|
|
30
|
+
codeowners,
|
|
31
|
+
coveragePath,
|
|
32
|
+
}) => {
|
|
33
|
+
const teamsReport = {}
|
|
34
|
+
|
|
35
|
+
for (const [reportPathKey, reportValue] of Object.entries(report)) {
|
|
36
|
+
const filePath = reportValue.path
|
|
37
|
+
const relativeWorkspacePath = path.relative(workspaceRoot, filePath)
|
|
38
|
+
const owners = codeowners.getOwner(relativeWorkspacePath)
|
|
39
|
+
|
|
40
|
+
owners.forEach(owner => {
|
|
41
|
+
teamsReport[owner] = {
|
|
42
|
+
...teamsReport[owner],
|
|
43
|
+
[reportPathKey]: reportValue,
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for await (const team of teams) {
|
|
49
|
+
const teamReport = teamsReport[team] || {}
|
|
50
|
+
const sanitizedTeamPath = sanitizeTeamName(team)
|
|
51
|
+
|
|
52
|
+
const reportPath = path.join(
|
|
53
|
+
workspaceRoot,
|
|
54
|
+
coveragePath,
|
|
55
|
+
`coverage-final-${sanitizedTeamPath}.json`
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return writeFile(reportPath, JSON.stringify(teamReport, null, 2))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const groupCoverageByCodeowners = async () => {
|
|
63
|
+
print.header('Grouping coverage by codeowners')
|
|
64
|
+
|
|
65
|
+
const [jestReport, cypressReport] = await Promise.all([
|
|
66
|
+
readCoverageReport(
|
|
67
|
+
path.join(JEST_COVERAGE_PATH, 'coverage-final-jest.json')
|
|
68
|
+
),
|
|
69
|
+
readCoverageReport(
|
|
70
|
+
path.join(CYPRESS_COVERAGE_PATH, 'coverage-final-cypress.json')
|
|
71
|
+
),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
const codeowners = await asyncCodeownersImport()
|
|
75
|
+
|
|
76
|
+
const teams = getUniqueTeams(codeowners.ownerEntries)
|
|
77
|
+
|
|
78
|
+
print.grey('Teams: ', teams)
|
|
79
|
+
|
|
80
|
+
return Promise.all([
|
|
81
|
+
splitReportIntoTeams({
|
|
82
|
+
report: jestReport,
|
|
83
|
+
teams,
|
|
84
|
+
codeowners,
|
|
85
|
+
coveragePath: JEST_COVERAGE_PATH,
|
|
86
|
+
}),
|
|
87
|
+
splitReportIntoTeams({
|
|
88
|
+
report: cypressReport,
|
|
89
|
+
teams,
|
|
90
|
+
codeowners,
|
|
91
|
+
coveragePath: CYPRESS_COVERAGE_PATH,
|
|
92
|
+
}),
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const createGroupCoverageByCodeownersCommand = program =>
|
|
97
|
+
program
|
|
98
|
+
.createCommand('group-coverage-by-codeowners')
|
|
99
|
+
.description(
|
|
100
|
+
`Reads code coverage reports for Jest and Cypress, groups the coverage data based on code owners, and writes separate coverage reports for each team.
|
|
101
|
+
The command utilizes the Codeowners class, which is responsible for identifying the code owners associated with each file.`
|
|
102
|
+
)
|
|
103
|
+
.action(groupCoverageByCodeowners)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
const { createGroupCoverageByCodeownersCommand } = await import(
|
|
6
|
+
'./create-group-coverage-by-codeowners-command.js'
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
describe('createGroupCoverageByCodeownersCommand', () => {
|
|
10
|
+
let program
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
program = new Command()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.clearAllMocks()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('has the correct command structure', () => {
|
|
21
|
+
const command = createGroupCoverageByCodeownersCommand(program)
|
|
22
|
+
|
|
23
|
+
expect(JSON.stringify(command, null, 2)).toMatchSnapshot()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createGroupCoverageByCodeownersCommand } from './create-group-coverage-by-codeowners-command.js'
|
|
2
|
+
import { createGenerateTeamCoverageCommand } from './create-generate-team-coverage-command.js'
|
|
3
3
|
|
|
4
|
-
export const
|
|
5
|
-
|
|
4
|
+
export const createCoverageCommand = program =>
|
|
5
|
+
program
|
|
6
6
|
.createCommand('coverage')
|
|
7
7
|
.description(
|
|
8
8
|
'Provides code coverage analysis, including organizing coverage reports by code owners and generating team-specific reports'
|
|
9
9
|
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
generateTeamCoverageCommand(coverage)
|
|
13
|
-
|
|
14
|
-
return coverage
|
|
15
|
-
}
|
|
10
|
+
.addCommand(createGroupCoverageByCodeownersCommand(program))
|
|
11
|
+
.addCommand(createGenerateTeamCoverageCommand(program))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const getUniqueTeams = teams =>
|
|
2
|
-
teams
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export const getUniqueTeams = teams => {
|
|
2
|
+
const allTeams = teams.flatMap(entry => entry.usernames)
|
|
3
|
+
|
|
4
|
+
return [...new Set(allTeams)]
|
|
5
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export const sanitizeTeamName = team =>
|
|
1
|
+
export const sanitizeTeamName = team =>
|
|
2
|
+
team.replace(/\//g, '-').replace(/@/g, '')
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import detectCircularityCommandCreator from './commands/detect-circularity.js'
|
|
|
2
2
|
import metricsCommandCreator from './commands/metrics.js'
|
|
3
3
|
import { createGraphGenerateCommand } from './commands/graph-generate.js'
|
|
4
4
|
import codeownersCommand from './commands/codeowners/index.js'
|
|
5
|
-
import {
|
|
5
|
+
import { createCoverageCommand } from './commands/coverage-command/index.js'
|
|
6
6
|
import checkIfMonorepo from './utils/check-if-monorepo.js'
|
|
7
7
|
import getPackages from './utils/get-packages.js'
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ export const commands = [
|
|
|
16
16
|
metricsCommandCreator,
|
|
17
17
|
createGraphGenerateCommand,
|
|
18
18
|
codeownersCommand,
|
|
19
|
-
|
|
19
|
+
createCoverageCommand,
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
export default {
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import execa from 'execa'
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import { print } from '@toptal/davinci-cli-shared'
|
|
5
|
-
import getWorkspaceRoot from 'find-yarn-workspace-root'
|
|
6
|
-
|
|
7
|
-
import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
|
|
8
|
-
import {
|
|
9
|
-
CYPRESS_COVERAGE_PATH,
|
|
10
|
-
JEST_COVERAGE_PATH,
|
|
11
|
-
TEAMS_COVERAGE_PATH,
|
|
12
|
-
} from './config.js'
|
|
13
|
-
import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
|
|
14
|
-
|
|
15
|
-
const workspaceRoot = getWorkspaceRoot()
|
|
16
|
-
|
|
17
|
-
const sanitizeNycPercentage = percentage =>
|
|
18
|
-
percentage === 'Unknown' ? 0 : percentage
|
|
19
|
-
|
|
20
|
-
const getCoverageSummary = (type, teamPath) => {
|
|
21
|
-
const result = {}
|
|
22
|
-
|
|
23
|
-
const coverageSummary = JSON.parse(
|
|
24
|
-
fs.readFileSync(`${teamPath}/${type}/coverage-summary.json`, 'utf8')
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
result[`${type}_lines_coverage`] = sanitizeNycPercentage(
|
|
28
|
-
coverageSummary.total.lines.pct
|
|
29
|
-
)
|
|
30
|
-
result[`${type}_statements_coverage`] = sanitizeNycPercentage(
|
|
31
|
-
coverageSummary.total.statements.pct
|
|
32
|
-
)
|
|
33
|
-
result[`${type}_functions_coverage`] = sanitizeNycPercentage(
|
|
34
|
-
coverageSummary.total.functions.pct
|
|
35
|
-
)
|
|
36
|
-
result[`${type}_branches_coverage`] = sanitizeNycPercentage(
|
|
37
|
-
coverageSummary.total.branches.pct
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
return result
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// eslint-disable-next-line max-statements
|
|
44
|
-
const generateTeamCoverage = async options => {
|
|
45
|
-
print.header('Generating team coverage')
|
|
46
|
-
|
|
47
|
-
let codeowners
|
|
48
|
-
const { default: CodeOwnersTool } = await import(
|
|
49
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
50
|
-
'codeowners/codeowners.js'
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
codeowners = new CodeOwnersTool(workspaceRoot)
|
|
55
|
-
} catch (e) {
|
|
56
|
-
console.error(e.message)
|
|
57
|
-
process.exit(1)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const teams = getUniqueTeams(codeowners.ownerEntries)
|
|
61
|
-
|
|
62
|
-
print.grey('Teams: ', teams)
|
|
63
|
-
|
|
64
|
-
await execa.command(`rm -rf ${TEAMS_COVERAGE_PATH}`)
|
|
65
|
-
await execa.command(`mkdir ${TEAMS_COVERAGE_PATH}`)
|
|
66
|
-
|
|
67
|
-
for await (const team of teams) {
|
|
68
|
-
print.grey('Team coverage: ', team)
|
|
69
|
-
|
|
70
|
-
const sanitizedTeam = sanitizeTeamName(team)
|
|
71
|
-
const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
|
|
72
|
-
|
|
73
|
-
await execa.command(`mkdir ${sanitizedTeamPath}`)
|
|
74
|
-
await execa.command(`mkdir ${sanitizedTeamPath}/jest`)
|
|
75
|
-
await execa.command(`mkdir ${sanitizedTeamPath}/cypress`)
|
|
76
|
-
|
|
77
|
-
await execa.command(
|
|
78
|
-
`cp ${JEST_COVERAGE_PATH}/coverage-final-${sanitizedTeam}.json ${sanitizedTeamPath}/jest/coverage-final.json`
|
|
79
|
-
)
|
|
80
|
-
await execa.command(
|
|
81
|
-
`yarn nyc report --temp-dir ${sanitizedTeamPath}/jest --reporter json-summary --reporter html --report-dir ${sanitizedTeamPath}/jest`
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
await execa.command(
|
|
85
|
-
`cp ${CYPRESS_COVERAGE_PATH}/coverage-final-${sanitizedTeam}.json ${sanitizedTeamPath}/cypress/coverage-final.json`
|
|
86
|
-
)
|
|
87
|
-
await execa.command(
|
|
88
|
-
`yarn nyc report --temp-dir ${sanitizedTeamPath}/cypress --reporter json-summary --reporter html --report-dir ${sanitizedTeamPath}/cypress`
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const results = []
|
|
93
|
-
const created_at = new Date().toISOString()
|
|
94
|
-
|
|
95
|
-
for await (const team of teams) {
|
|
96
|
-
const sanitizedTeam = sanitizeTeamName(team)
|
|
97
|
-
const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
|
|
98
|
-
|
|
99
|
-
const jestCoverageSummary = getCoverageSummary('jest', sanitizedTeamPath)
|
|
100
|
-
const cypressCoverageSummary = getCoverageSummary(
|
|
101
|
-
'cypress',
|
|
102
|
-
sanitizedTeamPath
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
const result = {
|
|
106
|
-
created_at,
|
|
107
|
-
team,
|
|
108
|
-
repo: options.repository,
|
|
109
|
-
...jestCoverageSummary,
|
|
110
|
-
...cypressCoverageSummary,
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
results.push(result)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
fs.writeFileSync(options.outputFileName, JSON.stringify(results, null, 2))
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export const generateTeamCoverageCommand = program =>
|
|
120
|
-
program
|
|
121
|
-
.command('generate-team-coverage')
|
|
122
|
-
.description('Generate team-specific coverage reports')
|
|
123
|
-
.requiredOption(
|
|
124
|
-
'-r, --repository <repository_name>',
|
|
125
|
-
'specify repository name'
|
|
126
|
-
)
|
|
127
|
-
.requiredOption(
|
|
128
|
-
'-o, --output-file-name <output_file_name>',
|
|
129
|
-
'specify output file name'
|
|
130
|
-
)
|
|
131
|
-
.action(generateTeamCoverage)
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import { print } from '@toptal/davinci-cli-shared'
|
|
4
|
-
import getWorkspaceRoot from 'find-yarn-workspace-root'
|
|
5
|
-
|
|
6
|
-
import { CYPRESS_COVERAGE_PATH, JEST_COVERAGE_PATH } from './config.js'
|
|
7
|
-
import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
|
|
8
|
-
import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
|
|
9
|
-
|
|
10
|
-
const workspaceRoot = getWorkspaceRoot()
|
|
11
|
-
|
|
12
|
-
const readCoverageReport = reportPathName => {
|
|
13
|
-
const jestCoverageReportPath = path.join(workspaceRoot, reportPathName)
|
|
14
|
-
const data = fs.readFileSync(jestCoverageReportPath, 'utf8')
|
|
15
|
-
|
|
16
|
-
return JSON.parse(data)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const splitReportIntoTeams = ({ report, teams, codeowners, coveragePath }) => {
|
|
20
|
-
const teamsReport = {}
|
|
21
|
-
|
|
22
|
-
Object.entries(report).forEach(([reportPathKey, reportValue]) => {
|
|
23
|
-
const filePath = reportValue.path
|
|
24
|
-
const relativeWorkspacePath = path.relative(workspaceRoot, filePath)
|
|
25
|
-
const owners = codeowners.getOwner(relativeWorkspacePath)
|
|
26
|
-
|
|
27
|
-
owners.forEach(owner => {
|
|
28
|
-
teamsReport[owner] = {
|
|
29
|
-
...teamsReport[owner],
|
|
30
|
-
[reportPathKey]: reportValue,
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
teams.map(team => {
|
|
36
|
-
const teamReport = teamsReport[team] || {}
|
|
37
|
-
const sanitizedTeamPath = sanitizeTeamName(team)
|
|
38
|
-
|
|
39
|
-
const reportPath = path.join(
|
|
40
|
-
workspaceRoot,
|
|
41
|
-
coveragePath,
|
|
42
|
-
`coverage-final-${sanitizedTeamPath}.json`
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
fs.writeFileSync(reportPath, JSON.stringify(teamReport, null, 2))
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const groupCoverageByCodeowners = async () => {
|
|
50
|
-
print.header('Grouping coverage by codeowners')
|
|
51
|
-
|
|
52
|
-
const jestReport = readCoverageReport(
|
|
53
|
-
path.join(JEST_COVERAGE_PATH, 'coverage-final-jest.json')
|
|
54
|
-
)
|
|
55
|
-
const cypressReport = readCoverageReport(
|
|
56
|
-
path.join(CYPRESS_COVERAGE_PATH, 'coverage-final-cypress.json')
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
let codeowners
|
|
60
|
-
const { default: CodeOwnersTool } = await import(
|
|
61
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
62
|
-
'codeowners/codeowners.js'
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
codeowners = new CodeOwnersTool(workspaceRoot)
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.error(e.message)
|
|
69
|
-
process.exit(1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const teams = getUniqueTeams(codeowners.ownerEntries)
|
|
73
|
-
|
|
74
|
-
print.grey('Teams: ', teams)
|
|
75
|
-
|
|
76
|
-
splitReportIntoTeams({
|
|
77
|
-
report: jestReport,
|
|
78
|
-
teams,
|
|
79
|
-
codeowners,
|
|
80
|
-
coveragePath: JEST_COVERAGE_PATH,
|
|
81
|
-
})
|
|
82
|
-
splitReportIntoTeams({
|
|
83
|
-
report: cypressReport,
|
|
84
|
-
teams,
|
|
85
|
-
codeowners,
|
|
86
|
-
coveragePath: CYPRESS_COVERAGE_PATH,
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const groupCoverageByCodeownersCommand = program =>
|
|
91
|
-
program
|
|
92
|
-
.command('group-coverage-by-codeowners')
|
|
93
|
-
.description('Group the coverage data based on code owners')
|
|
94
|
-
.action(groupCoverageByCodeowners)
|