@toptal/davinci-monorepo 8.1.4-alpha-CRT-5891-create-coverage-command-39ac7ea2.4 → 8.1.4-alpha-CRT-5891-create-coverage-command-e6e5bd51.7

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/README.md CHANGED
@@ -12,3 +12,4 @@ Use it by installing `yarn add @toptal/davinci-monorepo` in your project.
12
12
  - [`metrics -m, --metric package-afferent-relations`](./docs/metrics.md): calculate package Afferent relations
13
13
  - [`graph generate`](./docs/graph-generate.md): generate dependency graph into `monorepo-graph.svg` file
14
14
  - [`codeowners`](./docs/codeowners.md): show list of tools available for code ownership analysis
15
+ - [`coverage`](./docs/coverage.md): show list of code coverage commands
@@ -0,0 +1,25 @@
1
+ ### Code coverage commands
2
+
3
+ Streamline code coverage analysis and reporting by organizing coverage reports based on code owners and generating team-specific coverage reports, enabling teams to track and assess code quality and testing coverage more effectively.
4
+
5
+ #### `group-coverage-by-codeowners`
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
+ ```sh
12
+ codeowners group-coverage-by-codeowners
13
+ ```
14
+
15
+ #### `generage-team-coverage`
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.
18
+
19
+ ```sh
20
+ coverage generage-team-coverage [options]
21
+
22
+ Options:
23
+ -r, --repository <repository_name> specify repository name
24
+ -o, --output-file-name <output_file_name> specify output file name
25
+ ```
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-39ac7ea2.4+39ac7ea2",
3
+ "version": "8.1.4-alpha-CRT-5891-create-coverage-command-e6e5bd51.7+e6e5bd51",
4
4
  "description": "Monorepo utility tools",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,10 +35,9 @@
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-39ac7ea2.36+39ac7ea2",
38
+ "@toptal/davinci-cli-shared": "2.3.1-alpha-CRT-5891-create-coverage-command-e6e5bd51.39+e6e5bd51",
39
39
  "chalk": "^4.1.2",
40
40
  "codeowners": "5.1.1",
41
- "commander": "^10.0.0",
42
41
  "dependency-cruiser": "^12.5.0",
43
42
  "ervy": "^1.0.7",
44
43
  "execa": "^5.1.1",
@@ -55,5 +54,5 @@
55
54
  "devDependencies": {
56
55
  "@jest/globals": "^29.4.2"
57
56
  },
58
- "gitHead": "39ac7ea22e96507f6d67541bf2f17d2c76701e89"
57
+ "gitHead": "e6e5bd5114c669c12d4b245907649d617b9fdd96"
59
58
  }
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-param-reassign, id-length*/
2
2
  import { NO_OWNER } from '../../config.js'
3
3
  import formatOwnerName from '../utils/format-owner-name.js'
4
- import getPercentage from '../utils/get-persentage.js'
4
+ import { getPercentage } from '../utils/get-percentage.js'
5
5
  import totalFilesComparator from '../utils/total-files-comparator.js'
6
6
 
7
7
  export const getSingleOwnerSummary = (
@@ -0,0 +1,2 @@
1
+ export const getPercentage = (fraction, total) =>
2
+ `${((fraction / total) * 100).toFixed(2)}%`
@@ -5,7 +5,7 @@ import listCommand from './commands/list.js'
5
5
  import ownersInformationCommand from './commands/owners-information.js'
6
6
  import packagesInformationCommand from './commands/packages-information.js'
7
7
 
8
- const codeowners = program => {
8
+ const codeownersCommand = program => {
9
9
  const codeowners = program
10
10
  .createCommand('codeowners')
11
11
  .description(
@@ -22,4 +22,4 @@ const codeowners = program => {
22
22
  return codeowners
23
23
  }
24
24
 
25
- export default codeowners
25
+ export default codeownersCommand
@@ -4,53 +4,75 @@ import fs from 'fs'
4
4
  import { print } from '@toptal/davinci-cli-shared'
5
5
  import getWorkspaceRoot from 'find-yarn-workspace-root'
6
6
 
7
- // eslint-disable-next-line no-restricted-syntax
8
- import Codeowners from '../codeowners/core/codeowners/codeowners.js'
9
7
  import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
10
8
  import {
11
9
  CYPRESS_COVERAGE_PATH,
12
10
  JEST_COVERAGE_PATH,
13
11
  TEAMS_COVERAGE_PATH,
14
12
  } from './config.js'
13
+ import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
15
14
 
16
15
  const workspaceRoot = getWorkspaceRoot()
17
16
 
18
17
  const sanitizeNycPercentage = percentage =>
19
18
  percentage === 'Unknown' ? 0 : percentage
20
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
+
21
43
  // eslint-disable-next-line max-statements
22
44
  const generateTeamCoverage = async options => {
23
45
  print.header('Generating team coverage')
24
46
 
25
- const codeowners = new Codeowners(workspaceRoot)
26
-
27
- const ownerEntries = codeowners.ownerEntries
28
- const uniqueTeams = ownerEntries
29
- .flatMap(entry => entry.usernames)
30
- // eslint-disable-next-line id-length
31
- .filter((v, i, a) => a.indexOf(v) === i)
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
+ }
32
59
 
33
- print.grey('Teams: ', uniqueTeams)
60
+ const teams = getUniqueTeams(codeowners.ownerEntries)
34
61
 
35
- const teams = uniqueTeams
62
+ print.grey('Teams: ', teams)
36
63
 
37
64
  await execa.command(`rm -rf ${TEAMS_COVERAGE_PATH}`)
38
65
  await execa.command(`mkdir ${TEAMS_COVERAGE_PATH}`)
39
66
 
40
67
  for await (const team of teams) {
68
+ print.grey('Team coverage: ', team)
69
+
41
70
  const sanitizedTeam = sanitizeTeamName(team)
42
71
  const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
43
72
 
44
73
  await execa.command(`mkdir ${sanitizedTeamPath}`)
45
74
  await execa.command(`mkdir ${sanitizedTeamPath}/jest`)
46
75
  await execa.command(`mkdir ${sanitizedTeamPath}/cypress`)
47
- }
48
-
49
- for await (const team of teams) {
50
- print.grey('Team coverage: ', team)
51
-
52
- const sanitizedTeam = sanitizeTeamName(team)
53
- const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
54
76
 
55
77
  await execa.command(
56
78
  `cp ${JEST_COVERAGE_PATH}/coverage-final-${sanitizedTeam}.json ${sanitizedTeamPath}/jest/coverage-final.json`
@@ -74,49 +96,20 @@ const generateTeamCoverage = async options => {
74
96
  const sanitizedTeam = sanitizeTeamName(team)
75
97
  const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
76
98
 
99
+ const jestCoverageSummary = getCoverageSummary('jest', sanitizedTeamPath)
100
+ const cypressCoverageSummary = getCoverageSummary(
101
+ 'cypress',
102
+ sanitizedTeamPath
103
+ )
104
+
77
105
  const result = {
78
106
  created_at,
79
107
  team,
80
108
  repo: options.repository,
109
+ ...jestCoverageSummary,
110
+ ...cypressCoverageSummary,
81
111
  }
82
112
 
83
- const jestCoverageSummary = JSON.parse(
84
- fs.readFileSync(`${sanitizedTeamPath}/jest/coverage-summary.json`, 'utf8')
85
- )
86
-
87
- result.jest_lines_coverage = sanitizeNycPercentage(
88
- jestCoverageSummary.total.lines.pct
89
- )
90
- result.jest_statements_coverage = sanitizeNycPercentage(
91
- jestCoverageSummary.total.statements.pct
92
- )
93
- result.jest_functions_coverage = sanitizeNycPercentage(
94
- jestCoverageSummary.total.functions.pct
95
- )
96
- result.jest_branches_coverage = sanitizeNycPercentage(
97
- jestCoverageSummary.total.branches.pct
98
- )
99
-
100
- const cypressCoverageSummary = JSON.parse(
101
- fs.readFileSync(
102
- `${sanitizedTeamPath}/cypress/coverage-summary.json`,
103
- 'utf8'
104
- )
105
- )
106
-
107
- result.cypress_lines_coverage = sanitizeNycPercentage(
108
- cypressCoverageSummary.total.lines.pct
109
- )
110
- result.cypress_statements_coverage = sanitizeNycPercentage(
111
- cypressCoverageSummary.total.statements.pct
112
- )
113
- result.cypress_functions_coverage = sanitizeNycPercentage(
114
- cypressCoverageSummary.total.functions.pct
115
- )
116
- result.cypress_branches_coverage = sanitizeNycPercentage(
117
- cypressCoverageSummary.total.branches.pct
118
- )
119
-
120
113
  results.push(result)
121
114
  }
122
115
 
@@ -126,7 +119,7 @@ const generateTeamCoverage = async options => {
126
119
  export const generateTeamCoverageCommand = program =>
127
120
  program
128
121
  .command('generate-team-coverage')
129
- .description('TODO: generate-team-coverage')
122
+ .description('Generate team-specific coverage reports')
130
123
  .requiredOption(
131
124
  '-r, --repository <repository_name>',
132
125
  'specify repository name'
@@ -3,44 +3,25 @@ import fs from 'fs'
3
3
  import { print } from '@toptal/davinci-cli-shared'
4
4
  import getWorkspaceRoot from 'find-yarn-workspace-root'
5
5
 
6
- // eslint-disable-next-line no-restricted-syntax
7
- import Codeowners from '../codeowners/core/codeowners/codeowners.js'
8
6
  import { CYPRESS_COVERAGE_PATH, JEST_COVERAGE_PATH } from './config.js'
9
7
  import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
8
+ import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
10
9
 
11
10
  const workspaceRoot = getWorkspaceRoot()
12
11
 
13
- const readJestCoverageReport = () => {
14
- const jestCoverageReportPath = path.join(
15
- workspaceRoot,
16
- JEST_COVERAGE_PATH,
17
- 'coverage-final-jest.json'
18
- )
19
-
12
+ const readCoverageReport = reportPathName => {
13
+ const jestCoverageReportPath = path.join(workspaceRoot, reportPathName)
20
14
  const data = fs.readFileSync(jestCoverageReportPath, 'utf8')
21
15
 
22
16
  return JSON.parse(data)
23
17
  }
24
18
 
25
- const readCypressCoverageReport = () => {
26
- const cypressCoverageReportPath = path.join(
27
- workspaceRoot,
28
- CYPRESS_COVERAGE_PATH,
29
- 'coverage-final-cypress.json'
30
- )
31
-
32
- const data = fs.readFileSync(cypressCoverageReportPath, 'utf8')
33
-
34
- return JSON.parse(data)
35
- }
36
-
37
19
  const splitReportIntoTeams = ({ report, teams, codeowners, coveragePath }) => {
38
20
  const teamsReport = {}
39
21
 
40
22
  Object.entries(report).forEach(([reportPathKey, reportValue]) => {
41
23
  const filePath = reportValue.path
42
24
  const relativeWorkspacePath = path.relative(workspaceRoot, filePath)
43
-
44
25
  const owners = codeowners.getOwner(relativeWorkspacePath)
45
26
 
46
27
  owners.forEach(owner => {
@@ -65,26 +46,32 @@ const splitReportIntoTeams = ({ report, teams, codeowners, coveragePath }) => {
65
46
  })
66
47
  }
67
48
 
68
- const getAllTeams = codeowners => {
69
- const ownerEntries = codeowners.ownerEntries
70
- const uniqueTeams = ownerEntries
71
- .flatMap(entry => entry.usernames)
72
- // eslint-disable-next-line id-length
73
- .filter((v, i, a) => a.indexOf(v) === i)
49
+ const groupCoverageByCodeowners = async () => {
50
+ print.header('Grouping coverage by codeowners')
74
51
 
75
- print.grey('Teams: ', uniqueTeams)
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
+ )
76
58
 
77
- return uniqueTeams
78
- }
59
+ let codeowners
60
+ const { default: CodeOwnersTool } = await import(
61
+ // eslint-disable-next-line no-restricted-syntax
62
+ 'codeowners/codeowners.js'
63
+ )
79
64
 
80
- const groupCoverageByCodeowners = async () => {
81
- print.header('Grouping coverage by codeowners')
65
+ try {
66
+ codeowners = new CodeOwnersTool(workspaceRoot)
67
+ } catch (e) {
68
+ console.error(e.message)
69
+ process.exit(1)
70
+ }
82
71
 
83
- const jestReport = readJestCoverageReport()
84
- const cypressReport = readCypressCoverageReport()
72
+ const teams = getUniqueTeams(codeowners.ownerEntries)
85
73
 
86
- const codeowners = new Codeowners(workspaceRoot)
87
- const teams = getAllTeams(codeowners)
74
+ print.grey('Teams: ', teams)
88
75
 
89
76
  splitReportIntoTeams({
90
77
  report: jestReport,
@@ -103,5 +90,5 @@ const groupCoverageByCodeowners = async () => {
103
90
  export const groupCoverageByCodeownersCommand = program =>
104
91
  program
105
92
  .command('group-coverage-by-codeowners')
106
- .description('TODO: group-coverage-by-codeowners')
93
+ .description('Group the coverage data based on code owners')
107
94
  .action(groupCoverageByCodeowners)
@@ -2,7 +2,11 @@ import { generateTeamCoverageCommand } from './generate-team-coverage.js'
2
2
  import { groupCoverageByCodeownersCommand } from './group-coverage-by-codeowners.js'
3
3
 
4
4
  export const coverageCommand = program => {
5
- const coverage = program.createCommand('coverage').description('TODO ..')
5
+ const coverage = program
6
+ .createCommand('coverage')
7
+ .description(
8
+ 'Provides code coverage analysis, including organizing coverage reports by code owners and generating team-specific reports'
9
+ )
6
10
 
7
11
  groupCoverageByCodeownersCommand(coverage)
8
12
  generateTeamCoverageCommand(coverage)
@@ -0,0 +1,4 @@
1
+ export const getUniqueTeams = teams =>
2
+ teams
3
+ .flatMap(entry => entry.usernames)
4
+ .filter((teamName, index, allTeams) => allTeams.indexOf(teamName) === index)
@@ -0,0 +1,25 @@
1
+ import { getUniqueTeams } from './get-unique-teams'
2
+
3
+ describe('getUniqueTeams', () => {
4
+ it.each([
5
+ [
6
+ [
7
+ { usernames: ['team1', 'team2'] },
8
+ { usernames: ['team3', 'team4'] },
9
+ { usernames: ['team1', 'team5'] },
10
+ ],
11
+ ['team1', 'team2', 'team3', 'team4', 'team5'],
12
+ ],
13
+ [
14
+ [
15
+ { usernames: ['team1', 'team2', 'team2', 'team3'] },
16
+ { usernames: ['team4', 'team1', 'team4', 'team5'] },
17
+ ],
18
+ ['team1', 'team2', 'team3', 'team4', 'team5'],
19
+ ],
20
+ ])('returns the unique teams for the given input', (teams, expected) => {
21
+ const result = getUniqueTeams(teams)
22
+
23
+ expect(result).toEqual(expected)
24
+ })
25
+ })
@@ -0,0 +1,13 @@
1
+ import { sanitizeTeamName } from './sanitize-team-name'
2
+
3
+ describe('sanitizeTeamName', () => {
4
+ it.each([
5
+ ['@package/frontend-experience-eng', 'package-frontend-experience-eng'],
6
+ ['@package/screening-ops-eng', 'package-screening-ops-eng'],
7
+ ['@package/client-retention-eng', 'package-client-retention-eng'],
8
+ ])('sanitizes the team name correctly', (input, expected) => {
9
+ const result = sanitizeTeamName(input)
10
+
11
+ expect(result).toBe(expected)
12
+ })
13
+ })
@@ -1,5 +0,0 @@
1
- const getPercentage = (fraction, total) => {
2
- return `${((fraction / total) * 100).toFixed(2)}%`
3
- }
4
-
5
- export default getPercentage