Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(test-utils): runTestFixture utility for running out-of-process tests #1735

Merged
merged 18 commits into from
Nov 15, 2023

Conversation

trentm
Copy link
Contributor

@trentm trentm commented Oct 13, 2023

Running scripts out-of-process for testing allows running tests with different node options and with isolation. The former is useful for ESM testing. This change includes adding an ESM test for ioredis. This depends on #1694 to pass.

Closes: #1731

Which problem is this PR solving?

Short description of the changes

(Note: This is one option for #1731. An alternative is being discussed over there.)

  • This adds a runTestFixture(...) test utility function that takes a script to run out-of-process. It is expected that the script uses OTel tracing using NodeSDK from @opentelemetry/sdk-node. The utility first starts a TestCollector HTTP server that can collect OTLP (using the HTTP/JSON flavour). Then it execs the script with OTEL_ env vars set so that NodeSDK will export to that collector. When the script finishes, a couple given callbacks, checkResult and checkCollector, are called so that the mocha test code can assert expectations about the process result and the collected spans. The TestCollector provides a collector.sortedSpans that returns the exported spans sorted by start-time.
  • As a first user of this utility, I've added a "fixtures/use-ioredis.mjs" that uses ioredis from ESM code. It does a simple client.set(...) and client.get(...). An added case in "ioredis.test.ts" runs that and asserts that the script exited successfully and exported the expected spans.
  • The "fixtures/use-ioredis.mjs" script can be run independent of the test suite, when playing with developing these. By default it will export spans to the console and connect to a redis on the default port. For example:
% docker run --rm -p 6379:6379 -d redis
74cfbbf1d636aaaaacc2669d0d4b5fa10d0467feea530c8d3c98813e308b0c62

% node --experimental-loader=@opentelemetry/instrumentation/hook.mjs test/fixtures/use-ioredis.mjs
(node:29410) ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
{
  traceId: '4df8b32419a6252d4995a23bfe70b15c',
  parentId: '1d319fd2c2dea2dd',
  traceState: undefined,
  name: 'set',
  id: '9f207451efc4391d',
  kind: 2,
...

@pichlermarc and others interested, I would love some feedback on this.

Checklist

…ests

Running scripts out-of-process for testing allows running tests with
different node options and with isolation. The former is useful for
ESM testing. This change includes adding an ESM test for ioredis.
This depends on open-telemetry#1694 to pass.

Closes: open-telemetry#1731
@trentm
Copy link
Contributor Author

trentm commented Oct 13, 2023

The first commit does not include the fix from #1694 for ioredis ESM support, so the test fails:

% npm run test:local

> @opentelemetry/instrumentation-ioredis@0.35.2 test:local
> cross-env RUN_REDIS_TESTS_LOCAL=true npm run test


> @opentelemetry/instrumentation-ioredis@0.35.2 test
> nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'



  ioredis
    ✓ should have correct module name
    ✓ should work with ESM usage (450ms)
    1) should work with ESM usage


  2 passing (1s)
  1 failing

  1) ioredis
       should work with ESM usage:
     Uncaught ifError got unwanted exception: Command failed: /Users/trentm/.nvm/versions/node/v16.20.2/bin/node fixtures/use-ioredis.mjs redis://localhost:63790
/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/build/src/instrumentation.js:131
                if ((0, instrumentation_1.isWrapped)(moduleExports.prototype.sendCommand)) {
                                                                             ^

TypeError: Cannot read properties of undefined (reading 'sendCommand')
    at InstrumentationNodeModuleDefinition.patch (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/build/src/instrumentation.js:131:78)
    at IORedisInstrumentation._onRequire (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/@opentelemetry/instrumentation/build/src/platform/node/instrumentation.js:153:39)
    at hookFn (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/@opentelemetry/instrumentation/build/src/platform/node/instrumentation.js:194:29)
    at callHookFn (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/import-in-the-middle/index.js:28:22)
    at Hook._iitmHook (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/import-in-the-middle/index.js:76:11)
    at /Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/import-in-the-middle/index.js:17:41
    at Array.forEach (<anonymous>)
    at addHook (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/import-in-the-middle/index.js:17:10)
    at new Hook (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/import-in-the-middle/index.js:84:3)
    at IORedisInstrumentation.enable (/Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/@opentelemetry/instrumentation/build/src/platform/node/instrumentation.js:206:29)

  AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Command failed: /Users/trentm/.nvm/versions/node/v16.20.2/bin/node fixtures/use-ioredis.mjs redis://localhost:63790
  /Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/build/src/instrumentation.js:131
                  if ((0, instrumentation_1.isWrapped)(moduleExports.prototype.sendCommand)) {
                                                                               ^

  TypeError: Cannot read properties of undefined (reading 'sendCommand')
      at InstrumentationNodeModuleDefinition.patch (build/src/instrumentation.js:131:78)
      at IORedisInstrumentation._onRequire (node_modules/@opentelemetry/instrumentation/build/src/platform/node/instrumentation.js:153:39)
      at hookFn (node_modules/@opentelemetry/instrumentation/build/src/platform/node/instrumentation.js:194:29)
      at callHookFn (node_modules/import-in-the-middle/index.js:28:22)
      at Hook._iitmHook (node_modules/import-in-the-middle/index.js:76:11)
      at /Users/trentm/tm/opentelemetry-js-contrib3/plugins/node/opentelemetry-instrumentation-ioredis/node_modules/import-in-the-middle/index.js:17:41
      at Array.forEach (<anonymous>)
      at addHook (node_modules/import-in-the-middle/index.js:17:10)
      at new Hook (node_modules/import-in-the-middle/index.js:84:3)
      at IORedisInstrumentation.enable (node_modules/@opentelemetry/instrumentation/build/src/platform/node/instrumentation.js:206:29)

      at ChildProcess.exithandler (node:child_process:402:12)
      at ChildProcess.emit (node:events:513:28)
      at maybeClose (node:internal/child_process:1100:16)
      at Process.ChildProcess._handle.onexit (node:internal/child_process:304:5)
      at Process.callbackTrampoline (node:internal/async_hooks:130:17)



--------------------|---------|----------|---------|---------|-----------------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-----------------------------
All files           |   34.88 |     5.26 |   47.36 |   34.52 |
 instrumentation.ts |   34.17 |     5.45 |      50 |   34.61 | 55,63,73-76,100-178,187-213
 utils.ts           |   42.85 |        0 |       0 |   33.33 | 23-30
--------------------|---------|----------|---------|---------|-----------------------------

@trentm
Copy link
Contributor Author

trentm commented Oct 13, 2023

The second commit adds the fix from #1694 to show that the test now passes:

% npm run test:local
...

  ioredis
    ✓ should have correct module name
    ✓ should work with ESM usage (541ms)
...

@codecov
Copy link

codecov bot commented Oct 13, 2023

Codecov Report

Merging #1735 (972c734) into main (99db4bb) will not change coverage.
The diff coverage is n/a.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1735   +/-   ##
=======================================
  Coverage   91.42%   91.42%           
=======================================
  Files         143      143           
  Lines        7303     7303           
  Branches     1461     1461           
=======================================
  Hits         6677     6677           
  Misses        626      626           

@pichlermarc
Copy link
Member

Thanks for working on this; having your help on the topic is very appreciated. 🚀

I don't know if there is a good way with lerna to develop in "packages/opentelemetry-test-utils" and have that used in, for example, "plugins/node/opentelemetry-instrumentation-ioredis". I could npm install ../../../packages/opentelemetry-test-utils for local dev, but I'm not sure what can be done before merging.

It should be possible to add "@opentelemetry/contrib-test-utils": "^0.34.2", to devDependencies, lerna should link the version from the repo when running npm install from the root of the repository. When you re-install from the package directory, you can run npm run --prefix ../../../ lerna:link to restore the links. 🙂 Moving everything to the intended packages in this PR should then be possible.

  • This adds a runTestFixture(...) test utility function that takes a script to run out-of-process. It is expected that the script uses OTel tracing using NodeSDK from @opentelemetry/sdk-node.

I wonder if we should just do a simple manual tracing setup without @opentelemetry/sdk-node. As there'd be less moving parts, I'd expect such a setup to be more stable. 🤔

The utility first starts a TestCollector HTTP server that can collect OTLP (using the HTTP/JSON flavour). Then it execs the script with OTEL_ env vars set so that NodeSDK will export to that collector. When the script finishes, a couple given callbacks, checkResult and checkCollector, are called so that the mocha test code can assert expectations about the process result and the collected spans. The TestCollector provides a collector.sortedSpans that returns the exported spans sorted by start-time.

This is amazing; I can see this possibly being very useful for integration testing. I'm wondering though if we may be able to get away with something a bit simpler. Over at #1731 I added a comment that I tried running the existing tests in ESM tests, and was almost successful (% some trouble with iitm+mocha, which may be resolvable). This would basically take care of the telemetry testing with the same coverage as CJS, while out-of-process testing could catch some regressions that would be considered priority:p1 (anything that crashes the app). While I would prefer to do what I mentioned above, I'm unsure if it will pan out (or if there's concerns with going that way). Therefore let's keep the approach of this PR then iterate depending on the outcome of the comment I added to #1731.

@trentm
Copy link
Contributor Author

trentm commented Oct 17, 2023

you can run npm run --prefix ../../../ lerna:link to restore the links

That was the part I was missing, thanks.

I wonder if we should just do a simple manual tracing setup without @opentelemetry/sdk-node. As there'd be less moving parts, I'd expect such a setup to be more stable. 🤔

I don't have strong opinions here yet. Some reasons basing on NodeSDK was nice:

  • IIUC, using NodeSDK is the usage that users of OTel JS will be encouraged to take, so the fixture scripts will be using OTel setup code that is likely more typical of real usage. (One example I noticed is that some test modules that need a ContextManager tend to use the AsyncHooksContextManager -- presumably because it is the lowest common denominator -- rather than using the AsyncLocalStorageContextManager for recent-enough Node.js versions. However, I don't have reason to think that'd cause any test issues.)
  • The NodeSDK support for the OTEL_* envvars for configuring, for example for the OTLP endpoint, was very convenient. This same convenience would apply for testing metrics and logs output sent via OTLP.

I definitely agree that fewer moving parts should be more helpful. And if your other idea pans out, then testing ESM in-process means that using an InMemoryExporter for collecting test data is a lot lighter (faster, smaller deps) than having OTLP involved.

Therefore let's keep the approach of this PR then iterate depending on the outcome of the comment I added to #1731.

Sounds good.

@trentm
Copy link
Contributor Author

trentm commented Oct 17, 2023

@pichlermarc I've moved the test utility code over the test-utils package now, so it should be clearer what the actual usage would look like. I'm still totally fine waiting on the outcome of your other option on #1731.

@trentm trentm marked this pull request as ready for review October 17, 2023 20:21
@trentm trentm requested a review from a team October 17, 2023 20:21
@@ -51,13 +51,13 @@ const CONFIG = {
port: parseInt(process.env.OPENTELEMETRY_REDIS_PORT || '63790', 10),
};

const URL = `redis://${CONFIG.host}:${CONFIG.port}`;
const REDIS_URL = `redis://${CONFIG.host}:${CONFIG.port}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer note: I could revert this variable name change. I had changed it originally because at one point I had been using the URL object.

Copy link
Member

@pichlermarc pichlermarc Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay to keep the change imo. 🙂

trentm added a commit to trentm/opentelemetry-js-contrib that referenced this pull request Oct 25, 2023
This is an attempt to test ESM usage of ioredis based on Marc's comment at
open-telemetry#1731 (comment)

This *passes*, but the thing that scares me is enabling the ESM hook for
.test.ts tests as well. The IORedisInstrumentation patch method is being
called *both* for ESM and CommonJS for ioredis.test.ts -- I don't
understand that.

Refs: open-telemetry#1731
Refs: open-telemetry#1735
@trentm
Copy link
Contributor Author

trentm commented Oct 25, 2023

We discussed today in the SIG going ahead with this approach. Note that I opened a draft attempt of the other approach at #1752 with some questions/concerns.

I wonder if we should just do a simple manual tracing setup without @opentelemetry/sdk-node. As there'd be less moving parts, I'd expect such a setup to be more stable. 🤔

I don't have strong opinions here yet. Some reasons basing on NodeSDK was nice: [...]

@pichlermarc Let me know if you'd prefer me to update this PR to have it avoid using NodeSDK.

@trentm
Copy link
Contributor Author

trentm commented Nov 7, 2023

The build is failing because it needs an update to all otel core deps to 0.45.0 (equivalent to #1725 for the new release). I'm not sure which "schedule" entry in "renovate.json" is relevant.

Copy link
Member

@pichlermarc pichlermarc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for taking so long to review. I think keeping NodeSDK is fine, I'll be closer to what people are using as well.

Thank you for working on this. 🚀

Comment on lines +70 to +71
* Limitations: This only supports traces so far, no metrics or logs.
* There is little error checking here; we are assuming valid OTLP.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say this is likely fine. We can expand this later if necessary.
Little error checking also sounds okay to me, since it's for testing.

packages/opentelemetry-test-utils/src/test-fixtures.ts Outdated Show resolved Hide resolved
@@ -51,13 +51,13 @@ const CONFIG = {
port: parseInt(process.env.OPENTELEMETRY_REDIS_PORT || '63790', 10),
};

const URL = `redis://${CONFIG.host}:${CONFIG.port}`;
const REDIS_URL = `redis://${CONFIG.host}:${CONFIG.port}`;
Copy link
Member

@pichlermarc pichlermarc Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay to keep the change imo. 🙂

@pichlermarc pichlermarc merged commit 4c8b374 into open-telemetry:main Nov 15, 2023
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Introduce ESM testing strategy
7 participants