Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Test consensus: merging, ver 2 #9193

Merged
merged 26 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9e18612
cherry 2
Mar 10, 2025
a5335a2
cherry pick 3
Mar 10, 2025
a5a93b5
cherry -pick 3
Mar 8, 2025
0941d58
cherry -pick 4
Mar 8, 2025
52adfc6
fix archiver path checks
Mar 10, 2025
4ebad28
check tests pass, add eslint rules for fixed plugin
Mar 10, 2025
6235491
minor refactor
Mar 10, 2025
63c7f71
decouple tasks from test cases + add serverFiles logic to createAnnot…
Mar 10, 2025
701b406
hoist constants, add a todo
Mar 10, 2025
7d2fe19
refactor to merge consensus job by id
Mar 11, 2025
7f4853b
refactor functions, remove redundant logic
Mar 11, 2025
c9c9d0a
add .cvat-back-btn class to GoBackButton component
Mar 11, 2025
a883ebd
use headless function to create consensus tasks
Mar 12, 2025
f827d58
Merge branch 'develop' into ov/test-consensus-merge-ver2
Mar 12, 2025
77e6187
remove comment
archibald1418 Mar 12, 2025
028dae1
rename var
archibald1418 Mar 12, 2025
a8f8e10
revert serverFiles logic from command
Mar 12, 2025
d7ef562
fix variable
Mar 12, 2025
16f7f3b
merge fix #9178 from develop (depends on this)
Mar 12, 2025
343d091
update consensus management page test according to fix #9178
Mar 12, 2025
368a724
Create two different tasks for both test suites
Mar 12, 2025
8d66362
Revert "Create two different tasks for both test suites"
Mar 13, 2025
83aa23c
fix flaky form checks
Mar 13, 2025
fcab0df
tasks use same spec and handled in separate cases
Mar 13, 2025
cc869b9
Merge branch 'develop' into ov/test-consensus-merge-ver2
Mar 13, 2025
3aa9ca5
Merge branch 'develop' into ov/test-consensus-merge-ver2
klakhov Mar 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cvat-ui/src/components/common/go-back-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function GoBackButton(): JSX.Element {
const goBack = useGoBack();
return (
<>
<Button style={{ marginRight: 8 }} onClick={goBack}>
<Button style={{ marginRight: 8 }} onClick={goBack} className='cvat-back-btn'>
<LeftOutlined />
</Button>
<Text style={{ userSelect: 'none' }} strong>Back</Text>
Expand Down
214 changes: 185 additions & 29 deletions tests/cypress/e2e/features/consensus.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,69 @@

/// <reference types="cypress" />

import { translatePoints } from '../../support/utils';

context('Basic manipulations with consensus job replicas', () => {
describe('Consensus task creation', () => {
const maxReplicas = 10;
const taskName = 'Test consensus';
const labelName = 'test';
const serverFiles = ['archive.zip'];
const replicas = 3;
const labelName = 'Consensus';
const taskName = 'Test consensus';
const serverFiles = ['archive.zip'];
const replicas = 4;
const taskSpec = {
name: taskName,
labels: [{
name: labelName,
attributes: [],
type: 'any',
}],
};
const dataSpec = {
server_files: serverFiles,
image_quality: 70,
};
const extras = { consensus_replicas: replicas };

before(() => {
cy.visit('auth/login');
cy.login();
});

describe('Consensus job creation', () => {
const maxReplicas = 10;
let consensusTaskID = null;
before(() => {
cy.visit('auth/login');
cy.login();
cy.headlessCreateTask(taskSpec, dataSpec, extras).then(({ taskID }) => {
consensusTaskID = taskID;
});
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').should('be.visible').click();
});

it('Check allowed number of replicas', () => {
// Fill the fields to create the task
cy.get('#name').type(taskName);
cy.addNewLabel({ name: labelName });
cy.selectFilesFromShare(serverFiles);
cy.contains('[role="tab"]', 'My computer').click();

cy.contains('Advanced configuration').click();
// 'Consensus Replicas' field cannot equal to 1
cy.get('#consensusReplicas').type(`{backspace}${1}`);
cy.get('.ant-form-item-explain-error')
cy.get('.ant-form-item-has-error')
.should('be.visible')
.invoke('text').should('eq', 'Value can not be equal to 1');
.should('include.text', 'Value can not be equal to 1');
cy.contains('button', 'Submit & Continue').click();
cy.get('.ant-notification-notice-error').should('exist').and('be.visible');
cy.closeNotification('.ant-notification-notice-error');

// 'Consensus Replicas' field cannot be > 10
cy.get('#consensusReplicas').clear();
cy.get('.ant-form-item-has-error').should('not.exist');
cy.get('#consensusReplicas').type(`{backspace}${maxReplicas + 1}`);
cy.get('.ant-form-item-explain-error').should('be.visible')
.invoke('text').should('eq', `Value must be less than ${maxReplicas}`);
cy.get('.ant-form-item-has-error')
.should('be.visible')
.should('include.text', `Value must be less than ${maxReplicas}`);
cy.contains('button', 'Submit & Continue').click();
cy.get('.ant-notification-notice-error').should('exist').and('be.visible');
cy.closeNotification('.ant-notification-notice-error');
cy.get('#consensusReplicas').clear();
});

it('Check new consensus task has correct tags and drop-down with replicas', () => {
// Create task with consensus
cy.get('#consensusReplicas').type(replicas);
cy.contains('button', 'Submit & Open').click();
cy.goToTaskList();
cy.openTask(taskName);
cy.get('.cvat-task-details-wrapper').should('be.visible');
cy.get('.ant-notification-notice-error').should('not.exist');
// Check tags
Expand All @@ -64,20 +81,159 @@ context('Basic manipulations with consensus job replicas', () => {
expect($el.text()).to.equal(`${replicas} Replicas`);
cy.wrap($el).click();
});
});
after(() => {
cy.headlessDeleteTask(consensusTaskID);
});
});

describe('Cosensus jobs merging', () => {
let consensusTaskID = null;
const baseShape = {
objectType: 'shape',
labelName,
frame: 0,
type: 'rectangle',
points: [250, 64, 491, 228],
occluded: false,
};
const jobIDs = [];

before(() => {
cy.headlessCreateTask(taskSpec, dataSpec, extras).then(({ taskID }) => {
consensusTaskID = taskID;
});
cy.goToTaskList();
cy.openTask(taskName);
cy.get('.cvat-consensus-job-collapse').click();
});

// Check asc order of jobs
it("Check new merge buttons exist and are visible. Trying to merge 'new' jobs should trigger errors", () => {
// Check asc order of jobs in drop-down
function parseJobId(jobItem) {
const jobItemText = jobItem.innerText;
const [start, stop] = [0, jobItemText.indexOf('\n')];
return +(jobItemText.substring(start, stop).split('#')[1]);
}
cy.get('.cvat-job-item').then((jobItems) => {
const sourceJobId = parseJobId(jobItems[0]);
for (let i = 1; i <= replicas; i++) {
const jobId = parseJobId(jobItems[i]);
expect(jobId).equals(sourceJobId + i);
}
cy.get('.cvat-job-item').each(([$el], i) => {
const jobID = parseJobId($el);
jobIDs.push(jobID);
expect(jobID).equals(jobIDs[0] + i);
});

// Merge one consensus job
cy.then(() => {
cy.mergeConsensusJob(jobIDs[0], 400);
});
cy.get('.cvat-notification-notice-consensus-merge-task-failed')
.should('be.visible')
.invoke('text').should('include', 'Could not merge the job');
cy.closeNotification('.cvat-notification-notice-consensus-merge-task-failed');

// Merge all consensus jobs in task
cy.mergeConsensusTask(400);
cy.get('.cvat-notification-notice-consensus-merge-task-failed')
.should('be.visible')
.invoke('text')
.should('include', 'Could not merge the task');
cy.closeNotification('.cvat-notification-notice-consensus-merge-task-failed');
});

it('Check consensus management page', () => {
const defaultQuorum = 50;
const defaultIoU = 40;
cy.contains('button', 'Actions').click();
cy.contains('Consensus management').should('be.visible').click();
cy.get('.cvat-consensus-management-inner').should('be.visible');
// Save settings, confirm request is sent
cy.intercept('PATCH', 'api/consensus/settings/**').as('settingsMeta');
cy.contains('button', 'Save').click();
cy.wait('@settingsMeta');
cy.get('.ant-notification-notice-message')
.should('be.visible')
.invoke('text')
.should('eq', 'Settings have been updated');
cy.closeNotification('.ant-notification-notice-closable');

// Forms and invalid saving
function checkFieldValue(selector, value) {
return cy.get(selector).then(([$el]) => {
cy.wrap($el).invoke('val').should('eq', `${value}`);
return cy.wrap($el);
});
}
function attemptInvalidSaving(errorsCount) {
cy.get('.ant-form-item-explain-error').should('be.visible')
.should('have.length', errorsCount)
.each(($el) => {
cy.wrap($el).should('have.text', 'This field is required');
});
cy.contains('button', 'Save').click();
cy.closeNotification('.cvat-notification-save-consensus-settings-failed');
}
checkFieldValue('#quorum', defaultQuorum).clear();
attemptInvalidSaving(1);
checkFieldValue('#iouThreshold', defaultIoU).clear();
attemptInvalidSaving(2);
cy.get('.ant-notification-notice').should('not.exist');

// Go back to task page
cy.get('.cvat-back-btn').should('be.visible').click();
});

it('Create annotations and check that job replicas merge correctly', () => {
// Create annotations for job replicas
const delta = 50;
const [consensusJobID, ...replicaJobIDs] = jobIDs;
for (let i = 0, shape = baseShape; i < replicas; i++) {
cy.headlessCreateObjects([shape], jobIDs[i]); // only 'in progress' jobs can be merged
cy.headlessUpdateJob(replicaJobIDs[i], { state: 'in progress' });
const points = translatePoints(shape.points, delta, 'x');
shape = { ...shape, points };
}
// Merging of consensus job should go without errors in network and UI
cy.mergeConsensusJob(consensusJobID);
cy.get('.cvat-notification-notice-consensus-merge-job-failed').should('not.exist');
cy.get('.ant-notification-notice-message')
.should('be.visible')
.invoke('text')
.should('eq', `Consensus job #${consensusJobID} has been merged`);
cy.closeNotification('.ant-notification-notice-closable');

// Shapes in consensus job and a job replica in the middle should be equal
const middle = Math.floor(jobIDs.length / 2);
const consensusRect = {};
cy.openJob(0, false).then(() => {
cy.get('.cvat_canvas_shape').trigger('mousemove');
cy.get('.cvat_canvas_shape').then(($el) => {
consensusRect.x = $el.attr('x');
consensusRect.y = $el.attr('y');
consensusRect.width = $el.attr('width');
consensusRect.height = $el.attr('height');
});
cy.get('#cvat_canvas_text_content').should('be.visible')
.invoke('text')
.should('include', `${labelName}`)
.and('include', 'consensus');
});
cy.go('back'); // go to previous page
// After returning to task page, consensus job should be 'completed'
cy.get('.cvat-job-item').first()
.find('.cvat-job-item-state').first()
.invoke('text')
.should('eq', 'completed');
cy.contains('.cvat-job-item', `Job #${jobIDs[middle]}`).scrollIntoView();
cy.openJob(middle, false).then(() => {
cy.get('.cvat_canvas_shape').then(($el) => {
expect($el.attr('x')).to.equal(consensusRect.x);
expect($el.attr('y')).to.equal(consensusRect.y);
expect($el.attr('width')).to.equal(consensusRect.width);
expect($el.attr('height')).to.equal(consensusRect.height);
});
});
});
after(() => {
cy.headlessDeleteTask(consensusTaskID);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

/// <reference types="cypress" />

import { translatePoints } from '../../support/utils';

const taskName = '5frames';
const labelName = 'label';
const attrName = 'attr1';
Expand All @@ -27,26 +29,6 @@ const rect = [
30 + 23,
];

function translatePoints(points, delta, axis) {
if (axis === 'x') {
return [
points[0] + delta,
points[1],
points[2] + delta,
points[3],
];
}
if (axis === 'y') {
return [
points[0],
points[1] + delta,
points[2],
points[3] + delta,
];
}
return points;
}

context('Create any track, check if track works correctly after deleting some frames', () => {
function readShapeCoords() {
return cy.get('.cvat_canvas_shape').then(($shape) => ({
Expand Down
14 changes: 9 additions & 5 deletions tests/cypress/plugins/createZipArchive/addPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
//
// SPDX-License-Identifier: MIT

// eslint-disable-next-line no-use-before-define
/* eslint-disable
import/no-extraneous-dependencies,
security/detect-non-literal-fs-filename,
no-use-before-define
*/

exports.createZipArchive = createZipArchive;

const archiver = require('archiver');
// eslint-disable-next-line import/no-extraneous-dependencies
const fs = require('fs-extra');

function createZipArchive(args) {
const { directoryToArchive } = args;
const { directoryToArchive, archivePath } = args;
const { level } = args;
const output = fs.createWriteStream(args.arhivePath);
const output = fs.createWriteStream(archivePath);
const archive = archiver('zip', {
gzip: true,
zlib: { level },
Expand All @@ -27,5 +31,5 @@ function createZipArchive(args) {
archive.directory(`${directoryToArchive}/`, false);
archive.finalize();

return fs.pathExists(archive);
return fs.pathExists(archivePath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//
// SPDX-License-Identifier: MIT

Cypress.Commands.add('createZipArchive', (directoryToArchive, arhivePath, level = 9) => cy.task('createZipArchive', {
Cypress.Commands.add('createZipArchive', (directoryToArchive, archivePath, level = 9) => cy.task('createZipArchive', {
directoryToArchive,
arhivePath,
archivePath,
level,
}));
36 changes: 36 additions & 0 deletions tests/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,14 @@ Cypress.Commands.add('headlessCreateJob', (jobSpec) => {
});
});

Cypress.Commands.add('headlessUpdateJob', (jobID, updateJobParameters) => {
cy.window().then(async ($win) => {
const job = (await $win.cvat.jobs.get({ jobID }))[0];
const result = await job.save(updateJobParameters);
return cy.wrap(result);
});
});

Cypress.Commands.add('openTask', (taskName, projectSubsetFieldValue) => {
cy.contains('strong', new RegExp(`^${taskName}$`))
.parents('.cvat-tasks-list-item')
Expand Down Expand Up @@ -1833,3 +1841,31 @@ Cypress.Commands.add('applyActionToSliders', (wrapper, slidersClassNames, action
});
cy.get('.ant-tooltip').invoke('hide');
});

Cypress.Commands.add('mergeConsensusTask', (status = 202) => {
cy.intercept('POST', '/api/consensus/merges**').as('mergeTask');

cy.get('.cvat-task-details-wrapper').should('be.visible');
cy.contains('button', 'Actions').click();
cy.contains('Merge consensus jobs').should('be.visible').click();
cy.get('.cvat-modal-confirm-consensus-merge-task')
.contains('button', 'Merge')
.click();

cy.wait('@mergeTask').its('response.statusCode').should('eq', status);
});

Cypress.Commands.add('mergeConsensusJob', (jobID, status = 202) => {
cy.intercept('POST', '/api/consensus/merges**').as('mergeJob');
cy.get('.cvat-job-item')
.filter(':has(.cvat-tag-consensus)')
.filter(`:contains("Job #${jobID}")`)
.find('.anticon-more').first().click();

cy.get('.ant-dropdown-menu').contains('li', 'Merge consensus job').click();
cy.get('.cvat-modal-confirm-consensus-merge-job')
.contains('button', 'Merge')
.click();

cy.wait('@mergeJob').its('response.statusCode').should('eq', status);
});
Loading
Loading