Skip to content

Commit 58ea1a3

Browse files
authored
Add auto approve based on history (#95)
* Add auto approve based on history Visual-Regression-Tracker/Visual-Regression-Tracker#190
1 parent 245d406 commit 58ea1a3

File tree

10 files changed

+298
-23
lines changed

10 files changed

+298
-23
lines changed

.env

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ POSTGRES_DB=vrt_db_dev
1414

1515
# optional
1616
#HTTPS_KEY_PATH='./secrets/ssl.key'
17-
#HTTPS_CERT_PATH='./secrets/ssl.cert'
17+
#HTTPS_CERT_PATH='./secrets/ssl.cert'
18+
19+
# features
20+
AUTO_APPROVE_BASED_ON_HISTORY=true
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Migration `20210130115922-test-run-auto-approve-status-added`
2+
3+
This migration has been generated by Pavlo Strunkin at 1/30/2021, 1:59:22 PM.
4+
You can check out the [state of the schema](./schema.prisma) after the migration.
5+
6+
## Database Steps
7+
8+
```sql
9+
ALTER TYPE "TestStatus" ADD VALUE 'autoApproved'
10+
```
11+
12+
## Changes
13+
14+
```diff
15+
diff --git schema.prisma schema.prisma
16+
migration 20210118201534-build--project-id---ci-build-id-constraint..20210130115922-test-run-auto-approve-status-added
17+
--- datamodel.dml
18+
+++ datamodel.dml
19+
@@ -3,9 +3,9 @@
20+
}
21+
datasource db {
22+
provider = "postgresql"
23+
- url = "***"
24+
+ url = "***"
25+
}
26+
model Build {
27+
id String @id @default(uuid())
28+
@@ -118,5 +118,6 @@
29+
new
30+
ok
31+
unresolved
32+
approved
33+
+ autoApproved
34+
}
35+
```
36+
37+
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
generator client {
2+
provider = "prisma-client-js"
3+
}
4+
5+
datasource db {
6+
provider = "postgresql"
7+
url = "***"
8+
}
9+
10+
model Build {
11+
id String @id @default(uuid())
12+
ciBuildId String?
13+
number Int?
14+
branchName String?
15+
status String?
16+
testRuns TestRun[]
17+
projectId String
18+
project Project @relation(fields: [projectId], references: [id])
19+
updatedAt DateTime @updatedAt
20+
createdAt DateTime @default(now())
21+
user User? @relation(fields: [userId], references: [id])
22+
userId String?
23+
isRunning Boolean?
24+
25+
@@unique([projectId, ciBuildId])
26+
}
27+
28+
model Project {
29+
id String @id @default(uuid())
30+
name String
31+
mainBranchName String @default("master")
32+
builds Build[]
33+
buildsCounter Int @default(0)
34+
testVariations TestVariation[]
35+
updatedAt DateTime @updatedAt
36+
createdAt DateTime @default(now())
37+
38+
@@unique([name])
39+
}
40+
41+
model TestRun {
42+
id String @id @default(uuid())
43+
imageName String
44+
diffName String?
45+
diffPercent Float?
46+
diffTollerancePercent Float @default(0)
47+
pixelMisMatchCount Int?
48+
status TestStatus
49+
buildId String
50+
build Build @relation(fields: [buildId], references: [id])
51+
testVariationId String
52+
testVariation TestVariation @relation(fields: [testVariationId], references: [id])
53+
merge Boolean @default(false)
54+
updatedAt DateTime @updatedAt
55+
createdAt DateTime @default(now())
56+
// Test variation data
57+
name String @default("")
58+
browser String?
59+
device String?
60+
os String?
61+
viewport String?
62+
baselineName String?
63+
comment String?
64+
baseline Baseline?
65+
branchName String @default("master")
66+
baselineBranchName String?
67+
ignoreAreas String @default("[]")
68+
tempIgnoreAreas String @default("[]")
69+
}
70+
71+
model TestVariation {
72+
id String @id @default(uuid())
73+
name String
74+
branchName String @default("master")
75+
browser String?
76+
device String?
77+
os String?
78+
viewport String?
79+
baselineName String?
80+
ignoreAreas String @default("[]")
81+
projectId String
82+
project Project @relation(fields: [projectId], references: [id])
83+
testRuns TestRun[]
84+
baselines Baseline[]
85+
comment String?
86+
updatedAt DateTime @updatedAt
87+
createdAt DateTime @default(now())
88+
89+
@@unique([projectId, name, browser, device, os, viewport, branchName])
90+
}
91+
92+
model Baseline {
93+
id String @id @default(uuid())
94+
baselineName String
95+
testVariationId String
96+
testVariation TestVariation @relation(fields: [testVariationId], references: [id])
97+
testRunId String?
98+
testRun TestRun? @relation(fields: [testRunId], references: [id])
99+
updatedAt DateTime @updatedAt
100+
createdAt DateTime @default(now())
101+
}
102+
103+
model User {
104+
id String @id @default(uuid())
105+
email String @unique
106+
password String
107+
firstName String?
108+
lastName String?
109+
apiKey String @unique
110+
isActive Boolean @default(true)
111+
builds Build[]
112+
updatedAt DateTime @updatedAt
113+
createdAt DateTime @default(now())
114+
}
115+
116+
enum TestStatus {
117+
failed
118+
new
119+
ok
120+
unresolved
121+
approved
122+
autoApproved
123+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": "0.3.14-fixed",
3+
"steps": [
4+
{
5+
"tag": "UpdateEnum",
6+
"enum": "TestStatus",
7+
"createdValues": [
8+
"autoApproved"
9+
]
10+
}
11+
]
12+
}

prisma/migrations/migrate.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
20201007145002-builds-counter
1313
20201115155739-ci-build-id-added
1414
20201201211711-test-run--temp-ignore-areas-added
15-
20210118201534-build--project-id---ci-build-id-constraint
15+
20210118201534-build--project-id---ci-build-id-constraint
16+
20210130115922-test-run-auto-approve-status-added

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,5 @@ enum TestStatus {
119119
ok
120120
unresolved
121121
approved
122+
autoApproved
122123
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,8 @@ 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);
517519

518520
const result = await service.create(testVariation, createTestRequestDto);
519521

@@ -564,6 +566,20 @@ describe('TestRunsService', () => {
564566
},
565567
]);
566568
expect(saveDiffResultMock).toHaveBeenCalledWith(testRun.id, diffResult);
569+
expect(tryAutoApproveBasedOnHistory).toHaveBeenCalledWith(testVariation, testRunWithResult, image, [
570+
{
571+
x: 3,
572+
y: 4,
573+
width: 500,
574+
height: 600,
575+
},
576+
{
577+
x: 1,
578+
y: 2,
579+
width: 100,
580+
height: 200,
581+
},
582+
]);
567583
expect(eventTestRunCreatedMock).toHaveBeenCalledWith(testRunWithResult);
568584
expect(result).toBe(testRunWithResult);
569585
});

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

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { forwardRef, Inject, Injectable } from '@nestjs/common';
1+
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
22
import { PNG } from 'pngjs';
33
import Pixelmatch from 'pixelmatch';
44
import { CreateTestRequestDto } from './dto/create-test-request.dto';
@@ -14,9 +14,12 @@ import { TestVariationsService } from '../test-variations/test-variations.servic
1414
import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto';
1515
import { TestRunDto } from './dto/testRun.dto';
1616
import { PaginatedTestRunDto } from './dto/testRun-paginated.dto';
17+
import { getTestVariationUniqueData } from '../utils';
1718

1819
@Injectable()
1920
export class TestRunsService {
21+
private readonly logger: Logger = new Logger(TestRunsService.name);
22+
2023
constructor(
2124
@Inject(forwardRef(() => TestVariationsService))
2225
private testVariationService: TestVariationsService,
@@ -81,7 +84,8 @@ export class TestRunsService {
8184
return new TestRunResultDto(testRun, testVariation);
8285
}
8386

84-
async approve(id: string, merge: boolean): Promise<TestRun> {
87+
async approve(id: string, merge: boolean, autoApprove?: boolean): Promise<TestRun> {
88+
const status = autoApprove ? TestStatus.autoApproved : TestStatus.approved;
8589
const testRun = await this.findOne(id);
8690

8791
// save new baseline
@@ -92,7 +96,7 @@ export class TestRunsService {
9296
testRunUpdated = await this.prismaService.testRun.update({
9397
where: { id },
9498
data: {
95-
status: TestStatus.approved,
99+
status,
96100
testVariation: {
97101
update: {
98102
baselineName,
@@ -115,11 +119,7 @@ export class TestRunsService {
115119
data: {
116120
project: { connect: { id: testRun.testVariation.projectId } },
117121
baselineName,
118-
name: testRun.name,
119-
browser: testRun.browser,
120-
device: testRun.device,
121-
os: testRun.os,
122-
viewport: testRun.viewport,
122+
...getTestVariationUniqueData(testRun),
123123
ignoreAreas: testRun.ignoreAreas,
124124
comment: testRun.comment,
125125
branchName: testRun.branchName,
@@ -141,7 +141,7 @@ export class TestRunsService {
141141
testRunUpdated = await this.prismaService.testRun.update({
142142
where: { id },
143143
data: {
144-
status: TestStatus.approved,
144+
status,
145145
testVariation: {
146146
connect: { id: newTestVariation.id },
147147
},
@@ -209,11 +209,7 @@ export class TestRunsService {
209209
id: createTestRequestDto.buildId,
210210
},
211211
},
212-
name: testVariation.name,
213-
browser: testVariation.browser,
214-
device: testVariation.device,
215-
os: testVariation.os,
216-
viewport: testVariation.viewport,
212+
...getTestVariationUniqueData(testVariation),
217213
baselineName: testVariation.baselineName,
218214
baselineBranchName: testVariation.branchName,
219215
ignoreAreas: testVariation.ignoreAreas,
@@ -235,7 +231,10 @@ export class TestRunsService {
235231
}
236232
const diffResult = this.getDiff(baseline, image, testRun.diffTollerancePercent, ignoreAreas);
237233

238-
const testRunWithResult = await this.saveDiffResult(testRun.id, diffResult);
234+
let testRunWithResult = await this.saveDiffResult(testRun.id, diffResult);
235+
236+
testRunWithResult = await this.tryAutoApproveBasedOnHistory(testVariation, testRunWithResult, image, ignoreAreas);
237+
239238
this.eventsGateway.testRunCreated(testRunWithResult);
240239
return testRunWithResult;
241240
}
@@ -334,4 +333,70 @@ export class TestRunsService {
334333
});
335334
return image.data;
336335
}
336+
337+
private async tryAutoApproveBasedOnHistory(
338+
testVariation: TestVariation,
339+
testRun: TestRun,
340+
image: PNG,
341+
ignoreAreas: IgnoreAreaDto[]
342+
): 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+
});
353+
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+
);
363+
364+
const approvedTestVariation = await this.prismaService.testVariation.findUnique({
365+
where: {
366+
id: approvedTestRun.testVariationId,
367+
},
368+
});
369+
370+
const approvedBaseline = this.staticService.getImage(approvedTestVariation.baselineName);
371+
const diffResult = this.getDiff(approvedBaseline, image, testRun.diffTollerancePercent, ignoreAreas);
372+
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+
}
394+
395+
if (autoApproved) {
396+
return this.approve(testRun.id, false, true);
397+
}
398+
this.logger.log(`Cannot auto approve testRun: ${testRun.id}`);
399+
}
400+
return testRun;
401+
}
337402
}

0 commit comments

Comments
 (0)