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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"npm.packageManager": "npm"
}
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/common/src/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export interface MutationScoreOnlyResult {
}

export interface ReportIdentifier {
projectName: string;
moduleName: string | undefined;
projectName: string; // github.com/stryker-mutator/stryker-dashboard
moduleName: string | undefined; // feat/implement-metrics
realTime?: boolean;
version: string;
}
Expand Down
22 changes: 22 additions & 0 deletions packages/common/src/ReportStatisticsDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface ReportStatisticsDto {
project: string;
version: string | undefined;

createdAt: Date;
pending: number;
killed: number;
timeout: number;
survived: number;
noCoverage: number;
runtimeErrors: number;
compileErrors: number;
ignored: number;
totalDetected: number;
totalUndetected: number;
totalInvalid: number;
totalValid: number;
totalMutants: number;
totalCovered: number;
mutationScore: number;
mutationScoreBasedOnCoveredCode: number;
}
3 changes: 2 additions & 1 deletion packages/common/src/Uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ export function constructApiUri(
location: string,
slug: string,
queryParams: { module: string | undefined; realTime: string | undefined },
controller = 'reports',
) {
const url = new URL(`${location}/api/reports/${slug}`);
const url = new URL(`${location}/api/${controller}/${slug}`);
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined && value !== null) {
url.searchParams.append(key, value);
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Logger.js';
export * from './Report.js';
export * from './ReportStatisticsDto.js'
export * from './slug.js';
export * from './Uri.js';
7 changes: 7 additions & 0 deletions packages/data-access/src/mappers/factories.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { MutationTestingReport, Project } from '../models/index.js';
import { MutationTestingMetrics } from '../models/MutationTestingMetrics.js';
import type { Mapper } from './Mapper.js';
import TableStorageMapper from './TableStorageMapper.js';

export type MutationTestingReportMapper = Mapper<MutationTestingReport, 'projectName' | 'version', 'moduleName'>;

export type MutationTestingMetricsMapper = Mapper<MutationTestingMetrics, 'project', 'version'>;

export type ProjectMapper = Mapper<Project, 'owner', 'name'>;

export function createMutationTestingReportMapper(): MutationTestingReportMapper {
return new TableStorageMapper(MutationTestingReport);
}

export function createMutationTestingMetricsMapper(): MutationTestingMetricsMapper {
return new TableStorageMapper(MutationTestingMetrics);
}

export function createProjectMapper(): ProjectMapper {
return new TableStorageMapper(Project);
}
82 changes: 82 additions & 0 deletions packages/data-access/src/models/MutationTestingMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { type Metrics } from "mutation-testing-metrics";

export class MutationTestingMetrics implements Metrics {
public project: string; // github.com/stryker-mutator/stryker-dashboard
public version: string | undefined; // feat/implement-metrics

public createdAt: Date;
public pending: number;
public killed: number;
public timeout: number;
public survived: number;
public noCoverage: number;
public runtimeErrors: number;
public compileErrors: number;
public ignored: number;
public totalDetected: number;
public totalUndetected: number;
public totalInvalid: number;
public totalValid: number;
public totalMutants: number;
public totalCovered: number;
public mutationScore: number;
public mutationScoreBasedOnCoveredCode: number;

constructor(metrics: Metrics | undefined = undefined) {
if (metrics === undefined) {
return;
}

this.createdAt = new Date();
this.pending = metrics.pending;
this.killed = metrics.killed;
this.timeout = metrics.timeout;
this.survived = metrics.survived;
this.noCoverage = metrics.noCoverage;
this.runtimeErrors = metrics.runtimeErrors;
this.compileErrors = metrics.compileErrors;
this.ignored = metrics.ignored;
this.totalDetected = metrics.totalDetected;
this.totalUndetected = metrics.totalUndetected;
this.totalInvalid = metrics.totalInvalid;
this.totalValid = metrics.totalValid;
this.totalMutants = metrics.totalMutants;
this.totalCovered = metrics.totalCovered;
this.mutationScore = metrics.mutationScore;
this.mutationScoreBasedOnCoveredCode = metrics.mutationScoreBasedOnCoveredCode;
}

public static readonly persistedFields = [
'pending',
'killed',
'timeout',
'survived',
'noCoverage',
'runtimeErrors',
'compileErrors',
'ignored',
'totalDetected',
'totalUndetected',
'totalInvalid',
'totalValid',
'totalMutants',
'totalCovered',
'mutationScore',
'mutationScoreBasedOnCoveredCode',
] as const;
public static readonly tableName = MutationTestingMetrics.name;

public static createRowKey(identifier: Pick<MutationTestingMetrics, 'version'>): string | undefined {
const nowUtc = new Date().toISOString();
return identifier.version + nowUtc;
}

public static createPartitionKey(identifier: Pick<MutationTestingMetrics, 'project'>): string {
return identifier.project;
}

public static identify(entity: Partial<MutationTestingMetrics>, partitionKey: string, rowKey: string) {
entity.project = partitionKey;
entity.version = rowKey;
}
}
2 changes: 1 addition & 1 deletion packages/data-access/src/models/MutationTestingReport.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReportIdentifier } from '@stryker-mutator/dashboard-common';

export class MutationTestingReport implements ReportIdentifier {
export class MutationTestingReport implements ReportIdentifier{
/**
* The repo slug. /:provider/:owner/:name (could also have more components in the future, for example gitlab supports this)
* @example /github.com/stryker-mutator/mutation-testing-elements
Expand Down
1 change: 1 addition & 0 deletions packages/data-access/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './MutationTestingMetrics.js';
export * from './MutationTestingReport.js';
export * from './Project.js';
export * from './Timestamped.js';
20 changes: 20 additions & 0 deletions packages/data-access/src/services/MutationTestingMetricsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DashboardQuery } from "../mappers/DashboardQuery.js";
import type { MutationTestingMetricsMapper } from "../mappers/factories.js";
import { createMutationTestingMetricsMapper } from "../mappers/factories.js";
import { MutationTestingMetrics } from "../models/MutationTestingMetrics.js";

export class MutationTestingMetricsService {
constructor(
private readonly mutationMetricsMapper: MutationTestingMetricsMapper = createMutationTestingMetricsMapper(),
) {}


public async getMetrics(project: string): Promise<MutationTestingMetrics[]> {
const metricResults = await this.mutationMetricsMapper.findAll(
DashboardQuery.create(MutationTestingMetrics)
.wherePartitionKeyEquals({ project })
)

return metricResults.map((metricResult) => metricResult.model);
}
}
21 changes: 18 additions & 3 deletions packages/data-access/src/services/MutationTestingReportService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { Logger, MutationScoreOnlyResult, Report, ReportIdentifier } from '@stryker-mutator/dashboard-common';
import { isMutationTestResult } from '@stryker-mutator/dashboard-common';
import type { Metrics } from 'mutation-testing-metrics';
import { aggregateResultsByModule, calculateMetrics } from 'mutation-testing-metrics';
import type { MutationTestResult } from 'mutation-testing-report-schema';

import { OptimisticConcurrencyError } from '../errors/index.js';
import type { MutationTestingReportMapper } from '../mappers/index.js';
import { createMutationTestingReportMapper, DashboardQuery } from '../mappers/index.js';
import type { MutationTestingMetricsMapper, MutationTestingReportMapper } from '../mappers/index.js';
import { createMutationTestingMetricsMapper, createMutationTestingReportMapper, DashboardQuery } from '../mappers/index.js';
import { MutationTestingResultMapper } from '../mappers/MutationTestingResultMapper.js';
import { MutationTestingReport } from '../models/index.js';
import { MutationTestingMetrics } from '../models/MutationTestingMetrics.js';

function moduleHasResult(tuple: readonly [string, MutationTestResult | null]): tuple is [string, MutationTestResult] {
return !!tuple[1];
Expand All @@ -16,6 +18,7 @@ function moduleHasResult(tuple: readonly [string, MutationTestResult | null]): t
export class MutationTestingReportService {
constructor(
private readonly resultMapper: MutationTestingResultMapper = new MutationTestingResultMapper(),
private readonly mutationMetricsMapper: MutationTestingMetricsMapper = createMutationTestingMetricsMapper(),
private readonly mutationScoreMapper: MutationTestingReportMapper = createMutationTestingReportMapper(),
) {}

Expand All @@ -25,7 +28,11 @@ export class MutationTestingReportService {
}

public async saveReport(id: ReportIdentifier, result: MutationScoreOnlyResult | MutationTestResult, logger: Logger) {
const mutationScore = this.calculateMutationScore(result);
let metrics: Metrics | undefined;
if (isMutationTestResult(result)){
metrics = calculateMetrics(result.files).metrics;
}
const mutationScore = metrics?.mutationScore || 0;

await this.insertOrMergeReport(
id,
Expand All @@ -38,6 +45,14 @@ export class MutationTestingReportService {
if (isMutationTestResult(result) && id.moduleName) {
await this.aggregateProjectReport(id.projectName, id.version, logger);
}
if (metrics) {
const mutationTestingMetrics = new MutationTestingMetrics(metrics);

mutationTestingMetrics.project = id.projectName;
mutationTestingMetrics.version = id.version;

await this.mutationMetricsMapper.insert(mutationTestingMetrics)
}
}

private async aggregateProjectReport(projectName: string, version: string, logger: Logger) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Logger, Report, ReportIdentifier } from '@stryker-mutator/dashboard-common';
import { expect } from 'chai';
import { aggregateResultsByModule } from 'mutation-testing-metrics';
import { aggregateResultsByModule, MutationTestMetricsResult } from 'mutation-testing-metrics';
import sinon from 'sinon';

import type { MutationTestingReportMapper } from '../../../src/index.js';
import type { MutationTestingMetricsMapper, MutationTestingReportMapper } from '../../../src/index.js';
import { OptimisticConcurrencyError } from '../../../src/index.js';
import { MutationTestingResultMapper } from '../../../src/mappers/MutationTestingResultMapper.js';
import type { MutationTestingReport } from '../../../src/models/index.js';
Expand All @@ -18,6 +18,7 @@ import {
describe(MutationTestingReportService.name, () => {
let sut: MutationTestingReportService;
let reportMapperMock: sinon.SinonStubbedInstance<MutationTestingReportMapper>;
let metricsMapperMock: sinon.SinonStubbedInstance<MutationTestingMetricsMapper>;
let resultMapperMock: sinon.SinonStubbedInstance<MutationTestingResultMapper>;
let logger: sinon.SinonStubbedInstance<Logger>;

Expand All @@ -29,9 +30,11 @@ describe(MutationTestingReportService.name, () => {
error: sinon.stub(),
};
reportMapperMock = createTableMapperMock();
metricsMapperMock = createTableMapperMock();
resultMapperMock = sinon.createStubInstance(MutationTestingResultMapper);
sut = new MutationTestingReportService(
resultMapperMock as unknown as MutationTestingResultMapper,
metricsMapperMock,
reportMapperMock,
);
});
Expand Down
1 change: 1 addition & 0 deletions packages/stryker-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@storybook/web-components-vite": "8.4.7",
"@stryker-mutator/dashboard-common": "0.16.0",
"autoprefixer": "10.4.20",
"mutation-testing-metrics": "^3.5.1",
"postcss": "8.4.49",
"storybook": "8.4.7",
"tailwindcss": "3.4.16",
Expand Down
45 changes: 45 additions & 0 deletions packages/stryker-elements/src/lib/atoms/metrics-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ReportStatisticsDto } from '@stryker-mutator/dashboard-common';
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

import { BaseElement } from '../base-element.js';

@customElement('sme-report-selector')
export class ReportSelector extends BaseElement {
@property({ type: Array })
reports: ReportStatisticsDto[] | undefined = undefined;

#selectedReport: ReportStatisticsDto | undefined = undefined;

render() {
return html`
<div class="col-span-1 flex h-full flex-col items-center bg-zinc-700 text-white">
<h3 class="text-m mt-4">Select report</h3>
<select
class="mt-4 w-full rounded-lg border-2 border-neutral-600 bg-zinc-800 p-2 text-white"
@change="${this.#handleChange}"
>
${this.reports?.map((report, index) => html`<option ?selected="${index === 0}" value="${index}">${report.createdAt}</option>`)}
</select>
</div>
`;
}

#handleChange(e: Event) {
const target = e.target as HTMLSelectElement;
this.#selectedReport = this.reports?.[Number(target.value)];
this.dispatchEvent(
new CustomEvent('reportSelected', {
detail: {
metrics: this.#selectedReport,
},
}),
);
}
}

declare global {
interface HTMLElementTagNameMap {
'sme-report-selector': ReportSelector;
}
}
26 changes: 26 additions & 0 deletions packages/stryker-elements/src/lib/atoms/statistic-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { type Metrics } from 'mutation-testing-metrics';

import { BaseElement } from '../base-element.js';

@customElement('sme-statistic-graph')
export class StatisticGraph extends BaseElement {
@property({ type: Array })
metrics: Metrics[] | undefined = undefined;

render() {
return html`
<div class="col-span-1 flex h-full flex-col items-center bg-zinc-700 text-white">
<h3 class="text-m mt-4">Mutation score all files past 30 days</h3>
<!-- TODO: Implement Graph -->
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'sme-statistic-graph': StatisticGraph;
}
}
Loading