Skip to content

Commit 2c57aa2

Browse files
authored
AutoApproveNewFeatureBranchBasedOnHistory added (#99)
* AutoApproveNewFeatureBranchBasedOnHistory added
1 parent faa48af commit 2c57aa2

File tree

5 files changed

+221
-56
lines changed

5 files changed

+221
-56
lines changed

src/test-runs/test-runs.service.spec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,10 @@ describe('TestRunsService', () => {
514514
service.getDiff = getDiffMock;
515515
const saveDiffResultMock = jest.fn();
516516
service.saveDiffResult = saveDiffResultMock.mockResolvedValueOnce(testRunWithResult);
517-
const tryAutoApproveBasedOnHistory = jest.fn();
518-
service['tryAutoApproveBasedOnHistory'] = tryAutoApproveBasedOnHistory.mockResolvedValueOnce(testRunWithResult);
517+
const tryAutoApproveByPastBaselines = jest.fn();
518+
service['tryAutoApproveByPastBaselines'] = tryAutoApproveByPastBaselines.mockResolvedValueOnce(testRunWithResult);
519+
const tryAutoApproveByNewBaselines = jest.fn();
520+
service['tryAutoApproveByNewBaselines'] = tryAutoApproveByNewBaselines.mockResolvedValueOnce(testRunWithResult);
519521

520522
const result = await service.create(testVariation, createTestRequestDto);
521523

@@ -566,7 +568,21 @@ describe('TestRunsService', () => {
566568
},
567569
]);
568570
expect(saveDiffResultMock).toHaveBeenCalledWith(testRun.id, diffResult);
569-
expect(tryAutoApproveBasedOnHistory).toHaveBeenCalledWith(testVariation, testRunWithResult, image, [
571+
expect(tryAutoApproveByPastBaselines).toHaveBeenCalledWith(testVariation, testRunWithResult, [
572+
{
573+
x: 3,
574+
y: 4,
575+
width: 500,
576+
height: 600,
577+
},
578+
{
579+
x: 1,
580+
y: 2,
581+
width: 100,
582+
height: 200,
583+
},
584+
]);
585+
expect(tryAutoApproveByNewBaselines).toHaveBeenCalledWith(testVariation, testRunWithResult, [
570586
{
571587
x: 3,
572588
y: 4,

src/test-runs/test-runs.service.ts

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CreateTestRequestDto } from './dto/create-test-request.dto';
55
import { IgnoreAreaDto } from './dto/ignore-area.dto';
66
import { StaticService } from '../shared/static/static.service';
77
import { PrismaService } from '../prisma/prisma.service';
8-
import { TestRun, TestStatus, TestVariation } from '@prisma/client';
8+
import { Baseline, TestRun, TestStatus, TestVariation } from '@prisma/client';
99
import { DiffResult } from './diffResult';
1010
import { EventsGateway } from '../shared/events/events.gateway';
1111
import { CommentDto } from '../shared/dto/comment.dto';
@@ -233,7 +233,8 @@ export class TestRunsService {
233233

234234
let testRunWithResult = await this.saveDiffResult(testRun.id, diffResult);
235235

236-
testRunWithResult = await this.tryAutoApproveBasedOnHistory(testVariation, testRunWithResult, image, ignoreAreas);
236+
testRunWithResult = await this.tryAutoApproveByPastBaselines(testVariation, testRunWithResult, ignoreAreas);
237+
testRunWithResult = await this.tryAutoApproveByNewBaselines(testVariation, testRunWithResult, ignoreAreas);
237238

238239
this.eventsGateway.testRunCreated(testRunWithResult);
239240
return testRunWithResult;
@@ -334,69 +335,86 @@ export class TestRunsService {
334335
return image.data;
335336
}
336337

337-
private async tryAutoApproveBasedOnHistory(
338+
/**
339+
* Reason: not rebased code from feature branch is compared agains new main branch baseline thus diff is expected
340+
* Tries to find past baseline in main branch and autoApprove in case matched
341+
* @param testVariation
342+
* @param testRun
343+
* @param ignoreAreas
344+
*/
345+
private async tryAutoApproveByPastBaselines(
338346
testVariation: TestVariation,
339347
testRun: TestRun,
340-
image: PNG,
341348
ignoreAreas: IgnoreAreaDto[]
342349
): Promise<TestRun> {
343-
if (process.env.AUTO_APPROVE_BASED_ON_HISTORY && testRun.status !== TestStatus.ok) {
344-
this.logger.log(`Try auto approve testRun: ${testRun.id}`);
345-
346-
const alreadyApprovedTestRuns: TestRun[] = await this.prismaService.testRun.findMany({
347-
where: {
348-
...getTestVariationUniqueData(testVariation),
349-
baselineName: testVariation.baselineName,
350-
status: TestStatus.approved,
351-
},
352-
});
350+
if (
351+
!process.env.AUTO_APPROVE_BASED_ON_HISTORY ||
352+
testRun.status === TestStatus.ok ||
353+
testRun.branchName === testRun.baselineBranchName
354+
) {
355+
return testRun;
356+
}
353357

354-
let autoApproved = false;
355-
for (const approvedTestRun of alreadyApprovedTestRuns) {
356-
this.logger.log(
357-
`Found already approved baseline for testRun: ${testRun.id}
358-
testVariation: ${approvedTestRun.testVariationId}
359-
branch: ${approvedTestRun.branchName}
360-
testRun: ${approvedTestRun.id}
361-
build: ${approvedTestRun.buildId}`
362-
);
358+
this.logger.log(`Try AutoApproveByPastBaselines testRun: ${testRun.id}`);
359+
const testVariationHistory = await this.testVariationService.getDetails(testVariation.id);
360+
// skip first baseline as it was used by default in general flow
361+
for (const baseline of testVariationHistory.baselines.slice(1)) {
362+
if (this.shouldAutoApprove(baseline, testRun, ignoreAreas)) {
363+
return this.approve(testRun.id, false, true);
364+
}
365+
}
363366

364-
const approvedTestVariation = await this.prismaService.testVariation.findUnique({
365-
where: {
366-
id: approvedTestRun.testVariationId,
367-
},
368-
});
367+
return testRun;
368+
}
369+
370+
/**
371+
* Reason: branch got another one merged thus diff is expected
372+
* Tries to find latest baseline in test variation
373+
* that has already approved test agains the same baseline image
374+
* and autoApprove in case matched
375+
* @param testVariation
376+
* @param testRun
377+
* @param image
378+
* @param ignoreAreas
379+
*/
380+
private async tryAutoApproveByNewBaselines(
381+
testVariation: TestVariation,
382+
testRun: TestRun,
383+
ignoreAreas: IgnoreAreaDto[]
384+
): Promise<TestRun> {
385+
if (!process.env.AUTO_APPROVE_BASED_ON_HISTORY || testRun.status === TestStatus.ok) {
386+
return testRun;
387+
}
388+
this.logger.log(`Try AutoApproveByNewBaselines testRun: ${testRun.id}`);
369389

370-
const approvedBaseline = this.staticService.getImage(approvedTestVariation.baselineName);
371-
const diffResult = this.getDiff(approvedBaseline, image, testRun.diffTollerancePercent, ignoreAreas);
390+
const alreadyApprovedTestRuns: TestRun[] = await this.prismaService.testRun.findMany({
391+
where: {
392+
...getTestVariationUniqueData(testVariation),
393+
baselineName: testVariation.baselineName,
394+
status: TestStatus.approved,
395+
},
396+
});
372397

373-
if (diffResult.status === TestStatus.ok) {
374-
autoApproved = true;
375-
const baseline = await this.prismaService.baseline.findFirst({
376-
where: {
377-
testVariationId: approvedTestVariation.id,
378-
baselineName: approvedTestVariation.baselineName,
379-
},
380-
include: {
381-
testRun: true,
382-
},
383-
});
384-
this.logger.log(
385-
`Found reason to auto approve testRun: ${testRun.id}
386-
testVariation: ${baseline.testVariationId}
387-
baseline: ${baseline.id}
388-
branch: ${approvedTestVariation.branchName}
389-
testRun: ${baseline.testRunId}
390-
build: ${baseline.testRun.buildId}`
391-
);
392-
}
393-
}
398+
for (const approvedTestRun of alreadyApprovedTestRuns) {
399+
const approvedTestVariation = await this.testVariationService.getDetails(approvedTestRun.testVariationId);
400+
const baseline = approvedTestVariation.baselines.shift();
394401

395-
if (autoApproved) {
402+
if (this.shouldAutoApprove(baseline, testRun, ignoreAreas)) {
396403
return this.approve(testRun.id, false, true);
397404
}
398-
this.logger.log(`Cannot auto approve testRun: ${testRun.id}`);
399405
}
406+
400407
return testRun;
401408
}
409+
410+
private shouldAutoApprove(baseline: Baseline, testRun: TestRun, ignoreAreas: Array<IgnoreAreaDto>): boolean {
411+
const approvedImage = this.staticService.getImage(baseline.baselineName);
412+
const image = this.staticService.getImage(testRun.imageName);
413+
const diffResult = this.getDiff(approvedImage, image, testRun.diffTollerancePercent, ignoreAreas);
414+
415+
if (diffResult.status === TestStatus.ok) {
416+
this.logger.log(`TestRun ${testRun.id} could be auto approved based on Baseline ${baseline.id}`);
417+
return true;
418+
}
419+
}
402420
}

test/image_edited.png

100 KB
Loading

test/preconditions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { INestApplication } from '@nestjs/common';
22
import { UsersService } from 'src/users/users.service';
33
import uuidAPIKey from 'uuid-apikey';
44
import request, { Test } from 'supertest';
5+
import { BuildsService } from 'src/builds/builds.service';
6+
import { TestRunsService } from 'src/test-runs/test-runs.service';
7+
import { readFileSync } from 'fs';
8+
import { TestRunResultDto } from 'src/test-runs/dto/testRunResult.dto';
59

610
export const generateUser = (
711
password: string
@@ -41,3 +45,21 @@ export const haveUserLogged = async (usersService: UsersService) => {
4145
password,
4246
});
4347
};
48+
49+
export const haveTestRunCreated = async (
50+
buildsService: BuildsService,
51+
testRunsService: TestRunsService,
52+
projectId: string,
53+
branchName: string,
54+
imagePath: string
55+
): Promise<TestRunResultDto> => {
56+
const build = await buildsService.create({ project: projectId, branchName });
57+
return testRunsService.postTestRun({
58+
projectId: build.projectId,
59+
branchName: build.branchName,
60+
imageBase64: readFileSync(imagePath).toString('base64'),
61+
buildId: build.id,
62+
name: 'Image name',
63+
merge: false,
64+
});
65+
};

test/test-runs.e2e-spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { INestApplication } from '@nestjs/common';
3+
import { AppModule } from '../src/app.module';
4+
import { UsersService } from '../src/users/users.service';
5+
import { haveTestRunCreated, haveUserLogged } from './preconditions';
6+
import { UserLoginResponseDto } from '../src/users/dto/user-login-response.dto';
7+
import { TestRunsService } from '../src/test-runs/test-runs.service';
8+
import { ProjectsService } from '../src/projects/projects.service';
9+
import { Project, TestStatus } from '@prisma/client';
10+
import { BuildsService } from '../src/builds/builds.service';
11+
12+
jest.setTimeout(20000);
13+
14+
describe('TestRuns (e2e)', () => {
15+
let app: INestApplication;
16+
let testRunsService: TestRunsService;
17+
let usersService: UsersService;
18+
let projecstService: ProjectsService;
19+
let buildsService: BuildsService;
20+
let user: UserLoginResponseDto;
21+
let project: Project;
22+
23+
beforeAll(async () => {
24+
const moduleFixture: TestingModule = await Test.createTestingModule({
25+
imports: [AppModule],
26+
}).compile();
27+
28+
app = moduleFixture.createNestApplication();
29+
testRunsService = moduleFixture.get<TestRunsService>(TestRunsService);
30+
usersService = moduleFixture.get<UsersService>(UsersService);
31+
projecstService = moduleFixture.get<ProjectsService>(ProjectsService);
32+
buildsService = moduleFixture.get<BuildsService>(BuildsService);
33+
34+
await app.init();
35+
});
36+
37+
beforeEach(async () => {
38+
user = await haveUserLogged(usersService);
39+
project = await projecstService.create({ name: 'TestRun E2E test', mainBranchName: 'master' });
40+
});
41+
42+
afterEach(async () => {
43+
await projecstService.remove(project.id);
44+
await usersService.delete(user.id);
45+
});
46+
47+
afterAll(async () => {
48+
await app.close();
49+
});
50+
51+
describe('POST /', () => {
52+
const image_v1 = './test/image.png';
53+
const image_v2 = './test/image_edited.png';
54+
it('Auto approve not rebased feature branch', async () => {
55+
const testRun1 = await haveTestRunCreated(
56+
buildsService,
57+
testRunsService,
58+
project.id,
59+
project.mainBranchName,
60+
image_v1
61+
);
62+
await testRunsService.approve(testRun1.id, false, false);
63+
const testRun2 = await haveTestRunCreated(
64+
buildsService,
65+
testRunsService,
66+
project.id,
67+
project.mainBranchName,
68+
image_v2
69+
);
70+
await testRunsService.approve(testRun2.id, false, false);
71+
72+
const testRun = await haveTestRunCreated(buildsService, testRunsService, project.id, 'develop', image_v1);
73+
74+
expect(testRun.status).toBe(TestStatus.autoApproved);
75+
});
76+
77+
it('Auto approve merged feature into feature branch', async () => {
78+
const testRun1 = await haveTestRunCreated(buildsService, testRunsService, project.id, 'feature1', image_v1);
79+
await testRunsService.approve(testRun1.id, false, false);
80+
81+
const testRun = await haveTestRunCreated(buildsService, testRunsService, project.id, 'feature2', image_v1);
82+
83+
expect(testRun.status).toBe(TestStatus.autoApproved);
84+
});
85+
86+
it('Auto approve merged feature into main branch', async () => {
87+
const testRun1 = await haveTestRunCreated(
88+
buildsService,
89+
testRunsService,
90+
project.id,
91+
project.mainBranchName,
92+
image_v1
93+
);
94+
await testRunsService.approve(testRun1.id, false, false);
95+
const testRun2 = await haveTestRunCreated(buildsService, testRunsService, project.id, 'develop', image_v2);
96+
await testRunsService.approve(testRun2.id, false, false);
97+
98+
const testRun = await haveTestRunCreated(
99+
buildsService,
100+
testRunsService,
101+
project.id,
102+
project.mainBranchName,
103+
image_v2
104+
);
105+
106+
expect(testRun.status).toBe(TestStatus.autoApproved);
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)