Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/suites/memoryViewer/memoryViewer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test.describe('Memory Viewer Widget', () => {

const memTable = await memoryViewer.getMemTableInfo();
expect(memTable.status).toBe('good');
expect(memTable.text).toMatch(/\d+ \/ \d+\s*GB/);
expect(memTable.text).toMatch(/\d+(\.\d+)? \/ \d+(\.\d+)?\s*GB/);

// Check simple metrics
const allocatorCaches = await memoryViewer.getAllocatorCachesInfo();
Expand Down
63 changes: 62 additions & 1 deletion tests/suites/tenant/diagnostics/Diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export class Table {
}
return headerNames;
}

async getCellValueByHeader(row: number, header: string) {
const headers = await this.getHeaders();
const colIndex = headers.indexOf(header);
Expand All @@ -91,6 +90,18 @@ export class Table {
return cell.innerText();
}

async verifyHeaders(expectedHeaders: string[]) {
const actualHeaders = await this.getHeaders();
for (const header of expectedHeaders) {
if (!actualHeaders.includes(header)) {
throw new Error(
`Expected header "${header}" not found in actual headers: ${actualHeaders.join(', ')}`,
);
}
}
return true;
}

async waitForCellValueByHeader(row: number, header: string, value: string) {
await retryAction(async () => {
const headers = await this.getHeaders();
Expand All @@ -116,6 +127,42 @@ export enum QueriesSwitch {
Running = 'Running',
}

export enum TopShardsMode {
Immediate = 'Immediate',
Historical = 'Historical',
}

const TOP_SHARDS_COLUMNS_IDS = {
TabletId: 'TabletId',
CPUCores: 'CPUCores',
DataSize: 'DataSize (B)',
Path: 'Path',
NodeId: 'NodeId',
PeakTime: 'PeakTime',
InFlightTxCount: 'InFlightTxCount',
IntervalEnd: 'IntervalEnd',
} as const;

export const TopShardsImmediateColumns = [
TOP_SHARDS_COLUMNS_IDS.Path,
TOP_SHARDS_COLUMNS_IDS.CPUCores,
TOP_SHARDS_COLUMNS_IDS.DataSize,
TOP_SHARDS_COLUMNS_IDS.TabletId,
TOP_SHARDS_COLUMNS_IDS.NodeId,
TOP_SHARDS_COLUMNS_IDS.InFlightTxCount,
];

export const TopShardsHistoricalColumns = [
TOP_SHARDS_COLUMNS_IDS.Path,
TOP_SHARDS_COLUMNS_IDS.CPUCores,
TOP_SHARDS_COLUMNS_IDS.DataSize,
TOP_SHARDS_COLUMNS_IDS.TabletId,
TOP_SHARDS_COLUMNS_IDS.NodeId,
TOP_SHARDS_COLUMNS_IDS.PeakTime,
TOP_SHARDS_COLUMNS_IDS.InFlightTxCount,
TOP_SHARDS_COLUMNS_IDS.IntervalEnd,
];

export class Diagnostics {
table: Table;
storage: StoragePage;
Expand All @@ -133,6 +180,7 @@ export class Diagnostics {
private storageCard: Locator;
private memoryCard: Locator;
private healthcheckCard: Locator;
private tableRadioButton: Locator;

constructor(page: Page) {
this.storage = new StoragePage(page);
Expand All @@ -146,6 +194,9 @@ export class Diagnostics {
this.refreshButton = page.locator('button[aria-label="Refresh"]');
this.autoRefreshSelect = page.locator('.g-select');
this.table = new Table(page.locator('.object-general'));
this.tableRadioButton = page.locator(
'.ydb-table-with-controls-layout__controls .g-radio-button',
);

// Info tab cards
this.cpuCard = page.locator('.metrics-cards__tab:has-text("CPU")');
Expand Down Expand Up @@ -247,4 +298,14 @@ export class Diagnostics {
);
return (await statusElement.textContent())?.trim() || '';
}

async selectTopShardsMode(mode: TopShardsMode): Promise<void> {
const option = this.tableRadioButton.locator(`.g-radio-button__option:has-text("${mode}")`);
await option.evaluate((el) => (el as HTMLElement).click());
}

async getSelectedTopShardsMode(): Promise<string> {
const checkedOption = this.tableRadioButton.locator('.g-radio-button__option_checked');
return (await checkedOption.textContent())?.trim() || '';
}
}
141 changes: 140 additions & 1 deletion tests/suites/tenant/diagnostics/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ import {NavigationTabs, TenantPage} from '../TenantPage';
import {longRunningQuery} from '../constants';
import {QueryEditor} from '../queryEditor/models/QueryEditor';

import {Diagnostics, DiagnosticsTab, QueriesSwitch} from './Diagnostics';
import {
Diagnostics,
DiagnosticsTab,
QueriesSwitch,
TopShardsHistoricalColumns,
TopShardsImmediateColumns,
TopShardsMode,
} from './Diagnostics';
import {setupTopShardsHistoryMock} from './mocks';

test.describe('Diagnostics tab', async () => {
test('Info tab shows main page elements', async ({page}) => {
Expand Down Expand Up @@ -170,4 +178,135 @@ test.describe('Diagnostics tab', async () => {
await diagnostics.table.waitForCellValueByHeader(1, 'Query text', longRunningQuery),
).toBe(true);
});

test('TopShards tab defaults to Immediate mode', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
diagnosticsTab: 'topShards',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);

// Verify Immediate mode is selected by default
expect(await diagnostics.getSelectedTopShardsMode()).toBe(TopShardsMode.Immediate);
});

test('TopShards immediate tab shows all expected column headers', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
diagnosticsTab: 'topShards',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);

// Verify table has data
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
// Verify column headers exist
await diagnostics.table.verifyHeaders(TopShardsImmediateColumns);
});

test('TopShards history tab shows all expected column headers', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
diagnosticsTab: 'topShards',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);
await diagnostics.selectTopShardsMode(TopShardsMode.Historical);
// Verify table has data
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);

// Verify column headers exist
await diagnostics.table.verifyHeaders(TopShardsHistoricalColumns);
});

test('TopShards tab first row has values for all columns in Immediate mode', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
diagnosticsTab: 'topShards',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);

// Verify table has data
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);

// Verify first row has non-empty values for all columns
for (const column of TopShardsImmediateColumns) {
const columnValue = await diagnostics.table.getCellValueByHeader(1, column);
expect(columnValue.trim()).toBeTruthy();
}
});

test('TopShards tab first row has values for all columns in History mode', async ({page}) => {
// Setup mock for TopShards tab in History mode
await setupTopShardsHistoryMock(page);

// Now navigate to diagnostics page to check topShards
const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
diagnosticsTab: 'topShards',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);
await diagnostics.selectTopShardsMode(TopShardsMode.Historical);

// Verify table has data
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);

// Verify first row has non-empty values for all columns
for (const column of TopShardsHistoricalColumns) {
const columnValue = await diagnostics.table.getCellValueByHeader(1, column);
expect(columnValue.trim()).toBeTruthy();
}
});

test('TopShards tab can switch back to Immediate mode from Historical mode', async ({page}) => {
const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
diagnosticsTab: 'topShards',
};
const tenantPage = new TenantPage(page);
await tenantPage.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);

// Switch to Historical mode
await diagnostics.selectTopShardsMode(TopShardsMode.Historical);
expect(await diagnostics.getSelectedTopShardsMode()).toBe(TopShardsMode.Historical);

// Switch back to Immediate mode
await diagnostics.selectTopShardsMode(TopShardsMode.Immediate);
expect(await diagnostics.getSelectedTopShardsMode()).toBe(TopShardsMode.Immediate);

// Verify table still has data after switching back
await expect(diagnostics.table.isVisible()).resolves.toBe(true);
await expect(diagnostics.table.getRowCount()).resolves.toBeGreaterThan(0);
});
});
106 changes: 106 additions & 0 deletions tests/suites/tenant/diagnostics/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type {Page} from '@playwright/test';

import {backend} from '../../../utils/constants';

const MOCK_DELAY = 200; // 200ms delay to simulate network latency

/**
* Generates a mock row for the TopShards table in History mode
* @param index Row index (0-based)
* @returns An array of values for each column
*/
const generateTopShardsHistoryRow = (index: number) => {
const now = new Date();
const endTime = new Date(now);
endTime.setHours(endTime.getHours() + 1);
endTime.setMinutes(0);
endTime.setSeconds(0);
endTime.setMilliseconds(0);

// Calculate a peak time somewhere in the past hour
const peakTime = new Date(now);
peakTime.setMinutes(peakTime.getMinutes() - (index * 5 + Math.floor(Math.random() * 15)));

// Generate a CPU usage between 0.85 and 0.98
const cpuCores = 0.85 + Math.random() * 0.13;

// Generate a data size between 1GB and 2GB (in bytes)
const dataSize = Math.floor(1000000000 + Math.random() * 1000000000).toString();

// Generate row count (30-60 million rows)
const rowCount = Math.floor(30000000 + Math.random() * 30000000).toString();

// Generate index size (approximately 8-15% of data size)
const indexSize = Math.floor(parseInt(dataSize) * (0.08 + Math.random() * 0.07)).toString();

// NodeId between 50000 and 50020
const nodeId = 50000 + Math.floor(Math.random() * 20);

// In-flight transactions (usually low, 0-3)
const inFlightTxCount = Math.floor(Math.random() * 4);

// Generate unique tablet ID
const tabletId = `7207518622${100000 + index}`;

// Generate paths
const dbIndex = Math.floor(index / 3) + 1;
const relativePath = `/db${dbIndex}/shard_${index + 1}`;
const fullPath = `/dev01/home/ydbuser${relativePath}`;

return [
cpuCores, // CPUCores
dataSize, // DataSize
0, // FollowerId
inFlightTxCount, // InFlightTxCount
indexSize, // IndexSize
endTime.toISOString(), // IntervalEnd
nodeId, // NodeId
fullPath, // Path
peakTime.toISOString(), // PeakTime
index + 1, // Rank
relativePath, // RelativePath
rowCount, // RowCount
tabletId, // TabletId
];
};

/**
* Sets up a mock for the TopShards tab in History mode
* This ensures the first row has values for all columns
*/
export const setupTopShardsHistoryMock = async (page: Page) => {
await page.route(`${backend}/viewer/json/query?*`, async (route) => {
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));

// Generate 10 rows of data
const rows = Array.from({length: 10}, (_, i) => generateTopShardsHistoryRow(i));

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
version: 8,
result: [
{
rows: rows,
columns: [
{name: 'CPUCores', type: 'Double?'},
{name: 'DataSize', type: 'Uint64?'},
{name: 'FollowerId', type: 'Uint32?'},
{name: 'InFlightTxCount', type: 'Uint32?'},
{name: 'IndexSize', type: 'Uint64?'},
{name: 'IntervalEnd', type: 'Timestamp?'},
{name: 'NodeId', type: 'Uint32?'},
{name: 'Path', type: 'Utf8?'},
{name: 'PeakTime', type: 'Timestamp?'},
{name: 'Rank', type: 'Uint32?'},
{name: 'RelativePath', type: 'Utf8?'},
{name: 'RowCount', type: 'Uint64?'},
{name: 'TabletId', type: 'Uint64?'},
],
},
],
}),
});
});
};
Loading