From c926b03654bdfbb9155f7b573fdb0054471c25ed Mon Sep 17 00:00:00 2001 From: Ava Silver Date: Fri, 26 Jun 2026 17:22:52 -0400 Subject: [PATCH] chore: add e2e synced shared helpers --- e2e/helpers/exec.ts | 270 ++++++++++++++++++++++++ e2e/helpers/lambda-telemetry-checker.ts | 3 - 2 files changed, 270 insertions(+), 3 deletions(-) diff --git a/e2e/helpers/exec.ts b/e2e/helpers/exec.ts index abec21b6..8151a94d 100644 --- a/e2e/helpers/exec.ts +++ b/e2e/helpers/exec.ts @@ -1,3 +1,93 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed + * under the Apache License Version 2.0. + * + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2026 Datadog, Inc. + */ + +import child_process from 'node:child_process'; + +// Runner-agnostic exec + bounded-retry helper: no jest/vitest imports, so it backs any +// test runner. "Retry the cloud, not the assertions": only transient provider errors are +// retried, on a bounded budget; a real failure surfaces immediately. + +export interface ExecResult { + exitCode: number; + stdout: string; + stderr: string; +} + +export interface ExecOptions { + env?: Record; + cwd?: string; + // Serverless / AWS / CFN calls can emit large output; default generous but bounded. + // CFN deploys need ~16MB; sls/cdk need more, so callers raise it as needed. + maxBuffer?: number; + // Transient cloud-provider error substrings safe to retry. Supplied by the caller so + // each cloud/tool contributes its own patterns; empty means never retry. + retryPatterns?: string[]; + maxAttempts?: number; + delaySeconds?: number; +} + +const DEFAULT_MAX_BUFFER = 50 * 1024 * 1024; +const DEFAULT_MAX_ATTEMPTS = 3; +const DEFAULT_DELAY_SECONDS = 10; + +export const execPromise = async (command: string, options: ExecOptions = {}): Promise => { + const {env, cwd, maxBuffer = DEFAULT_MAX_BUFFER} = options; + + return new Promise((resolve) => { + const child = child_process.exec(command, {env: {...process.env, ...env}, cwd, maxBuffer}, (error, stdout, stderr) => { + resolve({ + exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0, + stdout: stdout.trim(), + stderr: stderr.trim(), + }); + }); + + child.stdout?.pipe(process.stdout); + child.stderr?.pipe(process.stderr); + }); +}; + +const isRetryable = (result: ExecResult, patterns: string[]): boolean => { + const output = `${result.stdout} ${result.stderr}`; + + return patterns.some((pattern) => output.includes(pattern)); +}; + +const waitFor = (seconds: number): Promise => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); + +export const execPromiseWithRetries = async (command: string, options: ExecOptions = {}): Promise => { + const { + retryPatterns = [], + maxAttempts = DEFAULT_MAX_ATTEMPTS, + delaySeconds = DEFAULT_DELAY_SECONDS, + } = options; + + let result: ExecResult = {exitCode: 1, stdout: '', stderr: 'not run'}; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + result = await execPromise(command, options); + if (result.exitCode === 0) { + return result; + } + if (attempt < maxAttempts && isRetryable(result, retryPatterns)) { + // eslint-disable-next-line no-console + console.log(`Command failed with retryable error (attempt ${attempt}/${maxAttempts}), retrying in ${delaySeconds}s...`); + // eslint-disable-next-line no-console + console.log(`stdout: ${result.stdout}`); + // eslint-disable-next-line no-console + console.log(`stderr: ${result.stderr}`); + await waitFor(delaySeconds); + } else { + return result; + } + } + + return result; +}; // Code generated by serverless-ci e2e-shared sync. DO NOT EDIT. // Source of truth lives in serverless-ci/e2e/shared -- edit there; local changes are overwritten. @@ -91,3 +181,183 @@ export const execPromiseWithRetries = async (command: string, options: ExecOptio return result; }; +||||||| parent of 253c48b (chore: add e2e synced shared helpers) +======= +// Code generated by serverless-ci e2e-shared sync. DO NOT EDIT. +// Source of truth lives in serverless-ci/e2e/shared -- edit there; local changes are overwritten. + +/* + * Unless explicitly stated otherwise all files in this repository are licensed + * under the Apache License Version 2.0. + * + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2026 Datadog, Inc. + */ + +import child_process from 'node:child_process'; + +// Runner-agnostic exec + bounded-retry helper: no jest/vitest imports, so it backs any +// test runner. "Retry the cloud, not the assertions": only transient provider errors are +// retried, on a bounded budget; a real failure surfaces immediately. + +export interface ExecResult { + exitCode: number; + stdout: string; + stderr: string; +} + +export interface ExecOptions { + env?: Record; + cwd?: string; + // Serverless / AWS / CFN calls can emit large output; default generous but bounded. + // CFN deploys need ~16MB; sls/cdk need more, so callers raise it as needed. + maxBuffer?: number; + // Transient cloud-provider error substrings safe to retry. Supplied by the caller so + // each cloud/tool contributes its own patterns; empty means never retry. + retryPatterns?: string[]; + maxAttempts?: number; + delaySeconds?: number; +} + +const DEFAULT_MAX_BUFFER = 50 * 1024 * 1024; +const DEFAULT_MAX_ATTEMPTS = 3; +const DEFAULT_DELAY_SECONDS = 10; + +export const execPromise = async (command: string, options: ExecOptions = {}): Promise => { + const {env, cwd, maxBuffer = DEFAULT_MAX_BUFFER} = options; + + return new Promise((resolve) => { + child_process.exec(command, {env: {...process.env, ...env}, cwd, maxBuffer}, (error, stdout, stderr) => { + resolve({ + exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0, + stdout: stdout.trim(), + stderr: stderr.trim(), + }); + }); + }); +}; + +const isRetryable = (result: ExecResult, patterns: string[]): boolean => { + const output = `${result.stdout} ${result.stderr}`; + + return patterns.some((pattern) => output.includes(pattern)); +}; + +const waitFor = (seconds: number): Promise => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); + +export const execPromiseWithRetries = async (command: string, options: ExecOptions = {}): Promise => { + const { + retryPatterns = [], + maxAttempts = DEFAULT_MAX_ATTEMPTS, + delaySeconds = DEFAULT_DELAY_SECONDS, + } = options; + + let result: ExecResult = {exitCode: 1, stdout: '', stderr: 'not run'}; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + result = await execPromise(command, options); + if (result.exitCode === 0) { + return result; + } + if (attempt < maxAttempts && isRetryable(result, retryPatterns)) { + // eslint-disable-next-line no-console + console.log(`Command failed with retryable error (attempt ${attempt}/${maxAttempts}), retrying in ${delaySeconds}s...`); + // eslint-disable-next-line no-console + console.log(`stdout: ${result.stdout}`); + // eslint-disable-next-line no-console + console.log(`stderr: ${result.stderr}`); + await waitFor(delaySeconds); + } else { + return result; + } + } + + /* + * Unless explicitly stated otherwise all files in this repository are licensed + * under the Apache License Version 2.0. + * + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2026 Datadog, Inc. + */ + + import child_process from 'node:child_process'; + + // Runner-agnostic exec + bounded-retry helper: no jest/vitest imports, so it backs any + // test runner. "Retry the cloud, not the assertions": only transient provider errors are + // retried, on a bounded budget; a real failure surfaces immediately. + + export interface ExecResult { + exitCode: number; + stdout: string; + stderr: string; + } + + export interface ExecOptions { + env?: Record; + cwd?: string; + // Serverless / AWS / CFN calls can emit large output; default generous but bounded. + // CFN deploys need ~16MB; sls/cdk need more, so callers raise it as needed. + maxBuffer?: number; + // Transient cloud-provider error substrings safe to retry. Supplied by the caller so + // each cloud/tool contributes its own patterns; empty means never retry. + retryPatterns?: string[]; + maxAttempts?: number; + delaySeconds?: number; + } + + const DEFAULT_MAX_BUFFER = 50 * 1024 * 1024; + const DEFAULT_MAX_ATTEMPTS = 3; + const DEFAULT_DELAY_SECONDS = 10; + + export const execPromise = async (command: string, options: ExecOptions = {}): Promise => { + const {env, cwd, maxBuffer = DEFAULT_MAX_BUFFER} = options; + + return new Promise((resolve) => { + const child = child_process.exec(command, {env: {...process.env, ...env}, cwd, maxBuffer}, (error, stdout, stderr) => { + resolve({ + exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0, + stdout: stdout.trim(), + stderr: stderr.trim(), + }); + }); + + child.stdout?.pipe(process.stdout); + child.stderr?.pipe(process.stderr); + }); + }; + + const isRetryable = (result: ExecResult, patterns: string[]): boolean => { + const output = `${result.stdout} ${result.stderr}`; + + return patterns.some((pattern) => output.includes(pattern)); + }; + + const waitFor = (seconds: number): Promise => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); + + export const execPromiseWithRetries = async (command: string, options: ExecOptions = {}): Promise => { + const { + retryPatterns = [], + maxAttempts = DEFAULT_MAX_ATTEMPTS, + delaySeconds = DEFAULT_DELAY_SECONDS, + } = options; + + let result: ExecResult = {exitCode: 1, stdout: '', stderr: 'not run'}; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + result = await execPromise(command, options); + if (result.exitCode === 0) { + return result; + } + if (attempt < maxAttempts && isRetryable(result, retryPatterns)) { + // eslint-disable-next-line no-console + console.log(`Command failed with retryable error (attempt ${attempt}/${maxAttempts}), retrying in ${delaySeconds}s...`); + // eslint-disable-next-line no-console + console.log(`stdout: ${result.stdout}`); + // eslint-disable-next-line no-console + console.log(`stderr: ${result.stderr}`); + await waitFor(delaySeconds); + } else { + return result; + } + } + + return result; + }; diff --git a/e2e/helpers/lambda-telemetry-checker.ts b/e2e/helpers/lambda-telemetry-checker.ts index 0954909e..6f8f0f1a 100644 --- a/e2e/helpers/lambda-telemetry-checker.ts +++ b/e2e/helpers/lambda-telemetry-checker.ts @@ -1,6 +1,3 @@ -// Code generated by serverless-ci e2e-shared sync. DO NOT EDIT. -// Source of truth lives in serverless-ci/e2e/shared -- edit there; local changes are overwritten. - /* * Unless explicitly stated otherwise all files in this repository are licensed * under the Apache License Version 2.0.