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
11 changes: 5 additions & 6 deletions .github/workflows/e2e-tests-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,15 @@ jobs:

experimental:
name: Experimental UI tests
uses: ./.github/workflows/runner-e2e-tests-codeceptjs.yml
secrets: inherit
uses: ./.github/workflows/runner-e2e-tests-playwright.yml
secrets:
LAUNCHABLE_TOKEN: ${{ secrets.LAUNCHABLE_TOKEN }}
with:
pmm_server_image: ${{ inputs.pmm_server_image || github.event.inputs.pmm_server_image || 'perconalab/pmm-server:3-dev-latest' }}
pmm_server_version: ${{ inputs.pmm_server_image || github.event.inputs.pmm_server_image || 'perconalab/pmm-server:3-dev-latest' }}
pmm_client_version: ${{ inputs.pmm_client_version || github.event.inputs.pmm_client_version || 'latest-tarball' }}
pmm_client_image: ${{ inputs.pmm_client_image || github.event.inputs.pmm_client_image || 'perconalab/pmm-client:3-dev-latest' }}
pmm_qa_branch: ${{ github.event_name == 'pull_request' && github.head_ref || (inputs.pmm_qa_branch || github.event.inputs.pmm_qa_branch || 'main') }}
sha: ${{ inputs.sha || github.event.pull_request.head.sha || github.event.inputs.sha || 'null' }}
setup_services: '--database pdpgsql'
tags_for_tests: '@experimental'
pmm_test_flag: '@experimental'

disconnect:
name: Disconnect from PMM Server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import PanelComponent from '@components/dashboards/panels/panel.component';

export default class BarGaugePanel extends PanelComponent {
private elements = {
elements = {
barGaugeValues: (panelName: string) =>
this.grafanaIframe().locator(
`//section[@data-testid="data-testid Panel header ${panelName}"]//div[contains(@data-testid, "Bar gauge value")]`,
),
barWithValue: (panelName: string) =>
this.grafanaIframe().locator(
`//section[@data-testid="data-testid Panel header ${panelName}"]//div[@data-testid="data-testid Bar gauge value"]//span[text() > "0"]`,
),
};

verifyPanelData = async (panelName: string) => {
Expand Down
40 changes: 8 additions & 32 deletions e2e_tests/fixtures/pmmTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,9 @@ import QueryAnalytics from '@pages/qan/queryAnalytics.page';
import RealTimeAnalyticsPage from '@pages/qan/rta/realTimeAnalytics.page';
import NodesPage from '@pages/inventory/nodes.page';
import MongoDBHelper from '@helpers/mongodb.helper';
import VacuumDashboard from '@pages/dashboards/postgresql/vacuumDashboard';
import apiEndpoints from '@helpers/apiEndpoints';

base.beforeEach(async ({ page }) => {
// Mock user details call to prevent the tours from showing
await page.route(apiEndpoints.users.me, (route) =>
route.fulfill({
body: JSON.stringify({
alerting_tour_completed: true,
product_tour_completed: true,
snoozed_pmm_version: '',
user_id: 1,
}),
status: 200,
}),
);
// Mock upgrade call to prevent upgrade modal from showing.
await page.route(apiEndpoints.server.updates, (route) =>
route.fulfill({
body: JSON.stringify({
installed: {},
last_check: new Date().toISOString(),
latest: {},
update_available: false,
}),
status: 200,
}),
);
});

const pmmTest = base.extend<{
agentsPage: AgentsPage;
cliHelper: CliHelper;
Expand All @@ -67,6 +41,7 @@ const pmmTest = base.extend<{
queryAnalytics: QueryAnalytics;
nodesPage: NodesPage;
realTimeAnalyticsPage: RealTimeAnalyticsPage;
vacuumDashboardPage: VacuumDashboard;
}>({
agentsPage: async ({ page }, use) => await use(new AgentsPage(page)),
api: async ({ page, request }, use) => {
Expand All @@ -80,7 +55,7 @@ const pmmTest = base.extend<{
await use(cliHelper);
},
context: async ({ context }, use) => {
await context.route('**/v1/users/me', (route) =>
await context.route(apiEndpoints.users.me, (route) =>
route.fulfill({
body: JSON.stringify({
alerting_tour_completed: true,
Expand All @@ -92,8 +67,8 @@ const pmmTest = base.extend<{
status: 200,
}),
);
await context.route('**/v1/server/updates**', (route) => {
return route.fulfill({
await context.route(apiEndpoints.server.updates, (route) =>
route.fulfill({
body: JSON.stringify({
installed: {},
last_check: new Date().toISOString(),
Expand All @@ -102,8 +77,8 @@ const pmmTest = base.extend<{
}),
contentType: 'application/json',
status: 200,
});
});
}),
);
await use(context);
},
credentials: async ({}, use) => {
Expand Down Expand Up @@ -179,6 +154,7 @@ const pmmTest = base.extend<{

await use(urlHelper);
},
vacuumDashboardPage: async ({ page }, use) => await use(new VacuumDashboard(page)),
});

export default pmmTest;
83 changes: 83 additions & 0 deletions e2e_tests/pages/dashboards/postgresql/vacuumDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect, Locator } from '@playwright/test';
import CliHelper from '@helpers/cli.helper';
import { Timeouts } from '@helpers/timeouts';
import pmmTest from '@fixtures/pmmTest';
import DashboardInterface from '@interfaces/dashboard';
import BasePage from '@pages/base.page';

export default class VacuumDashboard extends BasePage implements DashboardInterface {
readonly url = 'graph/d/postgres_vacuum_monitoring/postgresql-vacuum-monitoring';
metrics = [];
noDataMetrics = [];
builders = {};
buttons = {};
elements = {
lastAnalyzeValue: this.grafanaIframe().locator(
'//div[contains(@class, "react-grid-item")][6]//div[contains(text(), "dvdrental")]//following-sibling::*',
),
lastVacuumValue: this.grafanaIframe().locator(
'//div[contains(@class, "react-grid-item")][5]//div[contains(text(), "dvdrental")]//following-sibling::*',
),
};
inputs = {};
messages = {};

vacuumAnalyzeTables = async (tables: string[], containerName: string) => {
const allowedPrefixes = [
'film',
'actor',
'store',
'address',
'category',
'city',
'country',
'customer',
'inventory',
'language',
'rental',
'staff',
'payment',
];
const cliHelper = new CliHelper();

for (const rawTable of tables) {
const table = rawTable.trim();

if (!table) continue;
if (!allowedPrefixes.some((prefix) => table.includes(prefix))) continue;

await pmmTest.step(`Run VACUUM (ANALYZE) on ${table}`, async () => {
cliHelper
.execSilent(
`docker exec ${containerName} psql -U postgres -d dvdrental -c 'VACUUM (ANALYZE) ${table}'`,
)
.assertSuccess();
});
}
};

waitForLastAnalyzeValues = async (timeout: Timeouts = Timeouts.TEN_MINUTES) => {
await this.waitForValidDate(this.elements.lastAnalyzeValue, 'Last Analyze', timeout);
};

waitForLastVacuumValues = async (timeout: Timeouts = Timeouts.TEN_MINUTES) => {
await this.waitForValidDate(this.elements.lastVacuumValue, 'Last Vacuum', timeout);
};

private waitForValidDate = async (locator: Locator, label: string, timeout: number) => {
const pollInterval = Timeouts.FIVE_SECONDS;
const deadline = Date.now() + timeout;

while (Date.now() < deadline) {
const values = await locator.allTextContents();
const hasValidDate = values.some((value) => !Number.isNaN(new Date(value).valueOf()));

if (hasValidDate) return;

// eslint-disable-next-line playwright/no-wait-for-timeout -- polling the dashboard for async metric updates
await this.page.waitForTimeout(pollInterval);
}

expect(false, `No valid ${label} date was rendered within ${timeout / Timeouts.ONE_SECOND}s`).toBe(true);
};
}
23 changes: 23 additions & 0 deletions e2e_tests/testdata/vacuumDashboardSetup.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- Creates the `dvdrental` database and 10 film_testing_* tables seeded with
-- 5,000 rows each. Length values are spread across 100..220 so the churn
-- loop in the test can pick a random length and find rows to delete/update.
CREATE DATABASE dvdrental;
\connect dvdrental

DO $$
BEGIN
FOR i IN 1..10 LOOP
EXECUTE format(
'CREATE TABLE film_testing_%s (id int, title text, description text, length int)',
i
);
EXECUTE format(
$sql$
INSERT INTO film_testing_%s (id, title, description, length)
SELECT g, 'title for ' || g, 'Description for ' || g, 100 + (g %% 121)
FROM generate_series(1, 5000) AS g
$sql$,
i
);
END LOOP;
END $$;
78 changes: 78 additions & 0 deletions e2e_tests/tests/dashboards/postgresql/vacuumDashboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from '@playwright/test';
import pmmTest from '@fixtures/pmmTest';
import { Timeouts } from '@helpers/timeouts';

const SETUP_SQL = 'testdata/vacuumDashboardSetup.sql';
const randomInRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
let pgsqlContainerName: string;

pmmTest.beforeAll(async ({ cliHelper }) => {
pgsqlContainerName = cliHelper
.execSilent('docker ps -f name=pgsql --format "{{ .Names }}"')
.stdout.trim()
.split(/\r?\n/)[0];

if (!pgsqlContainerName) throw new Error('No running container matching "pgsql" was found.');

cliHelper
.execSilent(`docker exec -i ${pgsqlContainerName} psql -U postgres -v ON_ERROR_STOP=1 < ${SETUP_SQL}`)
.assertSuccess();
});

pmmTest.beforeEach(async ({ grafanaHelper }) => {
await grafanaHelper.authorize();
});

pmmTest(
'PMM-T1365 - Verify PostgreSQL Vacuum monitoring dashboard @dashboards @experimental @pmm-pgsql-integration',
async ({ cliHelper, dashboard, page, urlHelper, vacuumDashboardPage }) => {
const MIN_LENGTH = 100;
const MAX_LENGTH = 220;
const ITERATIONS = 3;


await pmmTest.step('Churn rows to produce vacuum/analyze activity', async () => {
for (let i = 0; i < ITERATIONS; i++) {
const table = randomInRange(1, 10);
const oldLength = randomInRange(MIN_LENGTH, MAX_LENGTH);
const newLength = randomInRange(MIN_LENGTH, MAX_LENGTH);
const churnSql = [
`DELETE FROM film_testing_${table} WHERE length = ${oldLength};`,
`INSERT INTO film_testing_${table} (id, title, description, length)`,
` SELECT g, 'title for ' || g, 'Description for ' || g, ${oldLength}`,
` FROM generate_series(1, 50) g;`,
`UPDATE film_testing_${table} SET length = ${newLength} WHERE length = ${oldLength};`,
].join(' ');

cliHelper
.execSilent(`docker exec ${pgsqlContainerName} psql -U postgres -d dvdrental -c "${churnSql}"`)
.assertSuccess();

// eslint-disable-next-line playwright/no-wait-for-timeout -- give PMM time to scrape updated stats
await page.waitForTimeout(Timeouts.FIVE_SECONDS);
}
});

await page.goto(
urlHelper.buildUrlWithParameters(vacuumDashboardPage.url, {
from: 'now-5m',
refresh: '10s',
serviceName: pgsqlContainerName,
}),
);

await expect(dashboard.panels().barGauge.elements.barWithValue('Dead Tuples').first()).toBeVisible({
timeout: Timeouts.FIVE_MINUTES,
});

const allTables = cliHelper
.execSilent(
`docker exec ${pgsqlContainerName} psql -U postgres -d dvdrental -t -A -c "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public'"`,
)
.stdout.split(/\r?\n/);

await vacuumDashboardPage.vacuumAnalyzeTables(allTables, pgsqlContainerName);
await vacuumDashboardPage.waitForLastVacuumValues(Timeouts.TEN_MINUTES);
await vacuumDashboardPage.waitForLastAnalyzeValues(Timeouts.TEN_MINUTES);
},
);
Loading