Skip to content

refactor(real-time): move update logic to metrics package #3068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
113 changes: 23 additions & 90 deletions packages/elements/src/components/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { MetricsResult, MutantModel, TestModel } from 'mutation-testing-met
import { calculateMutationTestMetrics } from 'mutation-testing-metrics';
import { tailwind, globals } from '../../style/index.js';
import { locationChange$, View } from '../../lib/router.js';
import type { Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import { fromEvent, sampleTime } from 'rxjs';
import theme from './theme.scss?inline';
import { createCustomEvent } from '../../lib/custom-events.js';
Expand Down Expand Up @@ -133,9 +133,6 @@ export class MutationTestReportAppComponent extends RealTimeElement {
}
}

private mutants = new Map<string, MutantModel>();
private tests = new Map<string, TestModel>();

public updated(changedProperties: PropertyValues) {
if (changedProperties.has('theme') && this.theme) {
this.dispatchEvent(
Expand Down Expand Up @@ -163,29 +160,6 @@ export class MutationTestReportAppComponent extends RealTimeElement {

private updateModel(report: MutationTestResult) {
this.rootModel = calculateMutationTestMetrics(report);
collectForEach<FileUnderTestModel, Metrics>((file, metric) => {
file.result = metric;
file.mutants.forEach((mutant) => this.mutants.set(mutant.id, mutant));
})(this.rootModel?.systemUnderTestMetrics);

collectForEach<TestFileModel, TestMetrics>((file, metric) => {
file.result = metric;
file.tests.forEach((test) => this.tests.set(test.id, test));
})(this.rootModel?.testMetrics);

this.rootModel.systemUnderTestMetrics.updateParent();
this.rootModel.testMetrics?.updateParent();

function collectForEach<TFile, TMetrics>(collect: (file: TFile, metrics: MetricsResult<TFile, TMetrics>) => void) {
return function forEachMetric(metrics: MetricsResult<TFile, TMetrics> | undefined): void {
if (metrics?.file) {
collect(metrics.file, metrics);
}
metrics?.childResults.forEach((child) => {
forEachMetric(child);
});
};
}
}

private updateContext() {
Expand Down Expand Up @@ -225,18 +199,16 @@ export class MutationTestReportAppComponent extends RealTimeElement {

public static styles = [globals, unsafeCSS(theme), tailwind];

public readonly subscriptions: Subscription[] = [];
private readonly subscription = new Subscription();
private readonly sseSubscriptions = new Set<Subscription>();

public connectedCallback() {
super.connectedCallback();
this.subscriptions.push(locationChange$.subscribe((path) => (this.path = path)));
this.subscription.add(locationChange$.subscribe((path) => (this.path = path)));
this.initializeSse();
}

private source: EventSource | undefined;
private sseSubscriptions = new Set<Subscription>();
private theMutant?: MutantModel;
private theTest?: TestModel;

private initializeSse() {
if (!this.sse) {
Expand All @@ -245,73 +217,34 @@ export class MutationTestReportAppComponent extends RealTimeElement {

this.source = new EventSource(this.sse);

const modifySubscription = fromEvent<MessageEvent>(this.source, 'mutant-tested').subscribe((event) => {
const newMutantData = JSON.parse(event.data as string) as Partial<MutantResult> & Pick<MutantResult, 'id' | 'status'>;
if (!this.report) {
return;
}

const mutant = this.mutants.get(newMutantData.id);
if (mutant === undefined) {
return;
}
this.theMutant = mutant;

for (const [prop, val] of Object.entries(newMutantData)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(this.theMutant as any)[prop] = val;
}

if (newMutantData.killedBy) {
newMutantData.killedBy.forEach((killedByTestId) => {
const test = this.tests.get(killedByTestId)!;
if (test === undefined) {
return;
}
this.theTest = test;
test.addKilled(this.theMutant!);
this.theMutant!.addKilledBy(test);
});
}

if (newMutantData.coveredBy) {
newMutantData.coveredBy.forEach((coveredByTestId) => {
const test = this.tests.get(coveredByTestId)!;
if (test === undefined) {
return;
}
this.theTest = test;
test.addCovered(this.theMutant!);
this.theMutant!.addCoveredBy(test);
});
}
});

const applySubscription = fromEvent(this.source, 'mutant-tested')
.pipe(sampleTime(UPDATE_CYCLE_TIME))
.subscribe(() => {
this.applyChanges();
});

this.sseSubscriptions.add(modifySubscription);
this.sseSubscriptions.add(applySubscription);
this.sseSubscriptions.add(
fromEvent<MessageEvent>(this.source, 'mutant-tested').subscribe((event) => {
const newMutantData = JSON.parse(event.data as string) as Partial<MutantResult> & Pick<MutantResult, 'id' | 'status'>;
if (!this.report) {
return;
}
this.rootModel?.updateMutant(newMutantData);
}),
);

this.sseSubscriptions.add(
fromEvent(this.source, 'mutant-tested')
.pipe(sampleTime(UPDATE_CYCLE_TIME))
.subscribe(() => {
mutantChanges.next();
}),
);

this.source.addEventListener('finished', () => {
this.source?.close();
this.applyChanges();
this.sseSubscriptions.forEach((s) => s.unsubscribe());
});
}

private applyChanges() {
this.theMutant?.update();
this.theTest?.update();
mutantChanges.next();
}

public disconnectedCallback() {
super.disconnectedCallback();
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
this.subscription.unsubscribe();
this.sseSubscriptions.forEach((s) => s.unsubscribe());
}

private renderTitle() {
Expand Down
43 changes: 0 additions & 43 deletions packages/elements/test/unit/components/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,49 +371,6 @@ describe(MutationTestReportAppComponent.name, () => {
expect(file.mutants[0].status).to.be.equal('Killed');
});

it('should update every mutant field when given in an SSE event', async () => {
// Arrange
const report = createReport();
const mutant = createMutantResult();
mutant.status = 'Pending';
report.files['foobar.js'].mutants = [mutant];
sut.element.report = report;
sut.element.sse = 'http://localhost:8080/sse';
sut.connect();
await sut.whenStable();

// Act
const newMutantData = JSON.stringify({
id: '1',
status: 'Killed',
description: 'test description',
coveredBy: ['test 1'],
duration: 100,
killedBy: ['test 1'],
replacement: 'test-r',
static: true,
statusReason: 'test reason',
testsCompleted: 12,
location: { start: { line: 12, column: 1 }, end: { line: 13, column: 2 } },
mutatorName: 'test mutator',
});
const message = new MessageEvent('mutant-tested', { data: newMutantData });
eventSource.dispatchEvent(message);

// Assert
const theMutant = sut.element.rootModel!.systemUnderTestMetrics.childResults[0].file!.mutants[0];
expect(theMutant.description).to.be.equal('test description');
expect(theMutant.coveredBy).to.have.same.members(['test 1']);
expect(theMutant.duration).to.be.equal(100);
expect(theMutant.killedBy).to.have.same.members(['test 1']);
expect(theMutant.replacement).to.be.equal('test-r');
expect(theMutant.static).to.be.true;
expect(theMutant.statusReason).to.be.equal('test reason');
expect(theMutant.testsCompleted).to.be.equal(12);
expect(theMutant.location).to.deep.equal({ start: { line: 12, column: 1 }, end: { line: 13, column: 2 } });
expect(theMutant.mutatorName).to.be.equal('test mutator');
});

it('should close the SSE process when the final event comes in', async () => {
// Arrange
const message = new MessageEvent('finished', { data: '' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,24 @@ describe(MutationTestReportDrawerMutant.name, () => {
});

it('should render the first killedBy test', async () => {
mutant.killedByTests = [new TestModel(createTestDefinition({ name: 'foo should bar' }))];
mutant.addKilledBy(new TestModel(createTestDefinition({ name: 'foo should bar' })));
sut.element.mutant = mutant;
await sut.whenStable();
expect(summaryText()).contain('🎯 Killed by: foo should bar');
});

it('should mention more killedBy tests when they exist', async () => {
mutant.killedByTests = [
new TestModel(createTestDefinition({ id: '1', name: 'foo should bar' })),
new TestModel(createTestDefinition({ id: '2' })),
];
mutant.addKilledBy(new TestModel(createTestDefinition({ id: '1', name: 'foo should bar' })));
mutant.addKilledBy(new TestModel(createTestDefinition({ id: '2' })));
sut.element.mutant = mutant;
await sut.whenStable();
expect(summaryText()).contain('(and 1 more)');
});

it('should mention when one test covered the mutant', async () => {
mutant.status = 'Killed';
mutant.coveredByTests = [new TestModel(createTestDefinition())];
mutant.addCoveredBy(new TestModel(createTestDefinition()));

sut.element.mutant = mutant;
await sut.whenStable();
const summary = summaryText();
Expand All @@ -82,15 +81,16 @@ describe(MutationTestReportDrawerMutant.name, () => {

it('should not mentioned that a killed mutant still survived', async () => {
mutant.status = 'Killed';
mutant.coveredByTests = [new TestModel(createTestDefinition())];
mutant.addCoveredBy(new TestModel(createTestDefinition()));
sut.element.mutant = mutant;
await sut.whenStable();
const summary = summaryText();
expect(summary).not.contains('yet still survived');
});

it('should mention when two tests covered the mutant', async () => {
mutant.coveredByTests = [new TestModel(createTestDefinition({ id: '1' })), new TestModel(createTestDefinition({ id: '2' }))];
mutant.addCoveredBy(new TestModel(createTestDefinition({ id: '1' })));
mutant.addCoveredBy(new TestModel(createTestDefinition({ id: '2' })));
sut.element.mutant = mutant;
await sut.whenStable();
const summary = summaryText();
Expand Down Expand Up @@ -138,8 +138,11 @@ describe(MutationTestReportDrawerMutant.name, () => {
it('should render the tests', async () => {
const test1 = new TestModel(createTestDefinition({ id: '1', name: 'foo should bar' }));
const test2 = new TestModel(createTestDefinition({ id: '2', name: 'baz should qux' }));
mutant.killedByTests = [test1, test2];
mutant.coveredByTests = [test1, test2, new TestModel(createTestDefinition({ id: '3', name: 'quux should corge' }))];
mutant.addKilledBy(test1);
mutant.addKilledBy(test2);
mutant.addCoveredBy(test1);
mutant.addCoveredBy(test2);
mutant.addCoveredBy(new TestModel(createTestDefinition({ id: '3', name: 'quux should corge' })));
sut.element.mutant = mutant;
await sut.whenStable();
const listItems = sut.$$('[slot="detail"] ul li');
Expand Down
12 changes: 3 additions & 9 deletions packages/elements/test/unit/helpers/factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metrics, TestMetrics } from 'mutation-testing-metrics';
import { MetricsResult, TestFileModel } from 'mutation-testing-metrics';
import type { FileResult, Location, MutantResult, MutationTestResult, TestDefinition, TestFile } from 'mutation-testing-report-schema/api';
import { accumulateFileUnderTestMetrics, accumulateTestMetrics, countFileUnderTestMetrics, countTestFileMetrics } from '../../../../metrics/src/calculateMetrics.js';

export function createMutantResult(overrides?: Partial<MutantResult>): MutantResult {
const defaults: MutantResult = {
Expand Down Expand Up @@ -46,7 +47,7 @@ export function createFileResult(overrides?: Partial<FileResult>): FileResult {
}

export function createMetricsResult(overrides?: Partial<MetricsResult>): MetricsResult {
const result = new MetricsResult('foo', [], createMetrics());
const result = new MetricsResult('foo', [], accumulateFileUnderTestMetrics, countFileUnderTestMetrics);
if (overrides?.file) {
result.file = overrides.file;
}
Expand All @@ -56,25 +57,18 @@ export function createMetricsResult(overrides?: Partial<MetricsResult>): Metrics
if (overrides?.name) {
result.name = overrides.name;
}
if (overrides?.metrics) {
result.metrics = overrides.metrics;
}

return result;
}

export function createTestMetricsResult(overrides?: Partial<MetricsResult<TestFileModel, TestMetrics>>): MetricsResult<TestFileModel, TestMetrics> {
const result = new MetricsResult('foo', [], createTestMetrics(), new TestFileModel(createTestFile(), ''));
const result = new MetricsResult('foo', [], accumulateTestMetrics, countTestFileMetrics, overrides?.file ?? new TestFileModel(createTestFile(), ''));
if (overrides?.childResults) {
result.childResults = overrides.childResults;
}
if (overrides?.name) {
result.name = overrides.name;
}
if (overrides?.metrics) {
result.metrics = overrides.metrics;
}

return result;
}

Expand Down
Loading