-
Notifications
You must be signed in to change notification settings - Fork 0
Node18/conformance test #25
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: node-18
Are you sure you want to change the base?
Conversation
Summary of ChangesHello @thiyaguk09, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refactors the conformance test suite by shifting many test methods to directly interact with the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
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.
Code Review
This pull request refactors the conformance tests to interact with the transport layer directly, which is a solid approach for testing retry logic. The changes also include improvements to the default retry function in the main library. The overall direction is good, but I've identified a few issues, primarily concerning error handling, type safety, and a bug in header manipulation within the test suite. Addressing these will improve the robustness and reliability of the tests.
|
/gemini review |
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.
Code Review
This pull request refactors the conformance tests to use the transport layer directly, allowing for more granular testing of retry logic. The changes are extensive and mostly involve rewriting library methods to make direct REST API calls. I've found a couple of critical issues in the new test setup logic that could lead to runtime errors or incorrect behavior. Additionally, there are a few instances of commented-out code that should be removed for cleanliness. The improvements to the retryable error detection function in src/storage.ts are well-done and increase robustness.
|
/gemini review |
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.
Code Review
This pull request introduces significant refactoring to the conformance tests, primarily to align with Node.js 18 and to test retry logic at the transport layer. The changes involve bypassing high-level SDK methods in favor of direct calls to storageTransport.makeRequest, implementing a complex custom retry strategy, and updating various test helper functions. My review focuses on improving type safety, removing leftover debugging code, and enhancing code clarity in these new implementations.
| export async function create(options: ConformanceTestOptions) { | ||
| const [bucketExists] = await options.bucket!.exists(); | ||
| if (!options.storageTransport || !options.projectId || !options.bucket) { | ||
| throw new Error( | ||
| 'storageTransport, projectId, and bucket are required for the create test.', | ||
| ); | ||
| } | ||
| const bucketName = options.bucket.name; | ||
| let bucketExists = false; | ||
| try { | ||
| const existsReq: StorageRequestOptions = { | ||
| method: 'GET', | ||
| url: `storage/v1/b/${bucketName}`, | ||
| }; | ||
| await options.storageTransport.makeRequest(existsReq); | ||
| bucketExists = true; | ||
| } catch (error: unknown) { | ||
| const gaxiosError = error as GaxiosError; | ||
| if (gaxiosError.response?.status === 404) { | ||
| console.log(`Bucket ${bucketName} does not exist.`); | ||
| } else { | ||
| console.warn(`Error checking existence of ${bucketName}:`, gaxiosError); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| if (bucketExists) { | ||
| await options.bucket!.deleteFiles(); | ||
| await options.bucket!.delete({ | ||
| ignoreNotFound: true, | ||
| }); | ||
| let pageToken: string | undefined = undefined; | ||
| do { | ||
| const listReq: StorageRequestOptions = { | ||
| method: 'GET', | ||
| url: `storage/v1/b/${bucketName}/o`, | ||
| queryParameters: pageToken ? {pageToken} : undefined, | ||
| }; | ||
| try { | ||
| const listResult = await options.storageTransport.makeRequest(listReq); | ||
| const objects = (listResult as any)?.items || []; | ||
|
|
||
| for (const obj of objects) { | ||
| const deleteObjReq: StorageRequestOptions = { | ||
| method: 'DELETE', | ||
| url: `storage/v1/b/${bucketName}/o/${obj.name}`, | ||
| }; | ||
| try { | ||
| await options.storageTransport.makeRequest(deleteObjReq); | ||
| } catch (deleteErr: unknown) { | ||
| console.warn(`Error deleting object ${obj.name}:`, deleteErr); | ||
| } | ||
| } | ||
| pageToken = (listResult as any)?.nextPageToken; | ||
| } catch (listErr: unknown) { | ||
| console.error( | ||
| `Error listing objects in bucket ${bucketName}:`, | ||
| listErr, | ||
| ); | ||
| throw listErr; | ||
| } | ||
| } while (pageToken); | ||
|
|
||
| const deleteBucketReq: StorageRequestOptions = { | ||
| method: 'DELETE', | ||
| url: `storage/v1/b/${bucketName}`, | ||
| }; | ||
| try { | ||
| await options.storageTransport.makeRequest(deleteBucketReq); | ||
| } catch (deleteErr: unknown) { | ||
| const gaxiosError = deleteErr as GaxiosError; | ||
| if (gaxiosError.response?.status !== 404) { | ||
| throw deleteErr; | ||
| } | ||
| } | ||
| } | ||
| await options.bucket!.create(); | ||
|
|
||
| const createRequest: StorageRequestOptions = { | ||
| method: 'POST', | ||
| url: `storage/v1/b?project=${options.projectId}`, | ||
| body: JSON.stringify({name: bucketName}), | ||
| headers: {'Content-Type': 'application/json'}, | ||
| }; | ||
| await options.storageTransport.makeRequest(createRequest); | ||
| } |
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.
This function has a few areas for improvement:
- It contains several
console.log,console.warn, andconsole.errorstatements (e.g., lines 139, 141, 166, 171). This logging seems intended for debugging and should be removed from the final test code to keep the test output clean. - It uses
anycasts in multiple places (e.g., lines 156, 169) to access properties on API responses. This bypasses TypeScript's type safety. To improve robustness, please consider defining interfaces for the expected response shapes.
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (data as any).headers = plainHeaders; |
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.
Attaching response headers directly to the data object via (data as any).headers = plainHeaders; is not type-safe and can be considered an unexpected side effect. While this is likely a workaround to pass headers to higher-level functions for the conformance tests, a cleaner approach would be to return a more structured object from makeRequest. If that's too large of a change, consider adding a TODO comment explaining this pattern or defining a specific response type that includes headers to avoid using any.
All 720 test cases have been fixed, and the code has been refactored.
|
/gemini review |
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.
Code Review
This pull request refactors the conformance tests to use a lower-level transport layer for making requests, which provides better control for retry tests. The changes also include a more sophisticated and specific retry logic in the StorageTransport. The overall direction is good, but I've found a few areas for improvement: some test helper functions are still using the old SDK methods, and there's a potential bug in header handling within the transport layer. Additionally, the new retry logic could be refactored for better readability.
| shouldRetry: (err: GaxiosError) => { | ||
| const status = err.response?.status; | ||
| const errorCode = err.code?.toString(); | ||
| const retryableStatuses = [408, 429, 500, 502, 503, 504]; | ||
| const nonRetryableStatuses = [401, 405, 412]; | ||
|
|
||
| const isMalformedResponse = | ||
| err.message?.includes('JSON') || | ||
| err.message?.includes('Unexpected token <') || | ||
| (err.stack && err.stack.includes('SyntaxError')); | ||
| if (isMalformedResponse) return true; | ||
|
|
||
| if (status && nonRetryableStatuses.includes(status)) return false; | ||
|
|
||
| const params = reqOpts.queryParameters || {}; | ||
| const hasPrecondition = | ||
| params.ifGenerationMatch !== undefined || | ||
| params.ifMetagenerationMatch !== undefined || | ||
| params.ifSourceGenerationMatch !== undefined; | ||
|
|
||
| const isPost = reqOpts.method?.toUpperCase() === 'POST'; | ||
| const isPatch = reqOpts.method?.toUpperCase() === 'PATCH'; | ||
| const isPut = reqOpts.method?.toUpperCase() === 'PUT'; | ||
| const isGet = reqOpts.method?.toUpperCase() === 'GET'; | ||
| const isHead = reqOpts.method?.toUpperCase() === 'HEAD'; | ||
|
|
||
| const isIam = urlString.includes('/iam'); | ||
| const isAcl = urlString.includes('/acl'); | ||
| const isHmacRequest = urlString.includes('/hmacKeys'); | ||
| const isNotificationRequest = urlString.includes( | ||
| '/notificationConfigs', | ||
| ); | ||
|
|
||
| // Logic for Mutations (POST, PATCH, DELETE) | ||
| if (isPost || isPatch || isDelete) { | ||
| const isRetryTest = urlString.includes('retry-test-id'); | ||
| if (isPost && isAcl) { | ||
| if (isRetryTest) { | ||
| return status ? retryableStatuses.includes(status) : false; | ||
| } | ||
| return false; | ||
| } | ||
| if (isPost && (isHmacRequest || isNotificationRequest)) | ||
| return false; | ||
|
|
||
| const isBucketCreate = | ||
| isPost && | ||
| urlString.includes('/v1/b') && | ||
| !urlString.includes('/o'); | ||
| const isSafeDelete = isDelete && !urlString.includes('/o/'); | ||
|
|
||
| if (!hasPrecondition) { | ||
| if (!isBucketCreate && !isSafeDelete) { | ||
| if (urlString.includes('uploadType=resumable') && isPost) { | ||
| return !!status && retryableStatuses.includes(status); | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| if (status === undefined) { | ||
| const isResumable = urlString.includes('uploadType=resumable'); | ||
|
|
||
| if (isResumable) return false; | ||
| return hasPrecondition || isBucketCreate || isSafeDelete; | ||
| } | ||
|
|
||
| return retryableStatuses.includes(status); | ||
| } | ||
|
|
||
| if (isPut) { | ||
| const url = err.config?.url.toString() || ''; | ||
| if (isHmacRequest) { | ||
| try { | ||
| const body = | ||
| typeof reqOpts.body === 'string' | ||
| ? JSON.parse(reqOpts.body) | ||
| : reqOpts.body; | ||
|
|
||
| if (!body || !body.etag) { | ||
| return false; | ||
| } | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| } else if (isIam) { | ||
| try { | ||
| let hasIamPrecondition = false; | ||
| const bodyStr = | ||
| typeof reqOpts.body === 'string' | ||
| ? reqOpts.body | ||
| : reqOpts.body instanceof Buffer | ||
| ? reqOpts.body.toString() | ||
| : ''; | ||
| hasIamPrecondition = !!JSON.parse(bodyStr || '{}').etag; | ||
| if (!hasIamPrecondition) { | ||
| return false; | ||
| } | ||
| return status === undefined || status === 503; | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| } else if (url.includes('upload_id=')) { | ||
| if (!status || retryableStatuses.includes(status)) { | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Logic for Idempotent Methods (GET, PUT, HEAD) | ||
| const isIdempotentMethod = isGet || isHead || isPut; | ||
| if (isIdempotentMethod) { | ||
| if (status === undefined) { | ||
| return true; | ||
| } | ||
| return retryableStatuses.includes(status); | ||
| } | ||
|
|
||
| if ( | ||
| isDelete && | ||
| !hasPrecondition && | ||
| !isNotificationRequest && | ||
| !isHmacRequest | ||
| ) | ||
| return false; | ||
|
|
||
| const transientNetworkErrors = [ | ||
| 'ECONNRESET', | ||
| 'ETIMEDOUT', | ||
| 'EADDRINUSE', | ||
| 'ECONNREFUSED', | ||
| 'EPIPE', | ||
| 'ENOTFOUND', | ||
| 'ENETUNREACH', | ||
| ]; | ||
| if (errorCode && transientNetworkErrors.includes(errorCode)) | ||
| return true; | ||
|
|
||
| const data = err.response?.data; | ||
| if (data && data.error && Array.isArray(data.error.errors)) { | ||
| for (const e of data.error.errors) { | ||
| const reason = e.reason; | ||
| if ( | ||
| reason === 'rateLimitExceeded' || | ||
| reason === 'userRateLimitExceeded' || | ||
| (reason && reason.includes('EAI_AGAIN')) | ||
| ) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| if (!status) return true; | ||
| return status ? retryableStatuses.includes(status) : false; | ||
| }, |
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.
- Replace high-level bucket and file calls with storageTransport.makeRequest. - Fix Scenario 1 failures by implementing "create-or-get" logic for buckets. - Resolve metageneration mismatch in lock() by dynamically fetching metadata. - Normalize header keys to lowercase in transport response processing. - Increase unit test coverage for shouldRetry logic and error handling.
Description
Impact
Testing
Additional Information
Checklist
Fixes #issue_number_goes_here 🦕