Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,063 changes: 629 additions & 434 deletions dist/index.js

Large diffs are not rendered by default.

3,257 changes: 1,911 additions & 1,346 deletions package-lock.json

Large diffs are not rendered by default.

89 changes: 79 additions & 10 deletions src/action/decorate/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { generateComment, generateExpandableAreaMarkdown, generateItalic, genera
import { githubConfig, ticsConfig } from '../../configuration/config';
import { getCurrentStepPath } from '../../github/runs';
import { GroupedConditions } from './interface';
import { AbstractCondition, Condition, ConditionDetails, ExtendedAnnotation } from '../../viewer/interfaces';
import { AbstractCondition, Condition, ConditionDetails, ExtendedAnnotation, LabelInfo } from '../../viewer/interfaces';
import { ViewerFeature, viewerVersion } from '../../viewer/version';

const capitalize = (s: string): string => s && s[0].toUpperCase() + s.slice(1);
Expand All @@ -23,14 +23,20 @@ export async function createSummaryBody(analysisResult: AnalysisResult): Promise
const groupedConditions = groupConditions(projectResult);
summary.addHeading(projectResult.project, 2);

for (const group of groupedConditions) {
if (await viewerVersion.viewerSupports(ViewerFeature.NEW_ANNOTATIONS)) {
newConditionsView(group);
} else {
oldConditionsView(group);
if (groupedConditions.length > 0) {
summary.addRaw(`<details><summary><h3>Evaluated Conditions</h3></summary>`, true);
for (const group of groupedConditions) {
if (await viewerVersion.viewerSupports(ViewerFeature.NEW_ANNOTATIONS)) {
newConditionsView(group);
} else {
oldConditionsView(group);
}
}
summary.addRaw('</details>', true);
}

summary.addEOL();
summary.addRaw(createQiLabelTable(projectResult.labelInfo), true);
summary.addEOL();
summary.addLink('See the results in the TICS Viewer', projectResult.explorerUrl);

Expand All @@ -50,7 +56,7 @@ export async function createSummaryBody(analysisResult: AnalysisResult): Promise
}

function newConditionsView(group: GroupedConditions): void {
summary.addRaw(`<details><summary><h3>${getConditionHeading(group)}</h3></summary>`, true);
summary.addRaw(`<h4>${getConditionHeading(group)}</h4>`, true);

for (const condition of group.conditions) {
const statusMarkdown = generateStatusMarkdown(getStatus(condition.passed, condition.passedWithWarning));
Expand All @@ -61,7 +67,6 @@ function newConditionsView(group: GroupedConditions): void {
summary.addRaw(`${EOL}${statusMarkdown}${condition.message}`, true);
}
}
summary.addRaw('</details>', true);
}

function oldConditionsView(group: GroupedConditions): void {
Expand All @@ -70,10 +75,9 @@ function oldConditionsView(group: GroupedConditions): void {
for (const condition of group.conditions) {
const statusMarkdown = generateStatusMarkdown(getStatus(condition.passed, condition.passedWithWarning));
if (condition.details && condition.details.items.length > 0) {
summary.addRaw(`${EOL}<details><summary>${statusMarkdown}${condition.message}</summary>${EOL}`);
summary.addRaw(`${EOL}${statusMarkdown}${condition.message}${EOL}`);
summary.addBreak();
createConditionTables(condition.details).forEach(table => summary.addTable(table));
summary.addRaw('</details>', true);
} else {
summary.addRaw(`${EOL}${statusMarkdown}${condition.message}`, true);
}
Expand Down Expand Up @@ -381,3 +385,68 @@ export function createUnpostableAnnotationsDetails(unpostableAnnotations: Extend
body += '</table>';
return generateExpandableAreaMarkdown(label, body);
}

export function createQiLabelTable(labelInfo: LabelInfo[]) {
if (labelInfo.length === 0) {
return '';
}

let body = '| Metric | Grade | Score | Δ Previous |';
body += `${EOL}| --- | --- | --- | --- |`;

for (const info of labelInfo) {
const grade = createColoredQiGrade(info.letter);
const score = twoDecimalValue(info.score);
const delta = createColoredDeltaValue(info.status, info.deltaValue);
body += `${EOL}| ${info.metric} | ${grade} | $\\large{\\textsf{${score}\\\\%}}$ | ${delta} |`;
}

return body;
}

function createColoredQiGrade(letter: string) {
let color = '';
switch (letter) {
case 'A':
color = '#00783d';
break;
case 'B':
color = '#3db349';
break;
case 'C':
color = '#f3ea29';
break;
case 'D':
color = '#eac01e';
break;
case 'E':
color = '#ef8022';
break;
case 'F':
color = '#d73d29';
break;
}

return `$\\color{${color}}{\\LARGE{\\textsf{\\textbf{${letter}}}}}$`;
}

function createColoredDeltaValue(status: string, delta: number) {
if (status !== 'PRESENT') {
return '-';
}
let color = '';
if (delta < 0) {
color = '\\color{#d73d29}';
} else if (delta > 0) {
color = '\\color{#00783d}';
}

const twoDecimalDelta = twoDecimalValue(delta);
const deltaString = delta > 0 ? `+${twoDecimalDelta}` : twoDecimalDelta;
return `$${color}{\\large{\\textsf{${deltaString}\\\\%}}}$`;
}

function twoDecimalValue(value: number) {
const [whole, decimal] = value.toString().split('.');
return `${whole}.${(decimal || '00').substring(0, 2).padEnd(2, '0')}`;
}
6 changes: 4 additions & 2 deletions src/analysis/client/analysis-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAnalyzedFiles, getAnalyzedFilesUrl } from '../../viewer/analyzed-fil
import { getAnnotations } from '../../viewer/annotations';
import { TicsRunIdentifier } from '../../viewer/interfaces';
import { getQualityGate, getQualityGateUrl } from '../../viewer/qualitygate';
import { getTqiLabel } from '../../viewer/tqi-label';

/**
* Retrieve all analysis results from the viewer in one convenient object.
Expand All @@ -22,15 +23,16 @@ export async function getClientAnalysisResults(explorerUrls: string[], changedFi

const analyzedFiles = await getAnalyzedFiles(getAnalyzedFilesUrl(identifier));
const qualityGate = await getQualityGate(await getQualityGateUrl(identifier));

const annotations = await getAnnotations(qualityGate, changedFiles, identifier);
const labelInfo = await getTqiLabel(identifier.project, identifier.cdtoken);

return {
project: identifier.project,
explorerUrl: url,
qualityGate: qualityGate,
analyzedFiles: analyzedFiles,
annotations: annotations
annotations: annotations,
labelInfo: labelInfo
};
})
);
Expand Down
5 changes: 4 additions & 1 deletion src/analysis/qserver/analysis-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAnalyzedFiles, getAnalyzedFilesUrl } from '../../viewer/analyzed-fil
import { getAnnotations } from '../../viewer/annotations';
import { TicsRunIdentifier } from '../../viewer/interfaces';
import { getQualityGate, getQualityGateUrl } from '../../viewer/qualitygate';
import { getTqiLabel } from '../../viewer/tqi-label';
import { getChangedFiles } from '../helper/changed-files';

export async function getAnalysisResult(date: number): Promise<AnalysisResult> {
Expand All @@ -14,6 +15,7 @@ export async function getAnalysisResult(date: number): Promise<AnalysisResult> {

const changedFiles = await getChangedFiles();
const annotations = await getAnnotations(qualityGate, changedFiles.files, identifier);
const labelInfo = await getTqiLabel(ticsCli.project);

return {
passed: qualityGate.passed,
Expand All @@ -25,7 +27,8 @@ export async function getAnalysisResult(date: number): Promise<AnalysisResult> {
explorerUrl: joinUrl(ticsConfig.baseUrl, qualityGate.url),
analyzedFiles: analyzedFiles,
qualityGate: qualityGate,
annotations: annotations
annotations: annotations,
labelInfo: labelInfo
}
]
};
Expand Down
3 changes: 2 additions & 1 deletion src/helper/interfaces.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangedFile } from '../github/interfaces';
import { ExtendedAnnotation, QualityGate } from '../viewer/interfaces';
import { ExtendedAnnotation, LabelInfo, QualityGate } from '../viewer/interfaces';

export interface ChangedFiles {
files: ChangedFile[];
Expand All @@ -25,6 +25,7 @@ export interface ProjectResult {
qualityGate: QualityGate;
analyzedFiles: string[];
annotations: ExtendedAnnotation[];
labelInfo: LabelInfo[];
}

export interface AnalysisResult {
Expand Down
21 changes: 17 additions & 4 deletions src/viewer/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,21 @@ export interface VersionResponse {
dbversion?: string;
}

export interface RunDateResponse {
export interface QiVersion {
major: number;
minor: number;
}

export interface MeasureApiResponse {
data: {
formattedValue: string;
letter: string | undefined;
letter?: string | null;
messages: string[];
coverage: number;
status: string;
value: number | null;
value: number | QiVersion;
}[];
dates: string;
dates: string[];
metrics: {
expression: string;
fullName: string;
Expand All @@ -182,3 +187,11 @@ export interface RunDateResponse {
fullPath: string;
}[];
}

export interface LabelInfo {
metric: string;
status: string;
letter: string;
score: number;
deltaValue: number;
}
52 changes: 52 additions & 0 deletions src/viewer/measure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ticsCli, ticsConfig } from '../configuration/config';
import { logger } from '../helper/logger';
import { getRetryErrorMessage, getRetryMessage } from '../helper/response';
import { joinUrl } from '../helper/url';
import { httpClient } from './http-client';
import { MeasureApiResponse } from './interfaces';

/**
* Creates a project in the viewer if it does not exist.
* @throws Error if project cannot be created.
*/
export async function getMeasureApiData(
metrics: string[],
project: string,
opts?: { cdtoken?: string; deltaDate?: number; deltaPrevious?: boolean }
): Promise<MeasureApiResponse> {
const filtersParam = getFilters(project, opts?.cdtoken);
const metricsParam = getMetrics(metrics, opts?.deltaDate, opts?.deltaPrevious);
const createProjectUrl = joinUrl(ticsConfig.baseUrl, `api/public/v1/Measure?filters=${filtersParam}&metrics=${metricsParam}`);
try {
logger.info('Retrieving metric data');
logger.debug(`With ${createProjectUrl}`);
const response = await httpClient.get<MeasureApiResponse>(createProjectUrl);
logger.info(getRetryMessage(response, 'Retrieved the last QServer run date.'));
logger.debug(JSON.stringify(response));
return response.data;
} catch (error: unknown) {
const message = getRetryErrorMessage(error);
throw Error(`There was an error calling the Measure API: ${message}`);
}
}

function getFilters(project: string, cdtoken?: string) {
let filters = `Project(${project})`;
if (ticsCli.branchname) {
filters += `,Branch(${ticsCli.branchname})`;
}
if (cdtoken) {
filters += `,ClientData(${cdtoken})`;
}
return filters;
}

function getMetrics(metrics: string[], deltaDate?: number, deltaPrevious = false) {
if (deltaDate) {
return metrics.map(m => `Delta(${m},${deltaDate.toString()})`).join(',');
}
if (deltaPrevious) {
return metrics.map(m => `Delta(${m},Run(-2))`).join(',');
}
return metrics.join(',');
}
34 changes: 11 additions & 23 deletions src/viewer/qserver.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import { ticsCli, ticsConfig } from '../configuration/config';
import { RunDateResponse } from './interfaces';
import { ticsCli } from '../configuration/config';
import { logger } from '../helper/logger';
import { getRetryMessage, getRetryErrorMessage } from '../helper/response';
import { joinUrl } from '../helper/url';
import { httpClient } from './http-client';
import { getMeasureApiData } from './measure';

/**
* Gets the date of the last QServer run the viewer knows of.
* @returns the last QServer run date.
* @throws Error if no date could be retrieved.
*/
export async function getLastQServerRunDate(): Promise<number> {
const getRunDateUrl = joinUrl(ticsConfig.baseUrl, `api/public/v1/Measure?filters=Project(${ticsCli.project})&metrics=lastRunInDatabase`);
try {
logger.header('Retrieving the last QServer run date');
logger.debug(`From ${getRunDateUrl}`);
const response = await httpClient.get<RunDateResponse>(getRunDateUrl);
logger.info(getRetryMessage(response, 'Retrieved the last QServer run date.'));
logger.debug(JSON.stringify(response));
if (response.data.data.length === 0) {
throw Error('Request returned empty array');
}
if (!response.data.data[0].value) {
// return -1 for projects that haven't run yet
return -1;
}
return response.data.data[0].value / 1000;
} catch (error: unknown) {
const message = getRetryErrorMessage(error);
throw Error(`There was an error retrieving last QServer run date: ${message}`);
logger.header('Retrieving the last QServer run date');
const response = await getMeasureApiData(['lastRunInDatabase'], ticsCli.project);
if (response.data.length === 0) {
throw Error('Request returned empty array');
}
if (!response.data[0].value) {
// return -1 for projects that haven't run yet
return -1;
}
return Number(response.data[0].value) / 1000;
}
50 changes: 50 additions & 0 deletions src/viewer/tqi-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { logger } from '../helper/logger';
import { LabelInfo, QiVersion } from './interfaces';
import { getMeasureApiData } from './measure';

export async function getTqiLabel(project: string, cdtoken?: string): Promise<LabelInfo[]> {
const labelInfo: LabelInfo[] = [];

const metrics = await getMetrics(project, cdtoken);
const currentData = await getMeasureApiData(metrics, project, { cdtoken });
const deltaData = await getMeasureApiData(metrics, project, { deltaPrevious: true, cdtoken });

if (currentData.data.length !== deltaData.data.length) {
logger.warning('Could not create TQI label information');
return [];
}

currentData.data.forEach((d, i) => {
const deltaValue = deltaData.data[i].value;
labelInfo.push({
metric: currentData.metrics[i].fullName.split(' for client data')[0],
status: d.status,
letter: d.letter ?? 'F',
score: typeof d.value == 'number' ? d.value : 0,
deltaValue: typeof deltaValue == 'number' ? deltaValue : 0
});
});

return labelInfo;
}

async function getMetrics(project: string, cdtoken?: string) {
const response = await getMeasureApiData(['tqiVersion'], project, { cdtoken });
let majorVersion = null;
if (response.data.length > 0) {
const qiVersion = response.data[0].value as QiVersion;
if (qiVersion.major) {
majorVersion = qiVersion.major;
}
}

switch (majorVersion) {
case 3:
return ['tqi', 'tqiTestCoverage', 'tqiAbstrInt', 'tqiComplexity', 'tqiCompWarn', 'tqiCodingStd', 'tqiDupCode', 'tqiFanOut', 'tqiDeadCode'];
case 4:
return ['tqi', 'tqiTestCoverage', 'tqiAbstrInt', 'tqiComplexity', 'tqiCompWarn', 'tqiCodingStd', 'tqiDupCode', 'tqiFanOut', 'tqiSecurity'];
case 5:
default: // If TQI version cannot be determined, assume it is version 5
return ['tqi', 'tqiTestCoverage', 'tqiAbstrInt', 'tqiSecurity', 'tqiCompWarn', 'tqiCodingStd', 'tqiComplexity', 'tqiDupCode', 'tqiFanOut'];
}
}
Loading
Loading