process: initial SourceMap support via NODE_V8_COVERAGE

PR-URL: https://github.com/nodejs/node/pull/28960
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: David Carlier <devnexen@gmail.com>
This commit is contained in:
Benjamin Coe 2019-09-20 11:43:02 -07:00 committed by Anna Henningsen
parent e74f30894c
commit 8f06773a8c
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
20 changed files with 497 additions and 13 deletions

View File

@ -1110,9 +1110,19 @@ variable is strongly discouraged.
### `NODE_V8_COVERAGE=dir`
When set, Node.js will begin outputting [V8 JavaScript code coverage][] to the
directory provided as an argument. Coverage is output as an array of
[ScriptCoverage][] objects:
When set, Node.js will begin outputting [V8 JavaScript code coverage][] and
[Source Map][] data to the directory provided as an argument (coverage
information is written as JSON to files with a `coverage` prefix).
`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it
easier to instrument applications that call the `child_process.spawn()` family
of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent
propagation.
#### Coverage Output
Coverage is output as an array of [ScriptCoverage][] objects on the top-level
key `result`:
```json
{
@ -1126,13 +1136,46 @@ directory provided as an argument. Coverage is output as an array of
}
```
`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it
easier to instrument applications that call the `child_process.spawn()` family
of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent
propagation.
#### Source Map Cache
At this time coverage is only collected in the main thread and will not be
output for code executed by worker threads.
> Stability: 1 - Experimental
If found, Source Map data is appended to the top-level key `source-map-cache`
on the JSON coverage object.
`source-map-cache` is an object with keys representing the files source maps
were extracted from, and the values include the raw source-map URL
(in the key `url`) and the parsed Source Map V3 information (in the key `data`).
```json
{
"result": [
{
"scriptId": "68",
"url": "file:///absolute/path/to/source.js",
"functions": []
}
],
"source-map-cache": {
"file:///absolute/path/to/source.js": {
"url": "./path-to-map.json",
"data": {
"version": 3,
"sources": [
"file:///absolute/path/to/original.js"
],
"names": [
"Foo",
"console",
"info"
],
"mappings": "MAAMA,IACJC,YAAaC",
"sourceRoot": "./"
}
}
}
}
```
### `OPENSSL_CONF=file`
<!-- YAML
@ -1203,6 +1246,7 @@ greater than `4` (its current default value). For more information, see the
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
[REPL]: repl.html
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
[Source Map]: https://sourcemaps.info/spec.html
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
[customizing esm specifier resolution]: esm.html#esm_customizing_esm_specifier_resolution_algorithm

View File

@ -119,7 +119,9 @@ function setupCoverageHooks(dir) {
const cwd = require('internal/process/execution').tryGetCwd();
const { resolve } = require('path');
const coverageDirectory = resolve(cwd, dir);
const { sourceMapCacheToObject } = require('internal/source_map');
internalBinding('profiler').setCoverageDirectory(coverageDirectory);
internalBinding('profiler').setSourceMapCacheGetter(sourceMapCacheToObject);
return coverageDirectory;
}

View File

@ -31,6 +31,7 @@ const {
} = primordials;
const { NativeModule } = require('internal/bootstrap/loaders');
const { maybeCacheSourceMap } = require('internal/source_map');
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
const { deprecate } = require('internal/util');
const vm = require('vm');
@ -845,7 +846,9 @@ Module.prototype.require = function(id) {
var resolvedArgv;
let hasPausedEntry = false;
function wrapSafe(filename, content) {
function wrapSafe(filename, content, cjsModuleInstance) {
maybeCacheSourceMap(filename, content, cjsModuleInstance);
if (patched) {
const wrapper = Module.wrap(content);
return vm.runInThisContext(wrapper, {
@ -910,7 +913,7 @@ Module.prototype._compile = function(content, filename) {
manifest.assertIntegrity(moduleURL, content);
}
const compiledWrapper = wrapSafe(filename, content);
const compiledWrapper = wrapSafe(filename, content, this);
var inspectorWrapper = null;
if (getOptionValue('--inspect-brk') && process._eval == null) {

View File

@ -31,6 +31,7 @@ const {
} = require('internal/errors').codes;
const readFileAsync = promisify(fs.readFile);
const JsonParse = JSON.parse;
const { maybeCacheSourceMap } = require('internal/source_map');
const debug = debuglog('esm');
@ -74,6 +75,7 @@ async function importModuleDynamically(specifier, { url }) {
// Strategy for loading a standard JavaScript module
translators.set('module', async function moduleStrategy(url) {
const source = `${await getSource(url)}`;
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const module = new ModuleWrap(source, url);

152
lib/internal/source_map.js Normal file
View File

@ -0,0 +1,152 @@
'use strict';
// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
const { Buffer } = require('buffer');
const debug = require('internal/util/debuglog').debuglog('source_map');
const { dirname, resolve } = require('path');
const fs = require('fs');
const {
normalizeReferrerURL,
} = require('internal/modules/cjs/helpers');
const { JSON, Object } = primordials;
// For cjs, since Module._cache is exposed to users, we use a WeakMap
// keyed on module, facilitating garbage collection.
const cjsSourceMapCache = new WeakMap();
// The esm cache is not exposed to users, so we can use a Map keyed
// on filenames.
const esmSourceMapCache = new Map();
const { fileURLToPath, URL } = require('url');
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
if (!process.env.NODE_V8_COVERAGE) return;
let basePath;
try {
filename = normalizeReferrerURL(filename);
basePath = dirname(fileURLToPath(filename));
} catch (err) {
// This is most likely an [eval]-wrapper, which is currently not
// supported.
debug(err.stack);
return;
}
const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
if (match) {
if (cjsModuleInstance) {
cjsSourceMapCache.set(cjsModuleInstance, {
url: match.groups.sourceMappingURL,
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
});
} else {
// If there is no cjsModuleInstance assume we are in a
// "modules/esm" context.
esmSourceMapCache.set(filename, {
url: match.groups.sourceMappingURL,
data: dataFromUrl(basePath, match.groups.sourceMappingURL)
});
}
}
}
function dataFromUrl(basePath, sourceMappingURL) {
try {
const url = new URL(sourceMappingURL);
switch (url.protocol) {
case 'data:':
return sourceMapFromDataUrl(basePath, url.pathname);
default:
debug(`unknown protocol ${url.protocol}`);
return null;
}
} catch (err) {
debug(err.stack);
// If no scheme is present, we assume we are dealing with a file path.
const sourceMapFile = resolve(basePath, sourceMappingURL);
return sourceMapFromFile(sourceMapFile);
}
}
function sourceMapFromFile(sourceMapFile) {
try {
const content = fs.readFileSync(sourceMapFile, 'utf8');
const data = JSON.parse(content);
return sourcesToAbsolute(dirname(sourceMapFile), data);
} catch (err) {
debug(err.stack);
return null;
}
}
// data:[<mediatype>][;base64],<data> see:
// https://tools.ietf.org/html/rfc2397#section-2
function sourceMapFromDataUrl(basePath, url) {
const [format, data] = url.split(',');
const splitFormat = format.split(';');
const contentType = splitFormat[0];
const base64 = splitFormat[splitFormat.length - 1] === 'base64';
if (contentType === 'application/json') {
const decodedData = base64 ?
Buffer.from(data, 'base64').toString('utf8') : data;
try {
const parsedData = JSON.parse(decodedData);
return sourcesToAbsolute(basePath, parsedData);
} catch (err) {
debug(err.stack);
return null;
}
} else {
debug(`unknown content-type ${contentType}`);
return null;
}
}
// If the sources are not absolute URLs after prepending of the "sourceRoot",
// the sources are resolved relative to the SourceMap (like resolving script
// src in a html document).
function sourcesToAbsolute(base, data) {
data.sources = data.sources.map((source) => {
source = (data.sourceRoot || '') + source;
if (!/^[\\/]/.test(source[0])) {
source = resolve(base, source);
}
if (!source.startsWith('file://')) source = `file://${source}`;
return source;
});
// The sources array is now resolved to absolute URLs, sourceRoot should
// be updated to noop.
data.sourceRoot = '';
return data;
}
function sourceMapCacheToObject() {
const obj = Object.create(null);
for (const [k, v] of esmSourceMapCache) {
obj[k] = v;
}
appendCJSCache(obj);
if (Object.keys(obj).length === 0) {
return undefined;
} else {
return obj;
}
}
// Since WeakMap can't be iterated over, we use Module._cache's
// keys to facilitate Source Map serialization.
function appendCJSCache(obj) {
const { Module } = require('internal/modules/cjs/loader');
Object.keys(Module._cache).forEach((key) => {
const value = cjsSourceMapCache.get(Module._cache[key]);
if (value) {
obj[`file://${key}`] = value;
}
});
}
module.exports = {
sourceMapCacheToObject,
maybeCacheSourceMap
};

View File

@ -175,6 +175,7 @@
'lib/internal/repl/history.js',
'lib/internal/repl/utils.js',
'lib/internal/socket_list.js',
'lib/internal/source_map.js',
'lib/internal/test/binding.js',
'lib/internal/timers.js',
'lib/internal/tls.js',

View File

@ -444,6 +444,7 @@ constexpr size_t kFsStatsBufferLength =
V(primordials, v8::Object) \
V(promise_reject_callback, v8::Function) \
V(script_data_constructor_function, v8::Function) \
V(source_map_cache_getter, v8::Function) \
V(tick_callback_function, v8::Function) \
V(timers_callback_function, v8::Function) \
V(tls_wrap_constructor_function, v8::Function) \

View File

@ -180,6 +180,58 @@ void V8ProfilerConnection::WriteProfile(Local<String> message) {
if (!GetProfile(result).ToLocal(&profile)) {
return;
}
Local<String> result_s;
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
fprintf(stderr, "Failed to stringify %s profile result\n", type());
return;
}
// Create the directory if necessary.
std::string directory = GetDirectory();
DCHECK(!directory.empty());
if (!EnsureDirectory(directory, type())) {
return;
}
std::string filename = GetFilename();
DCHECK(!filename.empty());
std::string path = directory + kPathSeparator + filename;
WriteResult(env_, path.c_str(), result_s);
}
void V8CoverageConnection::WriteProfile(Local<String> message) {
Isolate* isolate = env_->isolate();
Local<Context> context = env_->context();
HandleScope handle_scope(isolate);
Context::Scope context_scope(context);
// Get message.result from the response.
Local<Object> result;
if (!ParseProfile(env_, message, type()).ToLocal(&result)) {
return;
}
// Generate the profile output from the subclass.
Local<Object> profile;
if (!GetProfile(result).ToLocal(&profile)) {
return;
}
// append source-map cache information to coverage object:
Local<Function> source_map_cache_getter = env_->source_map_cache_getter();
Local<Value> source_map_cache_v;
if (!source_map_cache_getter->Call(env()->context(),
Undefined(isolate), 0, nullptr)
.ToLocal(&source_map_cache_v)) {
return;
}
// Avoid writing to disk if no source-map data:
if (!source_map_cache_v->IsUndefined()) {
profile->Set(context, FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"),
source_map_cache_v);
}
Local<String> result_s;
if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) {
fprintf(stderr, "Failed to stringify %s profile result\n", type());
@ -385,12 +437,20 @@ static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
env->set_coverage_directory(*directory);
}
static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsFunction());
Environment* env = Environment::GetCurrent(args);
env->set_source_map_cache_getter(args[0].As<Function>());
}
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory);
env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter);
}
} // namespace profiler

View File

@ -59,13 +59,15 @@ class V8ProfilerConnection {
// which will be then written as a JSON.
virtual v8::MaybeLocal<v8::Object> GetProfile(
v8::Local<v8::Object> result) = 0;
virtual void WriteProfile(v8::Local<v8::String> message);
private:
size_t next_id() { return id_++; }
void WriteProfile(v8::Local<v8::String> message);
std::unique_ptr<inspector::InspectorSession> session_;
Environment* env_ = nullptr;
size_t id_ = 1;
protected:
Environment* env_ = nullptr;
};
class V8CoverageConnection : public V8ProfilerConnection {
@ -81,6 +83,8 @@ class V8CoverageConnection : public V8ProfilerConnection {
std::string GetDirectory() const override;
std::string GetFilename() const override;
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
void WriteProfile(v8::Local<v8::String> message) override;
void WriteSourceMapCache();
private:
std::unique_ptr<inspector::InspectorSession> session_;

7
test/fixtures/source-map/basic.js vendored Normal file
View File

@ -0,0 +1,7 @@
const a = 99;
if (true) {
const b = 101;
} else {
const c = 102;
}
//# sourceMappingURL=https://http.cat/418

View File

@ -0,0 +1,2 @@
class Foo{constructor(x=33){this.x=x?x:99;if(this.x){console.info("covered")}else{console.info("uncovered")}this.methodC()}methodA(){console.info("covered")}methodB(){console.info("uncovered")}methodC(){console.info("covered")}methodD(){console.info("uncovered")}}const a=new Foo(0);const b=new Foo(33);a.methodA();
//# sourceMappingURL=./disk.map

27
test/fixtures/source-map/disk.js vendored Normal file
View File

@ -0,0 +1,27 @@
class Foo {
constructor (x=33) {
this.x = x ? x : 99
if (this.x) {
console.info('covered')
} else {
console.info('uncovered')
}
this.methodC()
}
methodA () {
console.info('covered')
}
methodB () {
console.info('uncovered')
}
methodC () {
console.info('covered')
}
methodD () {
console.info('uncovered')
}
}
const a = new Foo(0)
const b = new Foo(33)
a.methodA()

20
test/fixtures/source-map/disk.map vendored Normal file
View File

@ -0,0 +1,20 @@
{
"version": 3,
"sources": [
"disk.js"
],
"names": [
"Foo",
"[object Object]",
"x",
"this",
"console",
"info",
"methodC",
"a",
"b",
"methodA"
],
"mappings": "MAAMA,IACJC,YAAaC,EAAE,IACbC,KAAKD,EAAIA,EAAIA,EAAI,GACjB,GAAIC,KAAKD,EAAG,CACVE,QAAQC,KAAK,eACR,CACLD,QAAQC,KAAK,aAEfF,KAAKG,UAEPL,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,aAEfJ,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,cAIjB,MAAME,EAAI,IAAIP,IAAI,GAClB,MAAMQ,EAAI,IAAIR,IAAI,IAClBO,EAAEE",
"sourceRoot": "./"
}

View File

@ -0,0 +1,4 @@
import {foo} from './esm-dep.mjs';
import {strictEqual} from 'assert';
strictEqual(foo(), 'foo');
//# sourceMappingURL=https://http.cat/405

4
test/fixtures/source-map/esm-dep.mjs vendored Normal file
View File

@ -0,0 +1,4 @@
export function foo () {
return 'foo';
};
//# sourceMappingURL=https://http.cat/422

8
test/fixtures/source-map/exit-1.js vendored Normal file
View File

@ -0,0 +1,8 @@
const a = 99;
if (true) {
const b = 101;
} else {
const c = 102;
}
process.exit(1);
//# sourceMappingURL=https://http.cat/404

View File

@ -0,0 +1,2 @@
var cov_263bu3eqm8=function(){var path= "./branches.js";var hash="424788076537d051b5bf0e2564aef393124eabc7";var global=new Function("return this")();var gcv="__coverage__";var coverageData={path: "./branches.js",statementMap:{"0":{start:{line:1,column:0},end:{line:7,column:1}},"1":{start:{line:2,column:2},end:{line:2,column:29}},"2":{start:{line:3,column:7},end:{line:7,column:1}},"3":{start:{line:4,column:2},end:{line:4,column:27}},"4":{start:{line:6,column:2},end:{line:6,column:29}},"5":{start:{line:10,column:2},end:{line:16,column:3}},"6":{start:{line:11,column:4},end:{line:11,column:28}},"7":{start:{line:12,column:9},end:{line:16,column:3}},"8":{start:{line:13,column:4},end:{line:13,column:31}},"9":{start:{line:15,column:4},end:{line:15,column:29}},"10":{start:{line:19,column:0},end:{line:19,column:12}},"11":{start:{line:20,column:0},end:{line:20,column:13}}},fnMap:{"0":{name:"branch",decl:{start:{line:9,column:9},end:{line:9,column:15}},loc:{start:{line:9,column:20},end:{line:17,column:1}},line:9}},branchMap:{"0":{loc:{start:{line:1,column:0},end:{line:7,column:1}},type:"if",locations:[{start:{line:1,column:0},end:{line:7,column:1}},{start:{line:1,column:0},end:{line:7,column:1}}],line:1},"1":{loc:{start:{line:3,column:7},end:{line:7,column:1}},type:"if",locations:[{start:{line:3,column:7},end:{line:7,column:1}},{start:{line:3,column:7},end:{line:7,column:1}}],line:3},"2":{loc:{start:{line:10,column:2},end:{line:16,column:3}},type:"if",locations:[{start:{line:10,column:2},end:{line:16,column:3}},{start:{line:10,column:2},end:{line:16,column:3}}],line:10},"3":{loc:{start:{line:12,column:9},end:{line:16,column:3}},type:"if",locations:[{start:{line:12,column:9},end:{line:16,column:3}},{start:{line:12,column:9},end:{line:16,column:3}}],line:12}},s:{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0},f:{"0":0},b:{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]},_coverageSchema:"43e27e138ebf9cfc5966b082cf9a028302ed4184",hash:"424788076537d051b5bf0e2564aef393124eabc7"};var coverage=global[gcv]||(global[gcv]={});if(coverage[path]&&coverage[path].hash===hash){return coverage[path];}return coverage[path]=coverageData;}();cov_263bu3eqm8.s[0]++;if(false){cov_263bu3eqm8.b[0][0]++;cov_263bu3eqm8.s[1]++;console.info('unreachable');}else{cov_263bu3eqm8.b[0][1]++;cov_263bu3eqm8.s[2]++;if(true){cov_263bu3eqm8.b[1][0]++;cov_263bu3eqm8.s[3]++;console.info('reachable');}else{cov_263bu3eqm8.b[1][1]++;cov_263bu3eqm8.s[4]++;console.info('unreachable');}}function branch(a){cov_263bu3eqm8.f[0]++;cov_263bu3eqm8.s[5]++;if(a){cov_263bu3eqm8.b[2][0]++;cov_263bu3eqm8.s[6]++;console.info('a = true');}else{cov_263bu3eqm8.b[2][1]++;cov_263bu3eqm8.s[7]++;if(undefined){cov_263bu3eqm8.b[3][0]++;cov_263bu3eqm8.s[8]++;console.info('unreachable');}else{cov_263bu3eqm8.b[3][1]++;cov_263bu3eqm8.s[9]++;console.info('a = false');}}}cov_263bu3eqm8.s[10]++;branch(true);cov_263bu3eqm8.s[11]++;branch(false);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4vYnJhbmNoZXMuanMiXSwibmFtZXMiOlsiY29uc29sZSIsImluZm8iLCJicmFuY2giLCJhIiwidW5kZWZpbmVkIl0sIm1hcHBpbmdzIjoic3VFQUFBLEdBQUksS0FBSixDQUFXLGdEQUNUQSxPQUFPLENBQUNDLElBQVIsQ0FBYSxhQUFiLEVBQ0QsQ0FGRCxJQUVPLG1EQUFJLElBQUosQ0FBVSxnREFDZkQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELEVBRUQsUUFBU0MsQ0FBQUEsTUFBVCxDQUFpQkMsQ0FBakIsQ0FBb0IsNkNBQ2xCLEdBQUlBLENBQUosQ0FBTyxnREFDTEgsT0FBTyxDQUFDQyxJQUFSLENBQWEsVUFBYixFQUNELENBRkQsSUFFTyxtREFBSUcsU0FBSixDQUFlLGdEQUNwQkosT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELEVBQ0YsQyx1QkFFREMsTUFBTSxDQUFDLElBQUQsQ0FBTixDLHVCQUNBQSxNQUFNLENBQUMsS0FBRCxDQUFOIiwic291cmNlc0NvbnRlbnQiOlsiaWYgKGZhbHNlKSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufSBlbHNlIGlmICh0cnVlKSB7XG4gIGNvbnNvbGUuaW5mbygncmVhY2hhYmxlJylcbn0gZWxzZSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufVxuXG5mdW5jdGlvbiBicmFuY2ggKGEpIHtcbiAgaWYgKGEpIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSB0cnVlJylcbiAgfSBlbHNlIGlmICh1bmRlZmluZWQpIHtcbiAgICBjb25zb2xlLmluZm8oJ3VucmVhY2hhYmxlJylcbiAgfSBlbHNlIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSBmYWxzZScpXG4gIH1cbn1cblxuYnJhbmNoKHRydWUpXG5icmFuY2goZmFsc2UpXG4iXX0=

8
test/fixtures/source-map/sigint.js vendored Normal file
View File

@ -0,0 +1,8 @@
const a = 99;
if (true) {
const b = 101;
} else {
const c = 102;
}
process.kill(process.pid, "SIGINT");
//# sourceMappingURL=https://http.cat/402

View File

@ -55,6 +55,7 @@ const expectedModules = new Set([
'NativeModule internal/process/task_queues',
'NativeModule internal/process/warning',
'NativeModule internal/querystring',
'NativeModule internal/source_map',
'NativeModule internal/timers',
'NativeModule internal/url',
'NativeModule internal/util',

View File

@ -0,0 +1,132 @@
'use strict';
if (!process.features.inspector) return;
const common = require('../common');
const assert = require('assert');
const { dirname } = require('path');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
let dirc = 0;
function nextdir() {
return process.env.NODE_V8_COVERAGE ||
path.join(tmpdir.path, `source_map_${++dirc}`);
}
// Outputs source maps when event loop is drained, with no async logic.
{
const coverageDirectory = nextdir();
const output = spawnSync(process.execPath, [
require.resolve('../fixtures/source-map/basic')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
assert.strictEqual(output.stderr.toString(), '');
const sourceMap = getSourceMapFromCache('basic.js', coverageDirectory);
assert.strictEqual(sourceMap.url, 'https://http.cat/418');
}
// Outputs source maps when process.kill(process.pid, "SIGINT"); exits process.
{
const coverageDirectory = nextdir();
const output = spawnSync(process.execPath, [
require.resolve('../fixtures/source-map/sigint')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
if (!common.isWindows) {
if (output.signal !== 'SIGINT') {
console.log(output.stderr.toString());
}
assert.strictEqual(output.signal, 'SIGINT');
}
assert.strictEqual(output.stderr.toString(), '');
const sourceMap = getSourceMapFromCache('sigint.js', coverageDirectory);
assert.strictEqual(sourceMap.url, 'https://http.cat/402');
}
// Outputs source maps when source-file calls process.exit(1).
{
const coverageDirectory = nextdir();
const output = spawnSync(process.execPath, [
require.resolve('../fixtures/source-map/exit-1')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
assert.strictEqual(output.stderr.toString(), '');
const sourceMap = getSourceMapFromCache('exit-1.js', coverageDirectory);
assert.strictEqual(sourceMap.url, 'https://http.cat/404');
}
// Outputs source-maps for esm module.
{
const coverageDirectory = nextdir();
const output = spawnSync(process.execPath, [
'--no-warnings',
'--experimental-modules',
require.resolve('../fixtures/source-map/esm-basic.mjs')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
assert.strictEqual(output.stderr.toString(), '');
const sourceMap = getSourceMapFromCache('esm-basic.mjs', coverageDirectory);
assert.strictEqual(sourceMap.url, 'https://http.cat/405');
}
// Loads source-maps with relative path from .map file on disk.
{
const coverageDirectory = nextdir();
const output = spawnSync(process.execPath, [
require.resolve('../fixtures/source-map/disk-relative-path')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
assert.strictEqual(output.status, 0);
assert.strictEqual(output.stderr.toString(), '');
const sourceMap = getSourceMapFromCache(
'disk-relative-path.js',
coverageDirectory
);
// Source-map should have been loaded from disk and sources should have been
// rewritten, such that they're absolute paths.
assert.strictEqual(
dirname(
`file://${require.resolve('../fixtures/source-map/disk-relative-path')}`),
dirname(sourceMap.data.sources[0])
);
}
// Loads source-maps from inline data URL.
{
const coverageDirectory = nextdir();
const output = spawnSync(process.execPath, [
require.resolve('../fixtures/source-map/inline-base64.js')
], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } });
assert.strictEqual(output.status, 0);
assert.strictEqual(output.stderr.toString(), '');
const sourceMap = getSourceMapFromCache(
'inline-base64.js',
coverageDirectory
);
// base64 JSON should have been decoded, and paths to sources should have
// been rewritten such that they're absolute:
assert.strictEqual(
dirname(
`file://${require.resolve('../fixtures/source-map/inline-base64')}`),
dirname(sourceMap.data.sources[0])
);
}
function getSourceMapFromCache(fixtureFile, coverageDirectory) {
const jsonFiles = fs.readdirSync(coverageDirectory);
for (const jsonFile of jsonFiles) {
const maybeSourceMapCache = require(
path.join(coverageDirectory, jsonFile)
)['source-map-cache'] || {};
const keys = Object.keys(maybeSourceMapCache);
for (const key of keys) {
if (key.includes(fixtureFile)) {
return maybeSourceMapCache[key];
}
}
}
}