diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 941a71b1576..5663e257e4b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -21,6 +21,7 @@ "core/precise-date": "6.0.1", "core/projectify": "6.0.1", "core/promisify": "6.0.1", + "core/test-utils": "0.1.0", "handwritten/bigquery": "9.0.3", "handwritten/bigquery-storage": "6.0.1", "handwritten/bigtable": "7.2.0", diff --git a/core/test-utils/.compodocrc b/core/test-utils/.compodocrc new file mode 100644 index 00000000000..cd8b42152a6 --- /dev/null +++ b/core/test-utils/.compodocrc @@ -0,0 +1,10 @@ +--- +tsconfig: ./tsconfig.json +output: ./docs +theme: material +hideGenerator: true +disablePrivate: true +disableProtected: true +disableInternal: true +disableCoverage: true +disableGraph: true diff --git a/core/test-utils/.gitattributes b/core/test-utils/.gitattributes new file mode 100644 index 00000000000..33739cb74e4 --- /dev/null +++ b/core/test-utils/.gitattributes @@ -0,0 +1,4 @@ +*.ts text eol=lf +*.js text eol=lf +protos/* linguist-generated +**/api-extractor.json linguist-language=JSON-with-Comments diff --git a/core/test-utils/.gitignore b/core/test-utils/.gitignore new file mode 100644 index 00000000000..4b8206885b0 --- /dev/null +++ b/core/test-utils/.gitignore @@ -0,0 +1,15 @@ +**/*.log +**/node_modules +.coverage +coverage/ +.nyc_output +docs/ +out/ +system-test/secrets.js +system-test/*key.json +*.lock +*-lock.js* +build/ +.vscode +package-lock.json +__pycache__ diff --git a/core/test-utils/.repo-metadata.json b/core/test-utils/.repo-metadata.json new file mode 100644 index 00000000000..daa524307f6 --- /dev/null +++ b/core/test-utils/.repo-metadata.json @@ -0,0 +1,11 @@ +{ + "name": "test-utils", + "name_pretty": "Google Test Utilities", + "release_level": "preview", + "language": "nodejs", + "repo": "googleapis/google-cloud-node", + "distribution_name": "google-test-utils", + "client_documentation": "https://cloud.google.com/nodejs/docs/reference/test-utils/latest", + "library_type": "OTHER", + "codeowner_team": "jsteam-handwritten-libraries" +} diff --git a/core/test-utils/CHANGELOG.md b/core/test-utils/CHANGELOG.md new file mode 100644 index 00000000000..8a3d7478515 --- /dev/null +++ b/core/test-utils/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +[npm history][1] + +[1]: https://www.npmjs.com/package/google-test-utils?activeTab=versions diff --git a/core/test-utils/CODE_OF_CONDUCT.md b/core/test-utils/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..a044123a3f2 --- /dev/null +++ b/core/test-utils/CODE_OF_CONDUCT.md @@ -0,0 +1,94 @@ + +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *googleapis-stewards@google.com*, the +Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/core/test-utils/CONTRIBUTING.md b/core/test-utils/CONTRIBUTING.md new file mode 100644 index 00000000000..b9aca5ba8c7 --- /dev/null +++ b/core/test-utils/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# How to become a contributor and submit your own code + +**Table of contents** + +* [Contributor License Agreements](#contributor-license-agreements) +* [Contributing a patch](#contributing-a-patch) +* [Running the tests](#running-the-tests) +* [Releasing the library](#releasing-the-library) + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the code to which + you are contributing. +1. Ensure that your code has an appropriate set of tests which all pass. +1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. +1. Submit a pull request. + +### Before you begin + +1. [Select or create a Cloud Platform project][projects]. +1. [Set up authentication with a service account][auth] so you can access the + API from your local workstation. + + +## Running the tests + +1. [Prepare your environment for Node.js setup][setup]. + +1. Install dependencies: + + npm install + +1. Run the tests: + + # Run unit tests. + npm test + +1. Lint (and maybe fix) any changes: + + npm run fix + +[setup]: https://cloud.google.com/nodejs/docs/setup +[projects]: https://console.cloud.google.com/project +[billing]: https://support.google.com/cloud/answer/6293499#enable-billing + +[auth]: https://cloud.google.com/docs/authentication/getting-started diff --git a/core/test-utils/LICENSE b/core/test-utils/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/core/test-utils/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/core/test-utils/README.md b/core/test-utils/README.md new file mode 100644 index 00000000000..4f88b412e23 --- /dev/null +++ b/core/test-utils/README.md @@ -0,0 +1,59 @@ +Google Cloud Platform logo + +# [Google Test Utilities: Node.js](https://github.com/googleapis/google-cloud-node) + +[![release level](https://img.shields.io/badge/release%20level-stable-brightgreen.svg?style=flat)](https://cloud.google.com/terms/launch-stages) +[![npm version](https://img.shields.io/npm/v/google-test-utils.svg)](https://www.npmjs.org/package/google-test-utils) + +Testing utilities for Google Cloud client libraries. + +A comprehensive list of changes in each version may be found in +[the CHANGELOG](https://github.com/googleapis/google-cloud-node/blob/main/core/test-utils/CHANGELOG.md). + +* [github.com/googleapis/google-cloud-node](https://github.com/googleapis/google-cloud-node) + +**Table of contents:** + +* [Quickstart](#quickstart) + * [Installing the library](#installing-the-library) + * [Using the library](#using-the-library) +* [Versioning](#versioning) +* [Contributing](#contributing) +* [License](#license) + +## Quickstart + +### Installing the library + +```bash +npm install google-test-utils +``` + +It's unlikely you will need to install this package directly, as it is intended as an internal testing utility across Google Cloud Node.js client libraries. + +### Using the library + +```typescript +import {TestResources} from 'google-test-utils'; + +const testResources = new TestResources('my-test-suite'); +const resourceName = testResources.generateName('my-test'); +``` + +## Supported Node.js Versions + +This library is not publicly supported. + +## Versioning + +This library follows [Semantic Versioning](http://semver.org/). + +## Contributing + +Contributions welcome! See the [Contributing Guide](https://github.com/googleapis/google-cloud-node/blob/main/core/test-utils/CONTRIBUTING.md). + +## License + +Apache Version 2.0 + +See [LICENSE](https://github.com/googleapis/google-cloud-node/blob/main/core/test-utils/LICENSE) diff --git a/core/test-utils/jest.config.js b/core/test-utils/jest.config.js new file mode 100644 index 00000000000..2a860f42340 --- /dev/null +++ b/core/test-utils/jest.config.js @@ -0,0 +1,21 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = { + testMatch: ['/test/**/*.ts'], + transform: { + '^.+\\.tsx?$': ['ts-jest', {tsconfig: 'tsconfig.json'}], + }, + clearMocks: true, +}; diff --git a/core/test-utils/package.json b/core/test-utils/package.json new file mode 100644 index 00000000000..7b26072b653 --- /dev/null +++ b/core/test-utils/package.json @@ -0,0 +1,52 @@ +{ + "name": "google-test-utils", + "version": "0.1.0", + "description": "Testing utilities for Google Cloud client libraries", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": { + "type": "git", + "directory": "core/test-utils", + "url": "https://github.com/googleapis/google-cloud-node.git" + }, + "scripts": { + "test": "jest --coverage", + "compile": "tsc -p .", + "fix": "gts fix", + "lint": "gts check", + "prepare": "npm run compile", + "pretest": "npm run compile", + "docs": "jsdoc -c .jsdoc.js", + "samples-test": "echo 'no samples test'", + "system-test": "echo 'no system test'", + "clean": "gts clean", + "precompile": "gts clean" + }, + "keywords": [ + "google", + "testing", + "test", + "utilities" + ], + "files": [ + "build/src", + "!build/src/**/*.map" + ], + "author": "Google Inc.", + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "^22.13.8", + "gts": "^6.0.2", + "jest": "^29.7.0", + "jsdoc": "^4.0.4", + "jsdoc-fresh": "^6.0.0", + "jsdoc-region-tag": "^5.0.0", + "ts-jest": "^29.4.10", + "typescript": "^5.8.2" + }, + "engines": { + "node": ">=22" + }, + "homepage": "https://github.com/googleapis/google-cloud-node/tree/main/core/test-utils" +} diff --git a/core/test-utils/src/index.ts b/core/test-utils/src/index.ts new file mode 100644 index 00000000000..f2e68c1091c --- /dev/null +++ b/core/test-utils/src/index.ts @@ -0,0 +1,20 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { + Resource, + TestResources, + TokenMaker, + defaultMaker as defaultTokenMaker, +} from './testResources'; diff --git a/core/test-utils/src/testResources.ts b/core/test-utils/src/testResources.ts new file mode 100644 index 00000000000..440c4f1d1b3 --- /dev/null +++ b/core/test-utils/src/testResources.ts @@ -0,0 +1,179 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as crypto from 'node:crypto'; + +// Returns a shortened UUID that can be used to identify a +// specific run of a specific test. +function shortUUID(): string { + return crypto.randomUUID().split('-').shift()!; +} + +export interface TokenMaker { + uuid(): string; + timestamp(): number; +} + +export const defaultMaker: TokenMaker = { + uuid: shortUUID, + timestamp: () => Date.now(), +}; + +export interface Resource { + name?: string | null | undefined; + delete?(): Promise; +} + +function normalizeId(id: string): string { + return id.replace(/-/g, '_'); +} + +/** + * Manages the names of testing resources during a test run. It's + * easy to accidentally leak resources, and it's easy to accidentally + * have conflicts with tests running concurrently, so this class helps + * you manage them. + * + * Used nomenclature: + * Test - a single test for a single aspect of code; for example, + * "create a topic in pub/sub" + * Test Suite - a collection of tests that are generally run together; + * for example, "test topic operations in pub/sub" + * Test Run - a single run of a test suite (or single test within a suite); + * for example, "run the tests for PR #1234, 5th attempt" + */ +export class TestResources { + testSuiteId: string; + currentTime: string; + tokenMaker: TokenMaker; + + /** + * @param testSuiteId [string] A unique ID for a test suite (e.g. + * pubsub-topics). + * @param tokenMaker [TokenMaker] Optional token generator for IDs and timestamps. + */ + constructor(testSuiteId: string, tokenMaker: TokenMaker = defaultMaker) { + this.testSuiteId = normalizeId(testSuiteId); + this.currentTime = `${tokenMaker.timestamp()}`; + this.tokenMaker = tokenMaker; + } + + /** + * Returns the resource prefix for the current run of the test suite. + * Optionally, testId may specify the specific ID of a test in the + * suite. + */ + getPrefix(testId?: string): string { + if (testId) { + return [this.testSuiteId, this.currentTime, normalizeId(testId)].join( + '-', + ); + } else { + return [this.testSuiteId, this.currentTime].join('-'); + } + } + + /** + * Generates a unique resource name for one run of a test within + * a test suite. + */ + generateName(testId: string): string { + return [this.getPrefix(testId), this.tokenMaker.uuid()].join('-'); + } + + /** + * Generates a unique resource name for one run of a test within + * a test suite for BigQuery resources. + */ + generateBigQueryName(testId: string): string { + return [normalizeId(this.getPrefix(testId)), this.tokenMaker.uuid()].join( + '_', + ); + } + + /** + * Generates a unique resource name for one run of a test within + * a test suite for Cloud Storage resources. + */ + generateStorageName(testId: string): string { + return [normalizeId(this.getPrefix(testId)), this.tokenMaker.uuid()].join( + '_', + ); + } + + /** + * Given a list of resource names (and a test ID), this will return + * a list of all resources that should be deleted to clean up for + * the current run of that particular test. + */ + filterForTest(testId: string, allResources: Resource[]): Resource[] { + const prefix = this.getPrefix(testId); + return allResources.filter(n => n.name?.includes(prefix)); + } + + /** + * Given a list of resource names, this will return a list of all + * resources that should be deleted to clean up after the current + * run of a test suite. + */ + filterForCurrentRun(allResources: Resource[]): Resource[] { + const prefix = this.getPrefix(); + return allResources.filter(n => n.name?.includes(prefix)); + } + + /** + * Given a list of resource names, this will return a list of all + * resources that should be deleted to clean up after any run + * of the current test suite. Note that some of the names may + * still be in use. + */ + filterForSuite(allResources: Resource[]): Resource[] { + return allResources.filter(n => n.name?.includes(this.testSuiteId)); + } + + /** + * Given a list of resource names, this will return a list of all + * resources that should be deleted to generally clean up after any + * run of the current test suite. This is much like filterForSuite(), + * but it also filters by age - items that are less than 2 hours + * old will not be cleaned. + */ + filterForCleanup(allResources: Resource[]): Resource[] { + const currentRunPrefix = this.getPrefix(); + return allResources.filter(n => { + let name = n.name || undefined; + if (name === undefined) { + return false; + } + + // We'll always get at least one thing. + name = name.split('/').pop()!; + + if (name.startsWith(currentRunPrefix)) { + return true; + } + + if (name.startsWith(this.testSuiteId)) { + const parts = name.split('-'); + const createdAt = Number(parts[1]); + const timeDiff = (Date.now() - createdAt) / (1000 * 60 * 60); + if (timeDiff >= 2) { + return true; + } + } + + return false; + }); + } +} diff --git a/core/test-utils/test/testResources.test.ts b/core/test-utils/test/testResources.test.ts new file mode 100644 index 00000000000..5e95fc02125 --- /dev/null +++ b/core/test-utils/test/testResources.test.ts @@ -0,0 +1,127 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {TestResources, TokenMaker, Resource, defaultTokenMaker} from '../src'; + +describe('testResources (unit)', () => { + const fixedId = 'fixed'; + const fixedTime = Date.now(); + const fakeTokenMaker: TokenMaker = { + uuid: () => fixedId, + timestamp: () => fixedTime, + }; + + const suiteId = 'someSuite'; + let testResources: TestResources; + + beforeEach(() => { + testResources = new TestResources(suiteId, fakeTokenMaker); + }); + + it('has predictable prefixes', () => { + const prefix = testResources.getPrefix('testId'); + expect(prefix).toBe(`${suiteId}-${fixedTime}-testId`); + + const normalizedPrefix = testResources.getPrefix('test-id-dashes'); + expect(normalizedPrefix).toBe(`${suiteId}-${fixedTime}-test_id_dashes`); + + const suitePrefix = testResources.getPrefix(); + expect(suitePrefix).toBe(`${suiteId}-${fixedTime}`); + }); + + it('generates names', () => { + const prefix = testResources.getPrefix('testId'); + const name = testResources.generateName('testId'); + expect(name).toBe(`${prefix}-${fixedId}`); + }); + + it('generates BigQuery names', () => { + const name = testResources.generateBigQueryName('testId'); + expect(name).toBe(`${suiteId}_${fixedTime}_testId_${fixedId}`); + }); + + it('generates Storage names', () => { + const name = testResources.generateStorageName('testId'); + expect(name).toBe(`${suiteId}_${fixedTime}_testId_${fixedId}`); + }); + + it('filters for test', () => { + const resources: Resource[] = [ + {name: `${suiteId}-${fixedTime}-testA-123`}, + {name: `${suiteId}-${fixedTime}-testB-456`}, + {name: 'unrelated'}, + ]; + const filtered = testResources.filterForTest('testA', resources); + expect(filtered).toEqual([{name: `${suiteId}-${fixedTime}-testA-123`}]); + }); + + it('filters for current run', () => { + const resources: Resource[] = [ + {name: `${suiteId}-${fixedTime}-testA-123`}, + {name: `${suiteId}-${fixedTime - 1000}-testA-456`}, + {name: 'unrelated'}, + ]; + const filtered = testResources.filterForCurrentRun(resources); + expect(filtered).toEqual([{name: `${suiteId}-${fixedTime}-testA-123`}]); + }); + + it('filters for suite', () => { + const resources: Resource[] = [ + {name: `${suiteId}-${fixedTime}-testA-123`}, + {name: `${suiteId}-${fixedTime - 1000}-testB-456`}, + {name: 'unrelated'}, + ]; + const filtered = testResources.filterForSuite(resources); + expect(filtered).toEqual([ + {name: `${suiteId}-${fixedTime}-testA-123`}, + {name: `${suiteId}-${fixedTime - 1000}-testB-456`}, + ]); + }); + + it('filters for cleanup', () => { + const resources: Resource[] = [ + { + // Not related + name: 'ooga', + }, + { + // Resource without name + name: undefined, + }, + { + // For current test run + name: `${suiteId}-${fixedTime}-bob-98719284791`, + }, + { + // For previous test run, but not very old + name: `${suiteId}-${fixedTime - 100}-bob-124897912`, + }, + { + // For previous test run, but old (> 2 hours) + name: `${suiteId}-${fixedTime - 3000 * 60 * 60}-bob-57823975`, + }, + ]; + const filtered = testResources.filterForCleanup(resources); + expect(filtered.length).toBe(2); + expect(filtered.filter(r => r.name?.includes('bob-9871')).length).toBe(1); + expect(filtered.filter(r => r.name?.includes('bob-5782')).length).toBe(1); + }); + + it('supports default maker', () => { + const defaultResources = new TestResources('default-suite'); + expect(defaultResources.testSuiteId).toBe('default_suite'); + expect(typeof defaultTokenMaker.uuid()).toBe('string'); + expect(typeof defaultTokenMaker.timestamp()).toBe('number'); + }); +}); diff --git a/core/test-utils/tsconfig.json b/core/test-utils/tsconfig.json new file mode 100644 index 00000000000..26f33cd1f4d --- /dev/null +++ b/core/test-utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/*.ts", + "test/*.ts" + ] +} diff --git a/release-please-config.json b/release-please-config.json index a8723c83e58..8e8093cf315 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -69,6 +69,9 @@ "core/promisify": { "component": "promisify" }, + "core/test-utils": { + "component": "test-utils" + }, "handwritten/bigquery": { "component": "bigquery" },