@toptal/davinci-monorepo 8.1.4-alpha-CRT-5891-create-coverage-command-3ad3dcfd.4 → 8.1.4-alpha-CRT-5891-create-coverage-command-0a4adac5.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-3ad3dcfd.4+3ad3dcfd",
3
+ "version": "8.1.4-alpha-CRT-5891-create-coverage-command-0a4adac5.7+0a4adac5",
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-3ad3dcfd.36+3ad3dcfd",
38
+ "@toptal/davinci-cli-shared": "2.3.1-alpha-CRT-5891-create-coverage-command-0a4adac5.39+0a4adac5",
39
39
  "chalk": "^4.1.2",
40
40
  "codeowners": "5.1.1",
41
41
  "commander": "^10.0.0",
@@ -55,5 +55,5 @@
55
55
  "devDependencies": {
56
56
  "@jest/globals": "^29.4.2"
57
57
  },
58
- "gitHead": "3ad3dcfdfe83b96377701fd4b2534f7c209ca320"
58
+ "gitHead": "0a4adac5650c666f07b97522a76b2f30dd604738"
59
59
  }
@@ -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
@@ -12,45 +12,55 @@ import {
12
12
  JEST_COVERAGE_PATH,
13
13
  TEAMS_COVERAGE_PATH,
14
14
  } from './config.js'
15
+ import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
15
16
 
16
17
  const workspaceRoot = getWorkspaceRoot()
17
18
 
18
19
  const sanitizeNycPercentage = percentage =>
19
20
  percentage === 'Unknown' ? 0 : percentage
20
21
 
22
+ const getCoverageSummary = (type, teamPath) => {
23
+ const result = {}
24
+
25
+ const coverageSummary = JSON.parse(
26
+ fs.readFileSync(`${teamPath}/${type}/coverage-summary.json`, 'utf8')
27
+ )
28
+
29
+ result[`${type}_lines_coverage`] = sanitizeNycPercentage(
30
+ coverageSummary.total.lines.pct
31
+ )
32
+ result[`${type}_statements_coverage`] = sanitizeNycPercentage(
33
+ coverageSummary.total.statements.pct
34
+ )
35
+ result[`${type}_functions_coverage`] = sanitizeNycPercentage(
36
+ coverageSummary.total.functions.pct
37
+ )
38
+ result[`${type}_branches_coverage`] = sanitizeNycPercentage(
39
+ coverageSummary.total.branches.pct
40
+ )
41
+
42
+ return result
43
+ }
44
+
21
45
  // eslint-disable-next-line max-statements
22
46
  const generateTeamCoverage = async options => {
23
47
  print.header('Generating team coverage')
24
48
 
25
49
  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)
32
-
33
- print.grey('Teams: ', uniqueTeams)
34
-
35
- const teams = uniqueTeams
50
+ const teams = getUniqueTeams(codeowners.ownerEntries)
36
51
 
37
52
  await execa.command(`rm -rf ${TEAMS_COVERAGE_PATH}`)
38
53
  await execa.command(`mkdir ${TEAMS_COVERAGE_PATH}`)
39
54
 
40
55
  for await (const team of teams) {
56
+ print.grey('Team coverage: ', team)
57
+
41
58
  const sanitizedTeam = sanitizeTeamName(team)
42
59
  const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
43
60
 
44
61
  await execa.command(`mkdir ${sanitizedTeamPath}`)
45
62
  await execa.command(`mkdir ${sanitizedTeamPath}/jest`)
46
63
  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
64
 
55
65
  await execa.command(
56
66
  `cp ${JEST_COVERAGE_PATH}/coverage-final-${sanitizedTeam}.json ${sanitizedTeamPath}/jest/coverage-final.json`
@@ -74,49 +84,20 @@ const generateTeamCoverage = async options => {
74
84
  const sanitizedTeam = sanitizeTeamName(team)
75
85
  const sanitizedTeamPath = path.join(TEAMS_COVERAGE_PATH, sanitizedTeam)
76
86
 
87
+ const jestCoverageSummary = getCoverageSummary('jest', sanitizedTeamPath)
88
+ const cypressCoverageSummary = getCoverageSummary(
89
+ 'cypress',
90
+ sanitizedTeamPath
91
+ )
92
+
77
93
  const result = {
78
94
  created_at,
79
95
  team,
80
96
  repo: options.repository,
97
+ ...jestCoverageSummary,
98
+ ...cypressCoverageSummary,
81
99
  }
82
100
 
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
101
  results.push(result)
121
102
  }
122
103
 
@@ -126,7 +107,7 @@ const generateTeamCoverage = async options => {
126
107
  export const generateTeamCoverageCommand = program =>
127
108
  program
128
109
  .command('generate-team-coverage')
129
- .description('TODO: generate-team-coverage')
110
+ .description('Generate team-specific coverage reports')
130
111
  .requiredOption(
131
112
  '-r, --repository <repository_name>',
132
113
  'specify repository name'
@@ -7,51 +7,29 @@ import getWorkspaceRoot from 'find-yarn-workspace-root'
7
7
  import Codeowners from '../codeowners/core/codeowners/codeowners.js'
8
8
  import { CYPRESS_COVERAGE_PATH, JEST_COVERAGE_PATH } from './config.js'
9
9
  import { sanitizeTeamName } from './services/sanitize-team-name/sanitize-team-name.js'
10
+ import { getUniqueTeams } from './services/get-unique-teams/get-unique-teams.js'
10
11
 
11
12
  const workspaceRoot = getWorkspaceRoot()
12
13
 
13
- const readJestCoverageReport = () => {
14
- const jestCoverageReportPath = path.join(
15
- workspaceRoot,
16
- JEST_COVERAGE_PATH,
17
- 'coverage-final-jest.json'
18
- )
19
-
14
+ const readCoverageReport = reportPathName => {
15
+ const jestCoverageReportPath = path.join(workspaceRoot, reportPathName)
20
16
  const data = fs.readFileSync(jestCoverageReportPath, 'utf8')
21
17
 
22
18
  return JSON.parse(data)
23
19
  }
24
20
 
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
- const splitReportIntoTeams = ({
38
- reportObj,
39
- teams,
40
- codeowners,
41
- coveragePath,
42
- }) => {
21
+ const splitReportIntoTeams = ({ report, teams, codeowners, coveragePath }) => {
43
22
  const teamsReport = {}
44
23
 
45
- Object.entries(reportObj).forEach(([reportPathKey, report]) => {
46
- const file = report.path
47
- const relativeWorkspacePath = path.relative(workspaceRoot, file)
48
-
24
+ Object.entries(report).forEach(([reportPathKey, reportValue]) => {
25
+ const filePath = reportValue.path
26
+ const relativeWorkspacePath = path.relative(workspaceRoot, filePath)
49
27
  const owners = codeowners.getOwner(relativeWorkspacePath)
50
28
 
51
29
  owners.forEach(owner => {
52
30
  teamsReport[owner] = {
53
31
  ...teamsReport[owner],
54
- [reportPathKey]: report,
32
+ [reportPathKey]: reportValue,
55
33
  }
56
34
  })
57
35
  })
@@ -70,26 +48,18 @@ const splitReportIntoTeams = ({
70
48
  })
71
49
  }
72
50
 
73
- const getAllTeams = codeowners => {
74
- const ownerEntries = codeowners.ownerEntries
75
- const uniqueTeams = ownerEntries
76
- .flatMap(entry => entry.usernames)
77
- // eslint-disable-next-line id-length
78
- .filter((v, i, a) => a.indexOf(v) === i)
79
-
80
- print.grey('Teams: ', uniqueTeams)
81
-
82
- return uniqueTeams
83
- }
84
-
85
51
  const groupCoverageByCodeowners = async () => {
86
52
  print.header('Grouping coverage by codeowners')
87
53
 
88
- const jestReport = readJestCoverageReport()
89
- const cypressReport = readCypressCoverageReport()
54
+ const jestReport = readCoverageReport(
55
+ path.join(JEST_COVERAGE_PATH, 'coverage-final-jest.json')
56
+ )
57
+ const cypressReport = readCoverageReport(
58
+ path.join(CYPRESS_COVERAGE_PATH, 'coverage-final-cypress.json')
59
+ )
90
60
 
91
61
  const codeowners = new Codeowners(workspaceRoot)
92
- const teams = getAllTeams(codeowners)
62
+ const teams = getUniqueTeams(codeowners.ownerEntries)
93
63
 
94
64
  splitReportIntoTeams({
95
65
  report: jestReport,
@@ -108,5 +78,5 @@ const groupCoverageByCodeowners = async () => {
108
78
  export const groupCoverageByCodeownersCommand = program =>
109
79
  program
110
80
  .command('group-coverage-by-codeowners')
111
- .description('TODO: group-coverage-by-codeowners')
81
+ .description('Group the coverage data based on code owners')
112
82
  .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,11 @@
1
+ import { print } from '@toptal/davinci-cli-shared'
2
+
3
+ export const getUniqueTeams = teams => {
4
+ const uniqueTeams = teams
5
+ .flatMap(entry => entry.usernames)
6
+ .filter((teamName, index, allTeams) => allTeams.indexOf(teamName) === index)
7
+
8
+ print.grey('Teams: ', uniqueTeams)
9
+
10
+ return uniqueTeams
11
+ }
@@ -0,0 +1,40 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import { jest } from '@jest/globals'
3
+
4
+ import { getUniqueTeams } from './get-unique-teams'
5
+
6
+ jest.unstable_mockModule('@toptal/davinci-cli-shared', () => ({
7
+ print: {
8
+ grey: jest.fn(),
9
+ },
10
+ }))
11
+
12
+ const { print } = await import('@toptal/davinci-cli-shared')
13
+
14
+ describe('getUniqueTeams', () => {
15
+ beforeEach(() => {
16
+ print.grey.mockImplementation(jest.fn())
17
+ })
18
+
19
+ it.each([
20
+ [
21
+ [
22
+ { usernames: ['team1', 'team2'] },
23
+ { usernames: ['team3', 'team4'] },
24
+ { usernames: ['team1', 'team5'] },
25
+ ],
26
+ ['team1', 'team2', 'team3', 'team4', 'team5'],
27
+ ],
28
+ [
29
+ [
30
+ { usernames: ['team1', 'team2', 'team2', 'team3'] },
31
+ { usernames: ['team4', 'team1', 'team4', 'team5'] },
32
+ ],
33
+ ['team1', 'team2', 'team3', 'team4', 'team5'],
34
+ ],
35
+ ])('returns the unique teams for the given input', (teams, expected) => {
36
+ const result = getUniqueTeams(teams)
37
+
38
+ expect(result).toEqual(expected)
39
+ })
40
+ })
@@ -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