-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
QD-10723 Test summary and comment for azure pipelines
- Loading branch information
Andrei Iurko
committed
Jan 23, 2025
1 parent
79afc8e
commit 1a63fb8
Showing
12 changed files
with
65,965 additions
and
769 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright 2021-2024 JetBrains s.r.o. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export const FAILURE_LEVEL = 'failure' | ||
export const WARNING_LEVEL = 'warning' | ||
export const NOTICE_LEVEL = 'notice' | ||
|
||
export interface Annotation { | ||
title: string | undefined | ||
path: string | ||
start_line: number | ||
end_line: number | ||
level: 'failure' | 'warning' | 'notice' | ||
message: string | ||
start_column: number | undefined | ||
end_column: number | undefined | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
/* | ||
* Copyright 2021-2024 JetBrains s.r.o. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import {Coverage, QODANA_OPEN_IN_IDE_NAME, QODANA_REPORT_URL_NAME, VERSION} from "./qodana"; | ||
import * as fs from "fs"; | ||
import {Annotation, FAILURE_LEVEL, NOTICE_LEVEL, WARNING_LEVEL} from "./annotations"; | ||
|
||
export const QODANA_CHECK_NAME = 'Qodana' | ||
const UNKNOWN_RULE_ID = 'Unknown' | ||
const SUMMARY_TABLE_HEADER = '| Inspection name | Severity | Problems |' | ||
const SUMMARY_TABLE_SEP = '| --- | --- | --- |' | ||
const SUMMARY_MISC = `Contact us at [[email protected]](mailto:[email protected]) | ||
- Or via our issue tracker: https://jb.gg/qodana-issue | ||
- Or share your feedback: https://jb.gg/qodana-discussions` | ||
const VIEW_REPORT_OPTIONS = `To be able to view the detailed Qodana report, you can either: | ||
- Register at [Qodana Cloud](https://qodana.cloud/) and [configure the action](https://github.com/jetbrains/qodana-action#qodana-cloud) | ||
- Use [GitHub Code Scanning with Qodana](https://github.com/jetbrains/qodana-action#github-code-scanning) | ||
- Host [Qodana report at GitHub Pages](https://github.com/JetBrains/qodana-action/blob/3a8e25f5caad8d8b01c1435f1ef7b19fe8b039a0/README.md#github-pages) | ||
- Inspect and use \`qodana.sarif.json\` (see [the Qodana SARIF format](https://www.jetbrains.com/help/qodana/qodana-sarif-output.html#Report+structure) for details) | ||
To get \`*.log\` files or any other Qodana artifacts, run the action with \`upload-result\` option set to \`true\`, | ||
so that the action will upload the files as the job artifacts: | ||
\`\`\`yaml | ||
- name: 'Qodana Scan' | ||
uses: JetBrains/qodana-action@v${VERSION} | ||
with: | ||
upload-result: true | ||
\`\`\` | ||
` | ||
const SUMMARY_PR_MODE = `💡 Qodana analysis was run in the pull request mode: only the changed files were checked` | ||
|
||
interface CloudData { | ||
url?: string | ||
} | ||
|
||
interface OpenInIDEData { | ||
cloud?: CloudData | ||
} | ||
|
||
export interface LicenseEntry { | ||
name?: string | ||
version?: string | ||
license?: string | ||
} | ||
|
||
function wrapToDiffBlock(message: string): string { | ||
return `\`\`\`diff | ||
${message} | ||
\`\`\`` | ||
} | ||
|
||
export function getCoverageStats(c: Coverage): string { | ||
if (c.totalLines === 0 && c.totalCoveredLines === 0) { | ||
return '' | ||
} | ||
|
||
let stats = '' | ||
if (c.totalLines !== 0) { | ||
let conclusion = `${c.totalCoverage}% total lines covered` | ||
if (c.totalCoverage < c.totalCoverageThreshold) { | ||
conclusion = `- ${conclusion}` | ||
} else { | ||
conclusion = `+ ${conclusion}` | ||
} | ||
stats += `${conclusion} | ||
${c.totalLines} lines analyzed, ${c.totalCoveredLines} lines covered` | ||
} | ||
|
||
if (c.freshLines !== 0) { | ||
stats += ` | ||
! ${c.freshCoverage}% fresh lines covered | ||
${c.freshLines} lines analyzed, ${c.freshCoveredLines} lines covered` | ||
} | ||
|
||
return wrapToDiffBlock( | ||
[ | ||
`@@ Code coverage @@`, | ||
`${stats}`, | ||
`# Calculated according to the filters of your coverage tool` | ||
].join('\n') | ||
) | ||
} | ||
|
||
export function getReportURL(resultsDir: string): string { | ||
let reportUrlFile = `${resultsDir}/${QODANA_OPEN_IN_IDE_NAME}` | ||
if (fs.existsSync(reportUrlFile)) { | ||
const rawData = fs.readFileSync(reportUrlFile, {encoding: 'utf8'}) | ||
const data = JSON.parse(rawData) as OpenInIDEData | ||
if (data?.cloud?.url) { | ||
return data.cloud.url | ||
} | ||
} else { | ||
reportUrlFile = `${resultsDir}/${QODANA_REPORT_URL_NAME}` | ||
if (fs.existsSync(reportUrlFile)) { | ||
return fs.readFileSync(reportUrlFile, {encoding: 'utf8'}) | ||
} | ||
} | ||
return '' | ||
} | ||
|
||
function wrapToToggleBlock(header: string, body: string): string { | ||
return `<details> | ||
<summary>${header}</summary> | ||
${body} | ||
</details>` | ||
} | ||
|
||
function getViewReportText(reportUrl: string): string { | ||
if (reportUrl !== '') { | ||
return `☁️ [View the detailed Qodana report](${reportUrl})` | ||
} | ||
return wrapToToggleBlock( | ||
'View the detailed Qodana report', | ||
VIEW_REPORT_OPTIONS | ||
) | ||
} | ||
|
||
/** | ||
* Generates a table row for a given level. | ||
* @param annotations The annotations to generate the table row from. | ||
* @param level The level to generate the table row for. | ||
*/ | ||
function getRowsByLevel(annotations: Annotation[], level: string): string { | ||
const problems = annotations.reduce( | ||
(map: Map<string, number>, e) => | ||
map.set( | ||
e.title ?? UNKNOWN_RULE_ID, | ||
map.get(e.title ?? UNKNOWN_RULE_ID) !== undefined | ||
? map.get(e.title ?? UNKNOWN_RULE_ID)! + 1 | ||
: 1 | ||
), | ||
new Map() | ||
) | ||
return Array.from(problems.entries()) | ||
.sort((a, b) => b[1] - a[1]) | ||
.map(([title, count]) => `| \`${title}\` | ${level} | ${count} |`) | ||
.join('\n') | ||
} | ||
|
||
/** | ||
* Generates action summary string of annotations. | ||
* @param toolName The name of the tool to generate the summary from. | ||
* @param projectDir The path to the project. | ||
* @param annotations The annotations to generate the summary from. | ||
* @param coverageInfo The coverage is a Markdown text to generate the summary from. | ||
* @param packages The number of dependencies in the analyzed project. | ||
* @param licensesInfo The licenses a Markdown text to generate the summary from. | ||
* @param reportUrl The URL to the Qodana report. | ||
* @param prMode Whether the analysis was run in the pull request mode. | ||
* @param dependencyCharsLimit Limit on how many characters can be included in comment | ||
*/ | ||
export function getSummary( | ||
toolName: string, | ||
projectDir: string, | ||
annotations: Annotation[], | ||
coverageInfo: string, | ||
packages: number, | ||
licensesInfo: string, | ||
reportUrl: string, | ||
prMode: boolean, | ||
dependencyCharsLimit: number | ||
): string { | ||
const contactBlock = wrapToToggleBlock('Contact Qodana team', SUMMARY_MISC) | ||
let licensesBlock = '' | ||
if (licensesInfo !== '' && licensesInfo.length < dependencyCharsLimit) { | ||
licensesBlock = wrapToToggleBlock( | ||
`Detected ${packages} ${getDepencencyPlural(packages)}`, | ||
licensesInfo | ||
) | ||
} | ||
let prModeBlock = '' | ||
if (prMode) { | ||
prModeBlock = SUMMARY_PR_MODE | ||
} | ||
if (reportUrl !== '') { | ||
const firstToolName = toolName.split(' ')[0] | ||
toolName = toolName.replace( | ||
firstToolName, | ||
`[${firstToolName}](${reportUrl})` | ||
) | ||
} | ||
if (annotations.length === 0) { | ||
return [ | ||
`# ${toolName}`, | ||
projectDir === '' ? '' : ['`', projectDir, '/`\n'].join(''), | ||
'**It seems all right 👌**', | ||
'', | ||
'No new problems were found according to the checks applied', | ||
coverageInfo, | ||
prModeBlock, | ||
getViewReportText(reportUrl), | ||
licensesBlock, | ||
contactBlock | ||
].join('\n') | ||
} | ||
|
||
return [ | ||
`# ${toolName}`, | ||
projectDir === '' ? '' : ['`', projectDir, '/`\n'].join(''), | ||
`**${annotations.length} ${getProblemPlural( | ||
annotations.length | ||
)}** were found`, | ||
'', | ||
SUMMARY_TABLE_HEADER, | ||
SUMMARY_TABLE_SEP, | ||
[ | ||
getRowsByLevel( | ||
annotations.filter(a => a.level === FAILURE_LEVEL), | ||
'🔴 Failure' | ||
), | ||
getRowsByLevel( | ||
annotations.filter(a => a.level === WARNING_LEVEL), | ||
'🔶 Warning' | ||
), | ||
getRowsByLevel( | ||
annotations.filter(a => a.level === NOTICE_LEVEL), | ||
'◽️ Notice' | ||
) | ||
] | ||
.filter(e => e !== '') | ||
.join('\n'), | ||
'', | ||
coverageInfo, | ||
prModeBlock, | ||
getViewReportText(reportUrl), | ||
licensesBlock, | ||
contactBlock | ||
].join('\n') | ||
} | ||
|
||
/** | ||
* Generates a plural form of the word "problem" depending on the given count. | ||
* @param count A number representing the count of problems | ||
* @returns A formatted string with the correct plural form of "problem" | ||
*/ | ||
export function getProblemPlural(count: number): string { | ||
return `new problem${count !== 1 ? 's' : ''}` | ||
} | ||
|
||
/** | ||
* Generates a plural form of the word "dependency" depending on the given count. | ||
* @param count A number representing the count of dependencies | ||
* @returns A formatted string with the correct plural form of "dependency" | ||
*/ | ||
export function getDepencencyPlural(count: number): string { | ||
return `dependenc${count !== 1 ? 'ies' : 'y'}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/* | ||
* Copyright 2021-2024 JetBrains s.r.o. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import type {Tool} from 'sarif' | ||
|
||
export interface Rule { | ||
shortDescription: string | ||
fullDescription: string | ||
} | ||
|
||
/** | ||
* Extracts the rules descriptions from SARIF tool field. | ||
* @param tool the SARIF tool field. | ||
* @returns The map of SARIF rule IDs to their descriptions. | ||
*/ | ||
export function parseRules(tool: Tool): Map<string, Rule> { | ||
const rules = new Map<string, Rule>() | ||
tool.driver.rules?.forEach(rule => { | ||
rules.set(rule.id, { | ||
shortDescription: rule.shortDescription!.text, | ||
fullDescription: | ||
rule.fullDescription!.markdown || rule.fullDescription!.text | ||
}) | ||
}) | ||
|
||
tool?.extensions?.forEach(ext => { | ||
ext?.rules?.forEach(rule => { | ||
rules.set(rule.id, { | ||
shortDescription: rule.shortDescription!.text, | ||
fullDescription: | ||
rule.fullDescription!.markdown || rule.fullDescription!.text | ||
}) | ||
}) | ||
}) | ||
return rules | ||
} | ||
|
||
export function hashCode(str: string): number { | ||
let hash = 0; | ||
for (let i = 0; i < str.length; i++) { | ||
hash = ((hash << 5) - hash) + str.charCodeAt(i); | ||
hash |= 0; | ||
} | ||
return hash; | ||
} |
Oops, something went wrong.