From a18e060bd61f597b4eb046d3c10fd7fa170e9800 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Thu, 25 Mar 2021 09:41:33 +0200 Subject: [PATCH 01/11] migration added --- .../README.md | 46 +++++++ .../schema.prisma | 127 ++++++++++++++++++ .../steps.json | 104 ++++++++++++++ prisma/migrations/migrate.lock | 3 +- prisma/schema.prisma | 4 + 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md create mode 100644 prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma create mode 100644 prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json diff --git a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md new file mode 100644 index 00000000..4c5e2569 --- /dev/null +++ b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md @@ -0,0 +1,46 @@ +# Migration `20210324150944-main-to-feature-test-run-self-relation-added` + +This migration has been generated by Pavel Strunkin at 3/24/2021, 5:09:44 PM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +ALTER TABLE "TestRun" ADD COLUMN "mainBranchTestRunId" TEXT + +CREATE UNIQUE INDEX "TestRun_mainBranchTestRunId_unique" ON "TestRun"("mainBranchTestRunId") + +ALTER TABLE "TestRun" ADD FOREIGN KEY("mainBranchTestRunId")REFERENCES "TestRun"("id") ON DELETE SET NULL ON UPDATE CASCADE +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration 20210228121726-test-run--nullable-test-variation-id..20210324150944-main-to-feature-test-run-self-relation-added +--- datamodel.dml ++++ datamodel.dml +@@ -3,9 +3,9 @@ + } + datasource db { + provider = "postgresql" +- url = "***" ++ url = "***" + } + model Build { + id String @id @default(uuid()) +@@ -65,8 +65,12 @@ + branchName String @default("master") + baselineBranchName String? + ignoreAreas String @default("[]") + tempIgnoreAreas String @default("[]") ++ // autoApprove ++ mainBranchTestRunId String? ++ mainBranchTestRun TestRun? @relation("MainToFeatureTestRun", fields: [mainBranchTestRunId], references: [id]) ++ featureTestRun TestRun? @relation("MainToFeatureTestRun") + } + model TestVariation { + id String @id @default(uuid()) +``` + + diff --git a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma new file mode 100644 index 00000000..ebb36a62 --- /dev/null +++ b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma @@ -0,0 +1,127 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = "***" +} + +model Build { + id String @id @default(uuid()) + ciBuildId String? + number Int? + branchName String? + status String? + testRuns TestRun[] + projectId String + project Project @relation(fields: [projectId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) + userId String? + isRunning Boolean? + + @@unique([projectId, ciBuildId]) +} + +model Project { + id String @id @default(uuid()) + name String + mainBranchName String @default("master") + builds Build[] + buildsCounter Int @default(0) + testVariations TestVariation[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + + @@unique([name]) +} + +model TestRun { + id String @id @default(uuid()) + imageName String + diffName String? + diffPercent Float? + diffTollerancePercent Float @default(0) + pixelMisMatchCount Int? + status TestStatus + buildId String + build Build @relation(fields: [buildId], references: [id]) + testVariationId String? + testVariation TestVariation? @relation(fields: [testVariationId], references: [id]) + merge Boolean @default(false) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + // Test variation data + name String @default("") + browser String? + device String? + os String? + viewport String? + baselineName String? + comment String? + baseline Baseline? + branchName String @default("master") + baselineBranchName String? + ignoreAreas String @default("[]") + tempIgnoreAreas String @default("[]") + // autoApprove + mainBranchTestRunId String? + mainBranchTestRun TestRun? @relation("MainToFeatureTestRun", fields: [mainBranchTestRunId], references: [id]) + featureTestRun TestRun? @relation("MainToFeatureTestRun") +} + +model TestVariation { + id String @id @default(uuid()) + name String + branchName String @default("master") + browser String? + device String? + os String? + viewport String? + baselineName String? + ignoreAreas String @default("[]") + projectId String + project Project @relation(fields: [projectId], references: [id]) + testRuns TestRun[] + baselines Baseline[] + comment String? + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + + @@unique([projectId, name, browser, device, os, viewport, branchName]) +} + +model Baseline { + id String @id @default(uuid()) + baselineName String + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + testRunId String? + testRun TestRun? @relation(fields: [testRunId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model User { + id String @id @default(uuid()) + email String @unique + password String + firstName String? + lastName String? + apiKey String @unique + isActive Boolean @default(true) + builds Build[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +enum TestStatus { + failed + new + ok + unresolved + approved + autoApproved +} diff --git a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json new file mode 100644 index 00000000..3f11f395 --- /dev/null +++ b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json @@ -0,0 +1,104 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "CreateField", + "model": "TestRun", + "field": "mainBranchTestRunId", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "mainBranchTestRun", + "type": "TestRun", + "arity": "Optional" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestRun", + "field": "mainBranchTestRun" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestRun", + "field": "mainBranchTestRun" + }, + "directive": "relation" + }, + "argument": "", + "value": "\"MainToFeatureTestRun\"" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestRun", + "field": "mainBranchTestRun" + }, + "directive": "relation" + }, + "argument": "fields", + "value": "[mainBranchTestRunId]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestRun", + "field": "mainBranchTestRun" + }, + "directive": "relation" + }, + "argument": "references", + "value": "[id]" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "featureTestRun", + "type": "TestRun", + "arity": "Optional" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestRun", + "field": "featureTestRun" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestRun", + "field": "featureTestRun" + }, + "directive": "relation" + }, + "argument": "", + "value": "\"MainToFeatureTestRun\"" + } + ] +} \ No newline at end of file diff --git a/prisma/migrations/migrate.lock b/prisma/migrations/migrate.lock index 3d5761f1..3865c8ed 100644 --- a/prisma/migrations/migrate.lock +++ b/prisma/migrations/migrate.lock @@ -14,4 +14,5 @@ 20201201211711-test-run--temp-ignore-areas-added 20210118201534-build--project-id---ci-build-id-constraint 20210130115922-test-run-auto-approve-status-added -20210228121726-test-run--nullable-test-variation-id \ No newline at end of file +20210228121726-test-run--nullable-test-variation-id +20210324150944-main-to-feature-test-run-self-relation-added \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f2571305..41791681 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -66,6 +66,10 @@ model TestRun { baselineBranchName String? ignoreAreas String @default("[]") tempIgnoreAreas String @default("[]") + // autoApprove + mainBranchTestRunId String? + mainBranchTestRun TestRun? @relation("MainToFeatureTestRun", fields: [mainBranchTestRunId], references: [id]) + featureTestRun TestRun? @relation("MainToFeatureTestRun") } model TestVariation { From 3881af1334a992fa963335233ebfd2d87a7d31c5 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 27 Mar 2021 23:41:27 +0200 Subject: [PATCH 02/11] @types/lodash added --- package-lock.json | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index 70556acf..38b8b76d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1430,6 +1430,12 @@ "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", + "dev": true + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", diff --git a/package.json b/package.json index 099ffdff..95855c23 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@types/cache-manager": "^2.10.3", "@types/express": "^4.17.7", "@types/jest": "26.0.14", + "@types/lodash": "^4.14.168", "@types/node": "^14.0.27", "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", From 11aa6ab24eecdd0df1826ee93617cfab2dbe1739 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 27 Mar 2021 23:56:25 +0200 Subject: [PATCH 03/11] migration rolled back --- .../README.md | 46 ------- .../schema.prisma | 127 ------------------ .../steps.json | 104 -------------- prisma/migrations/migrate.lock | 3 +- prisma/schema.prisma | 4 - src/builds/builds.service.ts | 34 ++--- src/builds/dto/build.dto.ts | 5 +- src/test-runs/test-runs.service.ts | 59 +++++++- 8 files changed, 76 insertions(+), 306 deletions(-) delete mode 100644 prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md delete mode 100644 prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma delete mode 100644 prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json diff --git a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md deleted file mode 100644 index 4c5e2569..00000000 --- a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Migration `20210324150944-main-to-feature-test-run-self-relation-added` - -This migration has been generated by Pavel Strunkin at 3/24/2021, 5:09:44 PM. -You can check out the [state of the schema](./schema.prisma) after the migration. - -## Database Steps - -```sql -ALTER TABLE "TestRun" ADD COLUMN "mainBranchTestRunId" TEXT - -CREATE UNIQUE INDEX "TestRun_mainBranchTestRunId_unique" ON "TestRun"("mainBranchTestRunId") - -ALTER TABLE "TestRun" ADD FOREIGN KEY("mainBranchTestRunId")REFERENCES "TestRun"("id") ON DELETE SET NULL ON UPDATE CASCADE -``` - -## Changes - -```diff -diff --git schema.prisma schema.prisma -migration 20210228121726-test-run--nullable-test-variation-id..20210324150944-main-to-feature-test-run-self-relation-added ---- datamodel.dml -+++ datamodel.dml -@@ -3,9 +3,9 @@ - } - datasource db { - provider = "postgresql" -- url = "***" -+ url = "***" - } - model Build { - id String @id @default(uuid()) -@@ -65,8 +65,12 @@ - branchName String @default("master") - baselineBranchName String? - ignoreAreas String @default("[]") - tempIgnoreAreas String @default("[]") -+ // autoApprove -+ mainBranchTestRunId String? -+ mainBranchTestRun TestRun? @relation("MainToFeatureTestRun", fields: [mainBranchTestRunId], references: [id]) -+ featureTestRun TestRun? @relation("MainToFeatureTestRun") - } - model TestVariation { - id String @id @default(uuid()) -``` - - diff --git a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma deleted file mode 100644 index ebb36a62..00000000 --- a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/schema.prisma +++ /dev/null @@ -1,127 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = "***" -} - -model Build { - id String @id @default(uuid()) - ciBuildId String? - number Int? - branchName String? - status String? - testRuns TestRun[] - projectId String - project Project @relation(fields: [projectId], references: [id]) - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) - user User? @relation(fields: [userId], references: [id]) - userId String? - isRunning Boolean? - - @@unique([projectId, ciBuildId]) -} - -model Project { - id String @id @default(uuid()) - name String - mainBranchName String @default("master") - builds Build[] - buildsCounter Int @default(0) - testVariations TestVariation[] - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) - - @@unique([name]) -} - -model TestRun { - id String @id @default(uuid()) - imageName String - diffName String? - diffPercent Float? - diffTollerancePercent Float @default(0) - pixelMisMatchCount Int? - status TestStatus - buildId String - build Build @relation(fields: [buildId], references: [id]) - testVariationId String? - testVariation TestVariation? @relation(fields: [testVariationId], references: [id]) - merge Boolean @default(false) - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) - // Test variation data - name String @default("") - browser String? - device String? - os String? - viewport String? - baselineName String? - comment String? - baseline Baseline? - branchName String @default("master") - baselineBranchName String? - ignoreAreas String @default("[]") - tempIgnoreAreas String @default("[]") - // autoApprove - mainBranchTestRunId String? - mainBranchTestRun TestRun? @relation("MainToFeatureTestRun", fields: [mainBranchTestRunId], references: [id]) - featureTestRun TestRun? @relation("MainToFeatureTestRun") -} - -model TestVariation { - id String @id @default(uuid()) - name String - branchName String @default("master") - browser String? - device String? - os String? - viewport String? - baselineName String? - ignoreAreas String @default("[]") - projectId String - project Project @relation(fields: [projectId], references: [id]) - testRuns TestRun[] - baselines Baseline[] - comment String? - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) - - @@unique([projectId, name, browser, device, os, viewport, branchName]) -} - -model Baseline { - id String @id @default(uuid()) - baselineName String - testVariationId String - testVariation TestVariation @relation(fields: [testVariationId], references: [id]) - testRunId String? - testRun TestRun? @relation(fields: [testRunId], references: [id]) - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) -} - -model User { - id String @id @default(uuid()) - email String @unique - password String - firstName String? - lastName String? - apiKey String @unique - isActive Boolean @default(true) - builds Build[] - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) -} - -enum TestStatus { - failed - new - ok - unresolved - approved - autoApproved -} diff --git a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json b/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json deleted file mode 100644 index 3f11f395..00000000 --- a/prisma/migrations/20210324150944-main-to-feature-test-run-self-relation-added/steps.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "version": "0.3.14-fixed", - "steps": [ - { - "tag": "CreateField", - "model": "TestRun", - "field": "mainBranchTestRunId", - "type": "String", - "arity": "Optional" - }, - { - "tag": "CreateField", - "model": "TestRun", - "field": "mainBranchTestRun", - "type": "TestRun", - "arity": "Optional" - }, - { - "tag": "CreateDirective", - "location": { - "path": { - "tag": "Field", - "model": "TestRun", - "field": "mainBranchTestRun" - }, - "directive": "relation" - } - }, - { - "tag": "CreateArgument", - "location": { - "tag": "Directive", - "path": { - "tag": "Field", - "model": "TestRun", - "field": "mainBranchTestRun" - }, - "directive": "relation" - }, - "argument": "", - "value": "\"MainToFeatureTestRun\"" - }, - { - "tag": "CreateArgument", - "location": { - "tag": "Directive", - "path": { - "tag": "Field", - "model": "TestRun", - "field": "mainBranchTestRun" - }, - "directive": "relation" - }, - "argument": "fields", - "value": "[mainBranchTestRunId]" - }, - { - "tag": "CreateArgument", - "location": { - "tag": "Directive", - "path": { - "tag": "Field", - "model": "TestRun", - "field": "mainBranchTestRun" - }, - "directive": "relation" - }, - "argument": "references", - "value": "[id]" - }, - { - "tag": "CreateField", - "model": "TestRun", - "field": "featureTestRun", - "type": "TestRun", - "arity": "Optional" - }, - { - "tag": "CreateDirective", - "location": { - "path": { - "tag": "Field", - "model": "TestRun", - "field": "featureTestRun" - }, - "directive": "relation" - } - }, - { - "tag": "CreateArgument", - "location": { - "tag": "Directive", - "path": { - "tag": "Field", - "model": "TestRun", - "field": "featureTestRun" - }, - "directive": "relation" - }, - "argument": "", - "value": "\"MainToFeatureTestRun\"" - } - ] -} \ No newline at end of file diff --git a/prisma/migrations/migrate.lock b/prisma/migrations/migrate.lock index 3865c8ed..3d5761f1 100644 --- a/prisma/migrations/migrate.lock +++ b/prisma/migrations/migrate.lock @@ -14,5 +14,4 @@ 20201201211711-test-run--temp-ignore-areas-added 20210118201534-build--project-id---ci-build-id-constraint 20210130115922-test-run-auto-approve-status-added -20210228121726-test-run--nullable-test-variation-id -20210324150944-main-to-feature-test-run-self-relation-added \ No newline at end of file +20210228121726-test-run--nullable-test-variation-id \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 41791681..f2571305 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -66,10 +66,6 @@ model TestRun { baselineBranchName String? ignoreAreas String @default("[]") tempIgnoreAreas String @default("[]") - // autoApprove - mainBranchTestRunId String? - mainBranchTestRun TestRun? @relation("MainToFeatureTestRun", fields: [mainBranchTestRunId], references: [id]) - featureTestRun TestRun? @relation("MainToFeatureTestRun") } model TestVariation { diff --git a/src/builds/builds.service.ts b/src/builds/builds.service.ts index 3122dab1..14fd29ea 100644 --- a/src/builds/builds.service.ts +++ b/src/builds/builds.service.ts @@ -21,14 +21,16 @@ export class BuildsService { ) {} async findOne(id: string): Promise { - return this.prismaService.build - .findUnique({ + const [build, testRuns] = await Promise.all([ + this.prismaService.build.findUnique({ where: { id }, - include: { - testRuns: true, - }, - }) - .then((build) => new BuildDto(build)); + }), + this.testRunsService.findMany(id), + ]); + return new BuildDto({ + ...build, + testRuns, + }); } async findMany(projectId: string, take: number, skip: number): Promise { @@ -38,15 +40,12 @@ export class BuildsService { where: { projectId }, take, skip, - include: { - testRuns: true, - }, orderBy: { createdAt: 'desc' }, }), ]); return { - data: buildList.map((build) => new BuildDto(build)), + data: await Promise.all(buildList.map((build) => this.findOne(build.id))), total, take, skip, @@ -101,15 +100,12 @@ export class BuildsService { } async update(id: string, modifyBuildDto: ModifyBuildDto): Promise { - const build = await this.prismaService.build.update({ + await this.prismaService.build.update({ where: { id }, - include: { - testRuns: true, - }, data: modifyBuildDto, }); this.eventsGateway.buildUpdated(id); - return new BuildDto(build); + return this.findOne(id); } async remove(id: string): Promise { @@ -141,10 +137,8 @@ export class BuildsService { }, }); - build.testRuns = await Promise.all( - build.testRuns.map((testRun) => this.testRunsService.approve(testRun.id, merge)) - ); + await Promise.all(build.testRuns.map((testRun) => this.testRunsService.approve(testRun.id, merge))); - return new BuildDto(build); + return this.findOne(id); } } diff --git a/src/builds/dto/build.dto.ts b/src/builds/dto/build.dto.ts index ed6e6599..cc67073c 100644 --- a/src/builds/dto/build.dto.ts +++ b/src/builds/dto/build.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Build, TestRun, TestStatus } from '@prisma/client'; +import { Build, TestStatus } from '@prisma/client'; +import { TestRunDto } from 'src/test-runs/dto/testRun.dto'; export class BuildDto { @ApiProperty() @@ -40,7 +41,7 @@ export class BuildDto { @ApiProperty() merge: boolean; - constructor(build: Build & { testRuns?: TestRun[] }) { + constructor(build: Build & { testRuns?: TestRunDto[] }) { this.id = build.id; this.ciBuildId = build.ciBuildId; this.number = build.number; diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index 20a33bb3..462c52f3 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -5,7 +5,7 @@ import { CreateTestRequestDto } from './dto/create-test-request.dto'; import { IgnoreAreaDto } from './dto/ignore-area.dto'; import { StaticService } from '../shared/static/static.service'; import { PrismaService } from '../prisma/prisma.service'; -import { Baseline, TestRun, TestStatus, TestVariation } from '@prisma/client'; +import { Baseline, Project, TestRun, TestStatus, TestVariation } from '@prisma/client'; import { DiffResult } from './diffResult'; import { EventsGateway } from '../shared/events/events.gateway'; import { CommentDto } from '../shared/dto/comment.dto'; @@ -68,9 +68,11 @@ export class TestRunsService { // create test run result const testRun = await this.create(testVariation, createTestRequestDto); + const testRunAgainstMainBranch = await this.createAgainstMainBranch({ testVariation, testRun }); // calculate diff let testRunWithResult = await this.calculateDiff(testRun); + testRunAgainstMainBranch && (await this.calculateDiff(testRunAgainstMainBranch)); // try auto approve testRunWithResult = await this.tryAutoApproveByPastBaselines(testVariation, testRunWithResult); @@ -231,6 +233,61 @@ export class TestRunsService { return testRun; } + /** + * Emulate merge feature to main branch for autoApprove logic + */ + async createAgainstMainBranch({ + testVariation, + testRun, + }: { + testVariation: TestVariation; + testRun: TestRun; + }): Promise { + const project: Project = await this.prismaService.project.findUnique({ where: { id: testVariation.projectId } }); + + if ( + !process.env.AUTO_APPROVE_BASED_ON_HISTORY || + testRun.branchName === project.mainBranchName || + testRun.branchName !== testVariation.branchName + ) { + // create only for features branches that have own baseline + return; + } + this.logger.log(`Creating testRun agains main branch for ${testRun.id}`); + const [mainBranchTestVariation] = await this.prismaService.testVariation.findMany({ + where: { + projectId: project.id, + branchName: project.mainBranchName, + ...getTestVariationUniqueData(testVariation), + }, + }); + return this.prismaService.testRun.create({ + data: { + imageName: testRun.imageName, + ...getTestVariationUniqueData(testVariation), + baselineName: mainBranchTestVariation.baselineName, + baselineBranchName: mainBranchTestVariation.branchName, + ignoreAreas: testVariation.ignoreAreas, + tempIgnoreAreas: testRun.ignoreAreas, + comment: testVariation.comment, + diffTollerancePercent: testRun.diffTollerancePercent, + branchName: testRun.branchName, + merge: false, + status: TestStatus.new, + testVariation: { + connect: { + id: mainBranchTestVariation.id, + }, + }, + build: { + connect: { + id: testRun.buildId, + }, + }, + }, + }); + } + async delete(id: string): Promise { const testRun = await this.findOne(id); From 206755bd3b8d6e9225c19cfcabccdf1691f15d12 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Mon, 29 Mar 2021 00:12:05 +0300 Subject: [PATCH 04/11] unit tests are udpated --- src/_data_/index.ts | 10 +++++++ src/builds/builds.service.spec.ts | 28 +++++-------------- src/prisma/prisma.service.ts | 2 +- src/test-runs/test-runs.service.spec.ts | 10 +++++-- src/test-runs/test-runs.service.ts | 11 +++++--- .../test-variations.service.spec.ts | 2 +- 6 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 src/_data_/index.ts diff --git a/src/_data_/index.ts b/src/_data_/index.ts new file mode 100644 index 00000000..984c0d02 --- /dev/null +++ b/src/_data_/index.ts @@ -0,0 +1,10 @@ +import { Project } from '@prisma/client'; + +export const TEST_PROJECT: Project = { + id: '1', + name: 'Test Project', + buildsCounter: 2, + mainBranchName: 'master', + createdAt: new Date(), + updatedAt: new Date(), +}; diff --git a/src/builds/builds.service.spec.ts b/src/builds/builds.service.spec.ts index e478109f..70257949 100644 --- a/src/builds/builds.service.spec.ts +++ b/src/builds/builds.service.spec.ts @@ -21,6 +21,7 @@ const initService = async ({ buildCountMock = jest.fn(), testRunDeleteMock = jest.fn(), testRunApproveMock = jest.fn(), + testRunFindManyMock = jest.fn(), eventsBuildUpdatedMock = jest.fn(), eventsBuildCreatedMock = jest.fn(), projectFindOneMock = jest.fn(), @@ -51,6 +52,7 @@ const initService = async ({ useValue: { approve: testRunApproveMock, delete: testRunDeleteMock, + findMany: testRunFindManyMock, }, }, { @@ -136,17 +138,13 @@ describe('BuildsService', () => { it('findOne', async () => { const buildFindUniqueMock = jest.fn().mockResolvedValueOnce(build); + const testRunFindManyMock = jest.fn().mockResolvedValueOnce(build.testRuns); mocked(BuildDto).mockReturnValueOnce(buildDto); - service = await initService({ buildFindUniqueMock }); + service = await initService({ buildFindUniqueMock, testRunFindManyMock }); const result = await service.findOne('someId'); - expect(buildFindUniqueMock).toHaveBeenCalledWith({ - where: { id: 'someId' }, - include: { - testRuns: true, - }, - }); + expect(mocked(BuildDto)).toHaveBeenCalledWith({ ...build, testRuns: build.testRuns }); expect(result).toBe(buildDto); }); @@ -163,7 +161,6 @@ describe('BuildsService', () => { where: { projectId }, }); expect(buildFindManyMock).toHaveBeenCalledWith({ - include: { testRuns: true }, take: 10, skip: 20, orderBy: { createdAt: 'desc' }, @@ -290,9 +287,6 @@ describe('BuildsService', () => { expect(buildUpdateMock).toHaveBeenCalledWith({ where: { id }, - include: { - testRuns: true, - }, data: { isRunning: false }, }); expect(eventsBuildUpdatedMock).toHaveBeenCalledWith(id); @@ -305,8 +299,8 @@ describe('BuildsService', () => { ...build.testRuns[0], status: TestStatus.approved, }); - mocked(BuildDto).mockReturnValueOnce(buildDto); service = await initService({ buildFindUniqueMock, testRunApproveMock }); + service.findOne = jest.fn(); await service.approve('someId', true); @@ -323,14 +317,6 @@ describe('BuildsService', () => { }, }); expect(testRunApproveMock).toHaveBeenCalledWith(build.testRuns[0].id, true); - expect(mocked(BuildDto)).toHaveBeenCalledWith({ - ...build, - testRuns: [ - { - ...build.testRuns[0], - status: TestStatus.approved, - }, - ], - }); + expect(service.findOne).toHaveBeenCalledWith('someId'); }); }); diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 48715900..c2caebf1 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -1,4 +1,4 @@ -import { Injectable, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common'; +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() diff --git a/src/test-runs/test-runs.service.spec.ts b/src/test-runs/test-runs.service.spec.ts index 5bfbd55b..f68b65b7 100644 --- a/src/test-runs/test-runs.service.spec.ts +++ b/src/test-runs/test-runs.service.spec.ts @@ -13,8 +13,9 @@ import { EventsGateway } from '../shared/events/events.gateway'; import { CommentDto } from '../shared/dto/comment.dto'; import { TestVariationsService } from '../test-variations/test-variations.service'; import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto'; -import { TestRunDto } from './dto/testRun.dto'; +import { TestRunDto } from '../test-runs/dto/testRun.dto'; import { BuildsService } from '../builds/builds.service'; +import { TEST_PROJECT } from '../_data_'; jest.mock('pixelmatch'); jest.mock('./dto/testRunResult.dto'); @@ -38,7 +39,7 @@ const initService = async ({ testVariationFindManyMock = jest.fn(), baselineCreateMock = jest.fn(), testVariationFindOrCreateMock = jest.fn(), - projectFindOneMock = jest.fn(), + projectFindUniqueMock = jest.fn(), }) => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -62,7 +63,7 @@ const initService = async ({ create: baselineCreateMock, }, project: { - findOne: projectFindOneMock, + findUnique: projectFindUniqueMock, }, }, }, @@ -1006,6 +1007,7 @@ describe('TestRunsService', () => { }); it('postTestRun', async () => { + delete process.env.AUTO_APPROVE_BASED_ON_HISTORY; const createTestRequestDto: CreateTestRequestDto = { buildId: 'buildId', projectId: 'projectId', @@ -1057,11 +1059,13 @@ describe('TestRunsService', () => { branchName: 'develop', merge: false, }; + const projectFindUniqueMock = jest.fn().mockResolvedValueOnce(TEST_PROJECT); const testVariationFindOrCreateMock = jest.fn().mockResolvedValueOnce(testVariation); const testRunFindManyMock = jest.fn().mockResolvedValueOnce([testRun]); const deleteMock = jest.fn(); const createMock = jest.fn().mockResolvedValueOnce(testRun); const service = await initService({ + projectFindUniqueMock, testVariationFindOrCreateMock, testRunFindManyMock, }); diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index 462c52f3..39f64635 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -50,6 +50,9 @@ export class TestRunsService { } async postTestRun(createTestRequestDto: CreateTestRequestDto): Promise { + const project: Project = await this.prismaService.project.findUnique({ + where: { id: createTestRequestDto.projectId }, + }); const baselineData = convertBaselineDataToQuery(createTestRequestDto); // creates variatioin if does not exist @@ -68,7 +71,7 @@ export class TestRunsService { // create test run result const testRun = await this.create(testVariation, createTestRequestDto); - const testRunAgainstMainBranch = await this.createAgainstMainBranch({ testVariation, testRun }); + const testRunAgainstMainBranch = await this.createAgainstMainBranch({ project, testVariation, testRun }); // calculate diff let testRunWithResult = await this.calculateDiff(testRun); @@ -236,15 +239,15 @@ export class TestRunsService { /** * Emulate merge feature to main branch for autoApprove logic */ - async createAgainstMainBranch({ + private async createAgainstMainBranch({ + project, testVariation, testRun, }: { + project: Project; testVariation: TestVariation; testRun: TestRun; }): Promise { - const project: Project = await this.prismaService.project.findUnique({ where: { id: testVariation.projectId } }); - if ( !process.env.AUTO_APPROVE_BASED_ON_HISTORY || testRun.branchName === project.mainBranchName || diff --git a/src/test-variations/test-variations.service.spec.ts b/src/test-variations/test-variations.service.spec.ts index 1b4f2b9a..009b1819 100644 --- a/src/test-variations/test-variations.service.spec.ts +++ b/src/test-variations/test-variations.service.spec.ts @@ -4,7 +4,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { CreateTestRequestDto } from '../test-runs/dto/create-test-request.dto'; import { StaticService } from '../shared/static/static.service'; import { IgnoreAreaDto } from '../test-runs/dto/ignore-area.dto'; -import { TestVariation, Baseline, Project, Build, TestRun } from '@prisma/client'; +import { TestVariation, Baseline, Project, Build } from '@prisma/client'; import { CommentDto } from '../shared/dto/comment.dto'; import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto'; import { PNG } from 'pngjs'; From 6a66335407e8760873657c6134b786fea37776cc Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Mon, 5 Apr 2021 22:54:45 +0300 Subject: [PATCH 05/11] TestVariation empty tags as default instead null --- prisma/entrypoint.sh | 3 + prisma/manual_migrations.ts | 83 +++++++++++ .../README.md | 54 ++++++++ .../schema.prisma | 123 +++++++++++++++++ .../steps.json | 129 ++++++++++++++++++ prisma/migrations/migrate.lock | 3 +- prisma/schema.prisma | 8 +- src/test-runs/test-runs.service.ts | 25 ++-- .../test-variations.service.ts | 21 +-- src/utils/index.ts | 8 +- 10 files changed, 430 insertions(+), 27 deletions(-) create mode 100644 prisma/manual_migrations.ts create mode 100644 prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/README.md create mode 100644 prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/schema.prisma create mode 100644 prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/steps.json diff --git a/prisma/entrypoint.sh b/prisma/entrypoint.sh index c74152ab..b60ad51e 100644 --- a/prisma/entrypoint.sh +++ b/prisma/entrypoint.sh @@ -5,6 +5,9 @@ set -e echo Start applying migrations... +# apply manual migration +npx ts-node manual_migrations.ts + # apply migration npx prisma migrate up -c --auto-approve --experimental diff --git a/prisma/manual_migrations.ts b/prisma/manual_migrations.ts new file mode 100644 index 00000000..4efc6e92 --- /dev/null +++ b/prisma/manual_migrations.ts @@ -0,0 +1,83 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient({ + // log: ['query'], +}); + +async function dbSchemaExists(): Promise { + return prisma.$queryRaw`SELECT EXISTS + ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = '_Migration' + )` + .catch(() => false) + .then((result) => (result as Array<{ exists: boolean }>).shift()?.exists); +} + +async function shouldSkipMigration(migrationKey: string): Promise { + return prisma.$queryRaw` + SELECT revision, status from "public"."_Migration" + WHERE "name" like ${`%${migrationKey}%`} + AND "status" = 'MigrationSuccess' + LIMIT 1`.then((migration) => migration?.length > 0); +} + +//https://github.com/Visual-Regression-Tracker/Visual-Regression-Tracker/issues/243 +async function setEmptyTestVariationTags_github_243() { + const migrationKey = 'github_243'; + if (await shouldSkipMigration(migrationKey)) { + console.info(`Skipping migration ${migrationKey}...`); + return; + } + console.info(`Going to apply migration ${migrationKey}...`); + const testVariations = await prisma.testVariation.findMany({ + where: { + OR: [ + { + os: null, + }, + { + device: null, + }, + { + browser: null, + }, + { + viewport: null, + }, + ], + }, + }); + + return Promise.all( + testVariations.map((testVariation) => + prisma.testVariation.update({ + where: { id: testVariation.id }, + data: { + os: testVariation.os ?? '', + device: testVariation.device ?? '', + browser: testVariation.browser ?? '', + viewport: testVariation.viewport ?? '', + }, + }) + ) + ).then(() => console.info(`Finished migration ${migrationKey}`)); +} + +async function manualMigrations() { + await prisma.$connect(); + if (await dbSchemaExists()) { + console.info('Apply migrations...'); + await setEmptyTestVariationTags_github_243(); + } else { + console.info('DB schema not found. Skipping manual migrations...'); + } + + await prisma.$disconnect(); +} + +manualMigrations() + .catch((e) => console.error('Cannot run manual migrations:', e)) + .finally(async () => await prisma.$disconnect()); diff --git a/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/README.md b/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/README.md new file mode 100644 index 00000000..866b9e46 --- /dev/null +++ b/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/README.md @@ -0,0 +1,54 @@ +# Migration `20210405171118-github_243-set-empty-test-variation-tags-instead-of-null` + +This migration has been generated by Pavel Strunkin at 4/5/2021, 8:11:18 PM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +ALTER TABLE "TestVariation" ALTER COLUMN "browser" SET NOT NULL, +ALTER COLUMN "browser" SET DEFAULT E'', +ALTER COLUMN "device" SET NOT NULL, +ALTER COLUMN "device" SET DEFAULT E'', +ALTER COLUMN "os" SET NOT NULL, +ALTER COLUMN "os" SET DEFAULT E'', +ALTER COLUMN "viewport" SET NOT NULL, +ALTER COLUMN "viewport" SET DEFAULT E'' +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration 20210228121726-test-run--nullable-test-variation-id..20210405171118-github_243-set-empty-test-variation-tags-instead-of-null +--- datamodel.dml ++++ datamodel.dml +@@ -3,9 +3,9 @@ + } + datasource db { + provider = "postgresql" +- url = "***" ++ url = "***" + } + model Build { + id String @id @default(uuid()) +@@ -71,12 +71,12 @@ + model TestVariation { + id String @id @default(uuid()) + name String + branchName String @default("master") +- browser String? +- device String? +- os String? +- viewport String? ++ browser String @default("") ++ device String @default("") ++ os String @default("") ++ viewport String @default("") + baselineName String? + ignoreAreas String @default("[]") + projectId String + project Project @relation(fields: [projectId], references: [id]) +``` + + diff --git a/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/schema.prisma b/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/schema.prisma new file mode 100644 index 00000000..9019c4ab --- /dev/null +++ b/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/schema.prisma @@ -0,0 +1,123 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = "***" +} + +model Build { + id String @id @default(uuid()) + ciBuildId String? + number Int? + branchName String? + status String? + testRuns TestRun[] + projectId String + project Project @relation(fields: [projectId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) + userId String? + isRunning Boolean? + + @@unique([projectId, ciBuildId]) +} + +model Project { + id String @id @default(uuid()) + name String + mainBranchName String @default("master") + builds Build[] + buildsCounter Int @default(0) + testVariations TestVariation[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + + @@unique([name]) +} + +model TestRun { + id String @id @default(uuid()) + imageName String + diffName String? + diffPercent Float? + diffTollerancePercent Float @default(0) + pixelMisMatchCount Int? + status TestStatus + buildId String + build Build @relation(fields: [buildId], references: [id]) + testVariationId String? + testVariation TestVariation? @relation(fields: [testVariationId], references: [id]) + merge Boolean @default(false) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + // Test variation data + name String @default("") + browser String? + device String? + os String? + viewport String? + baselineName String? + comment String? + baseline Baseline? + branchName String @default("master") + baselineBranchName String? + ignoreAreas String @default("[]") + tempIgnoreAreas String @default("[]") +} + +model TestVariation { + id String @id @default(uuid()) + name String + branchName String @default("master") + browser String @default("") + device String @default("") + os String @default("") + viewport String @default("") + baselineName String? + ignoreAreas String @default("[]") + projectId String + project Project @relation(fields: [projectId], references: [id]) + testRuns TestRun[] + baselines Baseline[] + comment String? + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + + @@unique([projectId, name, browser, device, os, viewport, branchName]) +} + +model Baseline { + id String @id @default(uuid()) + baselineName String + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + testRunId String? + testRun TestRun? @relation(fields: [testRunId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model User { + id String @id @default(uuid()) + email String @unique + password String + firstName String? + lastName String? + apiKey String @unique + isActive Boolean @default(true) + builds Build[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +enum TestStatus { + failed + new + ok + unresolved + approved + autoApproved +} diff --git a/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/steps.json b/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/steps.json new file mode 100644 index 00000000..92b2c8b6 --- /dev/null +++ b/prisma/migrations/20210405171118-github_243-set-empty-test-variation-tags-instead-of-null/steps.json @@ -0,0 +1,129 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "UpdateField", + "model": "TestVariation", + "field": "browser", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "browser" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "browser" + }, + "directive": "default" + }, + "argument": "", + "value": "\"\"" + }, + { + "tag": "UpdateField", + "model": "TestVariation", + "field": "device", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "device" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "device" + }, + "directive": "default" + }, + "argument": "", + "value": "\"\"" + }, + { + "tag": "UpdateField", + "model": "TestVariation", + "field": "os", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "os" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "os" + }, + "directive": "default" + }, + "argument": "", + "value": "\"\"" + }, + { + "tag": "UpdateField", + "model": "TestVariation", + "field": "viewport", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "viewport" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestVariation", + "field": "viewport" + }, + "directive": "default" + }, + "argument": "", + "value": "\"\"" + } + ] +} \ No newline at end of file diff --git a/prisma/migrations/migrate.lock b/prisma/migrations/migrate.lock index 3d5761f1..c0cb4268 100644 --- a/prisma/migrations/migrate.lock +++ b/prisma/migrations/migrate.lock @@ -14,4 +14,5 @@ 20201201211711-test-run--temp-ignore-areas-added 20210118201534-build--project-id---ci-build-id-constraint 20210130115922-test-run-auto-approve-status-added -20210228121726-test-run--nullable-test-variation-id \ No newline at end of file +20210228121726-test-run--nullable-test-variation-id +20210405171118-github_243-set-empty-test-variation-tags-instead-of-null \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f2571305..9bf4b29a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,10 +72,10 @@ model TestVariation { id String @id @default(uuid()) name String branchName String @default("master") - browser String? - device String? - os String? - viewport String? + browser String @default("") + device String @default("") + os String @default("") + viewport String @default("") baselineName String? ignoreAreas String @default("[]") projectId String diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index 39f64635..d2815a68 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -59,15 +59,15 @@ export class TestRunsService { const testVariation = await this.testVariationService.findOrCreate(createTestRequestDto.projectId, baselineData); // delete previous test run if exists - const [previousTestRun] = await this.prismaService.testRun.findMany({ + const previousTestRuns = await this.prismaService.testRun.findMany({ where: { - buildId: createTestRequestDto.buildId, - ...baselineData, + OR: [ + { buildId: createTestRequestDto.buildId, ...baselineData }, + { buildId: createTestRequestDto.buildId, ...baselineData, branchName: project.mainBranchName }, + ], }, }); - if (!!previousTestRun) { - await this.delete(previousTestRun.id); - } + await Promise.all(previousTestRuns.map((tr) => this.delete(tr.id))); // create test run result const testRun = await this.create(testVariation, createTestRequestDto); @@ -257,13 +257,18 @@ export class TestRunsService { return; } this.logger.log(`Creating testRun agains main branch for ${testRun.id}`); - const [mainBranchTestVariation] = await this.prismaService.testVariation.findMany({ + const mainBranchTestVariation = await this.prismaService.testVariation.findUnique({ where: { - projectId: project.id, - branchName: project.mainBranchName, - ...getTestVariationUniqueData(testVariation), + projectId_name_browser_device_os_viewport_branchName: { + projectId: project.id, + branchName: project.mainBranchName, + ...getTestVariationUniqueData(testVariation), + }, }, }); + if (!mainBranchTestVariation) { + return; + } return this.prismaService.testRun.create({ data: { imageName: testRun.imageName, diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index 6ac60110..aa2bd22e 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -42,20 +42,25 @@ export class TestVariationsService { async findOrCreate(projectId: string, baselineData: BaselineDataDto): Promise { const project = await this.prismaService.project.findUnique({ where: { id: projectId } }); - const [[mainBranchTestVariation], [currentBranchTestVariation]] = await Promise.all([ + const [mainBranchTestVariation, currentBranchTestVariation] = await Promise.all([ // search main branch variation - this.prismaService.testVariation.findMany({ + this.prismaService.testVariation.findUnique({ where: { - projectId, - branchName: project.mainBranchName, - ...getTestVariationUniqueData(baselineData), + projectId_name_browser_device_os_viewport_branchName: { + projectId, + branchName: project.mainBranchName, + ...getTestVariationUniqueData(baselineData), + }, }, }), // search current branch variation - this.prismaService.testVariation.findMany({ + this.prismaService.testVariation.findUnique({ where: { - projectId, - ...baselineData, + projectId_name_browser_device_os_viewport_branchName: { + projectId, + branchName: baselineData.branchName, + ...getTestVariationUniqueData(baselineData), + }, }, }), ]); diff --git a/src/utils/index.ts b/src/utils/index.ts index 32d73667..4c999af6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,9 +12,9 @@ export const getTestVariationUniqueData = ( } => { return { name: object.name, - os: object.os, - device: object.device, - browser: object.browser, - viewport: object.viewport, + os: object.os ?? '', + device: object.device ?? '', + browser: object.browser ?? '', + viewport: object.viewport ?? '', }; }; From 7183b86ce032f4741dd66fe81bc81ca5a03eb0dd Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Mon, 5 Apr 2021 23:01:18 +0300 Subject: [PATCH 06/11] Update test-variations.service.ts --- src/test-variations/test-variations.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index aa2bd22e..07aacdcc 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -74,7 +74,8 @@ export class TestVariationsService { return this.prismaService.testVariation.create({ data: { project: { connect: { id: projectId } }, - ...baselineData, + branchName: baselineData.branchName, + ...getTestVariationUniqueData(baselineData), }, }); } From af7dc9cd0e10ce591117008da0fff8937a1266f0 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Wed, 7 Apr 2021 21:35:59 +0300 Subject: [PATCH 07/11] updated --- src/shared/dto/baseline-data.dto.ts | 11 -- src/shared/static/static.service.ts | 10 +- src/test-runs/test-runs.controller.ts | 4 +- src/test-runs/test-runs.service.ts | 174 ++++-------------- .../test-variations.service.ts | 119 +++++++++--- test/test-runs.e2e-spec.ts | 1 - 6 files changed, 141 insertions(+), 178 deletions(-) diff --git a/src/shared/dto/baseline-data.dto.ts b/src/shared/dto/baseline-data.dto.ts index 008007a5..5e2cf394 100644 --- a/src/shared/dto/baseline-data.dto.ts +++ b/src/shared/dto/baseline-data.dto.ts @@ -30,14 +30,3 @@ export class BaselineDataDto { @IsString() branchName: string; } - -export const convertBaselineDataToQuery = (data: BaselineDataDto) => { - return { - name: data.name, - branchName: data.branchName, - os: data.os ? data.os : null, - browser: data.browser ? data.browser : null, - device: data.device ? data.device : null, - viewport: data.viewport ? data.viewport : null, - }; -}; diff --git a/src/shared/static/static.service.ts b/src/shared/static/static.service.ts index 221c82f5..93727e43 100644 --- a/src/shared/static/static.service.ts +++ b/src/shared/static/static.service.ts @@ -1,12 +1,14 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import path from 'path'; import { writeFileSync, readFileSync, unlink, mkdirSync, existsSync } from 'fs'; import { PNG, PNGWithMetadata } from 'pngjs'; -export const IMAGE_PATH = 'imageUploads/' +export const IMAGE_PATH = 'imageUploads/'; @Injectable() export class StaticService { + private readonly logger: Logger = new Logger(StaticService.name); + saveImage(type: 'screenshot' | 'diff' | 'baseline', imageBuffer: Buffer): string { const imageName = `${Date.now()}.${type}.png`; writeFileSync(this.getImagePath(imageName), imageBuffer); @@ -19,7 +21,7 @@ export class StaticService { try { image = PNG.sync.read(readFileSync(this.getImagePath(imageName))); } catch (ex) { - console.log(`Cannot image: ${imageName}. ${ex}`); + this.logger.error(`Cannot get image: ${imageName}. ${ex}`); } return image; } @@ -29,7 +31,7 @@ export class StaticService { return new Promise((resolvePromise, reject) => { unlink(this.getImagePath(imageName), (err) => { if (err) { - reject(err); + this.logger.error(err); } resolvePromise(true); }); diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts index ce0e9bfd..22faa601 100644 --- a/src/test-runs/test-runs.controller.ts +++ b/src/test-runs/test-runs.controller.ts @@ -14,7 +14,7 @@ import { } from '@nestjs/common'; import { ApiTags, ApiParam, ApiBearerAuth, ApiQuery, ApiSecurity, ApiOkResponse } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/auth.guard'; -import { TestRun } from '@prisma/client'; +import { TestRun, TestStatus } from '@prisma/client'; import { TestRunsService } from './test-runs.service'; import { IgnoreAreaDto } from './dto/ignore-area.dto'; import { CommentDto } from '../shared/dto/comment.dto'; @@ -53,7 +53,7 @@ export class TestRunsController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) rejectTestRun(@Param('id', new ParseUUIDPipe()) id: string): Promise { - return this.testRunsService.reject(id); + return this.testRunsService.setStatus(id, TestStatus.failed); } @Delete('/:id') diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index d2815a68..e050cec6 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -5,13 +5,12 @@ import { CreateTestRequestDto } from './dto/create-test-request.dto'; import { IgnoreAreaDto } from './dto/ignore-area.dto'; import { StaticService } from '../shared/static/static.service'; import { PrismaService } from '../prisma/prisma.service'; -import { Baseline, Project, TestRun, TestStatus, TestVariation } from '@prisma/client'; +import { Baseline, TestRun, TestStatus, TestVariation } from '@prisma/client'; import { DiffResult } from './diffResult'; import { EventsGateway } from '../shared/events/events.gateway'; import { CommentDto } from '../shared/dto/comment.dto'; import { TestRunResultDto } from '../test-runs/dto/testRunResult.dto'; import { TestVariationsService } from '../test-variations/test-variations.service'; -import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto'; import { TestRunDto } from './dto/testRun.dto'; import { getTestVariationUniqueData } from '../utils'; @@ -50,32 +49,30 @@ export class TestRunsService { } async postTestRun(createTestRequestDto: CreateTestRequestDto): Promise { - const project: Project = await this.prismaService.project.findUnique({ - where: { id: createTestRequestDto.projectId }, - }); - const baselineData = convertBaselineDataToQuery(createTestRequestDto); - // creates variatioin if does not exist - const testVariation = await this.testVariationService.findOrCreate(createTestRequestDto.projectId, baselineData); + const testVariation = await this.testVariationService.findOrCreate(createTestRequestDto.projectId, { + ...getTestVariationUniqueData(createTestRequestDto), + branchName: createTestRequestDto.branchName, + }); // delete previous test run if exists - const previousTestRuns = await this.prismaService.testRun.findMany({ + const [previousTestRun] = await this.prismaService.testRun.findMany({ where: { - OR: [ - { buildId: createTestRequestDto.buildId, ...baselineData }, - { buildId: createTestRequestDto.buildId, ...baselineData, branchName: project.mainBranchName }, - ], + buildId: createTestRequestDto.buildId, + branchName: createTestRequestDto.branchName, + ...getTestVariationUniqueData(createTestRequestDto), + NOT: { OR: [{ status: TestStatus.approved }, { status: TestStatus.autoApproved }] }, }, }); - await Promise.all(previousTestRuns.map((tr) => this.delete(tr.id))); + if (!!previousTestRun) { + await this.delete(previousTestRun.id); + } // create test run result const testRun = await this.create(testVariation, createTestRequestDto); - const testRunAgainstMainBranch = await this.createAgainstMainBranch({ project, testVariation, testRun }); // calculate diff let testRunWithResult = await this.calculateDiff(testRun); - testRunAgainstMainBranch && (await this.calculateDiff(testRunAgainstMainBranch)); // try auto approve testRunWithResult = await this.tryAutoApproveByPastBaselines(testVariation, testRunWithResult); @@ -93,81 +90,42 @@ export class TestRunsService { */ async approve(id: string, merge = false, autoApprove = false): Promise { this.logger.log(`Approving testRun: ${id} merge: ${merge} autoApprove: ${autoApprove}`); - const status = autoApprove ? TestStatus.autoApproved : TestStatus.approved; const testRun = await this.findOne(id); + // update status + const status = autoApprove ? TestStatus.autoApproved : TestStatus.approved; + testRun.status = (await this.setStatus(id, status)).status; + // save new baseline const baseline = this.staticService.getImage(testRun.imageName); const baselineName = this.staticService.saveImage('baseline', PNG.sync.write(baseline)); - let testRunUpdated: TestRun; - if (merge || testRun.branchName === testRun.baselineBranchName) { - // update existing test variation - testRunUpdated = await this.prismaService.testRun.update({ - where: { id }, - data: { - status, - testVariation: { - update: { - baselineName, - }, - }, - baseline: { - create: { - baselineName, - testVariation: { - connect: { - id: testRun.testVariationId, - }, - }, - }, - }, - }, - }); - } else { - // create new feature branch test variation - testRunUpdated = await this.prismaService.testRun.update({ - where: { id }, - data: { - status, - baseline: autoApprove - ? undefined - : { - create: { - baselineName, - testVariation: { - create: { - baselineName, - ...getTestVariationUniqueData(testRun), - ignoreAreas: testRun.ignoreAreas, - comment: testRun.comment, - branchName: testRun.branchName, - project: { - connect: { - id: testRun.testVariation.projectId, - }, - }, - testRuns: { - connect: { - id, - }, - }, - }, - }, - }, - }, - }, + + if (!autoApprove) { + if (testRun.baselineBranchName !== testRun.branchName && !merge) { + testRun.testVariation = await this.testVariationService.createFeatureTestVariation({ + projectId: testRun.testVariation.projectId, + baselineName, + testRun, + }); + } + + // add baseline + testRun.testVariation = await this.testVariationService.addBaseline({ + id: testRun.testVariation.id, + testRunId: testRun.id, + baselineName, }); } - this.eventsGateway.testRunUpdated(testRunUpdated); - return testRunUpdated; + this.eventsGateway.testRunUpdated(testRun); + return testRun; } - async reject(id: string): Promise { + async setStatus(id: string, status: TestStatus): Promise { const testRun = await this.prismaService.testRun.update({ where: { id }, data: { - status: TestStatus.failed, + status, }, }); @@ -236,66 +194,6 @@ export class TestRunsService { return testRun; } - /** - * Emulate merge feature to main branch for autoApprove logic - */ - private async createAgainstMainBranch({ - project, - testVariation, - testRun, - }: { - project: Project; - testVariation: TestVariation; - testRun: TestRun; - }): Promise { - if ( - !process.env.AUTO_APPROVE_BASED_ON_HISTORY || - testRun.branchName === project.mainBranchName || - testRun.branchName !== testVariation.branchName - ) { - // create only for features branches that have own baseline - return; - } - this.logger.log(`Creating testRun agains main branch for ${testRun.id}`); - const mainBranchTestVariation = await this.prismaService.testVariation.findUnique({ - where: { - projectId_name_browser_device_os_viewport_branchName: { - projectId: project.id, - branchName: project.mainBranchName, - ...getTestVariationUniqueData(testVariation), - }, - }, - }); - if (!mainBranchTestVariation) { - return; - } - return this.prismaService.testRun.create({ - data: { - imageName: testRun.imageName, - ...getTestVariationUniqueData(testVariation), - baselineName: mainBranchTestVariation.baselineName, - baselineBranchName: mainBranchTestVariation.branchName, - ignoreAreas: testVariation.ignoreAreas, - tempIgnoreAreas: testRun.ignoreAreas, - comment: testVariation.comment, - diffTollerancePercent: testRun.diffTollerancePercent, - branchName: testRun.branchName, - merge: false, - status: TestStatus.new, - testVariation: { - connect: { - id: mainBranchTestVariation.id, - }, - }, - build: { - connect: { - id: testRun.buildId, - }, - }, - }, - }); - } - async delete(id: string): Promise { const testRun = await this.findOne(id); diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index 07aacdcc..efdfb9a6 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -1,10 +1,17 @@ import { Injectable, Inject, forwardRef } from '@nestjs/common'; import { IgnoreAreaDto } from '../test-runs/dto/ignore-area.dto'; import { PrismaService } from '../prisma/prisma.service'; -import { TestVariation, Baseline, Project } from '@prisma/client'; +import { + TestVariation, + Baseline, + Project, + ProjectIdNameBrowserDeviceOsViewportBranchNameCompoundUniqueInput, + Prisma, + TestRun, +} from '@prisma/client'; import { StaticService } from '../shared/static/static.service'; import { CommentDto } from '../shared/dto/comment.dto'; -import { BaselineDataDto, convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto'; +import { BaselineDataDto } from '../shared/dto/baseline-data.dto'; import { BuildsService } from '../builds/builds.service'; import { TestRunsService } from '../test-runs/test-runs.service'; import { PNG } from 'pngjs'; @@ -39,38 +46,80 @@ export class TestVariationsService { }); } + async findUnique( + uniqueInput: Prisma.ProjectIdNameBrowserDeviceOsViewportBranchNameCompoundUniqueInput + ): Promise { + return this.prismaService.testVariation.findUnique({ + where: { + projectId_name_browser_device_os_viewport_branchName: uniqueInput, + }, + }); + } + + async createFeatureTestVariation({ + projectId, + baselineName, + testRun, + }: { + projectId: string; + baselineName: string; + testRun: TestRun; + }) { + return this.prismaService.testVariation.create({ + data: { + baselineName, + ...getTestVariationUniqueData(testRun), + ignoreAreas: testRun.ignoreAreas, + comment: testRun.comment, + branchName: testRun.branchName, + project: { + connect: { + id: projectId, + }, + }, + testRuns: { + connect: { + id: testRun.id, + }, + }, + }, + }); + } + async findOrCreate(projectId: string, baselineData: BaselineDataDto): Promise { const project = await this.prismaService.project.findUnique({ where: { id: projectId } }); const [mainBranchTestVariation, currentBranchTestVariation] = await Promise.all([ // search main branch variation - this.prismaService.testVariation.findUnique({ - where: { - projectId_name_browser_device_os_viewport_branchName: { - projectId, - branchName: project.mainBranchName, - ...getTestVariationUniqueData(baselineData), - }, - }, + this.findUnique({ + projectId, + branchName: project.mainBranchName, + ...getTestVariationUniqueData(baselineData), }), // search current branch variation - this.prismaService.testVariation.findUnique({ - where: { - projectId_name_browser_device_os_viewport_branchName: { - projectId, - branchName: baselineData.branchName, - ...getTestVariationUniqueData(baselineData), - }, - }, - }), + baselineData.branchName !== project.mainBranchName && + this.findUnique({ + projectId, + branchName: baselineData.branchName, + ...getTestVariationUniqueData(baselineData), + }), ]); if (!!currentBranchTestVariation) { + if ( + process.env.AUTO_APPROVE_BASED_ON_HISTORY && + mainBranchTestVariation && + mainBranchTestVariation.updatedAt > currentBranchTestVariation.updatedAt + ) { + return mainBranchTestVariation; + } return currentBranchTestVariation; } + if (!!mainBranchTestVariation) { return mainBranchTestVariation; } + return this.prismaService.testVariation.create({ data: { project: { connect: { id: projectId } }, @@ -98,6 +147,33 @@ export class TestVariationsService { }); } + async addBaseline({ + id, + testRunId, + baselineName, + }: { + id: string; + testRunId: string; + baselineName: string; + }): Promise { + return this.prismaService.testVariation.update({ + where: { id }, + data: { + baselineName, + baselines: { + create: { + baselineName, + testRun: { + connect: { + id: testRunId, + }, + }, + }, + }, + }, + }); + } + async merge(projectId: string, branchName: string): Promise { const project: Project = await this.prismaService.project.findUnique({ where: { id: projectId } }); @@ -120,11 +196,10 @@ export class TestVariationsService { const imageBase64 = PNG.sync.write(baseline).toString('base64'); // get main branch variation - const baselineData = convertBaselineDataToQuery({ - ...sideBranchTestVariation, + const mainBranchTestVariation = await this.findOrCreate(projectId, { + ...getTestVariationUniqueData(sideBranchTestVariation), branchName: project.mainBranchName, }); - const mainBranchTestVariation = await this.findOrCreate(projectId, baselineData); // get side branch request const createTestRequestDto: CreateTestRequestDto = { diff --git a/test/test-runs.e2e-spec.ts b/test/test-runs.e2e-spec.ts index a78e3f8c..f803108b 100644 --- a/test/test-runs.e2e-spec.ts +++ b/test/test-runs.e2e-spec.ts @@ -8,7 +8,6 @@ import { TestRunsService } from '../src/test-runs/test-runs.service'; import { ProjectsService } from '../src/projects/projects.service'; import { Project, TestStatus } from '@prisma/client'; import { BuildsService } from '../src/builds/builds.service'; -import { PrismaService } from '../src/prisma/prisma.service'; import { TestVariationsService } from '../src/test-variations/test-variations.service'; jest.useFakeTimers(); From b88bb0147fdf67d946bdc48db842eaad2471b933 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Wed, 7 Apr 2021 21:52:25 +0300 Subject: [PATCH 08/11] tests are fixed --- src/test-runs/test-runs.service.spec.ts | 304 +----------------- .../test-variations.service.spec.ts | 101 +++--- 2 files changed, 60 insertions(+), 345 deletions(-) diff --git a/src/test-runs/test-runs.service.spec.ts b/src/test-runs/test-runs.service.spec.ts index f68b65b7..7c5ed654 100644 --- a/src/test-runs/test-runs.service.spec.ts +++ b/src/test-runs/test-runs.service.spec.ts @@ -12,10 +12,11 @@ import { DiffResult } from './diffResult'; import { EventsGateway } from '../shared/events/events.gateway'; import { CommentDto } from '../shared/dto/comment.dto'; import { TestVariationsService } from '../test-variations/test-variations.service'; -import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto'; import { TestRunDto } from '../test-runs/dto/testRun.dto'; import { BuildsService } from '../builds/builds.service'; import { TEST_PROJECT } from '../_data_'; +import { getTestVariationUniqueData } from '../utils'; +import { BaselineDataDto } from '../shared/dto/baseline-data.dto'; jest.mock('pixelmatch'); jest.mock('./dto/testRunResult.dto'); @@ -147,7 +148,7 @@ describe('TestRunsService', () => { }); }); - it('reject', async () => { + it('setStatus', async () => { const testRun = { id: 'id', imageName: 'imageName', @@ -167,7 +168,7 @@ describe('TestRunsService', () => { eventTestRunUpdatedMock, }); - await service.reject(testRun.id); + await service.setStatus(testRun.id, TestStatus.failed); expect(testRunUpdateMock).toHaveBeenCalledWith({ where: { id: testRun.id }, @@ -178,297 +179,6 @@ describe('TestRunsService', () => { expect(eventTestRunUpdatedMock).toBeCalledWith(testRun); }); - describe('approve', () => { - it('should approve the same branch', async () => { - const testRun: TestRun = { - id: 'id', - imageName: 'imageName', - diffName: 'diffName', - baselineName: 'baselineName', - diffPercent: 1, - pixelMisMatchCount: 10, - diffTollerancePercent: 12, - status: TestStatus.new, - buildId: 'buildId', - testVariationId: 'testVariationId', - updatedAt: new Date(), - createdAt: new Date(), - name: 'test run name', - ignoreAreas: '[]', - tempIgnoreAreas: '[]', - browser: 'browser', - device: 'device', - os: 'os', - viewport: 'viewport', - branchName: 'master', - baselineBranchName: 'master', - comment: 'some comment', - merge: false, - }; - const testRunUpdateMock = jest.fn().mockResolvedValueOnce({ - ...testRun, - status: TestStatus.approved, - }); - const eventTestRunUpdatedMock = jest.fn(); - const testRunFindOneMock = jest.fn().mockResolvedValueOnce(testRun); - const baselineName = 'some baseline name'; - const saveImageMock = jest.fn().mockReturnValueOnce(baselineName); - const getImageMock = jest.fn().mockReturnValueOnce( - new PNG({ - width: 10, - height: 10, - }) - ); - service = await initService({ - testRunUpdateMock, - saveImageMock, - getImageMock, - eventTestRunUpdatedMock, - }); - service.findOne = testRunFindOneMock; - - await service.approve(testRun.id); - - expect(testRunFindOneMock).toHaveBeenCalledWith(testRun.id); - expect(getImageMock).toHaveBeenCalledWith(testRun.imageName); - expect(saveImageMock).toHaveBeenCalledTimes(1); - expect(testRunUpdateMock).toHaveBeenCalledWith({ - where: { id: testRun.id }, - data: { - status: TestStatus.approved, - testVariation: { - update: { - baselineName, - }, - }, - baseline: { - create: { - baselineName, - testVariation: { - connect: { - id: testRun.testVariationId, - }, - }, - }, - }, - }, - }); - expect(eventTestRunUpdatedMock).toBeCalledWith({ - ...testRun, - status: TestStatus.approved, - }); - }); - - it('should approve merge', async () => { - const testRun: TestRun = { - id: 'id', - imageName: 'imageName', - diffName: 'diffName', - baselineName: 'baselineName', - diffPercent: 1, - pixelMisMatchCount: 10, - diffTollerancePercent: 12, - status: TestStatus.new, - buildId: 'buildId', - testVariationId: 'testVariationId', - updatedAt: new Date(), - createdAt: new Date(), - name: 'test run name', - ignoreAreas: '[]', - tempIgnoreAreas: '[]', - browser: 'browser', - device: 'device', - os: 'os', - viewport: 'viewport', - branchName: 'develop', - baselineBranchName: 'master', - comment: 'some comment', - merge: false, - }; - const testRunUpdateMock = jest.fn().mockResolvedValueOnce({ - ...testRun, - status: TestStatus.approved, - }); - const eventTestRunUpdatedMock = jest.fn(); - const testRunFindOneMock = jest.fn().mockResolvedValueOnce(testRun); - const baselineName = 'some baseline name'; - const saveImageMock = jest.fn().mockReturnValueOnce(baselineName); - const getImageMock = jest.fn().mockReturnValueOnce( - new PNG({ - width: 10, - height: 10, - }) - ); - service = await initService({ - testRunUpdateMock, - saveImageMock, - getImageMock, - eventTestRunUpdatedMock, - }); - service.findOne = testRunFindOneMock; - - await service.approve(testRun.id, true); - - expect(testRunFindOneMock).toHaveBeenCalledWith(testRun.id); - expect(getImageMock).toHaveBeenCalledWith(testRun.imageName); - expect(saveImageMock).toHaveBeenCalledTimes(1); - expect(testRunUpdateMock).toHaveBeenCalledWith({ - where: { id: testRun.id }, - data: { - status: TestStatus.approved, - testVariation: { - update: { - baselineName, - }, - }, - baseline: { - create: { - baselineName, - testVariation: { - connect: { - id: testRun.testVariationId, - }, - }, - }, - }, - }, - }); - expect(eventTestRunUpdatedMock).toBeCalledWith({ - ...testRun, - status: TestStatus.approved, - }); - }); - - it('should approve different branch', async () => { - const testRun: TestRun & { - testVariation: TestVariation; - } = { - id: 'id', - imageName: 'imageName', - diffName: 'diffName', - baselineName: 'baselineName', - diffPercent: 1, - pixelMisMatchCount: 10, - diffTollerancePercent: 12, - status: TestStatus.new, - buildId: 'buildId', - testVariationId: 'testVariationId', - updatedAt: new Date(), - createdAt: new Date(), - name: 'test run name', - ignoreAreas: '[]', - tempIgnoreAreas: '[]', - browser: 'browser', - device: 'device', - os: 'os', - viewport: 'viewport', - branchName: 'develop', - baselineBranchName: 'master', - comment: 'some comment', - merge: false, - testVariation: { - id: '123', - projectId: 'project Id', - name: 'Test name', - baselineName: 'baselineName', - os: 'OS', - browser: 'browser', - viewport: 'viewport', - device: 'device', - ignoreAreas: '[]', - comment: 'some comment', - createdAt: new Date(), - updatedAt: new Date(), - branchName: 'master', - }, - }; - const newTestVariation: TestVariation = { - id: '124', - projectId: 'project Id', - name: 'Test name', - baselineName: 'baselineName', - os: 'OS', - browser: 'browser', - viewport: 'viewport', - device: 'device', - ignoreAreas: '[]', - comment: 'some comment', - createdAt: new Date(), - updatedAt: new Date(), - branchName: 'develop', - }; - const testRunUpdateMock = jest.fn().mockResolvedValueOnce({ - ...testRun, - status: TestStatus.approved, - }); - const eventTestRunUpdatedMock = jest.fn(); - const testRunFindOneMock = jest.fn().mockResolvedValueOnce(testRun); - const baselineName = 'some baseline name'; - const saveImageMock = jest.fn().mockReturnValueOnce(baselineName); - const getImageMock = jest.fn().mockReturnValueOnce( - new PNG({ - width: 10, - height: 10, - }) - ); - const testVariationCreateMock = jest.fn().mockResolvedValueOnce(newTestVariation); - const baselineCreateMock = jest.fn(); - service = await initService({ - testRunUpdateMock, - saveImageMock, - getImageMock, - testVariationCreateMock, - baselineCreateMock, - eventTestRunUpdatedMock, - }); - service.findOne = testRunFindOneMock; - - await service.approve(testRun.id, false); - - expect(testRunFindOneMock).toHaveBeenCalledWith(testRun.id); - expect(getImageMock).toHaveBeenCalledWith(testRun.imageName); - expect(saveImageMock).toHaveBeenCalledTimes(1); - expect(testRunUpdateMock).toHaveBeenCalledWith({ - where: { id: testRun.id }, - data: { - status: TestStatus.approved, - baseline: { - create: { - baselineName, - testVariation: { - create: { - baselineName, - name: testRun.name, - browser: testRun.browser, - device: testRun.device, - os: testRun.os, - viewport: testRun.viewport, - ignoreAreas: testRun.ignoreAreas, - comment: testRun.comment, - branchName: testRun.branchName, - project: { - connect: { - id: testRun.testVariation.projectId, - }, - }, - testRuns: { - connect: { - id: testRun.id, - }, - }, - }, - }, - }, - }, - }, - }); - expect(eventTestRunUpdatedMock).toBeCalledWith({ - ...testRun, - status: TestStatus.approved, - }); - }); - }); - it('create', async () => { const initCreateTestRequestDto: CreateTestRequestDto = { buildId: 'buildId', @@ -1074,7 +784,10 @@ describe('TestRunsService', () => { service.calculateDiff = jest.fn().mockResolvedValueOnce(testRun); service['tryAutoApproveByPastBaselines'] = jest.fn().mockResolvedValueOnce(testRun); service['tryAutoApproveByNewBaselines'] = jest.fn().mockResolvedValueOnce(testRun); - const baselineData = convertBaselineDataToQuery(createTestRequestDto); + const baselineData: BaselineDataDto = { + ...getTestVariationUniqueData(createTestRequestDto), + branchName: createTestRequestDto.branchName, + }; await service.postTestRun(createTestRequestDto); @@ -1083,6 +796,7 @@ describe('TestRunsService', () => { where: { buildId: createTestRequestDto.buildId, ...baselineData, + NOT: { OR: [{ status: TestStatus.approved }, { status: TestStatus.autoApproved }] }, }, }); expect(deleteMock).toHaveBeenCalledWith(testRun.id); diff --git a/src/test-variations/test-variations.service.spec.ts b/src/test-variations/test-variations.service.spec.ts index 009b1819..5160a958 100644 --- a/src/test-variations/test-variations.service.spec.ts +++ b/src/test-variations/test-variations.service.spec.ts @@ -6,10 +6,10 @@ import { StaticService } from '../shared/static/static.service'; import { IgnoreAreaDto } from '../test-runs/dto/ignore-area.dto'; import { TestVariation, Baseline, Project, Build } from '@prisma/client'; import { CommentDto } from '../shared/dto/comment.dto'; -import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto'; import { PNG } from 'pngjs'; import { BuildsService } from '../builds/builds.service'; import { TestRunsService } from '../test-runs/test-runs.service'; +import { getTestVariationUniqueData } from '../utils'; const initModule = async ({ imageDeleteMock = jest.fn(), @@ -145,34 +145,33 @@ describe('TestVariationsService', () => { createdAt: new Date(), updatedAt: new Date(), }; - const variationFindManyMock = jest.fn().mockResolvedValueOnce([variationMock]).mockResolvedValueOnce([undefined]); const projectFindUniqueMock = jest.fn().mockReturnValueOnce(projectMock); - service = await initModule({ variationFindManyMock, projectFindUniqueMock }); + service = await initModule({ projectFindUniqueMock }); + service.findUnique = jest.fn().mockResolvedValueOnce(variationMock).mockResolvedValueOnce(undefined); - const result = await service.findOrCreate(createRequest.projectId, convertBaselineDataToQuery(createRequest)); + const result = await service.findOrCreate(createRequest.projectId, { + ...getTestVariationUniqueData(createRequest), + branchName: createRequest.branchName, + }); expect(projectFindUniqueMock).toHaveBeenCalledWith({ where: { id: createRequest.projectId } }); - expect(variationFindManyMock).toHaveBeenNthCalledWith(1, { - where: { - name: createRequest.name, - projectId: createRequest.projectId, - os: createRequest.os, - browser: createRequest.browser, - viewport: createRequest.viewport, - device: createRequest.device, - branchName: projectMock.mainBranchName, - }, + expect(service.findUnique).toHaveBeenNthCalledWith(1, { + name: createRequest.name, + projectId: createRequest.projectId, + os: createRequest.os, + browser: createRequest.browser, + viewport: createRequest.viewport, + device: createRequest.device, + branchName: projectMock.mainBranchName, }); - expect(variationFindManyMock).toHaveBeenNthCalledWith(2, { - where: { - name: createRequest.name, - projectId: createRequest.projectId, - os: createRequest.os, - browser: createRequest.browser, - viewport: createRequest.viewport, - device: createRequest.device, - branchName: createRequest.branchName, - }, + expect(service.findUnique).toHaveBeenNthCalledWith(2, { + name: createRequest.name, + projectId: createRequest.projectId, + os: createRequest.os, + browser: createRequest.browser, + viewport: createRequest.viewport, + device: createRequest.device, + branchName: createRequest.branchName, }); expect(result).toBe(variationMock); }); @@ -205,34 +204,33 @@ describe('TestVariationsService', () => { createdAt: new Date(), updatedAt: new Date(), }; - const variationFindManyMock = jest.fn().mockResolvedValueOnce([undefined]).mockResolvedValueOnce([variationMock]); const projectFindUniqueMock = jest.fn().mockReturnValueOnce(projectMock); - service = await initModule({ variationFindManyMock, projectFindUniqueMock }); + service = await initModule({ projectFindUniqueMock }); + service.findUnique = jest.fn().mockResolvedValueOnce(undefined).mockResolvedValueOnce(variationMock); - const result = await service.findOrCreate(createRequest.projectId, convertBaselineDataToQuery(createRequest)); + const result = await service.findOrCreate(createRequest.projectId, { + ...getTestVariationUniqueData(createRequest), + branchName: createRequest.branchName, + }); expect(projectFindUniqueMock).toHaveBeenCalledWith({ where: { id: createRequest.projectId } }); - expect(variationFindManyMock).toHaveBeenNthCalledWith(1, { - where: { - name: createRequest.name, - projectId: createRequest.projectId, - os: createRequest.os, - browser: createRequest.browser, - viewport: createRequest.viewport, - device: createRequest.device, - branchName: projectMock.mainBranchName, - }, + expect(service.findUnique).toHaveBeenNthCalledWith(1, { + name: createRequest.name, + projectId: createRequest.projectId, + os: createRequest.os, + browser: createRequest.browser, + viewport: createRequest.viewport, + device: createRequest.device, + branchName: projectMock.mainBranchName, }); - expect(variationFindManyMock).toHaveBeenNthCalledWith(2, { - where: { - name: createRequest.name, - projectId: createRequest.projectId, - os: createRequest.os, - browser: createRequest.browser, - viewport: createRequest.viewport, - device: createRequest.device, - branchName: createRequest.branchName, - }, + expect(service.findUnique).toHaveBeenNthCalledWith(2, { + name: createRequest.name, + projectId: createRequest.projectId, + os: createRequest.os, + browser: createRequest.browser, + viewport: createRequest.viewport, + device: createRequest.device, + branchName: createRequest.branchName, }); expect(result).toBe(variationMock); }); @@ -250,12 +248,15 @@ describe('TestVariationsService', () => { branchName: 'develop', }; - const variationFindManyMock = jest.fn().mockResolvedValueOnce([undefined]).mockResolvedValueOnce([undefined]); const projectFindUniqueMock = jest.fn().mockReturnValueOnce(projectMock); const variationCreateMock = jest.fn(); - service = await initModule({ variationFindManyMock, projectFindUniqueMock, variationCreateMock }); + service = await initModule({ projectFindUniqueMock, variationCreateMock }); + service.findUnique = jest.fn().mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined); - await service.findOrCreate(createRequest.projectId, convertBaselineDataToQuery(createRequest)); + await service.findOrCreate(createRequest.projectId, { + ...getTestVariationUniqueData(createRequest), + branchName: createRequest.branchName, + }); expect(variationCreateMock).toHaveBeenCalledWith({ data: { From f1f1ae81a4eb5da985c9d07422b2c32b57965b74 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Wed, 7 Apr 2021 22:04:35 +0300 Subject: [PATCH 09/11] new test added --- .../test-variations.service.spec.ts | 77 +++++++++++++++++++ .../test-variations.service.ts | 6 +- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/test-variations/test-variations.service.spec.ts b/src/test-variations/test-variations.service.spec.ts index 5160a958..81fb69f7 100644 --- a/src/test-variations/test-variations.service.spec.ts +++ b/src/test-variations/test-variations.service.spec.ts @@ -235,6 +235,83 @@ describe('TestVariationsService', () => { expect(result).toBe(variationMock); }); + it('can find by current branch but main branch is more relevant', async () => { + const createRequest: CreateTestRequestDto = { + buildId: 'buildId', + projectId: projectMock.id, + name: 'Test name', + imageBase64: 'Image', + os: 'OS', + browser: 'browser', + viewport: 'viewport', + device: 'device', + branchName: 'develop', + }; + + const variationMainMock: TestVariation = { + id: '123', + projectId: projectMock.id, + name: 'Test name', + baselineName: 'baselineName', + os: 'OS', + browser: 'browser', + viewport: 'viewport', + device: 'device', + ignoreAreas: '[]', + comment: 'some comment', + branchName: 'master', + createdAt: new Date(), + updatedAt: new Date(), + }; + const variationFeatureMock: TestVariation = { + id: '123', + projectId: projectMock.id, + name: 'Test name', + baselineName: 'baselineName', + os: 'OS', + browser: 'browser', + viewport: 'viewport', + device: 'device', + ignoreAreas: '[]', + comment: 'some comment', + branchName: 'develop', + createdAt: new Date(), + updatedAt: new Date(variationMainMock.updatedAt.getDate() - 1), + }; + const projectFindUniqueMock = jest.fn().mockReturnValueOnce(projectMock); + service = await initModule({ projectFindUniqueMock }); + service.findUnique = jest + .fn() + .mockResolvedValueOnce(variationMainMock) + .mockResolvedValueOnce(variationFeatureMock); + + const result = await service.findOrCreate(createRequest.projectId, { + ...getTestVariationUniqueData(createRequest), + branchName: createRequest.branchName, + }); + + expect(projectFindUniqueMock).toHaveBeenCalledWith({ where: { id: createRequest.projectId } }); + expect(service.findUnique).toHaveBeenNthCalledWith(1, { + name: createRequest.name, + projectId: createRequest.projectId, + os: createRequest.os, + browser: createRequest.browser, + viewport: createRequest.viewport, + device: createRequest.device, + branchName: projectMock.mainBranchName, + }); + expect(service.findUnique).toHaveBeenNthCalledWith(2, { + name: createRequest.name, + projectId: createRequest.projectId, + os: createRequest.os, + browser: createRequest.browser, + viewport: createRequest.viewport, + device: createRequest.device, + branchName: createRequest.branchName, + }); + expect(result).toBe(variationMainMock); + }); + it('can create if not found', async () => { const createRequest: CreateTestRequestDto = { buildId: 'buildId', diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index efdfb9a6..44020664 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -106,11 +106,7 @@ export class TestVariationsService { ]); if (!!currentBranchTestVariation) { - if ( - process.env.AUTO_APPROVE_BASED_ON_HISTORY && - mainBranchTestVariation && - mainBranchTestVariation.updatedAt > currentBranchTestVariation.updatedAt - ) { + if (mainBranchTestVariation && mainBranchTestVariation.updatedAt > currentBranchTestVariation.updatedAt) { return mainBranchTestVariation; } return currentBranchTestVariation; From e10e713266163c04e5d75afded33eca94f7acfe2 Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Wed, 7 Apr 2021 22:06:03 +0300 Subject: [PATCH 10/11] Update workflow.yml --- .github/workflows/workflow.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index f161e877..ed2de01d 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -40,7 +40,10 @@ jobs: uses: jakejarvis/wait-action@v0.1.0 with: time: '5s' - + + - name: Apply Manual DB migrations + run: npx ts-node ./prisma/manual_migrations.ts + - name: Apply DB migrations run: npx prisma migrate up -c --experimental From 114a54dd0a61bf0fb146cb252c0d86ee43617abf Mon Sep 17 00:00:00 2001 From: Pavel Strunkin Date: Sat, 10 Apr 2021 16:43:59 +0300 Subject: [PATCH 11/11] covered with test --- src/test-runs/test-runs.service.ts | 32 ++++---- .../test-variations.service.ts | 28 ++++++- test/test-runs.e2e-spec.ts | 80 ++++++++++++++++++- 3 files changed, 118 insertions(+), 22 deletions(-) diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index e050cec6..3666aa27 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -91,34 +91,32 @@ export class TestRunsService { async approve(id: string, merge = false, autoApprove = false): Promise { this.logger.log(`Approving testRun: ${id} merge: ${merge} autoApprove: ${autoApprove}`); const testRun = await this.findOne(id); - - // update status - const status = autoApprove ? TestStatus.autoApproved : TestStatus.approved; - testRun.status = (await this.setStatus(id, status)).status; + let testVariation = testRun.testVariation; // save new baseline const baseline = this.staticService.getImage(testRun.imageName); const baselineName = this.staticService.saveImage('baseline', PNG.sync.write(baseline)); - if (!autoApprove) { - if (testRun.baselineBranchName !== testRun.branchName && !merge) { - testRun.testVariation = await this.testVariationService.createFeatureTestVariation({ - projectId: testRun.testVariation.projectId, - baselineName, - testRun, - }); - } + if (testRun.baselineBranchName !== testRun.branchName && !merge && !autoApprove) { + testVariation = await this.testVariationService.updateOrCreate({ + projectId: testVariation.projectId, + baselineName, + testRun, + }); + } + if (!autoApprove || (autoApprove && testRun.baselineBranchName === testRun.branchName)) { // add baseline - testRun.testVariation = await this.testVariationService.addBaseline({ - id: testRun.testVariation.id, + await this.testVariationService.addBaseline({ + id: testVariation.id, testRunId: testRun.id, baselineName, }); } - this.eventsGateway.testRunUpdated(testRun); - return testRun; + // update status + const status = autoApprove ? TestStatus.autoApproved : TestStatus.approved; + return this.setStatus(id, status); } async setStatus(id: string, status: TestStatus): Promise { @@ -130,7 +128,7 @@ export class TestRunsService { }); this.eventsGateway.testRunUpdated(testRun); - return testRun; + return this.findOne(id); } async saveDiffResult(id: string, diffResult: DiffResult): Promise { diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index 44020664..72c9ddbe 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -56,7 +56,7 @@ export class TestVariationsService { }); } - async createFeatureTestVariation({ + async updateOrCreate({ projectId, baselineName, testRun, @@ -65,8 +65,30 @@ export class TestVariationsService { baselineName: string; testRun: TestRun; }) { - return this.prismaService.testVariation.create({ - data: { + return this.prismaService.testVariation.upsert({ + where: { + projectId_name_browser_device_os_viewport_branchName: { + projectId, + branchName: testRun.branchName, + ...getTestVariationUniqueData(testRun), + }, + }, + update: { + baselineName, + ignoreAreas: testRun.ignoreAreas, + comment: testRun.comment, + project: { + connect: { + id: projectId, + }, + }, + testRuns: { + connect: { + id: testRun.id, + }, + }, + }, + create: { baselineName, ...getTestVariationUniqueData(testRun), ignoreAreas: testRun.ignoreAreas, diff --git a/test/test-runs.e2e-spec.ts b/test/test-runs.e2e-spec.ts index f803108b..9257ad6c 100644 --- a/test/test-runs.e2e-spec.ts +++ b/test/test-runs.e2e-spec.ts @@ -176,7 +176,7 @@ describe('TestRuns (e2e)', () => { }); describe('POST /approve', () => { - it('approve changes in existing testVariation', async () => { + it('approve changes in new main branch', async () => { const { testRun: testRun1 } = await haveTestRunCreated( buildsService, testRunsService, @@ -192,7 +192,83 @@ describe('TestRuns (e2e)', () => { expect(testVariation.baselines).toHaveLength(1); }); - it('approve changes in existing testVariation with merge', async () => { + it('approve changes in new feature branch', async () => { + const { testRun: testRun1 } = await haveTestRunCreated( + buildsService, + testRunsService, + project.id, + 'develop', + image_v1 + ); + + const result = await testRunsService.approve(testRun1.id); + + expect(result.status).toBe(TestStatus.approved); + const testVariation = await testVariationsService.getDetails(result.testVariationId); + expect(testVariation.baselines).toHaveLength(1); + expect(testVariation.branchName).toBe('develop'); + }); + + it('approve changes in main vs feature branch', async () => { + const { testRun: testRun1 } = await haveTestRunCreated( + buildsService, + testRunsService, + project.id, + project.mainBranchName, + image_v1 + ); + const mainBranchResult = await testRunsService.approve(testRun1.id); + const { testRun: testRun2 } = await haveTestRunCreated( + buildsService, + testRunsService, + project.id, + 'develop', + image_v2, + ); + + const featureBranchResult = await testRunsService.approve(testRun2.id); + + expect(featureBranchResult.status).toBe(TestStatus.approved); + const testVariation = await testVariationsService.getDetails(featureBranchResult.testVariationId); + expect(testVariation.baselines).toHaveLength(1); + expect(testVariation.branchName).toBe('develop'); + }); + + it('approve changes in updated main vs feature branch', async () => { + const { testRun: testRun1 } = await haveTestRunCreated( + buildsService, + testRunsService, + project.id, + 'develop', + image_v1, + ); + await testRunsService.approve(testRun1.id); + const { testRun: testRun2 } = await haveTestRunCreated( + buildsService, + testRunsService, + project.id, + project.mainBranchName, + image_v2 + ); + await testRunsService.approve(testRun2.id); + const { testRun: testRun3 } = await haveTestRunCreated( + buildsService, + testRunsService, + project.id, + 'develop', + image_v1, + ); + + const result = await testRunsService.approve(testRun3.id); + + expect(result.status).toBe(TestStatus.approved); + expect(result.baselineBranchName).toBe(project.mainBranchName); + const testVariation = await testVariationsService.getDetails(result.testVariationId); + expect(testVariation.baselines).toHaveLength(2); + expect(testVariation.branchName).toBe('develop'); + }); + + it('approve changes with merge', async () => { const { testRun: testRun1 } = await haveTestRunCreated( buildsService, testRunsService,