-
Notifications
You must be signed in to change notification settings - Fork 34
OLS-2654 - adding artifact exporting. Should help investigations #1940
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
base: release-4.19
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,79 @@ | ||||||||||||||||||||||||
| import { defineConfig } from 'cypress'; | ||||||||||||||||||||||||
| import { plugin as cypressGrepPlugin } from '@cypress/grep/plugin'; | ||||||||||||||||||||||||
| import { execFileSync } from 'child_process'; | ||||||||||||||||||||||||
| import * as fs from 'fs'; | ||||||||||||||||||||||||
| import * as path from 'path'; | ||||||||||||||||||||||||
| import * as console from 'console'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const ARTIFACTS_DIR = './gui_test_screenshots/artifacts'; | ||||||||||||||||||||||||
| const OLS_NAMESPACE = 'openshift-lightspeed'; | ||||||||||||||||||||||||
| const OC_TIMEOUT = 30000; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const CLUSTER_RESOURCES = [ | ||||||||||||||||||||||||
| 'pods', | ||||||||||||||||||||||||
| 'services', | ||||||||||||||||||||||||
| 'deployments', | ||||||||||||||||||||||||
| 'replicasets', | ||||||||||||||||||||||||
| 'routes', | ||||||||||||||||||||||||
| 'rolebindings', | ||||||||||||||||||||||||
| 'serviceaccounts', | ||||||||||||||||||||||||
| 'olsconfig', | ||||||||||||||||||||||||
| 'clusterserviceversion', | ||||||||||||||||||||||||
| 'installplan', | ||||||||||||||||||||||||
| 'configmap', | ||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function runOC(args: string[], kubeconfigPath: string): string | null { | ||||||||||||||||||||||||
| const argv = kubeconfigPath ? [...args, '--kubeconfig', kubeconfigPath] : args; | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| return execFileSync('oc', argv, { | ||||||||||||||||||||||||
| encoding: 'utf-8', | ||||||||||||||||||||||||
| timeout: OC_TIMEOUT, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } catch (e: unknown) { | ||||||||||||||||||||||||
| console.error(`oc ${args.slice(0, 3).join(' ')} failed: ${e}`); | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function gatherClusterArtifacts(kubeconfigPath: string) { | ||||||||||||||||||||||||
| const clusterDir = path.join(ARTIFACTS_DIR, 'cluster'); | ||||||||||||||||||||||||
| const podLogsDir = path.join(clusterDir, 'podlogs'); | ||||||||||||||||||||||||
| fs.mkdirSync(podLogsDir, { recursive: true }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for (const resource of CLUSTER_RESOURCES) { | ||||||||||||||||||||||||
| const output = runOC(['get', resource, '-n', OLS_NAMESPACE, '-o', 'yaml'], kubeconfigPath); | ||||||||||||||||||||||||
| if (output) { | ||||||||||||||||||||||||
| fs.writeFileSync(path.join(clusterDir, `${resource}.yaml`), output); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Pod logs | ||||||||||||||||||||||||
| const podsJson = runOC(['get', 'pods', '-n', OLS_NAMESPACE, '-o', 'json'], kubeconfigPath); | ||||||||||||||||||||||||
| if (podsJson) { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| const pods = JSON.parse(podsJson); | ||||||||||||||||||||||||
| for (const pod of pods.items || []) { | ||||||||||||||||||||||||
| const podName = pod.metadata?.name; | ||||||||||||||||||||||||
| const containers = (pod.spec?.containers || []).map((c: { name: string }) => c.name); | ||||||||||||||||||||||||
| for (const container of containers) { | ||||||||||||||||||||||||
| const logs = runOC( | ||||||||||||||||||||||||
| ['logs', `pod/${podName}`, '-c', container, '-n', OLS_NAMESPACE], | ||||||||||||||||||||||||
| kubeconfigPath, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| if (logs) { | ||||||||||||||||||||||||
| fs.writeFileSync(path.join(podLogsDir, `${podName}-${container}.log`), logs); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } catch (e: unknown) { | ||||||||||||||||||||||||
| console.error(`Failed to parse pod JSON: ${e}`); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| console.log(`Cluster artifacts gathered in ${clusterDir}`); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export default defineConfig({ | ||||||||||||||||||||||||
| screenshotsFolder: './gui_test_screenshots/cypress/screenshots', | ||||||||||||||||||||||||
| screenshotOnRunFailure: true, | ||||||||||||||||||||||||
|
|
@@ -72,13 +143,17 @@ export default defineConfig({ | |||||||||||||||||||||||
| console.table(data); | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| gatherClusterArtifacts() { | ||||||||||||||||||||||||
| gatherClusterArtifacts(config.env.KUBECONFIG_PATH || ''); | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| cypressGrepPlugin(config); | ||||||||||||||||||||||||
| on('after:spec', (spec: Cypress.Spec, results: CypressCommandLine.RunResult) => { | ||||||||||||||||||||||||
| on('after:spec', (_spec: Cypress.Spec, results: CypressCommandLine.RunResult) => { | ||||||||||||||||||||||||
| if (results && results.video) { | ||||||||||||||||||||||||
| // Do we have failures for any retry attempts? | ||||||||||||||||||||||||
| const failures = results.tests.some((test) => | ||||||||||||||||||||||||
| test.attempts.some((attempt) => attempt.state === 'failed'), | ||||||||||||||||||||||||
| const failures = results.tests?.some((test) => | ||||||||||||||||||||||||
| test.attempts?.some((attempt) => attempt.state === 'failed'), | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| if (!failures && fs.existsSync(results.video)) { | ||||||||||||||||||||||||
|
Comment on lines
+155
to
158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd -t f "cypress.config.ts" | head -5Repository: openshift/lightspeed-console Length of output: 90 🏁 Script executed: cat -n cypress.config.ts | sed -n '150,165p'Repository: openshift/lightspeed-console Length of output: 790 Default to "keep video" when test outcome is unknown to prevent unintended deletion of failure videos. At line 155-157, Proposed patch- const failures = results.tests?.some((test) =>
+ const failures = results.tests?.some((test) =>
test.attempts?.some((attempt) => attempt.state === 'failed'),
- );
- if (!failures && fs.existsSync(results.video)) {
+ ) ?? true;
+ if (!failures && fs.existsSync(results.video)) {
// Delete the video if the spec passed and no tests retried
fs.unlinkSync(results.video);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| // Delete the video if the spec passed and no tests retried | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,16 @@ import { register } from '@cypress/grep'; | |
|
|
||
| register(); | ||
|
|
||
| Cypress.on('uncaught:exception', (err) => { | ||
| // The console defines __load_plugin_entry__ via webpack module federation | ||
| // before loading plugin bundles, but race conditions during page load can | ||
| // cause the plugin script to execute before the global is set. | ||
| // This is a console-internal timing issue, not a test failure. | ||
| if (err.message?.includes('__load_plugin_entry__')) { | ||
| return false; | ||
| } | ||
| }); | ||
|
Comment on lines
+7
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The recommended best practices for Cypress.on('uncaught:exception') handlers, as per official documentation, focus on selectively suppressing known errors while allowing unrelated failures to propagate [1][2]. 1. Use conditional logic based on error.message to ignore only specific known errors. Return false only for matching errors; otherwise, let the test fail naturally [1]. Example for global handler (e.g., in cypress/support/e2e.js): Citations:
Narrow the exception filter to add specificity and logging. At line 12, suppressing exceptions by message substring alone is a valid but minimal approach. While message-only filtering is standard in Cypress examples for known errors, adding a stack trace check makes the filter more specific and reduces the risk of accidentally masking unrelated failures. Additionally, logging when suppression occurs is an explicit best practice for debugging. Proposed patch Cypress.on('uncaught:exception', (err) => {
- if (err.message?.includes('__load_plugin_entry__')) {
+ const isPluginBootstrapRace =
+ err.message?.includes('__load_plugin_entry__') &&
+ /lightspeed-console-plugin|module federation|webpack/i.test(err.stack ?? '');
+
+ if (isPluginBootstrapRace) {
+ Cypress.log({
+ name: 'uncaught:exception:suppressed',
+ message: err.message,
+ });
return false;
}
});🤖 Prompt for AI Agents |
||
|
|
||
| // Collect browser console errors and warnings for output after each test | ||
| const browserLogs = []; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid collecting artifacts from an unintended cluster context.
At Line 147, falling back to
''meansoccan use ambient/default kubeconfig, which may export data from the wrong cluster. Skip collection (or fail fast) whenKUBECONFIG_PATHis not set.Proposed patch
gatherClusterArtifacts() { - gatherClusterArtifacts(config.env.KUBECONFIG_PATH || ''); + const kubeconfigPath = config.env.KUBECONFIG_PATH; + if (!kubeconfigPath) { + console.warn('Skipping cluster artifact gathering: KUBECONFIG_PATH is not set'); + return null; + } + gatherClusterArtifacts(kubeconfigPath); return null; },📝 Committable suggestion
🤖 Prompt for AI Agents