From 5df863ce280e3ab6896cd230cfad0b80a21ec943 Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Thu, 15 Oct 2026 20:54:40 +0000 Subject: [PATCH 01/10] test: remove --proxyquire-shim from common, bigquery, datastore, and dns --- core/common/package.json | 2 +- core/common/test/proxyquire.ts | 115 ++++++++++++++++++ core/common/test/service-object.ts | 3 +- core/common/test/service.ts | 2 +- core/common/test/util.ts | 3 +- handwritten/bigquery/package.json | 2 +- handwritten/bigquery/test/bigquery.ts | 2 +- handwritten/bigquery/test/dataset.ts | 2 +- handwritten/bigquery/test/job.ts | 2 +- handwritten/bigquery/test/model.ts | 2 +- handwritten/bigquery/test/proxyquire.ts | 115 ++++++++++++++++++ handwritten/bigquery/test/routine.ts | 2 +- handwritten/bigquery/test/rowQueue.ts | 2 +- handwritten/bigquery/test/table.ts | 2 +- handwritten/datastore/package.json | 2 +- .../datastore/system-test/transaction-spy.ts | 2 +- .../client-initialization-testing.ts | 2 +- handwritten/datastore/test/index-class.ts | 2 +- handwritten/datastore/test/index.ts | 2 +- handwritten/datastore/test/proxyquire.ts | 115 ++++++++++++++++++ handwritten/datastore/test/request.ts | 2 +- handwritten/datastore/test/transaction.ts | 2 +- handwritten/google-cloud-dns/package.json | 2 +- handwritten/google-cloud-dns/test/change.ts | 6 +- handwritten/google-cloud-dns/test/index.ts | 18 +-- .../google-cloud-dns/test/proxyquire.ts | 115 ++++++++++++++++++ handwritten/google-cloud-dns/test/record.ts | 2 +- handwritten/google-cloud-dns/test/zone.ts | 44 +++---- 28 files changed, 517 insertions(+), 55 deletions(-) create mode 100644 core/common/test/proxyquire.ts create mode 100644 handwritten/bigquery/test/proxyquire.ts create mode 100644 handwritten/datastore/test/proxyquire.ts create mode 100644 handwritten/google-cloud-dns/test/proxyquire.ts diff --git a/core/common/package.json b/core/common/package.json index 1957c7f8c44..3e2b37e200c 100644 --- a/core/common/package.json +++ b/core/common/package.json @@ -20,7 +20,7 @@ ], "scripts": { "docs": "jsdoc -c .jsdoc.js", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "prepare": "npm run compile", "pretest": "npm run compile", "compile": "tsc -p .", diff --git a/core/common/test/proxyquire.ts b/core/common/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/core/common/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/core/common/test/service-object.ts b/core/common/test/service-object.ts index 1d3ed35a5fe..e840224c6ed 100644 --- a/core/common/test/service-object.ts +++ b/core/common/test/service-object.ts @@ -20,7 +20,8 @@ import { import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; +// eslint-disable-next-line import/namespace import * as r from 'teeny-request'; import * as sinon from 'sinon'; diff --git a/core/common/test/service.ts b/core/common/test/service.ts index 768e7db4e4a..74d6adb779c 100644 --- a/core/common/test/service.ts +++ b/core/common/test/service.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, after} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Request} from 'teeny-request'; import {AuthClient, GoogleAuth, OAuth2Client} from 'google-auth-library'; diff --git a/core/common/test/util.ts b/core/common/test/util.ts index 6c018afd4d3..617a02e2c7e 100644 --- a/core/common/test/util.ts +++ b/core/common/test/util.ts @@ -26,7 +26,8 @@ import { OAuth2Client, } from 'google-auth-library'; import * as nock from 'nock'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; +// eslint-disable-next-line import/namespace import * as r from 'teeny-request'; import * as retryRequest from 'retry-request'; import * as sinon from 'sinon'; diff --git a/handwritten/bigquery/package.json b/handwritten/bigquery/package.json index f826e9b8072..f47531ee118 100644 --- a/handwritten/bigquery/package.json +++ b/handwritten/bigquery/package.json @@ -36,7 +36,7 @@ "docs": "jsdoc -c .jsdoc.js", "lint": "gts check", "samples-test": "cd samples/ && pnpm link ../ && pnpm test && cd ../", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "presystem-test": "pnpm run compile", "clean": "gts clean", diff --git a/handwritten/bigquery/test/bigquery.ts b/handwritten/bigquery/test/bigquery.ts index e5bbf0e222a..72d0743ac9f 100644 --- a/handwritten/bigquery/test/bigquery.ts +++ b/handwritten/bigquery/test/bigquery.ts @@ -24,7 +24,7 @@ import * as assert from 'assert'; import {describe, it, after, afterEach, before, beforeEach} from 'mocha'; import * as Big from 'big.js'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; diff --git a/handwritten/bigquery/test/dataset.ts b/handwritten/bigquery/test/dataset.ts index 26bb3ff8a83..cdf1a4e321d 100644 --- a/handwritten/bigquery/test/dataset.ts +++ b/handwritten/bigquery/test/dataset.ts @@ -22,7 +22,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as _root from '../src'; import {toArray} from '../src/util'; diff --git a/handwritten/bigquery/test/job.ts b/handwritten/bigquery/test/job.ts index afba50a3fc7..ece2bc1d340 100644 --- a/handwritten/bigquery/test/job.ts +++ b/handwritten/bigquery/test/job.ts @@ -16,7 +16,7 @@ import {DecorateRequestOptions, util} from '@google-cloud/common'; import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, beforeEach, afterEach, before} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {BigQuery} from '../src/bigquery'; diff --git a/handwritten/bigquery/test/model.ts b/handwritten/bigquery/test/model.ts index 49c59a5cec4..b07ab763e64 100644 --- a/handwritten/bigquery/test/model.ts +++ b/handwritten/bigquery/test/model.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as sinon from 'sinon'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as pfy from '@google-cloud/promisify'; import {EventEmitter} from 'events'; import {JobOptions} from '../src/job'; diff --git a/handwritten/bigquery/test/proxyquire.ts b/handwritten/bigquery/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/bigquery/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/bigquery/test/routine.ts b/handwritten/bigquery/test/routine.ts index d1cdb0966ac..dac2db92b63 100644 --- a/handwritten/bigquery/test/routine.ts +++ b/handwritten/bigquery/test/routine.ts @@ -17,7 +17,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as _root from '../src'; diff --git a/handwritten/bigquery/test/rowQueue.ts b/handwritten/bigquery/test/rowQueue.ts index f53dbf01148..11b0f8c971c 100644 --- a/handwritten/bigquery/test/rowQueue.ts +++ b/handwritten/bigquery/test/rowQueue.ts @@ -16,7 +16,7 @@ import {util} from '@google-cloud/common'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {Duplex, Stream} from 'stream'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as q from '../src/rowQueue'; import * as t from '../src/table'; diff --git a/handwritten/bigquery/test/table.ts b/handwritten/bigquery/test/table.ts index f3e59d7f3d9..b17c506a65e 100644 --- a/handwritten/bigquery/test/table.ts +++ b/handwritten/bigquery/test/table.ts @@ -26,7 +26,7 @@ import {describe, it, afterEach, beforeEach, before, after} from 'mocha'; import * as Big from 'big.js'; import {EventEmitter} from 'events'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as stream from 'stream'; import * as crypto from 'crypto'; diff --git a/handwritten/datastore/package.json b/handwritten/datastore/package.json index a031adee220..7271a5d9d22 100644 --- a/handwritten/datastore/package.json +++ b/handwritten/datastore/package.json @@ -38,7 +38,7 @@ "presystem-test": "npm run compile", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "pretest": "npm run compile", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "precompile": "gts clean" }, "dependencies": { diff --git a/handwritten/datastore/system-test/transaction-spy.ts b/handwritten/datastore/system-test/transaction-spy.ts index 23ea7a69524..fb4763aa0ea 100644 --- a/handwritten/datastore/system-test/transaction-spy.ts +++ b/handwritten/datastore/system-test/transaction-spy.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import {DatastoreRequest} from '../src'; import {RequestCallback, RequestConfig} from '../src/request'; diff --git a/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts b/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts index a3a16ef7dc3..4c4809b6978 100644 --- a/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts +++ b/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts @@ -21,7 +21,7 @@ import { DatastoreOptions, } from '../../src'; import * as assert from 'assert'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import {Callback, CallOptions} from 'google-gax'; import * as protos from '../../src/protos'; import * as ds from '../../src'; diff --git a/handwritten/datastore/test/index-class.ts b/handwritten/datastore/test/index-class.ts index 3754e467274..1f75cb436b3 100644 --- a/handwritten/datastore/test/index-class.ts +++ b/handwritten/datastore/test/index-class.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as ds from '../src'; diff --git a/handwritten/datastore/test/index.ts b/handwritten/datastore/test/index.ts index 03fedde213c..8545834c59c 100644 --- a/handwritten/datastore/test/index.ts +++ b/handwritten/datastore/test/index.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {before, beforeEach, after, afterEach, describe, it} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {PassThrough, Readable} from 'stream'; import * as ds from '../src'; diff --git a/handwritten/datastore/test/proxyquire.ts b/handwritten/datastore/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/datastore/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/datastore/test/request.ts b/handwritten/datastore/test/request.ts index 97366be73bb..1db6a3699ca 100644 --- a/handwritten/datastore/test/request.ts +++ b/handwritten/datastore/test/request.ts @@ -18,7 +18,7 @@ import {after, afterEach, before, beforeEach, describe, it} from 'mocha'; import * as extend from 'extend'; import * as gax from 'google-gax'; import * as is from 'is'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PassThrough, Transform} from 'stream'; diff --git a/handwritten/datastore/test/transaction.ts b/handwritten/datastore/test/transaction.ts index 36366c0d7e0..c322c5f4592 100644 --- a/handwritten/datastore/test/transaction.ts +++ b/handwritten/datastore/test/transaction.ts @@ -16,7 +16,7 @@ import * as pfy from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {afterEach, beforeEach, before, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import { Datastore, DatastoreOptions, diff --git a/handwritten/google-cloud-dns/package.json b/handwritten/google-cloud-dns/package.json index 9a8744bc31b..a58a630f0a2 100644 --- a/handwritten/google-cloud-dns/package.json +++ b/handwritten/google-cloud-dns/package.json @@ -33,7 +33,7 @@ "scripts": { "docs": "jsdoc -c .jsdoc.js", "lint": "gts check", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "samples-test": "pnpm run compile && cd samples/ && pnpm link ../ && pnpm i && pnpm test", "presystem-test": "pnpm run compile", "system-test": "pnpm run compile && node ../../bin/run-test.cjs build/system-test", diff --git a/handwritten/google-cloud-dns/test/change.ts b/handwritten/google-cloud-dns/test/change.ts index 4d37cfd21cd..7aaa61f9c0f 100644 --- a/handwritten/google-cloud-dns/test/change.ts +++ b/handwritten/google-cloud-dns/test/change.ts @@ -20,7 +20,7 @@ import { import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Change} from '../src/change'; let promisified = false; @@ -119,7 +119,7 @@ describe('Change', () => { assert.strictEqual(change, null); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -145,7 +145,7 @@ describe('Change', () => { assert.strictEqual(change_, change); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); diff --git a/handwritten/google-cloud-dns/test/index.ts b/handwritten/google-cloud-dns/test/index.ts index 0c3fd27da05..1d89ead7255 100644 --- a/handwritten/google-cloud-dns/test/index.ts +++ b/handwritten/google-cloud-dns/test/index.ts @@ -22,7 +22,7 @@ import * as promisify from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {CoreOptions, OptionsWithUri, Response} from 'request'; import {Zone} from '../src'; @@ -135,7 +135,7 @@ describe('DNS', () => { assert.deepStrictEqual( calledWith.packageJson, // eslint-disable-next-line @typescript-eslint/no-var-requires - require('../../package.json') + require('../../package.json'), ); }); @@ -226,7 +226,7 @@ describe('DNS', () => { assert.strictEqual(zone, null); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -265,7 +265,7 @@ describe('DNS', () => { assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); @@ -318,7 +318,7 @@ describe('DNS', () => { err: Error, zones: Zone[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.strictEqual(err, error); assert.strictEqual(zones, null); @@ -326,7 +326,7 @@ describe('DNS', () => { assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -377,7 +377,7 @@ describe('DNS', () => { nextQuery, Object.assign({}, query, { pageToken: apiResponseWithNextPageToken.nextPageToken, - }) + }), ); done(); @@ -391,7 +391,7 @@ describe('DNS', () => { err: Error, zones: Zone[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.ifError(err); assert.strictEqual(zones[0], zone); @@ -399,7 +399,7 @@ describe('DNS', () => { assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); diff --git a/handwritten/google-cloud-dns/test/proxyquire.ts b/handwritten/google-cloud-dns/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/google-cloud-dns/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/google-cloud-dns/test/record.ts b/handwritten/google-cloud-dns/test/record.ts index e7174c43d54..ffbba1ce547 100644 --- a/handwritten/google-cloud-dns/test/record.ts +++ b/handwritten/google-cloud-dns/test/record.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import {Record} from '../src'; diff --git a/handwritten/google-cloud-dns/test/zone.ts b/handwritten/google-cloud-dns/test/zone.ts index 07681f51e59..c2a639b6d66 100644 --- a/handwritten/google-cloud-dns/test/zone.ts +++ b/handwritten/google-cloud-dns/test/zone.ts @@ -17,7 +17,7 @@ import * as promisify from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {CoreOptions, OptionsWithUri, Response} from 'request'; import * as crypto from 'crypto'; @@ -187,7 +187,7 @@ describe('Zone', () => { zone.createChange = ( options: CreateChangeRequest, - callback: Function + callback: Function, ) => { assert.strictEqual(options.add, records); callback(); @@ -215,7 +215,7 @@ describe('Zone', () => { type: crypto.randomUUID(), rrdatas: [crypto.randomUUID(), crypto.randomUUID()], }, - recordJson + recordJson, ); return { @@ -312,7 +312,7 @@ describe('Zone', () => { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -342,7 +342,7 @@ describe('Zone', () => { assert.strictEqual(change_.metadata, apiResponse); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -390,7 +390,7 @@ describe('Zone', () => { zone.createChange = ( options: CreateChangeRequest, - callback: Function + callback: Function, ) => { assert.deepStrictEqual(options.delete, [recordsToDelete]); callback(); @@ -462,7 +462,7 @@ describe('Zone', () => { it('should delete non-NS and non-SOA records', done => { zone.deleteRecords = ( recordsToDelete: string[], - callback: Function + callback: Function, ) => { assert.deepStrictEqual(recordsToDelete, expectedRecordsToDelete); callback(); @@ -539,7 +539,7 @@ describe('Zone', () => { writeFileOverride = ( path_: string, content: string, - encoding: string + encoding: string, ) => { assert.strictEqual(path_, path); assert.strictEqual(content, expectedZonefileContents); @@ -559,7 +559,7 @@ describe('Zone', () => { path: string, content: string, encoding: string, - callback: Function + callback: Function, ) => { callback(error); }; @@ -579,7 +579,7 @@ describe('Zone', () => { path: string, content: string, encoding: string, - callback: Function + callback: Function, ) => { callback(); }; @@ -648,12 +648,12 @@ describe('Zone', () => { err: Error, changes: Change[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -703,14 +703,14 @@ describe('Zone', () => { err: Error, changes: Change[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.ifError(err); assert.strictEqual(changes[0], change); assert.strictEqual(changes[0].metadata, apiResponse.changes[0]); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -734,12 +734,12 @@ describe('Zone', () => { err: Error, changes: Change[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); @@ -795,13 +795,13 @@ describe('Zone', () => { err: Error, records: Record[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.ifError(err); assert.strictEqual(records[0], record); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); it('should not require a query', done => { @@ -866,7 +866,7 @@ describe('Zone', () => { readFileOverride = ( path: string, encoding: string, - callback: Function + callback: Function, ) => { callback(error); }; @@ -897,7 +897,7 @@ describe('Zone', () => { readFileOverride = ( path: string, encoding: string, - callback: Function + callback: Function, ) => { callback(); }; @@ -906,7 +906,7 @@ describe('Zone', () => { it('should add records', done => { zone.addRecords = ( recordsToCreate: FakeRecord[], - callback: Function + callback: Function, ) => { assert.strictEqual(recordsToCreate.length, 1); const recordToCreate = recordsToCreate[0]; @@ -998,7 +998,7 @@ describe('Zone', () => { it('should create a change', done => { zone.createChange = ( options: CreateChangeRequest, - callback: Function + callback: Function, ) => { assert.strictEqual(options.add, recordsToCreate); assert.strictEqual(options.delete, recordsToDelete); From d2820c8106529e8461aa9cd5d3109d836a60313a Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Mon, 19 Oct 2026 14:12:32 +0000 Subject: [PATCH 02/10] [Address CI errors] test: support function stubs and guard read-only properties in proxyquire helper --- core/common/test/proxyquire.ts | 12 ++++++++++-- handwritten/bigquery/test/proxyquire.ts | 12 ++++++++++-- handwritten/datastore/test/proxyquire.ts | 12 ++++++++++-- handwritten/google-cloud-dns/test/proxyquire.ts | 12 ++++++++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/core/common/test/proxyquire.ts b/core/common/test/proxyquire.ts index 44ff9f689c1..1145fae2c1f 100644 --- a/core/common/test/proxyquire.ts +++ b/core/common/test/proxyquire.ts @@ -25,7 +25,9 @@ const mod = Module as unknown as { }; const {hasOwnProperty} = Object.prototype; const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); + Boolean( + o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), + ); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +78,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { diff --git a/handwritten/bigquery/test/proxyquire.ts b/handwritten/bigquery/test/proxyquire.ts index 44ff9f689c1..1145fae2c1f 100644 --- a/handwritten/bigquery/test/proxyquire.ts +++ b/handwritten/bigquery/test/proxyquire.ts @@ -25,7 +25,9 @@ const mod = Module as unknown as { }; const {hasOwnProperty} = Object.prototype; const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); + Boolean( + o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), + ); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +78,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { diff --git a/handwritten/datastore/test/proxyquire.ts b/handwritten/datastore/test/proxyquire.ts index 44ff9f689c1..1145fae2c1f 100644 --- a/handwritten/datastore/test/proxyquire.ts +++ b/handwritten/datastore/test/proxyquire.ts @@ -25,7 +25,9 @@ const mod = Module as unknown as { }; const {hasOwnProperty} = Object.prototype; const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); + Boolean( + o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), + ); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +78,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { diff --git a/handwritten/google-cloud-dns/test/proxyquire.ts b/handwritten/google-cloud-dns/test/proxyquire.ts index 44ff9f689c1..1145fae2c1f 100644 --- a/handwritten/google-cloud-dns/test/proxyquire.ts +++ b/handwritten/google-cloud-dns/test/proxyquire.ts @@ -25,7 +25,9 @@ const mod = Module as unknown as { }; const {hasOwnProperty} = Object.prototype; const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); + Boolean( + o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), + ); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +78,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { From 0a61c5e7428a4b42d7a43ca94a1671927e10cf5f Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Thu, 15 Oct 2026 20:54:42 +0000 Subject: [PATCH 03/10] test: remove --proxyquire-shim from logging and pubsub packages --- handwritten/logging-bunyan/package.json | 2 +- handwritten/logging-bunyan/test/index.ts | 2 +- .../test/middleware/test-express.ts | 2 +- handwritten/logging-bunyan/test/proxyquire.ts | 115 ++++++++++++++++++ handwritten/logging-winston/package.json | 2 +- handwritten/logging-winston/test/common.ts | 2 +- handwritten/logging-winston/test/index.ts | 2 +- .../test/middleware/express.ts | 2 +- .../logging-winston/test/proxyquire.ts | 115 ++++++++++++++++++ .../test/stackdriver-trace-integration.ts | 2 +- handwritten/logging/package.json | 2 +- handwritten/logging/test/entry.ts | 34 +++--- handwritten/logging/test/index.ts | 66 +++++----- handwritten/logging/test/log.ts | 98 +++++++-------- .../express/test-make-middleware.ts | 6 +- handwritten/logging/test/proxyquire.ts | 115 ++++++++++++++++++ handwritten/logging/test/sink.ts | 16 +-- handwritten/logging/test/utils/metadata.ts | 6 +- handwritten/pubsub/package.json | 2 +- handwritten/pubsub/test/iam.ts | 2 +- handwritten/pubsub/test/lease-manager.ts | 17 +-- handwritten/pubsub/test/message-stream.ts | 2 +- handwritten/pubsub/test/proxyquire.ts | 115 ++++++++++++++++++ handwritten/pubsub/test/publisher/index.ts | 2 +- .../pubsub/test/publisher/message-queues.ts | 9 +- handwritten/pubsub/test/pubsub.ts | 4 +- handwritten/pubsub/test/snapshot.ts | 2 +- handwritten/pubsub/test/subscriber.ts | 2 +- handwritten/pubsub/test/subscription.ts | 2 +- handwritten/pubsub/test/topic.ts | 2 +- 30 files changed, 601 insertions(+), 149 deletions(-) create mode 100644 handwritten/logging-bunyan/test/proxyquire.ts create mode 100644 handwritten/logging-winston/test/proxyquire.ts create mode 100644 handwritten/logging/test/proxyquire.ts create mode 100644 handwritten/pubsub/test/proxyquire.ts diff --git a/handwritten/logging-bunyan/package.json b/handwritten/logging-bunyan/package.json index 3358325c6cc..4bd8a258110 100644 --- a/handwritten/logging-bunyan/package.json +++ b/handwritten/logging-bunyan/package.json @@ -42,7 +42,7 @@ "presystem-test": "pnpm run compile", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", - "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs build/test", "clean": "gts clean", "compile": "tsc -p .", "postcompile": "cpy \"src/types/*\" build/src/types", diff --git a/handwritten/logging-bunyan/test/index.ts b/handwritten/logging-bunyan/test/index.ts index 9e91fe2665f..0c82b56d35f 100644 --- a/handwritten/logging-bunyan/test/index.ts +++ b/handwritten/logging-bunyan/test/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {inspect} from 'util'; import {LoggingBunyan} from '../src'; diff --git a/handwritten/logging-bunyan/test/middleware/test-express.ts b/handwritten/logging-bunyan/test/middleware/test-express.ts index 99920c4ca52..ba5f651c6e8 100644 --- a/handwritten/logging-bunyan/test/middleware/test-express.ts +++ b/handwritten/logging-bunyan/test/middleware/test-express.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import {GCPEnv} from 'google-auth-library'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; // types-only import. Actual require is done through proxyquire below. import {MiddlewareOptions} from '../../src/middleware/express'; diff --git a/handwritten/logging-bunyan/test/proxyquire.ts b/handwritten/logging-bunyan/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/logging-bunyan/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/logging-winston/package.json b/handwritten/logging-winston/package.json index aaf7cd1b0af..81005402725 100644 --- a/handwritten/logging-winston/package.json +++ b/handwritten/logging-winston/package.json @@ -42,7 +42,7 @@ "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "presystem-test": "pnpm run pretest", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", - "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs build/test", "clean": "gts clean", "compile": "tsc -p .", "fix": "gts fix", diff --git a/handwritten/logging-winston/test/common.ts b/handwritten/logging-winston/test/common.ts index 39401702c6e..f3fb4011a6d 100644 --- a/handwritten/logging-winston/test/common.ts +++ b/handwritten/logging-winston/test/common.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as nodeutil from 'util'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Options} from '../src'; import {Entry, Logging, LogSync, Log} from '@google-cloud/logging'; import * as instrumentation from '@google-cloud/logging/build/src/utils/instrumentation'; diff --git a/handwritten/logging-winston/test/index.ts b/handwritten/logging-winston/test/index.ts index 13412aa4e74..1af6b888163 100644 --- a/handwritten/logging-winston/test/index.ts +++ b/handwritten/logging-winston/test/index.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as TransportStream from 'winston-transport'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Options} from '../src'; describe('logging-winston', () => { diff --git a/handwritten/logging-winston/test/middleware/express.ts b/handwritten/logging-winston/test/middleware/express.ts index 40a1853601d..b8ec33cc8f9 100644 --- a/handwritten/logging-winston/test/middleware/express.ts +++ b/handwritten/logging-winston/test/middleware/express.ts @@ -18,7 +18,7 @@ import {GCPEnv} from 'google-auth-library'; import {LogEntry} from 'winston'; import * as TransportStream from 'winston-transport'; import * as winston from 'winston'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import {Options} from '../../src'; // types-only import. Actual require is done through proxyquire below. diff --git a/handwritten/logging-winston/test/proxyquire.ts b/handwritten/logging-winston/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/logging-winston/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/logging-winston/test/stackdriver-trace-integration.ts b/handwritten/logging-winston/test/stackdriver-trace-integration.ts index ed0e668c4bb..9e62c33575e 100644 --- a/handwritten/logging-winston/test/stackdriver-trace-integration.ts +++ b/handwritten/logging-winston/test/stackdriver-trace-integration.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, after} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as winston from 'winston'; import * as loggingWinstonLibTypes from '../src/index'; diff --git a/handwritten/logging/package.json b/handwritten/logging/package.json index 63a3d326151..06363bb2195 100644 --- a/handwritten/logging/package.json +++ b/handwritten/logging/package.json @@ -40,7 +40,7 @@ "presystem-test": "pnpm run compile", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "pretest": "pnpm run compile", - "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs build/test", "precompile": "gts clean", "postpublish": "./.kokoro/publish-min.sh" }, diff --git a/handwritten/logging/test/entry.ts b/handwritten/logging/test/entry.ts index d17976edd13..d9d994a8e44 100644 --- a/handwritten/logging/test/entry.ts +++ b/handwritten/logging/test/entry.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as entryTypes from '../src/entry'; import * as common from '../src/utils/common'; import * as http from 'http'; @@ -222,7 +222,7 @@ describe('Entry', () => { it('should pass removeCircular to objToStruct_', done => { fakeObjToStruct = ( obj: {}, - options: common.ObjectToStructConverterConfig, + options: common.ObjectToStructConverterConfig ) => { assert.strictEqual(options.removeCircular, true); done(); @@ -325,7 +325,7 @@ describe('Entry', () => { }); after(() => { - sdk.shutdown(); + return sdk.shutdown(); }); it('should detect open telemetry trace and span if open telemetry context present', () => { @@ -335,12 +335,12 @@ describe('Entry', () => { const json = entry.toJSON(); assert.strictEqual( json.trace, - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual(json.spanId, span.spanContext().spanId); assert.strictEqual( json.traceSampled, - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -360,12 +360,12 @@ describe('Entry', () => { const json = entry.toJSON(); assert.strictEqual( json.trace, - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual(json.spanId, span.spanContext().spanId); assert.strictEqual( json.traceSampled, - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -413,7 +413,7 @@ describe('Entry', () => { entry.data = 'this is a log'; const json = entry.toStructuredJSON(); assert( - withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)), + withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)) ); delete json.timestamp; const expectedJSON = { @@ -441,7 +441,7 @@ describe('Entry', () => { entry.metadata.timestamp = new Date(); const json = entry.toStructuredJSON(); assert( - withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)), + withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)) ); }); @@ -503,7 +503,7 @@ describe('Entry', () => { }); after(() => { - sdk.shutdown(); + return sdk.shutdown(); }); it('should detect open telemetry trace and span if open telemetry context present', () => { @@ -513,15 +513,15 @@ describe('Entry', () => { const json = entry.toStructuredJSON(); assert.strictEqual( json[entryTypes.TRACE_KEY], - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual( json[entryTypes.SPAN_ID_KEY], - span.spanContext().spanId, + span.spanContext().spanId ); assert.strictEqual( json[entryTypes.TRACE_SAMPLED_KEY], - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -541,15 +541,15 @@ describe('Entry', () => { const json = entry.toStructuredJSON(); assert.strictEqual( json[entryTypes.TRACE_KEY], - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual( json[entryTypes.SPAN_ID_KEY], - span.spanContext().spanId, + span.spanContext().spanId ); assert.strictEqual( json[entryTypes.TRACE_SAMPLED_KEY], - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -571,7 +571,7 @@ describe('Entry', () => { assert.strictEqual(json[entryTypes.SPAN_ID_KEY], expected.spanId); assert.strictEqual( json[entryTypes.TRACE_SAMPLED_KEY], - expected.traceSampled, + expected.traceSampled ); }); }); diff --git a/handwritten/logging/test/index.ts b/handwritten/logging/test/index.ts index 73ae9f61581..03663ed512d 100644 --- a/handwritten/logging/test/index.ts +++ b/handwritten/logging/test/index.ts @@ -18,7 +18,7 @@ import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, beforeEach, before} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import { Logging as LOGGING, LoggingOptions, @@ -215,8 +215,8 @@ describe('Logging', () => { libVersion: version, scopes: EXPECTED_SCOPES, }, - options, - ), + options + ) ); return fakeGoogleAuthInstance; }; @@ -245,8 +245,8 @@ describe('Logging', () => { libVersion: version, scopes: EXPECTED_SCOPES, }, - options, - ), + options + ) ); }); @@ -270,7 +270,7 @@ describe('Logging', () => { it('should throw if a name is not provided', () => { const error = new Error('A sink name must be provided.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - (logging as any).createSink().then(noop, (err: Error) => { + return (logging as any).createSink().then(noop, (err: Error) => { assert.deepStrictEqual(err, error); }); }); @@ -278,7 +278,7 @@ describe('Logging', () => { it('should throw if a config object is not provided', () => { const error = new Error('A sink configuration object must be provided.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - (logging as any).createSink(SINK_NAME).then(noop, (err: Error) => { + return (logging as any).createSink(SINK_NAME).then(noop, (err: Error) => { assert.deepStrictEqual(err, error); }); }); @@ -354,7 +354,7 @@ describe('Logging', () => { logging.configService.createSink = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { const expectedParent = 'projects/' + logging.projectId; assert.strictEqual(reqOpts.parent, expectedParent); @@ -376,11 +376,11 @@ describe('Logging', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, // eslint-disable-next-line @typescript-eslint/no-unused-vars - gaxOpts: {}, + gaxOpts: {} ) => { assert.strictEqual( reqOpts.uniqueWriterIdentity, - config.uniqueWriterIdentity, + config.uniqueWriterIdentity ); assert.strictEqual(reqOpts.sink.uniqueWriterIdentity, undefined); return [{}]; @@ -399,7 +399,7 @@ describe('Logging', () => { logging.configService.createSink = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.strictEqual(reqOpts.sink.gaxOptions, undefined); assert.strictEqual(gaxOpts, config.gaxOptions); @@ -424,7 +424,7 @@ describe('Logging', () => { throw error; }; - logging + return logging .createSink(SINK_NAME, {} as CreateSinkRequest) .then(noop, (err: Error) => assert.deepStrictEqual(err, error)); }); @@ -455,7 +455,7 @@ describe('Logging', () => { const [sink_, apiResponse_] = await logging.createSink( SINK_NAME, - {} as CreateSinkRequest, + {} as CreateSinkRequest ); assert.strictEqual(sink_, sink); assert.strictEqual(sink_.metadata, apiResponse); @@ -487,7 +487,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { filter: reqOpts?.filter, @@ -512,7 +512,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any - reqOpts: any, + reqOpts: any ) => { assert.deepStrictEqual(reqOpts.resourceNames, resourceNames); return [[]]; @@ -526,7 +526,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual( reqOpts, @@ -534,7 +534,7 @@ describe('Logging', () => { filter: 'timestamp > "2020-11-11T15:01:23.045123456Z"', orderBy: 'timestamp desc', resourceNames: ['projects/' + logging.projectId], - }), + }) ); assert.deepStrictEqual(gaxOpts, { @@ -552,7 +552,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual( reqOpts, @@ -560,7 +560,7 @@ describe('Logging', () => { filter: reqOpts?.filter, orderBy: 'timestamp desc', resourceNames: ['projects/' + logging.projectId], - }), + }) ); assert.deepStrictEqual(gaxOpts, { autoPaginate: undefined, @@ -614,7 +614,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { a: 'b', @@ -643,7 +643,9 @@ describe('Logging', () => { }); it('should reject promise with error', () => { - logging.getEntries().then(noop, err => assert.strictEqual(err, error)); + return logging + .getEntries() + .then(noop, err => assert.strictEqual(err, error)); }); }); @@ -692,7 +694,7 @@ describe('Logging', () => { it('should make request once reading', done => { logging.loggingService.listLogEntriesStream = ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { resourceNames: ['projects/' + logging.projectId], @@ -721,7 +723,7 @@ describe('Logging', () => { logging = new LOGGING({projectId: PROJECT_ID}); logging.loggingService.listLogEntriesStream = ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { resourceNames: ['projects/' + logging.projectId], @@ -758,12 +760,12 @@ describe('Logging', () => { { filter: 'custom filter', }, - OPTIONS, + OPTIONS ); logging = new LOGGING({projectId: PROJECT_ID}); logging.loggingService.listLogEntriesStream = ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { resourceNames: ['projects/' + logging.projectId], @@ -908,7 +910,7 @@ describe('Logging', () => { logging.loggingService.listLogs = async () => { throw error; }; - logging + return logging .getLogs(OPTIONS) .then(noop, err => assert.strictEqual(err, error)); }); @@ -1079,7 +1081,7 @@ describe('Logging', () => { logging.configService.listSinks = async () => { throw error; }; - logging + return logging .getSinks(OPTIONS) .then(noop, err => assert.strictEqual(err, error)); }); @@ -1561,7 +1563,7 @@ describe('Logging', () => { }); it('should return error', () => { - logging + return logging .setAclForBucket_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1610,7 +1612,7 @@ describe('Logging', () => { }); it('should reject with error', () => { - logging + return logging .setAclForDataset_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1658,7 +1660,7 @@ describe('Logging', () => { }); it('should reject with error', () => { - logging + return logging .setAclForDataset_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1717,7 +1719,7 @@ describe('Logging', () => { }); it('should throw error', () => { - logging + return logging .setAclForTopic_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1764,7 +1766,7 @@ describe('Logging', () => { }); it('should throw error', () => { - logging + return logging .setAclForTopic_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); diff --git a/handwritten/logging/test/log.ts b/handwritten/logging/test/log.ts index e535e739b69..3efb9dfbf2a 100644 --- a/handwritten/logging/test/log.ts +++ b/handwritten/logging/test/log.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Entry, Logging} from '../src'; import {Log as LOG, LogOptions, WriteOptions} from '../src/log'; @@ -123,8 +123,8 @@ describe('Log', () => { assert( callbackifyFake.callbackifyAll.calledWithExactly( Log, - sinon.match({exclude: ['entry', 'getEntriesStream']}), - ), + sinon.match({exclude: ['entry', 'getEntriesStream']}) + ) ); }); @@ -140,7 +140,7 @@ describe('Log', () => { const log = new Log(LOGGING, LOG_NAME); assert.strictEqual( log.formattedName_, - logCommon.formatLogName('{{project-id}}', LOG_NAME), + logCommon.formatLogName('{{project-id}}', LOG_NAME) ); }); @@ -172,8 +172,8 @@ describe('Log', () => { logName: log.formattedName_, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -186,8 +186,8 @@ describe('Log', () => { logName: log.formattedName_, }, undefined, - log.defaultWriteDeleteCallback, - ), + log.defaultWriteDeleteCallback + ) ); log.defaultWriteDeleteCallback = undefined; }); @@ -195,7 +195,7 @@ describe('Log', () => { it('should accept gaxOptions', async () => { await log.delete({}); assert( - log.logging.loggingService.deleteLog.calledWith(sinon.match.any, {}), + log.logging.loggingService.deleteLog.calledWith(sinon.match.any, {}) ); }); }); @@ -235,7 +235,7 @@ describe('Log', () => { assert( log.logging.getEntries.calledWithExactly({ filter: `logName="${LOG_NAME_FORMATTED}"`, - }), + }) ); }); @@ -273,7 +273,7 @@ describe('Log', () => { assert( log.logging.getEntriesStream.calledWithExactly({ log: LOG_NAME_ENCODED, - }), + }) ); }); @@ -292,9 +292,9 @@ describe('Log', () => { { log: LOG_NAME_ENCODED, }, - options, - ), - ), + options + ) + ) ); }); }); @@ -312,7 +312,7 @@ describe('Log', () => { assert( log.logging.tailEntries.calledWithExactly({ log: LOG_NAME_ENCODED, - }), + }) ); }); @@ -331,9 +331,9 @@ describe('Log', () => { { log: LOG_NAME_ENCODED, }, - options, - ), - ), + options + ) + ) ); }); }); @@ -388,8 +388,8 @@ describe('Log', () => { }, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -424,8 +424,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ resource: reusableDetectedResource, - }), - ), + }) + ) ); }); @@ -454,8 +454,8 @@ describe('Log', () => { resource: EXPECTED_RESOURCE, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -470,8 +470,8 @@ describe('Log', () => { resource: FAKE_RESOURCE, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -487,8 +487,8 @@ describe('Log', () => { resource: FAKE_RESOURCE, }, undefined, - log.defaultWriteDeleteCallback, - ), + log.defaultWriteDeleteCallback + ) ); log.defaultWriteDeleteCallback = undefined; }); @@ -503,8 +503,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ entries: 'decorated entries', - }), - ), + }) + ) ); }); @@ -517,8 +517,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ entries: arrifiedEntries, - }), - ), + }) + ) ); }); @@ -530,8 +530,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ entries: ENTRIES, - }), - ), + }) + ) ); }); @@ -541,8 +541,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ partialSuccess: true, - }), - ), + }) + ) ); }); @@ -553,8 +553,8 @@ describe('Log', () => { sinon.match({ dryRun: true, partialSuccess: false, - }), - ), + }) + ) ); }); @@ -567,8 +567,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ partialSuccess: true, - }), - ), + }) + ) ); instrumentation.setInstrumentationStatus(true); }); @@ -582,8 +582,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ partialSuccess: true, - }), - ), + }) + ) ); }); @@ -597,8 +597,8 @@ describe('Log', () => { { maxRetries: 1, }, - sinon.match.any, - ), + sinon.match.any + ) ); log.logging.loggingService.writeLogEntries.reset(); await log.write(ENTRIES, {gaxOptions: {maxRetries: 10}}); @@ -608,8 +608,8 @@ describe('Log', () => { { maxRetries: 10, }, - sinon.match.any, - ), + sinon.match.any + ) ); }); }); @@ -656,7 +656,7 @@ describe('Log', () => { .returns({} as EntryJson); log.decorateEntries([entry]); assert( - localJSONStub.calledWithExactly({removeCircular: true}, PROJECT_ID), + localJSONStub.calledWithExactly({removeCircular: true}, PROJECT_ID) ); }); @@ -749,11 +749,11 @@ describe('Log', () => { assert.ok(log.jsonFieldsToTruncate.length > 1); assert.ok(log.jsonFieldsToTruncate[0] === TRUNCATE_FIELD); const notExists = log.jsonFieldsToTruncate.filter( - (str: string) => str === INVALID_TRUNCATE_FIELD, + (str: string) => str === INVALID_TRUNCATE_FIELD ); assert.strictEqual(notExists.length, 0); const existOnce = log.jsonFieldsToTruncate.filter( - (str: string) => str === TRUNCATE_FIELD, + (str: string) => str === TRUNCATE_FIELD ); assert.strictEqual(existOnce.length, 1); }); diff --git a/handwritten/logging/test/middleware/express/test-make-middleware.ts b/handwritten/logging/test/middleware/express/test-make-middleware.ts index 47740b2ad30..5444e2fc698 100644 --- a/handwritten/logging/test/middleware/express/test-make-middleware.ts +++ b/handwritten/logging/test/middleware/express/test-make-middleware.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as EventEmitter from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../../proxyquire'; const FAKE_PROJECT_ID = 'project-🦄'; @@ -45,7 +45,7 @@ describe('middleware/express/make-middleware', () => { '../../../src/middleware/express/make-middleware', { '../../../src/utils/context': FAKE_CONTEXT, - }, + } ); it('should return a function accepting 3 arguments', () => { @@ -133,7 +133,7 @@ describe('middleware/express/make-middleware', () => { const middleware = makeMiddleware( FAKE_PROJECT_ID, () => {}, - emitRequestLog, + emitRequestLog ); middleware(fakeRequest, fakeResponse, () => {}); diff --git a/handwritten/logging/test/proxyquire.ts b/handwritten/logging/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/logging/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/logging/test/sink.ts b/handwritten/logging/test/sink.ts index 598e63c70bf..f5a6c747bb2 100644 --- a/handwritten/logging/test/sink.ts +++ b/handwritten/logging/test/sink.ts @@ -16,7 +16,7 @@ import * as callbackify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Sink as SINK} from '../src/sink'; import {Logging, CreateSinkRequest, LogSink} from '../src/index'; @@ -75,7 +75,7 @@ describe('Sink', () => { it('should localize the formatted name', () => { assert.strictEqual( sink.formattedName_, - 'projects/' + LOGGING.projectId + '/sinks/' + SINK_NAME, + 'projects/' + LOGGING.projectId + '/sinks/' + SINK_NAME ); }); }); @@ -98,7 +98,7 @@ describe('Sink', () => { sink.logging.auth.getProjectId = async () => PROJECT_ID; sink.logging.configService.deleteSink = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { sinkName: sink.formattedName_, @@ -113,7 +113,7 @@ describe('Sink', () => { const gaxOptions = {}; sink.logging.configService.deleteSink = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(gaxOpts, gaxOptions); }; @@ -201,9 +201,9 @@ describe('Sink', () => { sink.getMetadata = async () => { throw error; }; - sink.setMetadata(METADATA).then( - () => {}, - err => assert.strictEqual(err, error), + return sink.setMetadata(METADATA).then( + () => null, + err => assert.strictEqual(err, error) ); }); @@ -213,7 +213,7 @@ describe('Sink', () => { sink.getMetadata = async () => [currentMetadata] as any; sink.logging.configService.updateSink = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { sinkName: sink.formattedName_, diff --git a/handwritten/logging/test/utils/metadata.ts b/handwritten/logging/test/utils/metadata.ts index 9425f8ff543..0e32e59f285 100644 --- a/handwritten/logging/test/utils/metadata.ts +++ b/handwritten/logging/test/utils/metadata.ts @@ -17,7 +17,7 @@ import {describe, it, beforeEach, before, after, afterEach} from 'mocha'; import BigNumber from 'bignumber.js'; import * as extend from 'extend'; import {GCPEnv} from 'google-auth-library'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; let instanceOverride: {} | null; const fakeGcpMetadata = { @@ -298,7 +298,7 @@ describe('metadata', () => { }; await assert.rejects( metadata.getGKEDescriptor(), - (err: Error) => err === FAKE_ERROR, + (err: Error) => err === FAKE_ERROR ); }); @@ -306,7 +306,7 @@ describe('metadata', () => { readFileShouldError = true; await assert.doesNotReject(metadata.getGKEDescriptor(), (err: Error) => - err.message.includes(FAKE_READFILE_ERROR_MESSAGE), + err.message.includes(FAKE_READFILE_ERROR_MESSAGE) ); }); }); diff --git a/handwritten/pubsub/package.json b/handwritten/pubsub/package.json index 866f3c60193..4f5993ad444 100644 --- a/handwritten/pubsub/package.json +++ b/handwritten/pubsub/package.json @@ -35,7 +35,7 @@ "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "samples-test": "pnpm run link-samples && pnpm test && cd ../", "link-samples": "cd samples/ && pnpm link ../ && pnpm install", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "lint": "gts check", "predocs": "pnpm run compile", "docs": "jsdoc -c .jsdoc.js", diff --git a/handwritten/pubsub/test/iam.ts b/handwritten/pubsub/test/iam.ts index a1f73f8bf35..6232a51e37a 100644 --- a/handwritten/pubsub/test/iam.ts +++ b/handwritten/pubsub/test/iam.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as iamTypes from '../src/iam'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/lease-manager.ts b/handwritten/pubsub/test/lease-manager.ts index d7fc2d8a2cf..a5088a5d181 100644 --- a/handwritten/pubsub/test/lease-manager.ts +++ b/handwritten/pubsub/test/lease-manager.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as defer from 'p-defer'; @@ -208,10 +208,7 @@ describe('LeaseManager', () => { subscriber.on('message', () => { assert.strictEqual(fakeLog!.called, true); - assert.strictEqual( - fakeLog!.fields!.severity, - 'INFO', - ); + assert.strictEqual(fakeLog!.fields!.severity, 'INFO'); assert.strictEqual(fakeLog!.args![1] as string, 'a'); assert.strictEqual(fakeLog!.args![2] as string, 'b'); done(); @@ -241,10 +238,7 @@ describe('LeaseManager', () => { await deferred.promise; assert.strictEqual(fakeLog.called, true); - assert.strictEqual( - fakeLog.fields!.severity, - 'ERROR', - ); + assert.strictEqual(fakeLog.fields!.severity, 'ERROR'); assert.strictEqual( (fakeLog.args![0] as string).includes('exception'), true, @@ -402,10 +396,7 @@ describe('LeaseManager', () => { // make sure the expired messages were forgotten assert.strictEqual(removeStub.callCount, badMessages.length); - assert.strictEqual( - fakeLog.fields!.severity, - 'WARNING', - ); + assert.strictEqual(fakeLog.fields!.severity, 'WARNING'); assert.strictEqual(fakeLog.called, true); badMessages.forEach((fakeMessage, i) => { diff --git a/handwritten/pubsub/test/message-stream.ts b/handwritten/pubsub/test/message-stream.ts index e9a0a5a1fad..0d921e038ba 100644 --- a/handwritten/pubsub/test/message-stream.ts +++ b/handwritten/pubsub/test/message-stream.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {grpc} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Duplex, PassThrough} from 'stream'; import * as defer from 'p-defer'; diff --git a/handwritten/pubsub/test/proxyquire.ts b/handwritten/pubsub/test/proxyquire.ts new file mode 100644 index 00000000000..44ff9f689c1 --- /dev/null +++ b/handwritten/pubsub/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = mod.prototype.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/pubsub/test/publisher/index.ts b/handwritten/pubsub/test/publisher/index.ts index 6930b3baa67..f4fa95908ba 100644 --- a/handwritten/pubsub/test/publisher/index.ts +++ b/handwritten/pubsub/test/publisher/index.ts @@ -18,7 +18,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import * as sinon from 'sinon'; import * as opentelemetry from '@opentelemetry/api'; import {Topic} from '../../src'; diff --git a/handwritten/pubsub/test/publisher/message-queues.ts b/handwritten/pubsub/test/publisher/message-queues.ts index c6d0b330998..343906dd683 100644 --- a/handwritten/pubsub/test/publisher/message-queues.ts +++ b/handwritten/pubsub/test/publisher/message-queues.ts @@ -18,7 +18,7 @@ import {loggingUtils, ServiceError} from 'google-gax'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import * as sinon from 'sinon'; import {RequestConfig, RequestCallback} from '../../src/pubsub'; @@ -187,10 +187,7 @@ describe('Message Queues', () => { fakeLog.remove(); assert.strictEqual(fakeLog.called, true); - assert.strictEqual( - fakeLog.fields!.severity, - 'INFO', - ); + assert.strictEqual(fakeLog.fields!.severity, 'INFO'); assert.strictEqual(fakeLog.args![1] as string, 'test'); }); @@ -394,6 +391,7 @@ describe('Message Queues', () => { queue.batch = new FakeMessageBatch(); queue.batch.messages = fakeMessages; queue.batch.callbacks = spies; + // eslint-disable-next-line promise/always-return void queue.publishDrain().then(() => { process.nextTick(() => { assert.strictEqual(stub.callCount, 2); @@ -429,6 +427,7 @@ describe('Message Queues', () => { sandbox.stub(queue, '_publish').callsFake(async () => {}); queue.on('drain', spy); + // eslint-disable-next-line promise/always-return void queue.publish('test').then(() => { process.nextTick(() => { assert.strictEqual(spy.callCount, 1); diff --git a/handwritten/pubsub/test/pubsub.ts b/handwritten/pubsub/test/pubsub.ts index b2ecbfa7d29..06f4547a09e 100644 --- a/handwritten/pubsub/test/pubsub.ts +++ b/handwritten/pubsub/test/pubsub.ts @@ -17,7 +17,7 @@ import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach, after, afterEach} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import defer = require('p-defer'); @@ -196,7 +196,7 @@ describe('PubSub', () => { './snapshot': {Snapshot: FakeSnapshot}, './subscription': {Subscription}, './topic': {Topic: FakeTopic}, - './index': {v1:v1Override}, + './index': {v1: v1Override}, './util': fakeUtil, }).PubSub; }); diff --git a/handwritten/pubsub/test/snapshot.ts b/handwritten/pubsub/test/snapshot.ts index 5e5da3efec7..cd781fe09e8 100644 --- a/handwritten/pubsub/test/snapshot.ts +++ b/handwritten/pubsub/test/snapshot.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {ServiceError} from 'google-gax'; import {describe, it, beforeEach, before, after, afterEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/subscriber.ts b/handwritten/pubsub/test/subscriber.ts index a0c4b0336b9..a6051a7aa45 100644 --- a/handwritten/pubsub/test/subscriber.ts +++ b/handwritten/pubsub/test/subscriber.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; import {common as protobuf} from 'protobufjs'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PassThrough} from 'stream'; import * as opentelemetry from '@opentelemetry/api'; diff --git a/handwritten/pubsub/test/subscription.ts b/handwritten/pubsub/test/subscription.ts index 4855ec9217a..5577c6b0e79 100644 --- a/handwritten/pubsub/test/subscription.ts +++ b/handwritten/pubsub/test/subscription.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; import {ServiceError} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/topic.ts b/handwritten/pubsub/test/topic.ts index c918b21c64e..4e2f736e48b 100644 --- a/handwritten/pubsub/test/topic.ts +++ b/handwritten/pubsub/test/topic.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {CallOptions, ServiceError} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {google} from '../protos/protos'; From 1b74f20040cd32307a433ae55b96fde9542b69ed Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Mon, 19 Oct 2026 14:43:47 +0000 Subject: [PATCH 04/10] [Address CI errors] test: support function stubs and guard read-only properties in proxyquire helper --- handwritten/logging-bunyan/test/proxyquire.ts | 13 ++++++++++--- handwritten/logging-winston/test/proxyquire.ts | 13 ++++++++++--- handwritten/logging/test/proxyquire.ts | 13 ++++++++++--- handwritten/pubsub/test/proxyquire.ts | 13 ++++++++++--- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/handwritten/logging-bunyan/test/proxyquire.ts b/handwritten/logging-bunyan/test/proxyquire.ts index 44ff9f689c1..5d344ba47da 100644 --- a/handwritten/logging-bunyan/test/proxyquire.ts +++ b/handwritten/logging-bunyan/test/proxyquire.ts @@ -24,8 +24,9 @@ const mod = Module as unknown as { _resolveFilename: (request: string, parent: NodeModule) => string; }; const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isObj = (o: unknown) => + Boolean(o && (typeof o === 'object' || typeof o === 'function')); +const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +77,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { diff --git a/handwritten/logging-winston/test/proxyquire.ts b/handwritten/logging-winston/test/proxyquire.ts index 44ff9f689c1..5d344ba47da 100644 --- a/handwritten/logging-winston/test/proxyquire.ts +++ b/handwritten/logging-winston/test/proxyquire.ts @@ -24,8 +24,9 @@ const mod = Module as unknown as { _resolveFilename: (request: string, parent: NodeModule) => string; }; const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isObj = (o: unknown) => + Boolean(o && (typeof o === 'object' || typeof o === 'function')); +const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +77,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { diff --git a/handwritten/logging/test/proxyquire.ts b/handwritten/logging/test/proxyquire.ts index 44ff9f689c1..5d344ba47da 100644 --- a/handwritten/logging/test/proxyquire.ts +++ b/handwritten/logging/test/proxyquire.ts @@ -24,8 +24,9 @@ const mod = Module as unknown as { _resolveFilename: (request: string, parent: NodeModule) => string; }; const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isObj = (o: unknown) => + Boolean(o && (typeof o === 'object' || typeof o === 'function')); +const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +77,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { diff --git a/handwritten/pubsub/test/proxyquire.ts b/handwritten/pubsub/test/proxyquire.ts index 44ff9f689c1..5d344ba47da 100644 --- a/handwritten/pubsub/test/proxyquire.ts +++ b/handwritten/pubsub/test/proxyquire.ts @@ -24,8 +24,9 @@ const mod = Module as unknown as { _resolveFilename: (request: string, parent: NodeModule) => string; }; const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isObj = (o: unknown) => + Boolean(o && (typeof o === 'object' || typeof o === 'function')); +const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); const isGlobal = (s: unknown) => hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); const resolveFrom = (base: string, req: string) => { @@ -76,7 +77,13 @@ function proxyquire(request: string, stubs: Stubs = {}): T { const real = prev.call(this, id); if (real && ['object', 'function'].includes(typeof real)) { for (const k of Object.keys(real)) { - if (!(k in stub)) stub[k] = real[k]; + if (!(k in stub)) { + try { + stub[k] = real[k]; + } catch { + // ignore + } + } } } } catch { From 3ab82400b83395f3510de173ece49f0ed60c6f71 Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Mon, 19 Oct 2026 16:02:11 +0000 Subject: [PATCH 05/10] [Address review comments] test(logging): use assert.rejects for promise rejection assertions --- handwritten/logging/test/index.ts | 47 +++++++++---------------------- handwritten/logging/test/sink.ts | 5 +--- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/handwritten/logging/test/index.ts b/handwritten/logging/test/index.ts index 03663ed512d..2bbdc411e91 100644 --- a/handwritten/logging/test/index.ts +++ b/handwritten/logging/test/index.ts @@ -270,17 +270,13 @@ describe('Logging', () => { it('should throw if a name is not provided', () => { const error = new Error('A sink name must be provided.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (logging as any).createSink().then(noop, (err: Error) => { - assert.deepStrictEqual(err, error); - }); + return assert.rejects((logging as any).createSink(), error); }); it('should throw if a config object is not provided', () => { const error = new Error('A sink configuration object must be provided.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (logging as any).createSink(SINK_NAME).then(noop, (err: Error) => { - assert.deepStrictEqual(err, error); - }); + return assert.rejects((logging as any).createSink(SINK_NAME), error); }); it('should set acls for a Dataset destination', async () => { @@ -424,9 +420,10 @@ describe('Logging', () => { throw error; }; - return logging - .createSink(SINK_NAME, {} as CreateSinkRequest) - .then(noop, (err: Error) => assert.deepStrictEqual(err, error)); + return assert.rejects( + logging.createSink(SINK_NAME, {} as CreateSinkRequest), + error + ); }); }); @@ -643,9 +640,7 @@ describe('Logging', () => { }); it('should reject promise with error', () => { - return logging - .getEntries() - .then(noop, err => assert.strictEqual(err, error)); + return assert.rejects(logging.getEntries(), error); }); }); @@ -910,9 +905,7 @@ describe('Logging', () => { logging.loggingService.listLogs = async () => { throw error; }; - return logging - .getLogs(OPTIONS) - .then(noop, err => assert.strictEqual(err, error)); + return assert.rejects(logging.getLogs(OPTIONS), error); }); }); @@ -1081,9 +1074,7 @@ describe('Logging', () => { logging.configService.listSinks = async () => { throw error; }; - return logging - .getSinks(OPTIONS) - .then(noop, err => assert.strictEqual(err, error)); + return assert.rejects(logging.getSinks(OPTIONS), error); }); }); @@ -1563,9 +1554,7 @@ describe('Logging', () => { }); it('should return error', () => { - return logging - .setAclForBucket_(CONFIG) - .then(noop, err => assert.deepStrictEqual(err, error)); + return assert.rejects(logging.setAclForBucket_(CONFIG), error); }); }); @@ -1612,9 +1601,7 @@ describe('Logging', () => { }); it('should reject with error', () => { - return logging - .setAclForDataset_(CONFIG) - .then(noop, err => assert.deepStrictEqual(err, error)); + return assert.rejects(logging.setAclForDataset_(CONFIG), error); }); }); @@ -1660,9 +1647,7 @@ describe('Logging', () => { }); it('should reject with error', () => { - return logging - .setAclForDataset_(CONFIG) - .then(noop, err => assert.deepStrictEqual(err, error)); + return assert.rejects(logging.setAclForDataset_(CONFIG), error); }); }); @@ -1719,9 +1704,7 @@ describe('Logging', () => { }); it('should throw error', () => { - return logging - .setAclForTopic_(CONFIG) - .then(noop, err => assert.deepStrictEqual(err, error)); + return assert.rejects(logging.setAclForTopic_(CONFIG), error); }); }); @@ -1766,9 +1749,7 @@ describe('Logging', () => { }); it('should throw error', () => { - return logging - .setAclForTopic_(CONFIG) - .then(noop, err => assert.deepStrictEqual(err, error)); + return assert.rejects(logging.setAclForTopic_(CONFIG), error); }); }); diff --git a/handwritten/logging/test/sink.ts b/handwritten/logging/test/sink.ts index f5a6c747bb2..303e14f8c80 100644 --- a/handwritten/logging/test/sink.ts +++ b/handwritten/logging/test/sink.ts @@ -201,10 +201,7 @@ describe('Sink', () => { sink.getMetadata = async () => { throw error; }; - return sink.setMetadata(METADATA).then( - () => null, - err => assert.strictEqual(err, error) - ); + return assert.rejects(sink.setMetadata(METADATA), error); }); it('should execute gax method', async () => { From f8fe2176cbf7e9c52140115356175e10ce9c4033 Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Tue, 20 Oct 2026 19:08:05 +0000 Subject: [PATCH 06/10] refactor(test-utils): centralize proxyquire helper in google-test-utils (Path 1B) --- .../actions/pnpm-lockfile-check/action.yaml | 2 + bin/run-test.cjs | 5 + core/common/package.json | 1 + core/common/test/proxyquire.ts | 123 --------- core/common/test/service-object.ts | 2 +- core/common/test/service.ts | 2 +- core/common/test/util.ts | 2 +- core/test-utils/src/index.ts | 6 + core/test-utils/src/proxyquire.ts | 245 ++++++++++++++++++ core/test-utils/test/proxyquire.test.ts | 145 +++++++++++ handwritten/bigquery/cloudbuild.yaml | 2 +- handwritten/bigquery/package.json | 1 + handwritten/bigquery/test/bigquery.ts | 2 +- handwritten/bigquery/test/dataset.ts | 2 +- handwritten/bigquery/test/job.ts | 2 +- handwritten/bigquery/test/model.ts | 2 +- handwritten/bigquery/test/proxyquire.ts | 123 --------- handwritten/bigquery/test/routine.ts | 2 +- handwritten/bigquery/test/rowQueue.ts | 2 +- handwritten/bigquery/test/table.ts | 2 +- handwritten/datastore/cloudbuild.yaml | 2 +- handwritten/datastore/package.json | 1 + .../datastore/system-test/transaction-spy.ts | 2 +- .../client-initialization-testing.ts | 2 +- handwritten/datastore/test/index-class.ts | 2 +- handwritten/datastore/test/index.ts | 2 +- handwritten/datastore/test/proxyquire.ts | 123 --------- handwritten/datastore/test/request.ts | 2 +- handwritten/datastore/test/transaction.ts | 2 +- handwritten/google-cloud-dns/cloudbuild.yaml | 2 +- handwritten/google-cloud-dns/package.json | 1 + handwritten/google-cloud-dns/test/change.ts | 2 +- handwritten/google-cloud-dns/test/index.ts | 2 +- .../google-cloud-dns/test/proxyquire.ts | 123 --------- handwritten/google-cloud-dns/test/record.ts | 2 +- handwritten/google-cloud-dns/test/zone.ts | 2 +- pnpm-lock.yaml | 30 ++- 37 files changed, 454 insertions(+), 521 deletions(-) delete mode 100644 core/common/test/proxyquire.ts create mode 100644 core/test-utils/src/proxyquire.ts create mode 100644 core/test-utils/test/proxyquire.test.ts delete mode 100644 handwritten/bigquery/test/proxyquire.ts delete mode 100644 handwritten/datastore/test/proxyquire.ts delete mode 100644 handwritten/google-cloud-dns/test/proxyquire.ts diff --git a/.github/actions/pnpm-lockfile-check/action.yaml b/.github/actions/pnpm-lockfile-check/action.yaml index 2ac007add90..554ba3db886 100644 --- a/.github/actions/pnpm-lockfile-check/action.yaml +++ b/.github/actions/pnpm-lockfile-check/action.yaml @@ -39,3 +39,5 @@ runs: git checkout -- pnpm-lock.yaml pnpm --dir core/packages/gaxios run compile pnpm --dir core/packages/teeny-request run compile + pnpm --dir core/test-utils run compile + node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" diff --git a/bin/run-test.cjs b/bin/run-test.cjs index 7f31b6e3df0..5dd86bdc129 100755 --- a/bin/run-test.cjs +++ b/bin/run-test.cjs @@ -112,6 +112,11 @@ const WORKSPACE_LINK_PACKAGES = [ relDir: 'core/packages/teeny-request', entryPoint: 'build/src/index.js', }, + { + name: 'google-test-utils', + relDir: 'core/test-utils', + entryPoint: 'build/src/index.js', + }, ]; function linkTargetToWorkspacePackage( diff --git a/core/common/package.json b/core/common/package.json index 3e2b37e200c..52c981a0b44 100644 --- a/core/common/package.json +++ b/core/common/package.json @@ -57,6 +57,7 @@ "@types/tmp": "^0.2.6", "c8": "^10.1.3", "codecov": "^3.8.3", + "google-test-utils": "^0.3.0", "gts": "^6.0.2", "jsdoc": "^4.0.4", "jsdoc-fresh": "^6.0.0", diff --git a/core/common/test/proxyquire.ts b/core/common/test/proxyquire.ts deleted file mode 100644 index 1145fae2c1f..00000000000 --- a/core/common/test/proxyquire.ts +++ /dev/null @@ -1,123 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean( - o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), - ); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/core/common/test/service-object.ts b/core/common/test/service-object.ts index e840224c6ed..a17038302bf 100644 --- a/core/common/test/service-object.ts +++ b/core/common/test/service-object.ts @@ -20,7 +20,7 @@ import { import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; // eslint-disable-next-line import/namespace import * as r from 'teeny-request'; import * as sinon from 'sinon'; diff --git a/core/common/test/service.ts b/core/common/test/service.ts index 74d6adb779c..6c378f197b9 100644 --- a/core/common/test/service.ts +++ b/core/common/test/service.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, after} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {Request} from 'teeny-request'; import {AuthClient, GoogleAuth, OAuth2Client} from 'google-auth-library'; diff --git a/core/common/test/util.ts b/core/common/test/util.ts index 617a02e2c7e..8c65ccb98d7 100644 --- a/core/common/test/util.ts +++ b/core/common/test/util.ts @@ -26,7 +26,7 @@ import { OAuth2Client, } from 'google-auth-library'; import * as nock from 'nock'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; // eslint-disable-next-line import/namespace import * as r from 'teeny-request'; import * as retryRequest from 'retry-request'; diff --git a/core/test-utils/src/index.ts b/core/test-utils/src/index.ts index 4d06ba191b6..23cd8724c2b 100644 --- a/core/test-utils/src/index.ts +++ b/core/test-utils/src/index.ts @@ -28,3 +28,9 @@ export { isReadOnlyPropertyError, requiresStrictArrayPrototypeEquality, } from './runtime'; +export { + type Proxyquire, + type ProxyquireStubs, + createProxyquire, + proxyquire, +} from './proxyquire'; diff --git a/core/test-utils/src/proxyquire.ts b/core/test-utils/src/proxyquire.ts new file mode 100644 index 00000000000..5b9009107e8 --- /dev/null +++ b/core/test-utils/src/proxyquire.ts @@ -0,0 +1,245 @@ +// 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 Module from 'module'; +import * as path from 'path'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ProxyquireStubs = Record; + +export interface Proxyquire { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (request: string, stubs?: ProxyquireStubs): T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + load(request: string, stubs?: ProxyquireStubs): T; + noCallThru(): Proxyquire; + callThru(): Proxyquire; + noPreserveCache(): Proxyquire; + preserveCache(): Proxyquire; +} + +type ModuleConstructor = { + new (id: string, parent?: NodeModule): NodeModule; + prototype: {require: NodeRequire}; + _cache?: NodeJS.Dict; + _resolveFilename: (request: string, parent: NodeModule) => string; + _nodeModulePaths: (from: string) => string[]; +}; + +const mod = ((Module as unknown as {Module?: ModuleConstructor}).Module ?? + Module) as unknown as ModuleConstructor; +const {hasOwnProperty} = Object.prototype; +const modProto: {require: NodeRequire} = hasOwnProperty.call( + mod.prototype, + 'require', +) + ? mod.prototype + : Object.getPrototypeOf(mod.prototype); +const isObj = (o: unknown): o is Record => + Boolean(o && (typeof o === 'object' || typeof o === 'function')); +const hasOwn = (o: unknown, k: string): boolean => + isObj(o) && hasOwnProperty.call(o, k); +const isGlobal = (s: unknown): boolean => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); + +const resolveFrom = (base: string, req: string): string => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +function getCallerFile(): string | undefined { + const origPrepare = Error.prepareStackTrace; + try { + const err = new Error(); + Error.prepareStackTrace = (_, stack) => stack; + const stack = err.stack as unknown as NodeJS.CallSite[] | undefined; + if (Array.isArray(stack)) { + for (const frame of stack) { + const file = frame.getFileName?.(); + if ( + file && + file !== __filename && + !file.startsWith('node:') && + !file.startsWith('internal/') + ) { + return file; + } + } + } + } finally { + Error.prepareStackTrace = origPrepare; + } + return undefined; +} + +function resolveModule(target: NodeModule | string | undefined): NodeModule { + if (target && typeof target === 'object') { + return target; + } + const filename = + typeof target === 'string' ? target : (getCallerFile() ?? __filename); + const cached = require.cache[filename]; + if (cached) { + return cached; + } + const fallback = new mod(filename); + fallback.filename = filename; + fallback.paths = mod._nodeModulePaths(path.dirname(filename)); + return fallback; +} + +interface CallerOptions { + skipCallThru: boolean; + keepCache: boolean; +} + +/** + * Creates a runtime-agnostic `proxyquire` function compatible with both + * Node.js and Bun (`JS_RUNTIME=bun`). + * + * @param caller Optional calling `NodeModule` or `__filename` used to resolve + * relative module paths. When omitted, the caller's filename is detected + * automatically from the call stack. + */ +export function createProxyquire(caller?: NodeModule | string): Proxyquire { + const defaultOpts: CallerOptions = {skipCallThru: false, keepCache: true}; + const callerOpts = new Map(); + + const getOpts = (callerFilename: string): CallerOptions => { + if (caller !== undefined) { + return defaultOpts; + } + let opts = callerOpts.get(callerFilename); + if (!opts) { + opts = {...defaultOpts}; + callerOpts.set(callerFilename, opts); + } + return opts; + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fn = function proxyquire( + request: string, + stubs: ProxyquireStubs = {}, + ): T { + const parent = resolveModule(caller); + const opts = getOpts(parent.filename); + const sut = mod._resolveFilename(request, parent); + const resolved: ProxyquireStubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const cache = mod._cache ?? require.cache; + const keys = global + ? Object.keys(cache).filter(id => !id.endsWith('.node')) + : [sut]; + const saved = keys.map(id => [id, cache[id]] as const); + for (const id of keys) delete cache[id]; + const prev = modProto.require; + const req = function (this: NodeModule, id: string) { + const isSut = this?.filename === sut; + if (this?.filename && (isSut || global)) { + const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); + const rId = hit ? '' : resolveFrom(this.filename, id); + if (hit || hasOwn(resolved, rId)) { + const stub = hit ? stubs[id] : resolved[rId]; + if (isSut || isGlobal(stub)) { + if (stub === null) { + const e = new Error(`Cannot find module '${id}'`); + throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); + } + const skip = hasOwn(stub, '@noCallThru') + ? Boolean(stub['@noCallThru']) + : opts.skipCallThru; + if (!skip && isObj(stub)) { + try { + const real = prev.call(this, id); + if (isObj(real)) { + return new Proxy(stub, { + get(target, prop) { + if (prop in target) { + return Reflect.get(target, prop); + } + return Reflect.get(real, prop); + }, + has(target, prop) { + return prop in target || prop in real; + }, + ownKeys(target) { + return Array.from( + new Set([ + ...Reflect.ownKeys(target), + ...Reflect.ownKeys(real), + ]), + ); + }, + getOwnPropertyDescriptor(target, prop) { + const desc = + Reflect.getOwnPropertyDescriptor(target, prop) || + Reflect.getOwnPropertyDescriptor(real, prop); + return desc ? {...desc, configurable: true} : undefined; + }, + }); + } + } catch { + // ignore when underlying module cannot be loaded + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + modProto.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + modProto.require = prev; + for (const id of global ? Object.keys(cache) : [sut]) { + if (!id.endsWith('.node')) delete cache[id]; + } + for (const [id, m] of saved) { + if (m && (opts.keepCache || id !== sut)) cache[id] = m; + } + } + }; + + const updateOpts = (patch: Partial): Proxyquire => { + const parent = resolveModule(caller); + Object.assign(getOpts(parent.filename), patch); + return instance; + }; + + const instance: Proxyquire = Object.assign(fn, { + load: fn, + noCallThru: () => updateOpts({skipCallThru: true}), + callThru: () => updateOpts({skipCallThru: false}), + noPreserveCache: () => updateOpts({keepCache: false}), + preserveCache: () => updateOpts({keepCache: true}), + }); + + return instance; +} + +/** + * Shared `proxyquire` instance that resolves relative module requests and + * option flags (`noCallThru`, `noPreserveCache`, etc.) per calling test file. + */ +export const proxyquire: Proxyquire = createProxyquire(); diff --git a/core/test-utils/test/proxyquire.test.ts b/core/test-utils/test/proxyquire.test.ts new file mode 100644 index 00000000000..40b006fe8e0 --- /dev/null +++ b/core/test-utils/test/proxyquire.test.ts @@ -0,0 +1,145 @@ +// 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 * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import {createProxyquire, proxyquire} from '../src'; + +describe('proxyquire utilities (unit)', () => { + let tmpDir: string; + let depFile: string; + let sutFile: string; + let transitiveFile: string; + + beforeAll(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxyquire-test-')); + depFile = path.join(tmpDir, 'dep.js'); + transitiveFile = path.join(tmpDir, 'transitive.js'); + sutFile = path.join(tmpDir, 'sut.js'); + + fs.writeFileSync( + depFile, + [ + 'module.exports = {', + ' greet: () => "real-greet",', + ' extra: () => "real-extra",', + ' constant: 42,', + '};', + ].join('\n'), + ); + fs.writeFileSync( + transitiveFile, + [ + 'const dep = require("./dep");', + 'module.exports = { run: () => dep.greet() };', + ].join('\n'), + ); + fs.writeFileSync( + sutFile, + [ + 'const dep = require("./dep");', + 'const trans = require("./transitive");', + 'const pathMod = require("path");', + 'module.exports = {', + ' greet: () => dep.greet(),', + ' extra: () => dep.extra && dep.extra(),', + ' constant: dep.constant,', + ' hasExtra: "extra" in dep,', + ' keys: Object.keys(dep),', + ' transGreet: () => trans.run(),', + ' sep: pathMod.sep,', + '};', + ].join('\n'), + ); + }); + + afterAll(() => { + fs.rmSync(tmpDir, {recursive: true, force: true}); + }); + + it('stubs direct dependencies and falls through to real module via Proxy without mutating stub', () => { + const stub = {greet: () => 'stubbed-greet'}; + const loaded = proxyquire(sutFile, {'./dep': stub}); + expect(loaded.greet()).toBe('stubbed-greet'); + expect(loaded.extra()).toBe('real-extra'); + expect(loaded.constant).toBe(42); + expect(loaded.hasExtra).toBe(true); + expect(loaded.keys).toEqual( + expect.arrayContaining(['greet', 'extra', 'constant']), + ); + // Stub object itself was not mutated in place + expect('extra' in stub).toBe(false); + expect(Object.keys(stub)).toEqual(['greet']); + }); + + it('supports @noCallThru on individual stubs and noCallThru() / callThru() on instance', () => { + const pq = createProxyquire(__filename); + const stubWithNoCallThru = { + greet: () => 'isolated', + '@noCallThru': true, + }; + const loaded1 = pq.load(sutFile, {'./dep': stubWithNoCallThru}); + expect(loaded1.greet()).toBe('isolated'); + expect(loaded1.extra()).toBeUndefined(); + expect(loaded1.constant).toBeUndefined(); + + pq.noCallThru(); + const loaded2 = pq(sutFile, {'./dep': {greet: () => 'instance-no-call'}}); + expect(loaded2.greet()).toBe('instance-no-call'); + expect(loaded2.extra()).toBeUndefined(); + + pq.callThru(); + const loaded3 = pq(sutFile, {'./dep': {greet: () => 'instance-call-thru'}}); + expect(loaded3.greet()).toBe('instance-call-thru'); + expect(loaded3.extra()).toBe('real-extra'); + }); + + it('supports @global stubs across transitive dependencies', () => { + const loadedNonGlobal = proxyquire(sutFile, { + './dep': {greet: () => 'direct-only'}, + }); + expect(loadedNonGlobal.greet()).toBe('direct-only'); + + const loadedGlobal = proxyquire(sutFile, { + './dep': {greet: () => 'global-stub', '@global': true}, + }); + expect(loadedGlobal.greet()).toBe('global-stub'); + expect(loadedGlobal.transGreet()).toBe('global-stub'); + }); + + it('throws MODULE_NOT_FOUND when stub is null', () => { + expect(() => proxyquire(sutFile, {'./dep': null})).toThrow( + /Cannot find module '\.\/dep'/, + ); + }); + + it('supports noPreserveCache() and preserveCache()', () => { + const pq = createProxyquire(module); + pq.noPreserveCache(); + const loaded1 = pq(sutFile, {'./dep': {greet: () => 'a'}}); + expect(loaded1.greet()).toBe('a'); + pq.preserveCache(); + const loaded2 = pq(sutFile, {'./dep': {greet: () => 'b'}}); + expect(loaded2.greet()).toBe('b'); + }); + + it('supports bare module stubs and unresolvable relative paths', () => { + const loaded = proxyquire(sutFile, { + path: {sep: '/custom-sep'}, + './non-existent-relative': {foo: 'bar'}, + }); + expect(loaded.sep).toBe('/custom-sep'); + }); +}); diff --git a/handwritten/bigquery/cloudbuild.yaml b/handwritten/bigquery/cloudbuild.yaml index 61057c32af6..94c291b817b 100644 --- a/handwritten/bigquery/cloudbuild.yaml +++ b/handwritten/bigquery/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/bigquery}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/bigquery}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/bigquery}" run prepare --if-present dir: 'handwritten/bigquery' id: 'install-dependencies' diff --git a/handwritten/bigquery/package.json b/handwritten/bigquery/package.json index f47531ee118..beefdd0f059 100644 --- a/handwritten/bigquery/package.json +++ b/handwritten/bigquery/package.json @@ -91,6 +91,7 @@ "codecov": "^3.8.3", "discovery-tsd": "^0.3.0", "eslint-plugin-prettier": "^5.2.6", + "google-test-utils": "^0.3.0", "gts": "^6.0.2", "jsdoc": "^4.0.4", "jsdoc-fresh": "^6.0.0", diff --git a/handwritten/bigquery/test/bigquery.ts b/handwritten/bigquery/test/bigquery.ts index 72d0743ac9f..397c9c582c2 100644 --- a/handwritten/bigquery/test/bigquery.ts +++ b/handwritten/bigquery/test/bigquery.ts @@ -24,7 +24,7 @@ import * as assert from 'assert'; import {describe, it, after, afterEach, before, beforeEach} from 'mocha'; import * as Big from 'big.js'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; diff --git a/handwritten/bigquery/test/dataset.ts b/handwritten/bigquery/test/dataset.ts index cdf1a4e321d..d369c322179 100644 --- a/handwritten/bigquery/test/dataset.ts +++ b/handwritten/bigquery/test/dataset.ts @@ -22,7 +22,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as _root from '../src'; import {toArray} from '../src/util'; diff --git a/handwritten/bigquery/test/job.ts b/handwritten/bigquery/test/job.ts index ece2bc1d340..9dcafc9f7ac 100644 --- a/handwritten/bigquery/test/job.ts +++ b/handwritten/bigquery/test/job.ts @@ -16,7 +16,7 @@ import {DecorateRequestOptions, util} from '@google-cloud/common'; import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, beforeEach, afterEach, before} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {BigQuery} from '../src/bigquery'; diff --git a/handwritten/bigquery/test/model.ts b/handwritten/bigquery/test/model.ts index b07ab763e64..895afea43b4 100644 --- a/handwritten/bigquery/test/model.ts +++ b/handwritten/bigquery/test/model.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as sinon from 'sinon'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as pfy from '@google-cloud/promisify'; import {EventEmitter} from 'events'; import {JobOptions} from '../src/job'; diff --git a/handwritten/bigquery/test/proxyquire.ts b/handwritten/bigquery/test/proxyquire.ts deleted file mode 100644 index 1145fae2c1f..00000000000 --- a/handwritten/bigquery/test/proxyquire.ts +++ /dev/null @@ -1,123 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean( - o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), - ); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/bigquery/test/routine.ts b/handwritten/bigquery/test/routine.ts index dac2db92b63..f550f2a3aec 100644 --- a/handwritten/bigquery/test/routine.ts +++ b/handwritten/bigquery/test/routine.ts @@ -17,7 +17,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as _root from '../src'; diff --git a/handwritten/bigquery/test/rowQueue.ts b/handwritten/bigquery/test/rowQueue.ts index 11b0f8c971c..bde9198edcc 100644 --- a/handwritten/bigquery/test/rowQueue.ts +++ b/handwritten/bigquery/test/rowQueue.ts @@ -16,7 +16,7 @@ import {util} from '@google-cloud/common'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {Duplex, Stream} from 'stream'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import * as q from '../src/rowQueue'; import * as t from '../src/table'; diff --git a/handwritten/bigquery/test/table.ts b/handwritten/bigquery/test/table.ts index b17c506a65e..eb78c7f00d1 100644 --- a/handwritten/bigquery/test/table.ts +++ b/handwritten/bigquery/test/table.ts @@ -26,7 +26,7 @@ import {describe, it, afterEach, beforeEach, before, after} from 'mocha'; import * as Big from 'big.js'; import {EventEmitter} from 'events'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import * as stream from 'stream'; import * as crypto from 'crypto'; diff --git a/handwritten/datastore/cloudbuild.yaml b/handwritten/datastore/cloudbuild.yaml index a8a82e5f75c..baf0365542b 100644 --- a/handwritten/datastore/cloudbuild.yaml +++ b/handwritten/datastore/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/datastore}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/datastore}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/datastore}" run prepare --if-present dir: 'handwritten/datastore' id: 'install-dependencies' diff --git a/handwritten/datastore/package.json b/handwritten/datastore/package.json index 7271a5d9d22..2c94da61069 100644 --- a/handwritten/datastore/package.json +++ b/handwritten/datastore/package.json @@ -68,6 +68,7 @@ "async": "^3.2.6", "c8": "^10.1.3", "gapic-tools": "^2.0.0", + "google-test-utils": "^0.3.0", "gts": "^6.0.2", "js-yaml": "^4.1.0", "jsdoc": "^4.0.4", diff --git a/handwritten/datastore/system-test/transaction-spy.ts b/handwritten/datastore/system-test/transaction-spy.ts index fb4763aa0ea..415492f8a50 100644 --- a/handwritten/datastore/system-test/transaction-spy.ts +++ b/handwritten/datastore/system-test/transaction-spy.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as proxyquire from '../test/proxyquire'; +import {proxyquire} from 'google-test-utils'; import {DatastoreRequest} from '../src'; import {RequestCallback, RequestConfig} from '../src/request'; diff --git a/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts b/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts index 4c4809b6978..8a978d916fd 100644 --- a/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts +++ b/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts @@ -21,7 +21,7 @@ import { DatastoreOptions, } from '../../src'; import * as assert from 'assert'; -import * as proxyquire from '../proxyquire'; +import {proxyquire} from 'google-test-utils'; import {Callback, CallOptions} from 'google-gax'; import * as protos from '../../src/protos'; import * as ds from '../../src'; diff --git a/handwritten/datastore/test/index-class.ts b/handwritten/datastore/test/index-class.ts index 1f75cb436b3..db7b2133984 100644 --- a/handwritten/datastore/test/index-class.ts +++ b/handwritten/datastore/test/index-class.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as ds from '../src'; diff --git a/handwritten/datastore/test/index.ts b/handwritten/datastore/test/index.ts index 8545834c59c..9ca046a1f99 100644 --- a/handwritten/datastore/test/index.ts +++ b/handwritten/datastore/test/index.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {before, beforeEach, after, afterEach, describe, it} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {PassThrough, Readable} from 'stream'; import * as ds from '../src'; diff --git a/handwritten/datastore/test/proxyquire.ts b/handwritten/datastore/test/proxyquire.ts deleted file mode 100644 index 1145fae2c1f..00000000000 --- a/handwritten/datastore/test/proxyquire.ts +++ /dev/null @@ -1,123 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean( - o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), - ); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/datastore/test/request.ts b/handwritten/datastore/test/request.ts index 1db6a3699ca..c90f71367ae 100644 --- a/handwritten/datastore/test/request.ts +++ b/handwritten/datastore/test/request.ts @@ -18,7 +18,7 @@ import {after, afterEach, before, beforeEach, describe, it} from 'mocha'; import * as extend from 'extend'; import * as gax from 'google-gax'; import * as is from 'is'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {PassThrough, Transform} from 'stream'; diff --git a/handwritten/datastore/test/transaction.ts b/handwritten/datastore/test/transaction.ts index c322c5f4592..a69f3f84588 100644 --- a/handwritten/datastore/test/transaction.ts +++ b/handwritten/datastore/test/transaction.ts @@ -16,7 +16,7 @@ import * as pfy from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {afterEach, beforeEach, before, describe, it} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import { Datastore, DatastoreOptions, diff --git a/handwritten/google-cloud-dns/cloudbuild.yaml b/handwritten/google-cloud-dns/cloudbuild.yaml index 24525563b03..2af42772c0c 100644 --- a/handwritten/google-cloud-dns/cloudbuild.yaml +++ b/handwritten/google-cloud-dns/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/google-cloud-dns}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/google-cloud-dns}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/google-cloud-dns}" run prepare --if-present dir: 'handwritten/google-cloud-dns' id: 'install-dependencies' diff --git a/handwritten/google-cloud-dns/package.json b/handwritten/google-cloud-dns/package.json index a58a630f0a2..38fbb29c6cb 100644 --- a/handwritten/google-cloud-dns/package.json +++ b/handwritten/google-cloud-dns/package.json @@ -64,6 +64,7 @@ "@types/tmp": "^0.2.6", "c8": "^10.1.3", "codecov": "^3.8.3", + "google-test-utils": "^0.3.0", "gts": "^6.0.2", "jsdoc": "^4.0.4", "jsdoc-fresh": "^6.0.0", diff --git a/handwritten/google-cloud-dns/test/change.ts b/handwritten/google-cloud-dns/test/change.ts index 7aaa61f9c0f..19768594784 100644 --- a/handwritten/google-cloud-dns/test/change.ts +++ b/handwritten/google-cloud-dns/test/change.ts @@ -20,7 +20,7 @@ import { import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {Change} from '../src/change'; let promisified = false; diff --git a/handwritten/google-cloud-dns/test/index.ts b/handwritten/google-cloud-dns/test/index.ts index 1d89ead7255..a3ef8499b7a 100644 --- a/handwritten/google-cloud-dns/test/index.ts +++ b/handwritten/google-cloud-dns/test/index.ts @@ -22,7 +22,7 @@ import * as promisify from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {CoreOptions, OptionsWithUri, Response} from 'request'; import {Zone} from '../src'; diff --git a/handwritten/google-cloud-dns/test/proxyquire.ts b/handwritten/google-cloud-dns/test/proxyquire.ts deleted file mode 100644 index 1145fae2c1f..00000000000 --- a/handwritten/google-cloud-dns/test/proxyquire.ts +++ /dev/null @@ -1,123 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const hasOwn = (o: unknown, k: string) => - Boolean( - o && ['object', 'function'].includes(typeof o) && hasOwnProperty.call(o, k), - ); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/google-cloud-dns/test/record.ts b/handwritten/google-cloud-dns/test/record.ts index ffbba1ce547..ee2f3fb4d49 100644 --- a/handwritten/google-cloud-dns/test/record.ts +++ b/handwritten/google-cloud-dns/test/record.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import {Record} from '../src'; diff --git a/handwritten/google-cloud-dns/test/zone.ts b/handwritten/google-cloud-dns/test/zone.ts index c2a639b6d66..cc5c517f780 100644 --- a/handwritten/google-cloud-dns/test/zone.ts +++ b/handwritten/google-cloud-dns/test/zone.ts @@ -17,7 +17,7 @@ import * as promisify from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {CoreOptions, OptionsWithUri, Response} from 'request'; import * as crypto from 'crypto'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f973509c5..3bab231f6cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: codecov: specifier: ^3.8.3 version: 3.8.3(encoding@0.1.13) + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.2 version: 6.0.2(typescript@5.9.3) @@ -1662,6 +1665,9 @@ importers: eslint-plugin-prettier: specifier: ^5.2.6 version: 5.5.6(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.9.6) + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.2 version: 6.0.2(typescript@5.9.3) @@ -2173,6 +2179,9 @@ importers: gapic-tools: specifier: ^2.0.0 version: 2.0.1(protobufjs@7.5.9) + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.2 version: 6.0.2(typescript@5.9.3) @@ -2515,6 +2524,9 @@ importers: codecov: specifier: ^3.8.3 version: 3.8.3(encoding@0.1.13) + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.2 version: 6.0.2(typescript@5.9.3) @@ -20758,6 +20770,10 @@ packages: resolution: {integrity: sha512-OP7B5iS65EjAlRctfq3uUdGWXN5AvIGRYQSx5luCVqkfh3uaGvVzuY1y3hCyxLk6MGf5DYkJ3rZxgJ6JsMmSog==} engines: {node: '>=22'} + google-test-utils@0.3.0: + resolution: {integrity: sha512-atGLcriC2MS6gWCJnCNrxLqYh4Xs5WkgaS9+lwxF/3rZdzt5eFv/Bc2VIeCCl5DGzGP3So8dj5f7pfBUNRhzXw==} + engines: {node: '>=22'} + googleapis-common@4.4.3: resolution: {integrity: sha512-W46WKCk3QtlCCfmZyQIH5zxmDOyeV5Qj+qs7nr2ox08eRkEJMWp6iwv542R/PsokXaGUSrmif4vCC4+rGzRSsQ==} engines: {node: '>=10.10.0'} @@ -28716,7 +28732,7 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.109.0)': dependencies: - webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) + webpack: 5.109.0(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.109.0) '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.109.0)': @@ -28726,7 +28742,7 @@ snapshots: '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.109.0)': dependencies: - webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) + webpack: 5.109.0(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.109.0) '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.109.0)': @@ -28736,7 +28752,7 @@ snapshots: '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.109.0)': dependencies: - webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) + webpack: 5.109.0(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.109.0) '@xtuc/ieee754@1.2.0': {} @@ -32042,6 +32058,8 @@ snapshots: google-test-utils@0.2.0: {} + google-test-utils@0.3.0: {} + googleapis-common@4.4.3(encoding@0.1.13): dependencies: extend: 3.0.2 @@ -34372,7 +34390,7 @@ snapshots: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) + webpack: 5.109.0(webpack-cli@6.0.1) nunjucks@3.2.4(chokidar@3.6.0): dependencies: @@ -36563,7 +36581,7 @@ snapshots: picomatch: 4.0.5 source-map: 0.7.6 typescript: 5.8.3 - webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) + webpack: 5.109.0(webpack-cli@6.0.1) optionalDependencies: loader-utils: 2.0.4 @@ -37051,7 +37069,7 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) + webpack: 5.109.0(webpack-cli@6.0.1) webpack-merge: 6.0.1 webpack-merge@4.2.2: From dd814a5e29850fe213fb1b636f630bea3b096283 Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Tue, 20 Oct 2026 19:09:44 +0000 Subject: [PATCH 07/10] refactor(logging,pubsub): use centralized proxyquire from google-test-utils (Path 1B) --- handwritten/logging-bunyan/cloudbuild.yaml | 2 +- handwritten/logging-bunyan/package.json | 1 + handwritten/logging-bunyan/test/index.ts | 2 +- .../test/middleware/test-express.ts | 2 +- handwritten/logging-bunyan/test/proxyquire.ts | 122 ------------------ handwritten/logging-winston/cloudbuild.yaml | 2 +- handwritten/logging-winston/package.json | 1 + handwritten/logging-winston/test/common.ts | 2 +- handwritten/logging-winston/test/index.ts | 2 +- .../test/middleware/express.ts | 2 +- .../logging-winston/test/proxyquire.ts | 122 ------------------ .../test/stackdriver-trace-integration.ts | 2 +- handwritten/logging/cloudbuild.yaml | 2 +- handwritten/logging/package.json | 1 + handwritten/logging/test/entry.ts | 2 +- handwritten/logging/test/index.ts | 2 +- handwritten/logging/test/log.ts | 2 +- .../express/test-make-middleware.ts | 2 +- handwritten/logging/test/proxyquire.ts | 122 ------------------ handwritten/logging/test/sink.ts | 2 +- handwritten/logging/test/utils/metadata.ts | 2 +- handwritten/pubsub/cloudbuild.yaml | 2 +- handwritten/pubsub/package.json | 2 +- handwritten/pubsub/test/iam.ts | 2 +- handwritten/pubsub/test/lease-manager.ts | 2 +- handwritten/pubsub/test/message-stream.ts | 2 +- handwritten/pubsub/test/proxyquire.ts | 122 ------------------ handwritten/pubsub/test/publisher/index.ts | 2 +- .../pubsub/test/publisher/message-queues.ts | 2 +- handwritten/pubsub/test/pubsub.ts | 2 +- handwritten/pubsub/test/snapshot.ts | 2 +- handwritten/pubsub/test/subscriber.ts | 2 +- handwritten/pubsub/test/subscription.ts | 2 +- handwritten/pubsub/test/topic.ts | 2 +- pnpm-lock.yaml | 25 ++-- 35 files changed, 47 insertions(+), 523 deletions(-) delete mode 100644 handwritten/logging-bunyan/test/proxyquire.ts delete mode 100644 handwritten/logging-winston/test/proxyquire.ts delete mode 100644 handwritten/logging/test/proxyquire.ts delete mode 100644 handwritten/pubsub/test/proxyquire.ts diff --git a/handwritten/logging-bunyan/cloudbuild.yaml b/handwritten/logging-bunyan/cloudbuild.yaml index 26d13e85139..59558f4a928 100644 --- a/handwritten/logging-bunyan/cloudbuild.yaml +++ b/handwritten/logging-bunyan/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/logging-bunyan}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/logging-bunyan}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/logging-bunyan}" run prepare --if-present dir: 'handwritten/logging-bunyan' id: 'install-dependencies' diff --git a/handwritten/logging-bunyan/package.json b/handwritten/logging-bunyan/package.json index 4bd8a258110..f064b1d888c 100644 --- a/handwritten/logging-bunyan/package.json +++ b/handwritten/logging-bunyan/package.json @@ -70,6 +70,7 @@ "cross-env": "^7.0.3", "delay": "^5.0.0", "express": "^4.16.3", + "google-test-utils": "^0.3.0", "gts": "^6.0.2", "jsdoc": "^4.0.0", "jsdoc-fresh": "^6.0.0", diff --git a/handwritten/logging-bunyan/test/index.ts b/handwritten/logging-bunyan/test/index.ts index 0c82b56d35f..2f39cc7a58f 100644 --- a/handwritten/logging-bunyan/test/index.ts +++ b/handwritten/logging-bunyan/test/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {inspect} from 'util'; import {LoggingBunyan} from '../src'; diff --git a/handwritten/logging-bunyan/test/middleware/test-express.ts b/handwritten/logging-bunyan/test/middleware/test-express.ts index ba5f651c6e8..b96fd0f4857 100644 --- a/handwritten/logging-bunyan/test/middleware/test-express.ts +++ b/handwritten/logging-bunyan/test/middleware/test-express.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import {GCPEnv} from 'google-auth-library'; -import * as proxyquire from '../proxyquire'; +import {proxyquire} from 'google-test-utils'; // types-only import. Actual require is done through proxyquire below. import {MiddlewareOptions} from '../../src/middleware/express'; diff --git a/handwritten/logging-bunyan/test/proxyquire.ts b/handwritten/logging-bunyan/test/proxyquire.ts deleted file mode 100644 index 5d344ba47da..00000000000 --- a/handwritten/logging-bunyan/test/proxyquire.ts +++ /dev/null @@ -1,122 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const isObj = (o: unknown) => - Boolean(o && (typeof o === 'object' || typeof o === 'function')); -const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/logging-winston/cloudbuild.yaml b/handwritten/logging-winston/cloudbuild.yaml index 82005cf5084..c4f4637e082 100644 --- a/handwritten/logging-winston/cloudbuild.yaml +++ b/handwritten/logging-winston/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/logging-winston}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/logging-winston}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/logging-winston}" run prepare --if-present dir: 'handwritten/logging-winston' id: 'install-dependencies' diff --git a/handwritten/logging-winston/package.json b/handwritten/logging-winston/package.json index 81005402725..95f97a09dcf 100644 --- a/handwritten/logging-winston/package.json +++ b/handwritten/logging-winston/package.json @@ -67,6 +67,7 @@ "c8": "^10.1.3", "codecov": "^3.8.3", "cross-env": "^7.0.3", + "google-test-utils": "^0.3.0", "gts": "^6.0.2", "jsdoc": "^4.0.5", "jsdoc-fresh": "^6.0.0", diff --git a/handwritten/logging-winston/test/common.ts b/handwritten/logging-winston/test/common.ts index f3fb4011a6d..e5acd8bc778 100644 --- a/handwritten/logging-winston/test/common.ts +++ b/handwritten/logging-winston/test/common.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as nodeutil from 'util'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {Options} from '../src'; import {Entry, Logging, LogSync, Log} from '@google-cloud/logging'; import * as instrumentation from '@google-cloud/logging/build/src/utils/instrumentation'; diff --git a/handwritten/logging-winston/test/index.ts b/handwritten/logging-winston/test/index.ts index 1af6b888163..bca6f2980aa 100644 --- a/handwritten/logging-winston/test/index.ts +++ b/handwritten/logging-winston/test/index.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as TransportStream from 'winston-transport'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import {Options} from '../src'; describe('logging-winston', () => { diff --git a/handwritten/logging-winston/test/middleware/express.ts b/handwritten/logging-winston/test/middleware/express.ts index b8ec33cc8f9..ea5d747217f 100644 --- a/handwritten/logging-winston/test/middleware/express.ts +++ b/handwritten/logging-winston/test/middleware/express.ts @@ -18,7 +18,7 @@ import {GCPEnv} from 'google-auth-library'; import {LogEntry} from 'winston'; import * as TransportStream from 'winston-transport'; import * as winston from 'winston'; -import * as proxyquire from '../proxyquire'; +import {proxyquire} from 'google-test-utils'; import {Options} from '../../src'; // types-only import. Actual require is done through proxyquire below. diff --git a/handwritten/logging-winston/test/proxyquire.ts b/handwritten/logging-winston/test/proxyquire.ts deleted file mode 100644 index 5d344ba47da..00000000000 --- a/handwritten/logging-winston/test/proxyquire.ts +++ /dev/null @@ -1,122 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const isObj = (o: unknown) => - Boolean(o && (typeof o === 'object' || typeof o === 'function')); -const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/logging-winston/test/stackdriver-trace-integration.ts b/handwritten/logging-winston/test/stackdriver-trace-integration.ts index 9e62c33575e..52142aad48d 100644 --- a/handwritten/logging-winston/test/stackdriver-trace-integration.ts +++ b/handwritten/logging-winston/test/stackdriver-trace-integration.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, after} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as winston from 'winston'; import * as loggingWinstonLibTypes from '../src/index'; diff --git a/handwritten/logging/cloudbuild.yaml b/handwritten/logging/cloudbuild.yaml index c8f34da5f96..7043819381f 100644 --- a/handwritten/logging/cloudbuild.yaml +++ b/handwritten/logging/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/logging}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/logging}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/logging}" run prepare --if-present dir: 'handwritten/logging' id: 'install-dependencies' diff --git a/handwritten/logging/package.json b/handwritten/logging/package.json index 06363bb2195..d86b8dbda7a 100644 --- a/handwritten/logging/package.json +++ b/handwritten/logging/package.json @@ -86,6 +86,7 @@ "codecov": "^3.6.5", "cross-env": "^7.0.3", "gapic-tools": "^2.0.0", + "google-test-utils": "^0.3.0", "gts": "^5.0.0", "http2spy": "^2.0.0", "jsdoc": "^4.0.0", diff --git a/handwritten/logging/test/entry.ts b/handwritten/logging/test/entry.ts index d9d994a8e44..d4abf0cd913 100644 --- a/handwritten/logging/test/entry.ts +++ b/handwritten/logging/test/entry.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as entryTypes from '../src/entry'; import * as common from '../src/utils/common'; import * as http from 'http'; diff --git a/handwritten/logging/test/index.ts b/handwritten/logging/test/index.ts index 2bbdc411e91..b7d8eb98a54 100644 --- a/handwritten/logging/test/index.ts +++ b/handwritten/logging/test/index.ts @@ -18,7 +18,7 @@ import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, beforeEach, before} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import { Logging as LOGGING, LoggingOptions, diff --git a/handwritten/logging/test/log.ts b/handwritten/logging/test/log.ts index 3efb9dfbf2a..7dbc42c5a43 100644 --- a/handwritten/logging/test/log.ts +++ b/handwritten/logging/test/log.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {Entry, Logging} from '../src'; import {Log as LOG, LogOptions, WriteOptions} from '../src/log'; diff --git a/handwritten/logging/test/middleware/express/test-make-middleware.ts b/handwritten/logging/test/middleware/express/test-make-middleware.ts index 5444e2fc698..3b65cc91581 100644 --- a/handwritten/logging/test/middleware/express/test-make-middleware.ts +++ b/handwritten/logging/test/middleware/express/test-make-middleware.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as EventEmitter from 'events'; -import * as proxyquire from '../../proxyquire'; +import {proxyquire} from 'google-test-utils'; const FAKE_PROJECT_ID = 'project-🦄'; diff --git a/handwritten/logging/test/proxyquire.ts b/handwritten/logging/test/proxyquire.ts deleted file mode 100644 index 5d344ba47da..00000000000 --- a/handwritten/logging/test/proxyquire.ts +++ /dev/null @@ -1,122 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const isObj = (o: unknown) => - Boolean(o && (typeof o === 'object' || typeof o === 'function')); -const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/logging/test/sink.ts b/handwritten/logging/test/sink.ts index 303e14f8c80..ddffc2b2836 100644 --- a/handwritten/logging/test/sink.ts +++ b/handwritten/logging/test/sink.ts @@ -16,7 +16,7 @@ import * as callbackify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {Sink as SINK} from '../src/sink'; import {Logging, CreateSinkRequest, LogSink} from '../src/index'; diff --git a/handwritten/logging/test/utils/metadata.ts b/handwritten/logging/test/utils/metadata.ts index 0e32e59f285..f2184c06fa2 100644 --- a/handwritten/logging/test/utils/metadata.ts +++ b/handwritten/logging/test/utils/metadata.ts @@ -17,7 +17,7 @@ import {describe, it, beforeEach, before, after, afterEach} from 'mocha'; import BigNumber from 'bignumber.js'; import * as extend from 'extend'; import {GCPEnv} from 'google-auth-library'; -import * as proxyquire from '../proxyquire'; +import {proxyquire} from 'google-test-utils'; let instanceOverride: {} | null; const fakeGcpMetadata = { diff --git a/handwritten/pubsub/cloudbuild.yaml b/handwritten/pubsub/cloudbuild.yaml index 80a726b32a6..b541e3e759b 100644 --- a/handwritten/pubsub/cloudbuild.yaml +++ b/handwritten/pubsub/cloudbuild.yaml @@ -5,7 +5,7 @@ steps: args: - '-c' - | - cd ../.. && corepack enable pnpm && pnpm install --filter "...{./handwritten/pubsub}" + cd ../.. && corepack enable pnpm && pnpm install --filter "./core/test-utils" --filter "...{./handwritten/pubsub}" --ignore-scripts && pnpm --dir core/test-utils run compile && node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()" && pnpm --filter "...{./handwritten/pubsub}" run prepare --if-present dir: 'handwritten/pubsub' id: 'install-dependencies' diff --git a/handwritten/pubsub/package.json b/handwritten/pubsub/package.json index 4f5993ad444..5d41a1d4fcb 100644 --- a/handwritten/pubsub/package.json +++ b/handwritten/pubsub/package.json @@ -88,7 +88,7 @@ "codecov": "^3.8.3", "execa": "~5.1.0", "gapic-tools": "^2.0.0", - "google-test-utils": "^0.2.0", + "google-test-utils": "^0.3.0", "gts": "^6.0.0", "jsdoc": "^4.0.4", "jsdoc-fresh": "^6.0.0", diff --git a/handwritten/pubsub/test/iam.ts b/handwritten/pubsub/test/iam.ts index 6232a51e37a..bc20797c21a 100644 --- a/handwritten/pubsub/test/iam.ts +++ b/handwritten/pubsub/test/iam.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as iamTypes from '../src/iam'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/lease-manager.ts b/handwritten/pubsub/test/lease-manager.ts index a5088a5d181..603e7015b30 100644 --- a/handwritten/pubsub/test/lease-manager.ts +++ b/handwritten/pubsub/test/lease-manager.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import * as defer from 'p-defer'; diff --git a/handwritten/pubsub/test/message-stream.ts b/handwritten/pubsub/test/message-stream.ts index 0d921e038ba..8c1916665e5 100644 --- a/handwritten/pubsub/test/message-stream.ts +++ b/handwritten/pubsub/test/message-stream.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {grpc} from 'google-gax'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {Duplex, PassThrough} from 'stream'; import * as defer from 'p-defer'; diff --git a/handwritten/pubsub/test/proxyquire.ts b/handwritten/pubsub/test/proxyquire.ts deleted file mode 100644 index 5d344ba47da..00000000000 --- a/handwritten/pubsub/test/proxyquire.ts +++ /dev/null @@ -1,122 +0,0 @@ -// 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 Module from 'module'; -import * as path from 'path'; - -delete require.cache[require.resolve(__filename)]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Stubs = Record; -const mod = Module as unknown as { - prototype: {require: NodeRequire}; - _resolveFilename: (request: string, parent: NodeModule) => string; -}; -const {hasOwnProperty} = Object.prototype; -const isObj = (o: unknown) => - Boolean(o && (typeof o === 'object' || typeof o === 'function')); -const hasOwn = (o: unknown, k: string) => isObj(o) && hasOwnProperty.call(o, k); -const isGlobal = (s: unknown) => - hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); -const resolveFrom = (base: string, req: string) => { - try { - return require.resolve(req, {paths: [path.dirname(base)]}); - } catch { - return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; - } -}; - -const parent = module.parent || module; -let skipCallThru = false; -let keepCache = true; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyquire(request: string, stubs: Stubs = {}): T { - const sut = mod._resolveFilename(request, parent); - const resolved: Stubs = {}; - let global = false; - for (const [k, v] of Object.entries(stubs)) { - if (isGlobal(v)) global = true; - resolved[resolveFrom(sut, k)] = v; - } - const cache = require.cache; - const keys = global - ? Object.keys(cache).filter(id => !id.endsWith('.node')) - : [sut]; - const saved = keys.map(id => [id, cache[id]] as const); - for (const id of keys) delete cache[id]; - const prev = mod.prototype.require; - const req = function (this: NodeModule, id: string) { - const isSut = this?.filename === sut; - if (this?.filename && (isSut || global)) { - const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id); - const rId = hit ? '' : resolveFrom(this.filename, id); - if (hit || hasOwn(resolved, rId)) { - const stub = hit ? stubs[id] : resolved[rId]; - if (isSut || isGlobal(stub)) { - if (stub === null) { - const e = new Error(`Cannot find module '${id}'`); - throw Object.assign(e, {code: 'MODULE_NOT_FOUND'}); - } - const skip = hasOwn(stub, '@noCallThru') - ? stub['@noCallThru'] - : skipCallThru; - if (!skip && stub && ['object', 'function'].includes(typeof stub)) { - try { - const real = prev.call(this, id); - if (real && ['object', 'function'].includes(typeof real)) { - for (const k of Object.keys(real)) { - if (!(k in stub)) { - try { - stub[k] = real[k]; - } catch { - // ignore - } - } - } - } - } catch { - // ignore - } - } - return stub; - } - } - } - return prev.call(this, id); - }; - mod.prototype.require = Object.assign(req, prev); - try { - return prev.call(parent, request); - } finally { - mod.prototype.require = prev; - for (const id of global ? Object.keys(cache) : [sut]) { - if (!id.endsWith('.node')) delete cache[id]; - } - for (const [id, m] of saved) { - if (m && (keepCache || id !== sut)) cache[id] = m; - } - } -} - -// eslint-disable-next-line @typescript-eslint/no-namespace -namespace proxyquire { - export const load = proxyquire; - export const noCallThru = () => ((skipCallThru = true), proxyquire); - export const callThru = () => ((skipCallThru = false), proxyquire); - export const noPreserveCache = () => ((keepCache = false), proxyquire); - export const preserveCache = () => ((keepCache = true), proxyquire); -} - -export = proxyquire; diff --git a/handwritten/pubsub/test/publisher/index.ts b/handwritten/pubsub/test/publisher/index.ts index f4fa95908ba..8a6657d1097 100644 --- a/handwritten/pubsub/test/publisher/index.ts +++ b/handwritten/pubsub/test/publisher/index.ts @@ -18,7 +18,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from '../proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import * as opentelemetry from '@opentelemetry/api'; import {Topic} from '../../src'; diff --git a/handwritten/pubsub/test/publisher/message-queues.ts b/handwritten/pubsub/test/publisher/message-queues.ts index 343906dd683..3b0486e6dca 100644 --- a/handwritten/pubsub/test/publisher/message-queues.ts +++ b/handwritten/pubsub/test/publisher/message-queues.ts @@ -18,7 +18,7 @@ import {loggingUtils, ServiceError} from 'google-gax'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from '../proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {RequestConfig, RequestCallback} from '../../src/pubsub'; diff --git a/handwritten/pubsub/test/pubsub.ts b/handwritten/pubsub/test/pubsub.ts index 06f4547a09e..4cfdb484c3d 100644 --- a/handwritten/pubsub/test/pubsub.ts +++ b/handwritten/pubsub/test/pubsub.ts @@ -17,7 +17,7 @@ import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach, after, afterEach} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import defer = require('p-defer'); diff --git a/handwritten/pubsub/test/snapshot.ts b/handwritten/pubsub/test/snapshot.ts index cd781fe09e8..63660a72e77 100644 --- a/handwritten/pubsub/test/snapshot.ts +++ b/handwritten/pubsub/test/snapshot.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {ServiceError} from 'google-gax'; import {describe, it, beforeEach, before, after, afterEach} from 'mocha'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/subscriber.ts b/handwritten/pubsub/test/subscriber.ts index a6051a7aa45..280e4469898 100644 --- a/handwritten/pubsub/test/subscriber.ts +++ b/handwritten/pubsub/test/subscriber.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; import {common as protobuf} from 'protobufjs'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {PassThrough} from 'stream'; import * as opentelemetry from '@opentelemetry/api'; diff --git a/handwritten/pubsub/test/subscription.ts b/handwritten/pubsub/test/subscription.ts index 5577c6b0e79..b86c5a60d73 100644 --- a/handwritten/pubsub/test/subscription.ts +++ b/handwritten/pubsub/test/subscription.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; import {ServiceError} from 'google-gax'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/topic.ts b/handwritten/pubsub/test/topic.ts index 4e2f736e48b..c8e31cd3894 100644 --- a/handwritten/pubsub/test/topic.ts +++ b/handwritten/pubsub/test/topic.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {CallOptions, ServiceError} from 'google-gax'; -import * as proxyquire from './proxyquire'; +import {proxyquire} from 'google-test-utils'; import * as sinon from 'sinon'; import {google} from '../protos/protos'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bab231f6cb..d3e79666766 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2672,6 +2672,9 @@ importers: gapic-tools: specifier: ^2.0.0 version: 2.0.1(protobufjs@8.8.0) + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^5.0.0 version: 5.3.1(typescript@5.9.3) @@ -2769,6 +2772,9 @@ importers: express: specifier: ^4.16.3 version: 4.22.2 + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.2 version: 6.0.2(typescript@5.9.3) @@ -2836,6 +2842,9 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 + google-test-utils: + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.2 version: 6.0.2(typescript@5.9.3) @@ -2979,8 +2988,8 @@ importers: specifier: ^2.0.0 version: 2.0.1(protobufjs@7.5.9) google-test-utils: - specifier: ^0.2.0 - version: 0.2.0 + specifier: ^0.3.0 + version: 0.3.0 gts: specifier: ^6.0.0 version: 6.0.2(typescript@5.9.3) @@ -28732,7 +28741,7 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.109.0)': dependencies: - webpack: 5.109.0(webpack-cli@6.0.1) + webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.109.0) '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.109.0)': @@ -28742,7 +28751,7 @@ snapshots: '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.109.0)': dependencies: - webpack: 5.109.0(webpack-cli@6.0.1) + webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.109.0) '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.109.0)': @@ -28752,7 +28761,7 @@ snapshots: '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.109.0)': dependencies: - webpack: 5.109.0(webpack-cli@6.0.1) + webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.109.0) '@xtuc/ieee754@1.2.0': {} @@ -34390,7 +34399,7 @@ snapshots: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.109.0(webpack-cli@6.0.1) + webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) nunjucks@3.2.4(chokidar@3.6.0): dependencies: @@ -36581,7 +36590,7 @@ snapshots: picomatch: 4.0.5 source-map: 0.7.6 typescript: 5.8.3 - webpack: 5.109.0(webpack-cli@6.0.1) + webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) optionalDependencies: loader-utils: 2.0.4 @@ -37069,7 +37078,7 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.109.0(webpack-cli@6.0.1) + webpack: 5.109.0(uglify-js@3.19.3)(webpack-cli@6.0.1) webpack-merge: 6.0.1 webpack-merge@4.2.2: From c239d88a448b9d9efda9526842a4fff769f68d4b Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Tue, 20 Oct 2026 19:30:02 +0000 Subject: [PATCH 08/10] [Address CI errors] test(test-utils): use native Module.createRequire under Bun + Jest --- core/test-utils/src/proxyquire.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/core/test-utils/src/proxyquire.ts b/core/test-utils/src/proxyquire.ts index 5b9009107e8..bb793bd66be 100644 --- a/core/test-utils/src/proxyquire.ts +++ b/core/test-utils/src/proxyquire.ts @@ -35,17 +35,19 @@ type ModuleConstructor = { _cache?: NodeJS.Dict; _resolveFilename: (request: string, parent: NodeModule) => string; _nodeModulePaths: (from: string) => string[]; + createRequire?: (filename: string) => NodeRequire; }; -const mod = ((Module as unknown as {Module?: ModuleConstructor}).Module ?? +const rawMod = ((Module as unknown as {Module?: ModuleConstructor}).Module ?? Module) as unknown as ModuleConstructor; const {hasOwnProperty} = Object.prototype; -const modProto: {require: NodeRequire} = hasOwnProperty.call( - mod.prototype, - 'require', -) - ? mod.prototype - : Object.getPrototypeOf(mod.prototype); +const isDirectMod = hasOwnProperty.call(rawMod.prototype, 'require'); +const mod: ModuleConstructor = isDirectMod + ? rawMod + : (Object.getPrototypeOf(rawMod) as ModuleConstructor); +const modProto: {require: NodeRequire} = isDirectMod + ? rawMod.prototype + : Object.getPrototypeOf(rawMod.prototype); const isObj = (o: unknown): o is Record => Boolean(o && (typeof o === 'object' || typeof o === 'function')); const hasOwn = (o: unknown, k: string): boolean => @@ -209,7 +211,11 @@ export function createProxyquire(caller?: NodeModule | string): Proxyquire { }; modProto.require = Object.assign(req, prev); try { - return prev.call(parent, request); + return typeof parent.require === 'function' && parent instanceof mod + ? prev.call(parent, request) + : mod.createRequire + ? mod.createRequire(parent.filename)(request) + : prev.call(parent, request); } finally { modProto.require = prev; for (const id of global ? Object.keys(cache) : [sut]) { From 31b82b9086b705d2a235c80762fa1f2b0afb49d2 Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Tue, 27 Oct 2026 19:53:11 +0000 Subject: [PATCH 09/10] [Address CI errors] fix(test-utils): remove precompile clean to avoid parallel turbo build race --- core/test-utils/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/test-utils/package.json b/core/test-utils/package.json index ef91c379c20..dbac4d2d521 100644 --- a/core/test-utils/package.json +++ b/core/test-utils/package.json @@ -19,8 +19,7 @@ "docs": "jsdoc -c .jsdoc.js", "samples-test": "echo 'no samples test'", "system-test": "echo 'no system test'", - "clean": "gts clean", - "precompile": "gts clean" + "clean": "gts clean" }, "keywords": [ "google", From 36f3c048259d5808861106768470c364421bdaba Mon Sep 17 00:00:00 2001 From: Daniel Bruce Date: Tue, 27 Oct 2026 20:23:52 +0000 Subject: [PATCH 10/10] [Address CI errors] test(bigquery): avoid jobs.insert completion race in arrow failure test --- handwritten/bigquery/system-test/arrow.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/handwritten/bigquery/system-test/arrow.ts b/handwritten/bigquery/system-test/arrow.ts index 2781172b106..b06d4dc36dd 100644 --- a/handwritten/bigquery/system-test/arrow.ts +++ b/handwritten/bigquery/system-test/arrow.ts @@ -562,7 +562,8 @@ describe('Arrow Query Results', () => { it('should raise the same error as query() when a jobs.insert query fails during execution', async () => { const failingQuery = { - query: 'SELECT 1 / 0', + query: + 'DECLARE n INT64 DEFAULT 50; SELECT 1 / (x - 50) FROM UNNEST(GENERATE_ARRAY(1, n)) AS x ORDER BY x;', jobTimeoutMs: 60000, useQueryCache: false, };