[scout] fix playwright configs discovery script and add save flag for CI (#213147)

## Summary

This PR fixes the search logic to look for playwright configs in all
possible & expected locations (`src/*` was not working), matching one of
3 regexp:
```
      /(x-pack\/platform\/plugins\/(?:private|shared|[^\/]+)\/([^\/]+))\/ui_tests\//,
      /(x-pack\/solutions\/[^\/]+\/plugins\/([^\/]+))\/ui_tests\//,
      /(src\/platform\/plugins\/(?:private|shared)?\/?([^\/]+))\/ui_tests\//,
```

For each plugin we also have `usesParallelWorkers` prop (`true` if at
least 1 config runs with concurrent workers) to decide later, if we need
worker with 4 or 8 VCPUs.

The idea is to run `node scripts/scout discover-playwright-configs
--save` on CI and use generated json as source to build test run
pipeline.

Current output:

```
{
  "discover_enhanced": {
    "group": "platform",
    "pluginPath": "x-pack/platform/plugins/private/discover_enhanced",
    "configs": [
      "x-pack/platform/plugins/private/discover_enhanced/ui_tests/parallel.playwright.config.ts",
      "x-pack/platform/plugins/private/discover_enhanced/ui_tests/playwright.config.ts"
    ],
    "usesParallelWorkers": true
  },
  "maps": {
    "group": "platform",
    "pluginPath": "x-pack/platform/plugins/shared/maps",
    "configs": [
      "x-pack/platform/plugins/shared/maps/ui_tests/playwright.config.ts"
    ],
    "usesParallelWorkers": false
  },
  "observability_onboarding": {
    "group": "observability",
    "pluginPath": "x-pack/solutions/observability/plugins/observability_onboarding",
    "configs": [
      "x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/parallel.playwright.config.ts",
      "x-pack/solutions/observability/plugins/observability_onboarding/ui_tests/playwright.config.ts"
    ],
    "usesParallelWorkers": true
  }
}
```
This commit is contained in:
Dzmitry Lemechko 2025-03-05 14:49:43 +01:00 committed by GitHub
parent 75f9c6113d
commit 1e3bb05734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 34 deletions

View file

@ -7,7 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import fs from 'fs';
import { Command } from '@kbn/dev-cli-runner';
import { SCOUT_OUTPUT_ROOT } from '@kbn/scout-info';
import { resolve } from 'path';
import { getScoutPlaywrightConfigs, DEFAULT_TEST_PATH_PATTERNS } from '../config';
import { measurePerformance } from '../common';
@ -21,29 +24,41 @@ export const discoverPlaywrightConfigs: Command<void> = {
Common usage:
node scripts/scout discover-playwright-configs --searchPaths <search_paths>
node scripts/scout discover-playwright-configs --save
node scripts/scout discover-playwright-configs
`,
flags: {
string: ['searchPaths'],
default: { searchPaths: DEFAULT_TEST_PATH_PATTERNS },
boolean: ['save'],
default: { searchPaths: DEFAULT_TEST_PATH_PATTERNS, save: false },
},
run: ({ flagsReader, log }) => {
const searchPaths = flagsReader.arrayOfStrings('searchPaths')!;
const plugins = measurePerformance(log, 'Discovering playwright config files', () => {
const pluginsMap = measurePerformance(log, 'Discovering playwright config files', () => {
return getScoutPlaywrightConfigs(searchPaths, log);
});
const finalMessage =
plugins.size === 0
? 'No playwright config files found'
: `Found playwright config files in '${plugins.size}' plugins`;
pluginsMap.size === 0
? 'No Playwright config files found'
: `Found Playwright config files in '${pluginsMap.size}' plugins`;
if (pluginsMap.size > 0 && flagsReader.boolean('save')) {
const scoutConfigsFilePath = resolve(SCOUT_OUTPUT_ROOT, 'scout_playwright_configs.json');
fs.writeFileSync(
scoutConfigsFilePath,
JSON.stringify(Object.fromEntries(pluginsMap), null, 2)
);
log.info(`${finalMessage}. Saved to '${scoutConfigsFilePath}'`);
return;
}
log.info(finalMessage);
plugins.forEach((files, plugin) => {
log.info(`[${plugin}] plugin:`);
files.forEach((file) => {
pluginsMap.forEach((data, plugin) => {
log.info(`${data.group} / [${plugin}] plugin:`);
data.configs.map((file) => {
log.info(`- ${file}`);
});
});

View file

@ -35,21 +35,36 @@ describe('getScoutPlaywrightConfigs', () => {
it('should correctly extract plugin names and group config files', () => {
(fastGlob.sync as jest.Mock).mockReturnValue([
'x-pack/platform/plugins/plugin_a/ui_tests/playwright.config.ts',
'x-pack/platform/plugins/plugin_a/ui_tests/parallel.playwright.config.ts',
'x-pack/platform/plugins/private/plugin_a/ui_tests/playwright.config.ts',
'x-pack/platform/plugins/private/plugin_a/ui_tests/parallel.playwright.config.ts',
'x-pack/solutions/security/plugins/plugin_b/ui_tests/playwright.config.ts',
'src/platform/plugins/shared/plugin_c/ui_tests/playwright.config.ts',
]);
const plugins = getScoutPlaywrightConfigs(['x-pack/'], mockLog);
const plugins = getScoutPlaywrightConfigs(['x-pack/', 'src/'], mockLog);
expect(plugins.size).toBe(2);
expect(plugins.get('plugin_a')).toEqual([
'x-pack/platform/plugins/plugin_a/ui_tests/playwright.config.ts',
'x-pack/platform/plugins/plugin_a/ui_tests/parallel.playwright.config.ts',
]);
expect(plugins.get('plugin_b')).toEqual([
'x-pack/solutions/security/plugins/plugin_b/ui_tests/playwright.config.ts',
]);
expect(plugins.size).toBe(3);
expect(plugins.get('plugin_a')).toEqual({
configs: [
'x-pack/platform/plugins/private/plugin_a/ui_tests/playwright.config.ts',
'x-pack/platform/plugins/private/plugin_a/ui_tests/parallel.playwright.config.ts',
],
usesParallelWorkers: true,
group: 'platform',
pluginPath: 'x-pack/platform/plugins/private/plugin_a',
});
expect(plugins.get('plugin_b')).toEqual({
configs: ['x-pack/solutions/security/plugins/plugin_b/ui_tests/playwright.config.ts'],
usesParallelWorkers: false,
group: 'security',
pluginPath: 'x-pack/solutions/security/plugins/plugin_b',
});
expect(plugins.get('plugin_c')).toEqual({
configs: ['src/platform/plugins/shared/plugin_c/ui_tests/playwright.config.ts'],
usesParallelWorkers: false,
group: 'platform',
pluginPath: 'src/platform/plugins/shared/plugin_c',
});
});
it('should log a warning if a file path does not match the expected pattern', () => {

View file

@ -13,35 +13,78 @@ import { ToolingLog } from '@kbn/tooling-log';
export const DEFAULT_TEST_PATH_PATTERNS = ['src/platform/plugins', 'x-pack/**/plugins'];
interface PluginScoutConfig {
group: string;
pluginPath: string;
usesParallelWorkers: boolean;
configs: string[];
}
export const getScoutPlaywrightConfigs = (searchPaths: string[], log: ToolingLog) => {
const patterns = searchPaths.map((basePath) =>
path.join(basePath, '**/ui_tests/{playwright.config.ts,parallel.playwright.config.ts}')
);
log.info('Searching for playwright config files in the following paths:');
log.info('Searching for Playwright config files in the following paths:');
patterns.forEach((pattern) => log.info(`- ${pattern}`));
log.info(''); // Add a newline for better readability
const files = patterns.flatMap((pattern) => fastGlob.sync(pattern, { onlyFiles: true }));
// Group config files by plugin
const plugins = files.reduce((acc: Map<string, string[]>, filePath: string) => {
const match = filePath.match(
/(?:src\/platform\/plugins|x-pack\/.*?\/plugins)\/(?:.*?\/)?([^\/]+)\/ui_tests\//
);
const pluginName = match ? match[1] : null;
const typeMappings: Record<string, string> = {
'x-pack/solutions/security': 'security',
'x-pack/solutions/search': 'search',
'x-pack/solutions/observability': 'observability',
'x-pack/platform/plugins': 'platform',
'src/platform/plugins': 'platform',
};
if (pluginName) {
if (!acc.has(pluginName)) {
acc.set(pluginName, []);
const matchPluginPath = (filePath: string): { pluginPath: string; pluginName: string } | null => {
const regexes = [
/(x-pack\/platform\/plugins\/(?:private|shared|[^\/]+)\/([^\/]+))\/ui_tests\//,
/(x-pack\/solutions\/[^\/]+\/plugins\/([^\/]+))\/ui_tests\//,
/(src\/platform\/plugins\/(?:private|shared)?\/?([^\/]+))\/ui_tests\//,
];
for (const regex of regexes) {
const match = filePath.match(regex);
if (match) {
return { pluginPath: match[1], pluginName: match[2] };
}
acc.get(pluginName)!.push(filePath);
} else {
}
return null;
};
const pluginsWithConfigs = new Map<string, PluginScoutConfig>();
files.forEach((filePath) => {
const matchResult = matchPluginPath(filePath);
if (!matchResult) {
log.warning(`Unable to extract plugin name from path: ${filePath}`);
return;
}
return acc;
}, new Map<string, string[]>());
const { pluginPath, pluginName } = matchResult;
const group =
Object.entries(typeMappings).find(([key]) => filePath.includes(key))?.[1] || 'unknown';
return plugins;
if (!pluginsWithConfigs.has(pluginName)) {
pluginsWithConfigs.set(pluginName, {
group,
pluginPath,
configs: [],
usesParallelWorkers: false,
});
}
const pluginData = pluginsWithConfigs.get(pluginName)!;
if (!pluginData.configs.includes(filePath)) {
pluginData.configs.push(filePath);
if (filePath.endsWith('parallel.playwright.config.ts')) {
pluginData.usesParallelWorkers = true;
}
}
});
return pluginsWithConfigs;
};