Skip to content

Commit 51b490a

Browse files
authored
test: restore the validate parity test and close coverage gaps (#8)
Signed-off-by: Bryan English <bryan@bryanenglish.com>
1 parent c248885 commit 51b490a

8 files changed

Lines changed: 371 additions & 18 deletions

File tree

‎lib/validate.js‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,8 @@
33
// Faithful port of the small subset of Node.js core validators used by
44
// this package (lib/internal/validators.js, lib/internal/errors.js),
55
// matching core's error shapes (name, code, message) exactly.
6-
// TODO(bengl): add back the parity test that proved it. Spawn a child
7-
// with --expose-internals that runs one shared battery of calls through
8-
// require('internal/validators') and the ERR_INVALID_ARG_TYPE,
9-
// ERR_INVALID_ARG_VALUE, and ERR_OUT_OF_RANGE constructors from
10-
// internal/errors, print the outcomes (thrown or not, and the thrown
11-
// error's name, code, message) as JSON, and diff them against the same
12-
// battery run through this module. When core changes these message
13-
// formats, such a test is what catches the drift.
6+
// test/test-validate-parity.js proves the parity by diffing this module
7+
// against the real internals under --expose-internals.
148

159
const { inspect } = require('node:util');
1610

@@ -220,4 +214,5 @@ module.exports = {
220214
validateInteger,
221215
invalidArgTypeError,
222216
invalidArgValueError,
217+
outOfRangeError,
223218
};

‎test/common/validate-battery.js‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict';
2+
3+
// The input battery for test-validate-parity.js. Both sides share
4+
// this module, so the compared inputs can never drift apart.
5+
6+
// A JSON-safe, human-readable label for a call.
7+
function label(fn, args) {
8+
const rendered = args.map((a) =>
9+
typeof a === 'string' ? `'${a}'` : String(a),
10+
);
11+
return `${fn}(${rendered.join(', ')})`;
12+
}
13+
14+
// Calls fn with args and describes the outcome: nothing thrown, or the
15+
// thrown error's name, code, and message.
16+
function outcome(fn, args) {
17+
try {
18+
fn(...args);
19+
return { thrown: false };
20+
} catch (err) {
21+
return {
22+
thrown: true,
23+
name: err.name,
24+
code: err.code,
25+
message: err.message,
26+
};
27+
}
28+
}
29+
30+
const kValidatorCases = [
31+
['validateString', [123, 'x']],
32+
['validateString', [undefined, 'x']],
33+
['validateString', [null, 'x']],
34+
['validateString', [true, 'x']],
35+
['validateString', [Symbol('s'), 'x']],
36+
['validateString', [{ a: 1 }, 'x']],
37+
['validateString', [['a'], 'x']],
38+
['validateString', [() => {}, 'x']],
39+
['validateString', ['ok', 'x']],
40+
['validateObject', [null, 'x']],
41+
['validateObject', [[1, 2], 'x']],
42+
['validateObject', ['str', 'x']],
43+
['validateObject', [123, 'x']],
44+
['validateObject', [() => {}, 'x']],
45+
['validateObject', [{}, 'x']],
46+
['validateInteger', [0, 'x', 1]],
47+
['validateInteger', ['abc', 'x', 1]],
48+
['validateInteger', [1.5, 'x', 1]],
49+
['validateInteger', [NaN, 'x', 1]],
50+
['validateInteger', [5, 'x', 1]],
51+
['validateInteger', [9007199254740999, 'x', 1]],
52+
];
53+
54+
// Error-constructor cases: impl.fn(...args) must return the error object.
55+
const kErrorCases = [
56+
[
57+
'invalidArgTypeError',
58+
['options.filter', ['string', 'null', 'undefined'], 42],
59+
],
60+
['invalidArgTypeError', ['options.endpoint', ['string'], 123]],
61+
['invalidArgTypeError', ['options.maxBufferSize', ['integer'], 'abc']],
62+
['invalidArgTypeError', ['options.foo', ['Object'], []]],
63+
[
64+
'invalidArgTypeError',
65+
['first argument', ['string', 'null', 'undefined'], 42],
66+
],
67+
[
68+
'invalidArgValueError',
69+
['options.endpoint', 'not-a-valid-url', 'must be a valid URL'],
70+
],
71+
[
72+
'invalidArgValueError',
73+
['options.serviceName', '', 'must be a non-empty string'],
74+
],
75+
['invalidArgValueError', ['options.maxBufferSize', 0]],
76+
['outOfRangeError', ['x', '>= 1 && <= 100', 0]],
77+
['outOfRangeError', ['n', 'an integer', 10000000000]],
78+
];
79+
80+
// Runs the battery through impl: either this package's lib/validate.js
81+
// exports or wrappers around core's internal error constructors.
82+
function runBattery(impl) {
83+
const results = [];
84+
85+
for (const [name, args] of kValidatorCases) {
86+
results.push({
87+
call: label(name, args),
88+
outcome: outcome(impl[name], args),
89+
});
90+
}
91+
92+
for (const [name, args] of kErrorCases) {
93+
const err = impl[name](...args);
94+
results.push({
95+
call: label(name, args),
96+
outcome: {
97+
thrown: true,
98+
name: err.name,
99+
code: err.code,
100+
message: err.message,
101+
},
102+
});
103+
}
104+
105+
return results;
106+
}
107+
108+
module.exports = {
109+
runBattery,
110+
};

‎test/fixtures/parity-child.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
// Spawned with --expose-internals and the battery module in
4+
// BATTERY_MODULE: runs the battery through core's real internals and
5+
// prints the outcomes as JSON for the parent to compare.
6+
7+
const {
8+
validateString,
9+
validateObject,
10+
validateInteger,
11+
} = require('internal/validators');
12+
const {
13+
codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE },
14+
} = require('internal/errors');
15+
const { runBattery } = require(process.env.BATTERY_MODULE);
16+
17+
const impl = {
18+
validateString,
19+
validateObject,
20+
validateInteger,
21+
invalidArgTypeError: (name, expected, actual) =>
22+
new ERR_INVALID_ARG_TYPE(name, expected, actual),
23+
invalidArgValueError: (name, value, reason) =>
24+
new ERR_INVALID_ARG_VALUE(name, value, reason),
25+
outOfRangeError: (str, range, input) =>
26+
new ERR_OUT_OF_RANGE(str, range, input),
27+
};
28+
29+
process.stdout.write(JSON.stringify(runBattery(impl)));

‎test/test-exporter.js‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,53 @@
33
const assert = require('node:assert');
44
const http = require('node:http');
55
const { describe, it } = require('node:test');
6-
const { fixture, spawnMicrotel } = require('./common/otel');
6+
const { flush } = require('../lib/flush');
7+
const {
8+
fixture,
9+
httpGet,
10+
startHTTPServer,
11+
spawnMicrotel,
12+
} = require('./common/otel');
713

814
// This test verifies exporter error handling and process lifecycle behavior.
915
// Tracing is activated in the child processes through the public init() API;
1016
// the children run fixtures from test/fixtures with JSON init options on
1117
// argv.
1218

1319
describe('otel exporter behavior', () => {
20+
it('warns when the collector responds non-2xx', async () => {
21+
// An in-process collector that rejects every export with a 500. The
22+
// spans are dropped, and a throttled OTelExportWarning reports it.
23+
const collector = http.createServer((req, res) => {
24+
res.writeHead(500);
25+
res.end();
26+
});
27+
await new Promise((resolve) => collector.listen(0, '127.0.0.1', resolve));
28+
29+
const warningPromise = new Promise((resolve) => {
30+
process.on('warning', function onWarning(w) {
31+
if (w.name === 'OTelExportWarning') {
32+
process.removeListener('warning', onWarning);
33+
resolve(w);
34+
}
35+
});
36+
});
37+
38+
const { start } = require('../lib/core');
39+
start({ endpoint: `http://127.0.0.1:${collector.address().port}` });
40+
41+
const target = await startHTTPServer();
42+
await httpGet(target.port, '/x');
43+
flush();
44+
45+
const warning = await warningPromise;
46+
47+
await collector.close();
48+
await target.close();
49+
50+
assert.match(warning.message, /HTTP 500/);
51+
});
52+
1453
it('does not crash when collector is unreachable', async () => {
1554
// The child traces a request against its own server and flushes to a
1655
// dead endpoint. The export fails, but the child exits normally.

‎test/test-flush-coverage.js‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const { after, afterEach, before, describe, it } = require('node:test');
55

66
const { Span, SPAN_KIND_INTERNAL } = require('../lib/span');
77
const { addSpan, flush, resetCaches } = require('../lib/flush');
8-
const { startTracing, delay } = require('./common/otel');
8+
const { startTracing, getSpans, delay } = require('./common/otel');
99

1010
describe('flush.js coverage', () => {
1111
let collector;
@@ -22,12 +22,51 @@ describe('flush.js coverage', () => {
2222
});
2323

2424
it('flush is a no-op when buffer is empty', async () => {
25+
const before = collector.requests.length;
26+
2527
flush();
2628

2729
// Wait for any potential export request.
2830
await delay(100);
2931

30-
assert.strictEqual(collector.requests.length, 0);
32+
assert.strictEqual(collector.requests.length, before);
33+
});
34+
35+
it('encodes boolean, double, and fallback attribute values', async () => {
36+
// A span-like object covering the OTLP encodings the instrumentations
37+
// never produce: booleans, non-integer numbers, and values of other
38+
// types, which fall back to strings.
39+
const span = {
40+
name: 'mixed-values',
41+
traceId: 'a'.repeat(32),
42+
spanId: 'b'.repeat(16),
43+
kind: 1,
44+
startTime: 100,
45+
endTime: 200,
46+
getAttributes() {
47+
return {
48+
bool: true,
49+
ratio: 1.5,
50+
misc: null,
51+
};
52+
},
53+
getEvents() {
54+
return [];
55+
},
56+
status: { code: 0, message: '' },
57+
};
58+
59+
addSpan(span);
60+
flush();
61+
62+
const payload = await collector.next();
63+
const attrs = {};
64+
for (const a of getSpans(payload)[0].attributes) {
65+
attrs[a.key] = a.value;
66+
}
67+
assert.deepStrictEqual(attrs.bool, { boolValue: true });
68+
assert.deepStrictEqual(attrs.ratio, { doubleValue: 1.5 });
69+
assert.deepStrictEqual(attrs.misc, { stringValue: 'null' });
3170
});
3271

3372
it('flush handles spanToOtlp errors gracefully', async () => {
@@ -95,6 +134,8 @@ describe('flush.js coverage', () => {
95134
});
96135

97136
it('resetCaches clears the span buffer', async () => {
137+
const before = collector.requests.length;
138+
98139
// A span so the buffer is non-empty.
99140
const span = new Span('test', SPAN_KIND_INTERNAL);
100141
span.end();
@@ -106,6 +147,6 @@ describe('flush.js coverage', () => {
106147
// Wait for any potential export request.
107148
await delay(100);
108149

109-
assert.strictEqual(collector.requests.length, 0);
150+
assert.strictEqual(collector.requests.length, before);
110151
});
111152
});

‎test/test-http-client.js‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,49 @@ describe('otel HTTP client spans', () => {
218218
);
219219
});
220220

221+
it('parses IPv6 authorities and default ports from the Host header', async () => {
222+
const target = await startHTTPServer((req, res) => {
223+
res.writeHead(200);
224+
res.end('ok');
225+
});
226+
227+
const seen = [];
228+
const request = (headers, path) =>
229+
new Promise((resolve, reject) => {
230+
const req = http.get(
231+
{ host: '127.0.0.1', port: target.port, path, headers },
232+
(res) => {
233+
res.resume();
234+
res.on('end', resolve);
235+
},
236+
);
237+
req.on('error', reject);
238+
});
239+
240+
// A bracketed IPv6 authority: the port follows the closing bracket.
241+
await request({ Host: `[::1]:9999` }, '/ipv6');
242+
// A portless Host header means the scheme default.
243+
await request({ Host: 'example.test' }, '/default-port');
244+
245+
const spans = await flushAndCollect(collector);
246+
await target.close();
247+
248+
const v6 = getClientSpan(spans, '/ipv6');
249+
assert.ok(v6, 'Expected a client span for the IPv6 request');
250+
assert.partialDeepStrictEqual(spanAttrs(v6), {
251+
'server.port': '9999',
252+
'url.full': 'http://[::1]:9999/ipv6',
253+
});
254+
255+
const def = getClientSpan(spans, '/default-port');
256+
assert.ok(def, 'Expected a client span for the portless Host request');
257+
assert.strictEqual(
258+
spanAttrs(def)['server.port'],
259+
'80',
260+
'A portless Host header must yield the scheme default',
261+
);
262+
});
263+
221264
it('creates error span for HTTP client connection refused', async () => {
222265
await httpGet(1, '/fail').catch(() => {});
223266

0 commit comments

Comments
 (0)