From 3ea53c5dc8eaeecfe5d13dc84f59a8f8f67ce3e7 Mon Sep 17 00:00:00 2001 From: sepehrst Date: Sat, 4 Feb 2023 22:17:11 +0000 Subject: [PATCH 01/93] deps: V8: cherry-pick c875e86df1d7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original commit message: [bigint] Convert BigInt property names to decimal Hexadecimal/octal/binary BigInt property names should be converted to decimal, i.e. the following object literals should all be equivalent: var o = {0xF: 1}, p = {0xFn: 1}, q = {15: 1}, r = {15n: 1}. Test case by yangwenming@bytedance.com, uploaded at https://chromium-review.googlesource.com/c/v8/v8/+/3634937 Fixed: v8:10600 Change-Id: Ie1d8a16e95697cd31cbc0784843779c921ce91fa Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3642302 Reviewed-by: Toon Verwaest Commit-Queue: Jakob Kummerow Cr-Commit-Position: refs/heads/main@{#80490} Refs: v8/v8@c875e86 PR-URL: https://github.com/nodejs/node/pull/46501 Refs: https://github.com/v8/v8/commit/c875e86 Reviewed-By: Michaël Zasso Reviewed-By: Jiawen Geng --- common.gypi | 2 +- deps/v8/src/numbers/conversions.cc | 25 +++++++++++++++++++ deps/v8/src/numbers/conversions.h | 2 ++ deps/v8/src/parsing/parser-base.h | 2 +- deps/v8/src/parsing/parser.cc | 10 ++++++++ deps/v8/src/parsing/parser.h | 2 ++ deps/v8/src/parsing/preparser.h | 4 +++ deps/v8/src/parsing/scanner.h | 3 +++ .../mjsunit/harmony/bigint/property-names.js | 23 +++++++++++++++++ 9 files changed, 71 insertions(+), 2 deletions(-) diff --git a/common.gypi b/common.gypi index 5372c8d42de531..47b9453124631f 100644 --- a/common.gypi +++ b/common.gypi @@ -36,7 +36,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.22', + 'v8_embedder_string': '-node.23', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/src/numbers/conversions.cc b/deps/v8/src/numbers/conversions.cc index 0683402794dbe7..93f5ba7be50a75 100644 --- a/deps/v8/src/numbers/conversions.cc +++ b/deps/v8/src/numbers/conversions.cc @@ -12,6 +12,7 @@ #include "src/base/numbers/dtoa.h" #include "src/base/numbers/strtod.h" #include "src/base/platform/wrappers.h" +#include "src/base/small-vector.h" #include "src/bigint/bigint.h" #include "src/common/assert-scope.h" #include "src/handles/handles.h" @@ -970,6 +971,23 @@ class StringToBigIntHelper : public StringToIntHelper { UNREACHABLE(); } + // Used for converting BigInt literals. The scanner has already checked + // that the literal is valid and not too big, so this always succeeds. + std::unique_ptr DecimalString(bigint::Processor* processor) { + DCHECK_EQ(behavior_, Behavior::kLiteral); + this->ParseInt(); + DCHECK_EQ(this->state(), State::kDone); + int num_digits = accumulator_.ResultLength(); + base::SmallVector digit_storage(num_digits); + bigint::RWDigits digits(digit_storage.data(), num_digits); + processor->FromString(digits, &accumulator_); + int num_chars = bigint::ToStringResultLength(digits, 10, false); + std::unique_ptr out(new char[num_chars + 1]); + processor->ToString(out.get(), &num_chars, digits, 10, false); + out[num_chars] = '\0'; + return out; + } + private: template void ParseInternal(Char start) { @@ -1018,6 +1036,13 @@ template EXPORT_TEMPLATE_DEFINE(V8_EXPORT_PRIVATE) MaybeHandle BigIntLiteral(LocalIsolate* isolate, const char* string); +std::unique_ptr BigIntLiteralToDecimal( + LocalIsolate* isolate, base::Vector literal) { + StringToBigIntHelper helper(nullptr, literal.begin(), + literal.length()); + return helper.DecimalString(isolate->bigint_processor()); +} + const char* DoubleToCString(double v, base::Vector buffer) { switch (FPCLASSIFY_NAMESPACE::fpclassify(v)) { case FP_NAN: diff --git a/deps/v8/src/numbers/conversions.h b/deps/v8/src/numbers/conversions.h index 9232de93cafe90..fce0d726544527 100644 --- a/deps/v8/src/numbers/conversions.h +++ b/deps/v8/src/numbers/conversions.h @@ -119,6 +119,8 @@ const int kDoubleToCStringMinBufferSize = 100; V8_EXPORT_PRIVATE const char* DoubleToCString(double value, base::Vector buffer); +V8_EXPORT_PRIVATE std::unique_ptr BigIntLiteralToDecimal( + LocalIsolate* isolate, base::Vector literal); // Convert an int to a null-terminated string. The returned string is // located inside the buffer, but not necessarily at the start. V8_EXPORT_PRIVATE const char* IntToCString(int n, base::Vector buffer); diff --git a/deps/v8/src/parsing/parser-base.h b/deps/v8/src/parsing/parser-base.h index df829ff8cac43c..cfa91e446f82e9 100644 --- a/deps/v8/src/parsing/parser-base.h +++ b/deps/v8/src/parsing/parser-base.h @@ -2288,7 +2288,7 @@ typename ParserBase::ExpressionT ParserBase::ParseProperty( case Token::BIGINT: { Consume(Token::BIGINT); - prop_info->name = impl()->GetSymbol(); + prop_info->name = impl()->GetBigIntAsSymbol(); is_array_index = impl()->IsArrayIndex(prop_info->name, &index); break; } diff --git a/deps/v8/src/parsing/parser.cc b/deps/v8/src/parsing/parser.cc index 6ee70886a99f8d..9789909456ee5e 100644 --- a/deps/v8/src/parsing/parser.cc +++ b/deps/v8/src/parsing/parser.cc @@ -247,6 +247,16 @@ bool Parser::CollapseNaryExpression(Expression** x, Expression* y, return true; } +const AstRawString* Parser::GetBigIntAsSymbol() { + base::Vector literal = scanner()->BigIntLiteral(); + if (literal[0] != '0' || literal.length() == 1) { + return ast_value_factory()->GetOneByteString(literal); + } + std::unique_ptr decimal = + BigIntLiteralToDecimal(local_isolate_, literal); + return ast_value_factory()->GetOneByteString(decimal.get()); +} + Expression* Parser::BuildUnaryExpression(Expression* expression, Token::Value op, int pos) { DCHECK_NOT_NULL(expression); diff --git a/deps/v8/src/parsing/parser.h b/deps/v8/src/parsing/parser.h index 1d8fa2515df380..909fc7defdb0cc 100644 --- a/deps/v8/src/parsing/parser.h +++ b/deps/v8/src/parsing/parser.h @@ -790,6 +790,8 @@ class V8_EXPORT_PRIVATE Parser : public NON_EXPORTED_BASE(ParserBase) { return ast_value_factory()->GetOneByteString(string); } + const AstRawString* GetBigIntAsSymbol(); + class ThisExpression* ThisExpression() { UseThis(); return factory()->ThisExpression(); diff --git a/deps/v8/src/parsing/preparser.h b/deps/v8/src/parsing/preparser.h index ff64f9a5f55bba..9b1d54ed7d6001 100644 --- a/deps/v8/src/parsing/preparser.h +++ b/deps/v8/src/parsing/preparser.h @@ -1522,6 +1522,10 @@ class PreParser : public ParserBase { return PreParserIdentifier::Default(); } + V8_INLINE PreParserIdentifier GetBigIntAsSymbol() const { + return PreParserIdentifier::Default(); + } + V8_INLINE PreParserExpression ThisExpression() { UseThis(); return PreParserExpression::This(); diff --git a/deps/v8/src/parsing/scanner.h b/deps/v8/src/parsing/scanner.h index 0fe907632de4bf..3a38ed243bb1dd 100644 --- a/deps/v8/src/parsing/scanner.h +++ b/deps/v8/src/parsing/scanner.h @@ -336,6 +336,9 @@ class V8_EXPORT_PRIVATE Scanner { AstValueFactory* ast_value_factory) const; double DoubleValue(); + base::Vector BigIntLiteral() const { + return literal_one_byte_string(); + } const char* CurrentLiteralAsCString(Zone* zone) const; diff --git a/deps/v8/test/mjsunit/harmony/bigint/property-names.js b/deps/v8/test/mjsunit/harmony/bigint/property-names.js index ac65e1041d50fa..400b51f51c67a3 100644 --- a/deps/v8/test/mjsunit/harmony/bigint/property-names.js +++ b/deps/v8/test/mjsunit/harmony/bigint/property-names.js @@ -7,3 +7,26 @@ assertEquals(it, 1); var { 999999999999999999n: it } = { 999999999999999999n: 1 }; // greater than max safe integer assertEquals(it, 1); + +var obj = { 0xfffffffffffffffffffffn: 1}; +assertEquals(1, obj["19342813113834066795298815"]); + +var obj2 = {0o777777777777777777777777777n: 1}; +assertEquals(1, obj2["2417851639229258349412351"]); + +var obj3 = { 0x4n: 'hi' }; + +assertEquals('hi', obj3[4]); +assertEquals(undefined, obj3["0x4"]); + +let obj4 = + {12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890n: 1}; +assertEquals( + 1, + obj4["12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"]); + +// 130 hex digits +let obj5 = {0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn: 1}; +assertEquals( + 1, + obj5["3432398830065304857490950399540696608634717650071652704697231729592771591698828026061279820330727277488648155695740429018560993999858321906287014145557528575"]); From 5019b5473f982c8a8d3f4de3128c5132e0405290 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Mon, 9 Jan 2023 20:15:49 +0100 Subject: [PATCH 02/93] http: res.setHeaders first implementation PR-URL: https://github.com/nodejs/node/pull/46109 Backport-PR-URL: https://github.com/nodejs/node/pull/46365 Reviewed-By: Matteo Collina Reviewed-By: Paolo Insogna Reviewed-By: Yagiz Nizipli --- doc/api/http.md | 44 ++++++ lib/_http_outgoing.js | 22 +++ .../parallel/test-http-response-setheaders.js | 131 ++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 test/parallel/test-http-response-setheaders.js diff --git a/doc/api/http.md b/doc/api/http.md index 3d66acccbb97a1..4a3675d83bd7c3 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -2950,6 +2950,48 @@ Sets a single header value. If the header already exists in the to-be-sent headers, its value will be replaced. Use an array of strings to send multiple headers with the same name. +### `outgoingMessage.setHeaders(headers)` + + + +* `headers` {Headers|Map} +* Returns: {http.ServerResponse} + +Returns the response object. + +Sets multiple header values for implicit headers. +`headers` must be an instance of [`Headers`][] or `Map`, +if a header already exists in the to-be-sent headers, +its value will be replaced. + +```js +const headers = new Headers({ foo: 'bar' }); +response.setHeaders(headers); +``` + +or + +```js +const headers = new Map([['foo', 'bar']]); +res.setHeaders(headers); +``` + +When headers have been set with [`outgoingMessage.setHeaders()`][], +they will be merged with any headers passed to [`response.writeHead()`][], +with the headers passed to [`response.writeHead()`][] given precedence. + +```js +// Returns content-type = text/plain +const server = http.createServer((req, res) => { + const headers = new Headers({ 'Content-Type': 'text/html' }); + res.setHeaders(headers); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + ### `outgoingMessage.setTimeout(msesc[, callback])` + +* input {Buffer | ArrayBuffer | TypedArray} The input to validate. +* Returns: {boolean} + +This function returns `true` if `input` contains only valid ASCII-encoded data, +including the case in which `input` is empty. + +Throws if the `input` is a detached array buffer. + ### `buffer.isUtf8(input)` + +> Stability: 1 - Experimental + +* {number|undefined} + +Gets the amount of memory available to the process (in bytes) based on +limits imposed by the OS. If there is no such constraint, or the constraint +is unknown, `undefined` is returned. + +See [`uv_get_constrained_memory`][uv_get_constrained_memory] for more +information. + ## `process.cpuUsage([previousValue])` + +This API collects GC data in current thread. + +### `new v8.GCProfiler()` + + + +Create a new instance of the `v8.GCProfiler` class. + +### `profiler.start()` + + + +Start collecting GC data. + +### `profiler.stop()` + + + +Stop collecting GC data and return a object.The content of object +is as follows. + +```json +{ + "version": 1, + "startTime": 1674059033862, + "statistics": [ + { + "gcType": "Scavenge", + "beforeGC": { + "heapStatistics": { + "totalHeapSize": 5005312, + "totalHeapSizeExecutable": 524288, + "totalPhysicalSize": 5226496, + "totalAvailableSize": 4341325216, + "totalGlobalHandlesSize": 8192, + "usedGlobalHandlesSize": 2112, + "usedHeapSize": 4883840, + "heapSizeLimit": 4345298944, + "mallocedMemory": 254128, + "externalMemory": 225138, + "peakMallocedMemory": 181760 + }, + "heapSpaceStatistics": [ + { + "spaceName": "read_only_space", + "spaceSize": 0, + "spaceUsedSize": 0, + "spaceAvailableSize": 0, + "physicalSpaceSize": 0 + } + ] + }, + "cost": 1574.14, + "afterGC": { + "heapStatistics": { + "totalHeapSize": 6053888, + "totalHeapSizeExecutable": 524288, + "totalPhysicalSize": 5500928, + "totalAvailableSize": 4341101384, + "totalGlobalHandlesSize": 8192, + "usedGlobalHandlesSize": 2112, + "usedHeapSize": 4059096, + "heapSizeLimit": 4345298944, + "mallocedMemory": 254128, + "externalMemory": 225138, + "peakMallocedMemory": 181760 + }, + "heapSpaceStatistics": [ + { + "spaceName": "read_only_space", + "spaceSize": 0, + "spaceUsedSize": 0, + "spaceAvailableSize": 0, + "physicalSpaceSize": 0 + } + ] + } + } + ], + "endTime": 1674059036865 +} +``` + +Here's an example. + +```js +const { GCProfiler } = require('v8'); +const profiler = new GCProfiler(); +profiler.start(); +setTimeout(() => { + console.log(profiler.stop()); +}, 1000); +``` + [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm [Hook Callbacks]: #hook-callbacks [V8]: https://developers.google.com/v8/ diff --git a/lib/v8.js b/lib/v8.js index 479e8b13efc96d..bfca704f5e32cc 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -60,7 +60,7 @@ const { const { HeapSnapshotStream } = require('internal/heap_utils'); const promiseHooks = require('internal/promise_hooks'); const { getOptionValue } = require('internal/options'); - +const { JSONParse } = primordials; /** * Generates a snapshot of the current V8 heap * and writes it to a JSON file. @@ -384,6 +384,25 @@ function deserialize(buffer) { return der.readValue(); } +class GCProfiler { + #profiler = null; + + start() { + if (!this.#profiler) { + this.#profiler = new binding.GCProfiler(); + this.#profiler.start(); + } + } + + stop() { + if (this.#profiler) { + const data = this.#profiler.stop(); + this.#profiler = null; + return JSONParse(data); + } + } +} + module.exports = { cachedDataVersionTag, getHeapSnapshot, @@ -403,4 +422,5 @@ module.exports = { promiseHooks, startupSnapshot, setHeapSnapshotNearHeapLimit, + GCProfiler, }; diff --git a/src/node_v8.cc b/src/node_v8.cc index 91a3dbb93e0155..890f59eea673c5 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -33,6 +33,7 @@ namespace v8_utils { using v8::Array; using v8::Context; using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; using v8::HandleScope; using v8::HeapCodeStatistics; using v8::HeapSpaceStatistics; @@ -210,6 +211,184 @@ void SetFlagsFromString(const FunctionCallbackInfo& args) { V8::SetFlagsFromString(*flags, static_cast(flags.length())); } +static const char* GetGCTypeName(v8::GCType gc_type) { + switch (gc_type) { + case v8::GCType::kGCTypeScavenge: + return "Scavenge"; + case v8::GCType::kGCTypeMarkSweepCompact: + return "MarkSweepCompact"; + case v8::GCType::kGCTypeIncrementalMarking: + return "IncrementalMarking"; + case v8::GCType::kGCTypeProcessWeakCallbacks: + return "ProcessWeakCallbacks"; + default: + return "Unknown"; + } +} + +static void SetHeapStatistics(JSONWriter* writer, Isolate* isolate) { + HeapStatistics heap_statistics; + isolate->GetHeapStatistics(&heap_statistics); + writer->json_objectstart("heapStatistics"); + writer->json_keyvalue("totalHeapSize", heap_statistics.total_heap_size()); + writer->json_keyvalue("totalHeapSizeExecutable", + heap_statistics.total_heap_size_executable()); + writer->json_keyvalue("totalPhysicalSize", + heap_statistics.total_physical_size()); + writer->json_keyvalue("totalAvailableSize", + heap_statistics.total_available_size()); + writer->json_keyvalue("totalGlobalHandlesSize", + heap_statistics.total_global_handles_size()); + writer->json_keyvalue("usedGlobalHandlesSize", + heap_statistics.used_global_handles_size()); + writer->json_keyvalue("usedHeapSize", heap_statistics.used_heap_size()); + writer->json_keyvalue("heapSizeLimit", heap_statistics.heap_size_limit()); + writer->json_keyvalue("mallocedMemory", heap_statistics.malloced_memory()); + writer->json_keyvalue("externalMemory", heap_statistics.external_memory()); + writer->json_keyvalue("peakMallocedMemory", + heap_statistics.peak_malloced_memory()); + writer->json_objectend(); + + int space_count = isolate->NumberOfHeapSpaces(); + writer->json_arraystart("heapSpaceStatistics"); + for (int i = 0; i < space_count; i++) { + HeapSpaceStatistics heap_space_statistics; + isolate->GetHeapSpaceStatistics(&heap_space_statistics, i); + writer->json_start(); + writer->json_keyvalue("spaceName", heap_space_statistics.space_name()); + writer->json_keyvalue("spaceSize", heap_space_statistics.space_size()); + writer->json_keyvalue("spaceUsedSize", + heap_space_statistics.space_used_size()); + writer->json_keyvalue("spaceAvailableSize", + heap_space_statistics.space_available_size()); + writer->json_keyvalue("physicalSpaceSize", + heap_space_statistics.physical_space_size()); + writer->json_end(); + } + writer->json_arrayend(); +} + +static void BeforeGCCallback(Isolate* isolate, + v8::GCType gc_type, + v8::GCCallbackFlags flags, + void* data) { + GCProfiler* profiler = static_cast(data); + if (profiler->current_gc_type != 0) { + return; + } + JSONWriter* writer = profiler->writer(); + writer->json_start(); + writer->json_keyvalue("gcType", GetGCTypeName(gc_type)); + writer->json_objectstart("beforeGC"); + SetHeapStatistics(writer, isolate); + writer->json_objectend(); + profiler->current_gc_type = gc_type; + profiler->start_time = uv_hrtime(); +} + +static void AfterGCCallback(Isolate* isolate, + v8::GCType gc_type, + v8::GCCallbackFlags flags, + void* data) { + GCProfiler* profiler = static_cast(data); + if (profiler->current_gc_type != gc_type) { + return; + } + JSONWriter* writer = profiler->writer(); + profiler->current_gc_type = 0; + writer->json_keyvalue("cost", (uv_hrtime() - profiler->start_time) / 1e3); + profiler->start_time = 0; + writer->json_objectstart("afterGC"); + SetHeapStatistics(writer, isolate); + writer->json_objectend(); + writer->json_end(); +} + +GCProfiler::GCProfiler(Environment* env, Local object) + : BaseObject(env, object), + start_time(0), + current_gc_type(0), + state(GCProfilerState::kInitialized), + writer_(out_stream_, false) { + MakeWeak(); +} + +// This function will be called when +// 1. StartGCProfile and StopGCProfile are called and +// JS land does not keep the object anymore. +// 2. StartGCProfile is called then the env exits before +// StopGCProfile is called. +GCProfiler::~GCProfiler() { + if (state != GCProfiler::GCProfilerState::kInitialized) { + env()->isolate()->RemoveGCPrologueCallback(BeforeGCCallback, this); + env()->isolate()->RemoveGCEpilogueCallback(AfterGCCallback, this); + } +} + +JSONWriter* GCProfiler::writer() { + return &writer_; +} + +std::ostringstream* GCProfiler::out_stream() { + return &out_stream_; +} + +void GCProfiler::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new GCProfiler(env, args.This()); +} + +void GCProfiler::Start(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + GCProfiler* profiler; + ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder()); + if (profiler->state != GCProfiler::GCProfilerState::kInitialized) { + return; + } + profiler->writer()->json_start(); + profiler->writer()->json_keyvalue("version", 1); + + uv_timeval64_t ts; + if (uv_gettimeofday(&ts) == 0) { + profiler->writer()->json_keyvalue("startTime", + ts.tv_sec * 1000 + ts.tv_usec / 1000); + } else { + profiler->writer()->json_keyvalue("startTime", 0); + } + profiler->writer()->json_arraystart("statistics"); + env->isolate()->AddGCPrologueCallback(BeforeGCCallback, + static_cast(profiler)); + env->isolate()->AddGCEpilogueCallback(AfterGCCallback, + static_cast(profiler)); + profiler->state = GCProfiler::GCProfilerState::kStarted; +} + +void GCProfiler::Stop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + GCProfiler* profiler; + ASSIGN_OR_RETURN_UNWRAP(&profiler, args.Holder()); + if (profiler->state != GCProfiler::GCProfilerState::kStarted) { + return; + } + profiler->writer()->json_arrayend(); + uv_timeval64_t ts; + if (uv_gettimeofday(&ts) == 0) { + profiler->writer()->json_keyvalue("endTime", + ts.tv_sec * 1000 + ts.tv_usec / 1000); + } else { + profiler->writer()->json_keyvalue("endTime", 0); + } + profiler->writer()->json_end(); + profiler->state = GCProfiler::GCProfilerState::kStopped; + auto string = profiler->out_stream()->str(); + args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(), + string.data(), + v8::NewStringType::kNormal, + string.size()) + .ToLocalChecked()); +} + void Initialize(Local target, Local unused, Local context, @@ -272,6 +451,14 @@ void Initialize(Local target, // Export symbols used by v8.setFlagsFromString() SetMethod(context, target, "setFlagsFromString", SetFlagsFromString); + + // GCProfiler + Local t = + NewFunctionTemplate(env->isolate(), GCProfiler::New); + t->InstanceTemplate()->SetInternalFieldCount(BaseObject::kInternalFieldCount); + SetProtoMethod(env->isolate(), t, "start", GCProfiler::Start); + SetProtoMethod(env->isolate(), t, "stop", GCProfiler::Stop); + SetConstructorFunction(context, target, "GCProfiler", t); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -281,6 +468,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(UpdateHeapSpaceStatisticsBuffer); registry->Register(SetFlagsFromString); registry->Register(SetHeapSnapshotNearHeapLimit); + registry->Register(GCProfiler::New); + registry->Register(GCProfiler::Start); + registry->Register(GCProfiler::Stop); } } // namespace v8_utils diff --git a/src/node_v8.h b/src/node_v8.h index 18b3621a2a5d6a..ecab454603b36b 100644 --- a/src/node_v8.h +++ b/src/node_v8.h @@ -3,8 +3,10 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include #include "aliased_buffer.h" #include "base_object.h" +#include "json_utils.h" #include "node_snapshotable.h" #include "util.h" #include "v8.h" @@ -34,6 +36,32 @@ class BindingData : public SnapshotableObject { SET_MEMORY_INFO_NAME(BindingData) }; +class GCProfiler : public BaseObject { + public: + enum class GCProfilerState { kInitialized, kStarted, kStopped }; + GCProfiler(Environment* env, v8::Local object); + inline ~GCProfiler() override; + static void New(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + + JSONWriter* writer(); + + std::ostringstream* out_stream(); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(GCProfiler) + SET_SELF_SIZE(GCProfiler) + + uint64_t start_time; + uint8_t current_gc_type; + GCProfilerState state; + + private: + std::ostringstream out_stream_; + JSONWriter writer_; +}; + } // namespace v8_utils } // namespace node diff --git a/test/common/v8.js b/test/common/v8.js new file mode 100644 index 00000000000000..9c247bde50eb8c --- /dev/null +++ b/test/common/v8.js @@ -0,0 +1,70 @@ +'use strict'; +const assert = require('assert'); +const { GCProfiler } = require('v8'); + +function collectGCProfile({ duration }) { + return new Promise((resolve) => { + const profiler = new GCProfiler(); + profiler.start(); + setTimeout(() => { + resolve(profiler.stop()); + }, duration); + }); +} + +function checkGCProfile(data) { + assert.ok(data.version > 0); + assert.ok(data.startTime >= 0); + assert.ok(data.endTime >= 0); + assert.ok(Array.isArray(data.statistics)); + // If the array is not empty, check it + if (data.statistics.length) { + // Just check the first one + const item = data.statistics[0]; + assert.ok(typeof item.gcType === 'string'); + assert.ok(item.cost >= 0); + assert.ok(typeof item.beforeGC === 'object'); + assert.ok(typeof item.afterGC === 'object'); + // The content of beforeGC and afterGC is same, so we just check afterGC + assert.ok(typeof item.afterGC.heapStatistics === 'object'); + const heapStatisticsKeys = [ + 'externalMemory', + 'heapSizeLimit', + 'mallocedMemory', + 'peakMallocedMemory', + 'totalAvailableSize', + 'totalGlobalHandlesSize', + 'totalHeapSize', + 'totalHeapSizeExecutable', + 'totalPhysicalSize', + 'usedGlobalHandlesSize', + 'usedHeapSize', + ]; + heapStatisticsKeys.forEach((key) => { + assert.ok(item.afterGC.heapStatistics[key] >= 0); + }); + assert.ok(typeof item.afterGC.heapSpaceStatistics === 'object'); + const heapSpaceStatisticsKeys = [ + 'physicalSpaceSize', + 'spaceAvailableSize', + 'spaceName', + 'spaceSize', + 'spaceUsedSize', + ]; + heapSpaceStatisticsKeys.forEach((key) => { + const value = item.afterGC.heapSpaceStatistics[0][key]; + assert.ok(key === 'spaceName' ? typeof value === 'string' : value >= 0); + }); + } +} + +async function testGCProfiler() { + const data = await collectGCProfile({ duration: 5000 }); + checkGCProfile(data); +} + +module.exports = { + collectGCProfile, + checkGCProfile, + testGCProfiler, +}; diff --git a/test/parallel/test-v8-collect-gc-profile-exit-before-stop.js b/test/parallel/test-v8-collect-gc-profile-exit-before-stop.js new file mode 100644 index 00000000000000..4ef44001236209 --- /dev/null +++ b/test/parallel/test-v8-collect-gc-profile-exit-before-stop.js @@ -0,0 +1,17 @@ +'use strict'; +require('../common'); +const { GCProfiler } = require('v8'); + +// Test if it makes the process crash. +{ + const profiler = new GCProfiler(); + profiler.start(); + profiler.stop(); + profiler.start(); + profiler.stop(); +} +{ + const profiler = new GCProfiler(); + profiler.start(); + profiler.stop(); +} diff --git a/test/parallel/test-v8-collect-gc-profile-in-worker.js b/test/parallel/test-v8-collect-gc-profile-in-worker.js new file mode 100644 index 00000000000000..49762c50fb1c73 --- /dev/null +++ b/test/parallel/test-v8-collect-gc-profile-in-worker.js @@ -0,0 +1,16 @@ +// Flags: --expose-gc +'use strict'; +require('../common'); +const { Worker } = require('worker_threads'); +const { testGCProfiler } = require('../common/v8'); + +if (process.env.isWorker) { + process.env.isWorker = 1; + new Worker(__filename); +} else { + testGCProfiler(); + for (let i = 0; i < 100; i++) { + new Array(100); + } + global?.gc(); +} diff --git a/test/parallel/test-v8-collect-gc-profile.js b/test/parallel/test-v8-collect-gc-profile.js new file mode 100644 index 00000000000000..70a8a0d842ef9e --- /dev/null +++ b/test/parallel/test-v8-collect-gc-profile.js @@ -0,0 +1,12 @@ +// Flags: --expose-gc +'use strict'; +require('../common'); +const { testGCProfiler } = require('../common/v8'); + +testGCProfiler(); + +for (let i = 0; i < 100; i++) { + new Array(100); +} + +global?.gc(); From bdb793a0821417605b97cd902f7471fd6df00802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 25 Jan 2023 15:00:21 +0100 Subject: [PATCH 39/93] src: use UNREACHABLE instead of CHECK(falsy) Also remove some dead code following such statements. PR-URL: https://github.com/nodejs/node/pull/46317 Reviewed-By: Darshan Sen Reviewed-By: Anna Henningsen Reviewed-By: Yagiz Nizipli Reviewed-By: Luigi Pinca Reviewed-By: Franziska Hinkelmann --- src/cares_wrap.cc | 7 +++---- src/crypto/crypto_bio.cc | 7 ++----- src/crypto/crypto_keys.cc | 2 +- src/debug_utils.cc | 2 +- src/fs_event_wrap.cc | 2 +- src/node_zlib.cc | 4 ++-- src/spawn_sync.cc | 3 +-- src/string_bytes.cc | 6 ++---- src/udp_wrap.cc | 2 +- 9 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 2dafdf7a650436..997c3ba00d6150 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -233,8 +233,7 @@ int ParseGeneralReply( status = ares_parse_ptr_reply(buf, len, nullptr, 0, AF_INET, &host); break; default: - CHECK(0 && "Bad NS type"); - break; + UNREACHABLE("Bad NS type"); } if (status != ARES_SUCCESS) @@ -1578,7 +1577,7 @@ void GetAddrInfo(const FunctionCallbackInfo& args) { family = AF_INET6; break; default: - CHECK(0 && "bad address family"); + UNREACHABLE("bad address family"); } auto req_wrap = std::make_unique(env, @@ -1736,7 +1735,7 @@ void SetServers(const FunctionCallbackInfo& args) { err = uv_inet_pton(AF_INET6, *ip, &cur->addr); break; default: - CHECK(0 && "Bad address family."); + UNREACHABLE("Bad address family"); } if (err) diff --git a/src/crypto/crypto_bio.cc b/src/crypto/crypto_bio.cc index 099b11ee72520d..47045365ceaf81 100644 --- a/src/crypto/crypto_bio.cc +++ b/src/crypto/crypto_bio.cc @@ -190,12 +190,9 @@ long NodeBIO::Ctrl(BIO* bio, int cmd, long num, // NOLINT(runtime/int) *reinterpret_cast(ptr) = nullptr; break; case BIO_C_SET_BUF_MEM: - CHECK(0 && "Can't use SET_BUF_MEM_PTR with NodeBIO"); - break; + UNREACHABLE("Can't use SET_BUF_MEM_PTR with NodeBIO"); case BIO_C_GET_BUF_MEM_PTR: - CHECK(0 && "Can't use GET_BUF_MEM_PTR with NodeBIO"); - ret = 0; - break; + UNREACHABLE("Can't use GET_BUF_MEM_PTR with NodeBIO"); case BIO_CTRL_GET_CLOSE: ret = BIO_get_shutdown(bio); break; diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index f5661ccedad4f7..cc2ff2e631ff77 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1393,7 +1393,7 @@ BaseObjectPtr NativeKeyObject::KeyObjectTransferData::Deserialize( key_ctor = env->crypto_key_object_private_constructor(); break; default: - CHECK(false); + UNREACHABLE(); } Local key; diff --git a/src/debug_utils.cc b/src/debug_utils.cc index 69ef383ed22ca8..fadf321c3d925f 100644 --- a/src/debug_utils.cc +++ b/src/debug_utils.cc @@ -321,7 +321,7 @@ void CheckedUvLoopClose(uv_loop_t* loop) { fflush(stderr); // Finally, abort. - CHECK(0 && "uv_loop_close() while having open handles"); + UNREACHABLE("uv_loop_close() while having open handles"); } void PrintLibuvHandleInformation(uv_loop_t* loop, FILE* stream) { diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index dafcc065ac612b..048b72666c0570 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -204,7 +204,7 @@ void FSEventWrap::OnEvent(uv_fs_event_t* handle, const char* filename, } else if (events & UV_CHANGE) { event_string = env->change_string(); } else { - CHECK(0 && "bad fs events flag"); + UNREACHABLE("bad fs events flag"); } Local argv[] = { diff --git a/src/node_zlib.cc b/src/node_zlib.cc index 3d130e8eb64859..fac116f9e6b3e2 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -313,7 +313,7 @@ class CompressionStream : public AsyncWrap, public ThreadPoolWork { flush != Z_FULL_FLUSH && flush != Z_FINISH && flush != Z_BLOCK) { - CHECK(0 && "Invalid flush value"); + UNREACHABLE("Invalid flush value"); } if (args[1]->IsNull()) { @@ -814,7 +814,7 @@ void ZlibContext::DoThreadPoolWork() { break; default: - CHECK(0 && "invalid number of gzip magic number bytes read"); + UNREACHABLE("invalid number of gzip magic number bytes read"); } [[fallthrough]]; diff --git a/src/spawn_sync.cc b/src/spawn_sync.cc index a602795e54fdca..ae4a85a42d6166 100644 --- a/src/spawn_sync.cc +++ b/src/spawn_sync.cc @@ -931,8 +931,7 @@ int SyncProcessRunner::ParseStdioOption(int child_fd, return AddStdioInheritFD(child_fd, inherit_fd); } else { - CHECK(0 && "invalid child stdio type"); - return UV_EINVAL; + UNREACHABLE("invalid child stdio type"); } } diff --git a/src/string_bytes.cc b/src/string_bytes.cc index 0e6b8b842141c5..98da55a9d71f21 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -368,8 +368,7 @@ size_t StringBytes::Write(Isolate* isolate, break; default: - CHECK(0 && "unknown encoding"); - break; + UNREACHABLE("unknown encoding"); } return nbytes; @@ -423,8 +422,7 @@ Maybe StringBytes::StorageSize(Isolate* isolate, break; default: - CHECK(0 && "unknown encoding"); - break; + UNREACHABLE("unknown encoding"); } return Just(data_size); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index 772021ff653089..cad50fec409730 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -233,7 +233,7 @@ int sockaddr_for_family(int address_family, case AF_INET6: return uv_ip6_addr(address, port, reinterpret_cast(addr)); default: - CHECK(0 && "unexpected address family"); + UNREACHABLE("unexpected address family"); } } From a97774603b6c9baddbf8bd0cafafea920e1866af Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 25 Jan 2023 19:07:53 +0100 Subject: [PATCH 40/93] tools: require more trailing commas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All of the import and export statements in the codebase already use trailing commas, this commit adds a linter rule to enforce that. PR-URL: https://github.com/nodejs/node/pull/46346 Reviewed-By: Michaël Zasso Reviewed-By: Filip Skokan Reviewed-By: Darshan Sen Reviewed-By: Colin Ihrig --- benchmark/.eslintrc.yaml | 4 ++-- lib/.eslintrc.yaml | 4 ++-- test/.eslintrc.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmark/.eslintrc.yaml b/benchmark/.eslintrc.yaml index 821b2381d3e215..7802df7c2f97cb 100644 --- a/benchmark/.eslintrc.yaml +++ b/benchmark/.eslintrc.yaml @@ -7,9 +7,9 @@ env: rules: comma-dangle: [error, { arrays: always-multiline, - exports: only-multiline, + exports: always-multiline, functions: only-multiline, - imports: only-multiline, + imports: always-multiline, objects: only-multiline, }] prefer-arrow-callback: error diff --git a/lib/.eslintrc.yaml b/lib/.eslintrc.yaml index 9dbdc218174a10..3bff01c0fd84c4 100644 --- a/lib/.eslintrc.yaml +++ b/lib/.eslintrc.yaml @@ -4,9 +4,9 @@ env: rules: comma-dangle: [error, { arrays: always-multiline, - exports: only-multiline, + exports: always-multiline, functions: only-multiline, - imports: only-multiline, + imports: always-multiline, objects: only-multiline, }] prefer-object-spread: error diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml index b0a648738eb086..cfd28738eff015 100644 --- a/test/.eslintrc.yaml +++ b/test/.eslintrc.yaml @@ -88,8 +88,8 @@ overrides: rules: comma-dangle: [error, { arrays: always-multiline, - exports: only-multiline, + exports: always-multiline, functions: only-multiline, - imports: only-multiline, + imports: always-multiline, objects: only-multiline, }] From 110ead9abb6351bc9fffac1ed2cca99728e0477c Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 25 Jan 2023 21:06:55 +0100 Subject: [PATCH 41/93] vm: expose cachedDataRejected for vm.compileFunction Having this information available is useful for functions just as it is for scripts. Therefore, expose it in the same way that other information related to code caching is reported. As part of this, de-duplify the code for setting the properties on the C++ side and add proper exception handling to it. PR-URL: https://github.com/nodejs/node/pull/46320 Reviewed-By: Gus Caplan Reviewed-By: Chengzhong Wu --- doc/api/vm.md | 6 ++ lib/internal/vm.js | 4 ++ src/node_contextify.cc | 125 ++++++++++++++++++++------------- src/node_contextify.h | 8 +++ test/parallel/test-vm-basic.js | 26 +++++-- 5 files changed, 114 insertions(+), 55 deletions(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index 09081fad2240bf..481aded758150b 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -962,6 +962,12 @@ const vm = require('node:vm'); + $(IntDir)%(FileName)%(Extension).pp + + + diff --git a/tools/v8_gypfiles/v8.gyp b/tools/v8_gypfiles/v8.gyp index 42d74b619bb47a..c29d66976ffb1f 100644 --- a/tools/v8_gypfiles/v8.gyp +++ b/tools/v8_gypfiles/v8.gyp @@ -1437,6 +1437,14 @@ ['want_separate_host_toolset', { 'toolsets': ['host'], }], + ['OS=="win"', { + 'msvs_precompiled_header': '<(V8_ROOT)/../../tools/msvs/pch/v8_pch.h', + 'msvs_precompiled_source': '<(V8_ROOT)/../../tools/msvs/pch/v8_pch.cc', + 'sources': [ + '<(_msvs_precompiled_header)', + '<(_msvs_precompiled_source)', + ], + }], ], }, # mksnapshot { From 919e581732093359a3402b86160e79f1e8b7b007 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sat, 28 Jan 2023 07:00:45 +0100 Subject: [PATCH 45/93] doc: add documentation for socket.destroySoon() PR-URL: https://github.com/nodejs/node/pull/46337 Reviewed-By: Paolo Insogna Reviewed-By: Matteo Collina --- doc/api/net.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/api/net.md b/doc/api/net.md index 1e9288d9c734a3..1a807ccac3de1d 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -1024,6 +1024,16 @@ See [`writable.destroy()`][] for further details. See [`writable.destroyed`][] for further details. +### `socket.destroySoon()` + + + +Destroys the socket after all data is written. If the `'finish'` event was +already emitted the socket is destroyed immediately. If the socket is still +writable it implicitly calls `socket.end()`. + ### `socket.end([data[, encoding]][, callback])` -Stop collecting GC data and return a object.The content of object +Stop collecting GC data and return an object.The content of object is as follows. ```json From 643545ab7942ad5ac19b63d585b80de21418a89f Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Sun, 29 Jan 2023 12:43:20 -0500 Subject: [PATCH 51/93] fs: add statfs() functions This commit adds statfs() and statfsSync() to the fs module, and statfs() to the fsPromises module. Co-authored-by: cjihrig Fixes: https://github.com/nodejs/node/issues/10745 Refs: https://github.com/nodejs/node/pull/31351 PR-URL: https://github.com/nodejs/node/pull/46358 Reviewed-By: Matteo Collina Reviewed-By: James M Snell --- doc/api/fs.md | 159 ++++++++++++++++++++++++++++++ lib/fs.js | 30 ++++++ lib/internal/fs/promises.js | 9 ++ lib/internal/fs/utils.js | 19 ++++ src/node_file-inl.h | 61 +++++++++--- src/node_file.cc | 76 +++++++++++++- src/node_file.h | 29 ++++++ test/parallel/test-fs-promises.js | 26 +++++ test/parallel/test-fs-statfs.js | 59 +++++++++++ tools/doc/type-parser.mjs | 1 + 10 files changed, 457 insertions(+), 12 deletions(-) create mode 100644 test/parallel/test-fs-statfs.js diff --git a/doc/api/fs.md b/doc/api/fs.md index f8de227a9ad848..c6436b3ce10a19 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1525,6 +1525,19 @@ changes: * Returns: {Promise} Fulfills with the {fs.Stats} object for the given `path`. +### `fsPromises.statfs(path[, options])` + + + +* `path` {string|Buffer|URL} +* `options` {Object} + * `bigint` {boolean} Whether the numeric values in the returned + {fs.StatFs} object should be `bigint`. **Default:** `false`. +* Returns: {Promise} Fulfills with the {fs.StatFs} object for the + given `path`. + ### `fsPromises.symlink(target, path[, type])` + +* `path` {string|Buffer|URL} +* `options` {Object} + * `bigint` {boolean} Whether the numeric values in the returned + {fs.StatFs} object should be `bigint`. **Default:** `false`. +* `callback` {Function} + * `err` {Error} + * `stats` {fs.StatFs} + +Asynchronous statfs(2). Returns information about the mounted file system which +contains `path`. The callback gets two arguments `(err, stats)` where `stats` +is an {fs.StatFs} object. + +In case of an error, the `err.code` will be one of [Common System Errors][]. + ### `fs.symlink(target, path[, type], callback)` + +* `path` {string|Buffer|URL} +* `options` {Object} + * `bigint` {boolean} Whether the numeric values in the returned + {fs.StatFs} object should be `bigint`. **Default:** `false`. +* Returns: {fs.StatFs} + +Synchronous statfs(2). Returns information about the mounted file system which +contains `path`. + +In case of an error, the `err.code` will be one of [Common System Errors][]. + ### `fs.symlinkSync(target, path[, type])` + +Provides information about a mounted file system. + +Objects returned from [`fs.statfs()`][] and its synchronous counterpart are of +this type. If `bigint` in the `options` passed to those methods is `true`, the +numeric values will be `bigint` instead of `number`. + +```console +StatFs { + type: 1397114950, + bsize: 4096, + blocks: 121938943, + bfree: 61058895, + bavail: 61058895, + files: 999, + ffree: 1000000 +} +``` + +`bigint` version: + +```console +StatFs { + type: 1397114950n, + bsize: 4096n, + blocks: 121938943n, + bfree: 61058895n, + bavail: 61058895n, + files: 999n, + ffree: 1000000n +} +``` + +#### `statfs.bavail` + + + +* {number|bigint} + +Free blocks available to unprivileged users. + +#### `statfs.bfree` + + + +* {number|bigint} + +Free blocks in file system. + +#### `statfs.blocks` + + + +* {number|bigint} + +Total data blocks in file system. + +#### `statfs.bsize` + + + +* {number|bigint} + +Optimal transfer block size. + +#### `statfs.ffree` + + + +* {number|bigint} + +Free file nodes in file system. + +#### `statfs.files` + + + +* {number|bigint} + +Total file nodes in file system. + +#### `statfs.type` + + + +* {number|bigint} + +Type of file system. + ### Class: `fs.WriteStream` + +A test reporter to use when running tests. See the documentation on +[test reporters][] for more details. + +### `--test-reporter-destination` + + + +The destination for the corresponding test reporter. See the documentation on +[test reporters][] for more details. + ### `--test-only` -The `node:test` module facilitates the creation of JavaScript tests that -report results in [TAP][] format. To access it: +The `node:test` module facilitates the creation of JavaScript tests. +To access it: ```mjs import test from 'node:test'; @@ -91,9 +91,7 @@ test('callback failing test', (t, done) => { }); ``` -As a test file executes, TAP is written to the standard output of the Node.js -process. This output can be interpreted by any test harness that understands -the TAP format. If any tests fail, the process exit code is set to `1`. +If any tests fail, the process exit code is set to `1`. ## Subtests @@ -122,8 +120,7 @@ test to fail. ## Skipping tests Individual tests can be skipped by passing the `skip` option to the test, or by -calling the test context's `skip()` method. Both of these options support -including a message that is displayed in the TAP output as shown in the +calling the test context's `skip()` method as shown in the following example. ```js @@ -258,7 +255,7 @@ Test name patterns do not change the set of files that the test runner executes. ## Extraneous asynchronous activity -Once a test function finishes executing, the TAP results are output as quickly +Once a test function finishes executing, the results are reported as quickly as possible while maintaining the order of the tests. However, it is possible for the test function to generate asynchronous activity that outlives the test itself. The test runner handles this type of activity, but does not delay the @@ -267,13 +264,13 @@ reporting of test results in order to accommodate it. In the following example, a test completes with two `setImmediate()` operations still outstanding. The first `setImmediate()` attempts to create a new subtest. Because the parent test has already finished and output its -results, the new subtest is immediately marked as failed, and reported in the -top level of the file's TAP output. +results, the new subtest is immediately marked as failed, and reported later +to the {TestsStream}. The second `setImmediate()` creates an `uncaughtException` event. `uncaughtException` and `unhandledRejection` events originating from a completed test are marked as failed by the `test` module and reported as diagnostic -warnings in the top level of the file's TAP output. +warnings at the top level by the {TestsStream}. ```js test('a test that creates asynchronous activity', (t) => { @@ -454,6 +451,166 @@ test('spies on an object method', (t) => { }); ``` +## Test reporters + + + +The `node:test` module supports passing [`--test-reporter`][] +flags for the test runner to use a specific reporter. + +The following built-reporters are supported: + +* `tap` + The `tap` reporter is the default reporter used by the test runner. It outputs + the test results in the [TAP][] format. + +* `spec` + The `spec` reporter outputs the test results in a human-readable format. + +* `dot` + The `dot` reporter outputs the test results in a comact format, + where each passing test is represented by a `.`, + and each failing test is represented by a `X`. + +### Custom reporters + +[`--test-reporter`][] can be used to specify a path to custom reporter. +a custom reporter is a module that exports a value +accepted by [stream.compose][]. +Reporters should transform events emitted by a {TestsStream} + +Example of a custom reporter using {stream.Transform}: + +```mjs +import { Transform } from 'node:stream'; + +const customReporter = new Transform({ + writableObjectMode: true, + transform(event, encoding, callback) { + switch (event.type) { + case 'test:start': + callback(null, `test ${event.data.name} started`); + break; + case 'test:pass': + callback(null, `test ${event.data.name} passed`); + break; + case 'test:fail': + callback(null, `test ${event.data.name} failed`); + break; + case 'test:plan': + callback(null, 'test plan'); + break; + case 'test:diagnostic': + callback(null, event.data.message); + break; + } + }, +}); + +export default customReporter; +``` + +```cjs +const { Transform } = require('node:stream'); + +const customReporter = new Transform({ + writableObjectMode: true, + transform(event, encoding, callback) { + switch (event.type) { + case 'test:start': + callback(null, `test ${event.data.name} started`); + break; + case 'test:pass': + callback(null, `test ${event.data.name} passed`); + break; + case 'test:fail': + callback(null, `test ${event.data.name} failed`); + break; + case 'test:plan': + callback(null, 'test plan'); + break; + case 'test:diagnostic': + callback(null, event.data.message); + break; + } + }, +}); + +module.exports = customReporter; +``` + +Example of a custom reporter using a generator function: + +```mjs +export default async function * customReporter(source) { + for await (const event of source) { + switch (event.type) { + case 'test:start': + yield `test ${event.data.name} started\n`; + break; + case 'test:pass': + yield `test ${event.data.name} passed\n`; + break; + case 'test:fail': + yield `test ${event.data.name} failed\n`; + break; + case 'test:plan': + yield 'test plan'; + break; + case 'test:diagnostic': + yield `${event.data.message}\n`; + break; + } + } +} +``` + +```cjs +module.exports = async function * customReporter(source) { + for await (const event of source) { + switch (event.type) { + case 'test:start': + yield `test ${event.data.name} started\n`; + break; + case 'test:pass': + yield `test ${event.data.name} passed\n`; + break; + case 'test:fail': + yield `test ${event.data.name} failed\n`; + break; + case 'test:plan': + yield 'test plan\n'; + break; + case 'test:diagnostic': + yield `${event.data.message}\n`; + break; + } + } +}; +``` + +### Multiple reporters + +The [`--test-reporter`][] flag can be specified multiple times to report test +results in several formats. In this situation +it is required to specify a destination for each reporter +using [`--test-reporter-destination`][]. +Destination can be `stdout`, `stderr`, or a file path. +Reporters and destinations are paired according +to the order they were specified. + +In the following example, the `spec` reporter will output to `stdout`, +and the `dot` reporter will output to `file.txt`: + +```bash +node --test-reporter=spec --test-reporter=dot --test-reporter-destination=stdout --test-reporter-destination=file.txt +``` + +When a single reporter is specified, the destination will default to `stdout`, +unless a destination is explicitly provided. + ## `run([options])` -* `message` {string} Message to be displayed as a TAP diagnostic. +* `message` {string} Message to be reported. -This function is used to write TAP diagnostics to the output. Any diagnostic +This function is used to write diagnostics to the output. Any diagnostic information is included at the end of the test's results. This function does not return a value. @@ -1245,10 +1421,10 @@ test('top level test', async (t) => { added: v18.0.0 --> -* `message` {string} Optional skip message to be displayed in TAP output. +* `message` {string} Optional skip message. This function causes the test's output to indicate the test as skipped. If -`message` is provided, it is included in the TAP output. Calling `skip()` does +`message` is provided, it is included in the output. Calling `skip()` does not terminate execution of the test function. This function does not return a value. @@ -1265,10 +1441,10 @@ test('top level test', (t) => { added: v18.0.0 --> -* `message` {string} Optional `TODO` message to be displayed in TAP output. +* `message` {string} Optional `TODO` message. This function adds a `TODO` directive to the test's output. If `message` is -provided, it is included in the TAP output. Calling `todo()` does not terminate +provided, it is included in the output. Calling `todo()` does not terminate execution of the test function. This function does not return a value. ```js @@ -1363,6 +1539,8 @@ added: v18.7.0 [TAP]: https://testanything.org/ [`--test-name-pattern`]: cli.md#--test-name-pattern [`--test-only`]: cli.md#--test-only +[`--test-reporter-destination`]: cli.md#--test-reporter-destination +[`--test-reporter`]: cli.md#--test-reporter [`--test`]: cli.md#--test [`MockFunctionContext`]: #class-mockfunctioncontext [`MockTracker.method`]: #mockmethodobject-methodname-implementation-options @@ -1376,4 +1554,5 @@ added: v18.7.0 [`test()`]: #testname-options-fn [describe options]: #describename-options-fn [it options]: #testname-options-fn +[stream.compose]: stream.md#streamcomposestreams [test runner execution model]: #test-runner-execution-model diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index 3ad34562a94b0e..cefe2f9fad0ce8 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -6,6 +6,7 @@ const { const { getOptionValue } = require('internal/options'); const { isUsingInspector } = require('internal/util/inspector'); const { run } = require('internal/test_runner/runner'); +const { setupTestReporters } = require('internal/test_runner/utils'); prepareMainThreadExecution(false); markBootstrapComplete(); @@ -20,8 +21,8 @@ if (isUsingInspector()) { inspectPort = process.debugPort; } -const tapStream = run({ concurrency, inspectPort, watch: getOptionValue('--watch') }); -tapStream.pipe(process.stdout); -tapStream.once('test:fail', () => { +const testsStream = run({ concurrency, inspectPort, watch: getOptionValue('--watch') }); +testsStream.once('test:fail', () => { process.exitCode = 1; }); +setupTestReporters(testsStream); diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 5a50d5d6afab6e..a9ec80b4c92adc 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -2,12 +2,12 @@ const { ObjectCreate, - StringPrototypeEndsWith, } = primordials; const CJSLoader = require('internal/modules/cjs/loader'); -const { Module, toRealPath, readPackageScope } = CJSLoader; +const { Module, toRealPath } = CJSLoader; const { getOptionValue } = require('internal/options'); const path = require('path'); +const { shouldUseESMLoader } = require('internal/modules/utils'); const { handleProcessExit, } = require('internal/modules/esm/handle_process_exit'); @@ -27,27 +27,6 @@ function resolveMainPath(main) { return mainPath; } -function shouldUseESMLoader(mainPath) { - /** - * @type {string[]} userLoaders A list of custom loaders registered by the user - * (or an empty list when none have been registered). - */ - const userLoaders = getOptionValue('--experimental-loader'); - if (userLoaders.length > 0) - return true; - const esModuleSpecifierResolution = - getOptionValue('--experimental-specifier-resolution'); - if (esModuleSpecifierResolution === 'node') - return true; - // Determine the module format of the main - if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) - return true; - if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) - return false; - const pkg = readPackageScope(mainPath); - return pkg && pkg.data.type === 'module'; -} - function runMainESM(mainPath) { const { loadESM } = require('internal/process/esm_loader'); const { pathToFileURL } = require('internal/url'); diff --git a/lib/internal/modules/utils.js b/lib/internal/modules/utils.js new file mode 100644 index 00000000000000..d2ba0a352f7571 --- /dev/null +++ b/lib/internal/modules/utils.js @@ -0,0 +1,54 @@ +'use strict'; + +const { + ObjectCreate, + StringPrototypeEndsWith, +} = primordials; +const { getOptionValue } = require('internal/options'); + + +function shouldUseESMLoader(mainPath) { + /** + * @type {string[]} userLoaders A list of custom loaders registered by the user + * (or an empty list when none have been registered). + */ + const userLoaders = getOptionValue('--experimental-loader'); + if (userLoaders.length > 0) + return true; + const esModuleSpecifierResolution = + getOptionValue('--experimental-specifier-resolution'); + if (esModuleSpecifierResolution === 'node') + return true; + // Determine the module format of the main + if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) + return true; + if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) + return false; + const { readPackageScope } = require('internal/modules/cjs/loader'); + const pkg = readPackageScope(mainPath); + return pkg && pkg.data.type === 'module'; +} + +/** + * @param {string} filePath + * @returns {any} + * requireOrImport imports a module if the file is an ES module, otherwise it requires it. + */ +function requireOrImport(filePath) { + const useESMLoader = shouldUseESMLoader(filePath); + if (useESMLoader) { + const { esmLoader } = require('internal/process/esm_loader'); + const { pathToFileURL } = require('internal/url'); + const { isAbsolute } = require('path'); + const file = isAbsolute(filePath) ? pathToFileURL(filePath).href : filePath; + return esmLoader.import(file, undefined, ObjectCreate(null)); + } + const { Module } = require('internal/modules/cjs/loader'); + + return new Module._load(filePath, null, false); +} + +module.exports = { + shouldUseESMLoader, + requireOrImport, +}; diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index e6af310f9ea623..0bae82d8e7417c 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -16,6 +16,7 @@ const { const { kEmptyObject } = require('internal/util'); const { getOptionValue } = require('internal/options'); const { kCancelledByParent, Test, ItTest, Suite } = require('internal/test_runner/test'); +const { setupTestReporters } = require('internal/test_runner/utils'); const { bigint: hrtime } = process.hrtime; const isTestRunnerCli = getOptionValue('--test'); @@ -107,7 +108,6 @@ function setup(root) { } root.startTime = hrtime(); - root.reporter.version(); wasRootSetup.add(root); return root; @@ -117,10 +117,10 @@ let globalRoot; function getGlobalRoot() { if (!globalRoot) { globalRoot = createTestTree(); - globalRoot.reporter.pipe(process.stdout); globalRoot.reporter.once('test:fail', () => { process.exitCode = 1; }); + setupTestReporters(globalRoot.reporter); } return globalRoot; } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 1e5c0dda0fabd9..4659073bfb61b4 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -6,6 +6,7 @@ const { ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypeSlice, + ArrayPrototypeSome, ArrayPrototypeSort, ObjectAssign, PromisePrototypeThen, @@ -14,7 +15,7 @@ const { SafePromiseAllSettledReturnVoid, SafeMap, SafeSet, - StringPrototypeRepeat, + StringPrototypeStartsWith, } = primordials; const { spawn } = require('child_process'); @@ -32,9 +33,9 @@ const { validateArray, validateBoolean } = require('internal/validators'); const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector'); const { kEmptyObject } = require('internal/util'); const { createTestTree } = require('internal/test_runner/harness'); -const { kDefaultIndent, kSubtestsFailed, Test } = require('internal/test_runner/test'); +const { kSubtestsFailed, Test } = require('internal/test_runner/test'); const { TapParser } = require('internal/test_runner/tap_parser'); -const { YAMLToJs } = require('internal/test_runner/yaml_parser'); +const { YAMLToJs } = require('internal/test_runner/yaml_to_js'); const { TokenKind } = require('internal/test_runner/tap_lexer'); const { @@ -48,6 +49,7 @@ const { } = internalBinding('errors'); const kFilterArgs = ['--test', '--watch']; +const kFilterArgValues = ['--test-reporter', '--test-reporter-destination']; // TODO(cjihrig): Replace this with recursive readdir once it lands. function processPath(path, testFiles, options) { @@ -111,8 +113,9 @@ function createTestFileList() { return ArrayPrototypeSort(ArrayFrom(testFiles)); } -function filterExecArgv(arg) { - return !ArrayPrototypeIncludes(kFilterArgs, arg); +function filterExecArgv(arg, i, arr) { + return !ArrayPrototypeIncludes(kFilterArgs, arg) && + !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); } function getRunArgs({ path, inspectPort }) { @@ -127,7 +130,7 @@ function getRunArgs({ path, inspectPort }) { class FileTest extends Test { #buffer = []; #handleReportItem({ kind, node, nesting = 0 }) { - const indent = StringPrototypeRepeat(kDefaultIndent, nesting + 1); + nesting += 1; switch (kind) { case TokenKind.TAP_VERSION: @@ -136,11 +139,11 @@ class FileTest extends Test { break; case TokenKind.TAP_PLAN: - this.reporter.plan(indent, node.end - node.start + 1); + this.reporter.plan(nesting, node.end - node.start + 1); break; case TokenKind.TAP_SUBTEST_POINT: - this.reporter.subtest(indent, node.name); + this.reporter.start(nesting, node.name); break; case TokenKind.TAP_TEST_POINT: @@ -159,7 +162,7 @@ class FileTest extends Test { if (pass) { this.reporter.ok( - indent, + nesting, node.id, node.description, YAMLToJs(node.diagnostics), @@ -167,7 +170,7 @@ class FileTest extends Test { ); } else { this.reporter.fail( - indent, + nesting, node.id, node.description, YAMLToJs(node.diagnostics), @@ -177,15 +180,15 @@ class FileTest extends Test { break; case TokenKind.COMMENT: - if (indent === kDefaultIndent) { + if (nesting === 1) { // Ignore file top level diagnostics break; } - this.reporter.diagnostic(indent, node.comment); + this.reporter.diagnostic(nesting, node.comment); break; case TokenKind.UNKNOWN: - this.reporter.diagnostic(indent, node.value); + this.reporter.diagnostic(nesting, node.value); break; } } @@ -194,11 +197,11 @@ class FileTest extends Test { ArrayPrototypePush(this.#buffer, ast); return; } - this.reportSubtest(); + this.reportStarted(); this.#handleReportItem(ast); } report() { - this.reportSubtest(); + this.reportStarted(); ArrayPrototypeForEach(this.#buffer, (ast) => this.#handleReportItem(ast)); super.report(); } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 8b0ba16f1a6a79..14ddb96d1155be 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -33,7 +33,7 @@ const { } = require('internal/errors'); const { getOptionValue } = require('internal/options'); const { MockTracker } = require('internal/test_runner/mock'); -const { TapStream } = require('internal/test_runner/tap_stream'); +const { TestsStream } = require('internal/test_runner/tests_stream'); const { convertStringToRegExp, createDeferredCallback, @@ -63,7 +63,6 @@ const kTestCodeFailure = 'testCodeFailure'; const kTestTimeoutFailure = 'testTimeoutFailure'; const kHookFailure = 'hookFailed'; const kDefaultTimeout = null; -const kDefaultIndent = ' '; // 4 spaces const noop = FunctionPrototype; const isTestRunner = getOptionValue('--test'); const testOnlyFlag = !isTestRunner && getOptionValue('--test-only'); @@ -190,18 +189,18 @@ class Test extends AsyncResource { if (parent === null) { this.concurrency = 1; - this.indent = ''; + this.nesting = 0; this.only = testOnlyFlag; - this.reporter = new TapStream(); + this.reporter = new TestsStream(); this.runOnlySubtests = this.only; this.testNumber = 0; this.timeout = kDefaultTimeout; } else { - const indent = parent.parent === null ? parent.indent : - parent.indent + kDefaultIndent; + const nesting = parent.parent === null ? parent.nesting : + parent.nesting + 1; this.concurrency = parent.concurrency; - this.indent = indent; + this.nesting = nesting; this.only = only ?? !parent.runOnlySubtests; this.reporter = parent.reporter; this.runOnlySubtests = !this.only; @@ -334,7 +333,7 @@ class Test extends AsyncResource { } if (i === 1 && this.parent !== null) { - this.reportSubtest(); + this.reportStarted(); } // Report the subtest's results and remove it from the ready map. @@ -633,19 +632,19 @@ class Test extends AsyncResource { this.parent.processPendingSubtests(); } else if (!this.reported) { this.reported = true; - this.reporter.plan(this.indent, this.subtests.length); + this.reporter.plan(this.nesting, this.subtests.length); for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.indent, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, this.diagnostics[i]); } - this.reporter.diagnostic(this.indent, `tests ${this.subtests.length}`); - this.reporter.diagnostic(this.indent, `pass ${counters.passed}`); - this.reporter.diagnostic(this.indent, `fail ${counters.failed}`); - this.reporter.diagnostic(this.indent, `cancelled ${counters.cancelled}`); - this.reporter.diagnostic(this.indent, `skipped ${counters.skipped}`); - this.reporter.diagnostic(this.indent, `todo ${counters.todo}`); - this.reporter.diagnostic(this.indent, `duration_ms ${this.#duration()}`); + this.reporter.diagnostic(this.nesting, `tests ${this.subtests.length}`); + this.reporter.diagnostic(this.nesting, `pass ${counters.passed}`); + this.reporter.diagnostic(this.nesting, `fail ${counters.failed}`); + this.reporter.diagnostic(this.nesting, `cancelled ${counters.cancelled}`); + this.reporter.diagnostic(this.nesting, `skipped ${counters.skipped}`); + this.reporter.diagnostic(this.nesting, `todo ${counters.todo}`); + this.reporter.diagnostic(this.nesting, `duration_ms ${this.#duration()}`); this.reporter.push(null); } } @@ -681,9 +680,9 @@ class Test extends AsyncResource { report() { if (this.subtests.length > 0) { - this.reporter.plan(this.subtests[0].indent, this.subtests.length); + this.reporter.plan(this.subtests[0].nesting, this.subtests.length); } else { - this.reportSubtest(); + this.reportStarted(); } let directive; const details = { __proto__: null, duration_ms: this.#duration() }; @@ -695,24 +694,24 @@ class Test extends AsyncResource { } if (this.passed) { - this.reporter.ok(this.indent, this.testNumber, this.name, details, directive); + this.reporter.ok(this.nesting, this.testNumber, this.name, details, directive); } else { details.error = this.error; - this.reporter.fail(this.indent, this.testNumber, this.name, details, directive); + this.reporter.fail(this.nesting, this.testNumber, this.name, details, directive); } for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.indent, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, this.diagnostics[i]); } } - reportSubtest() { + reportStarted() { if (this.#reportedSubtest || this.parent === null) { return; } this.#reportedSubtest = true; - this.parent.reportSubtest(); - this.reporter.subtest(this.indent, this.name); + this.parent.reportStarted(); + this.reporter.start(this.nesting, this.name); } } @@ -817,7 +816,6 @@ class Suite extends Test { module.exports = { ItTest, kCancelledByParent, - kDefaultIndent, kSubtestsFailed, kTestCodeFailure, kUnwrapErrors, diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js new file mode 100644 index 00000000000000..b016d316154807 --- /dev/null +++ b/lib/internal/test_runner/tests_stream.js @@ -0,0 +1,74 @@ +'use strict'; +const { + ArrayPrototypePush, + ArrayPrototypeShift, +} = primordials; +const Readable = require('internal/streams/readable'); + +class TestsStream extends Readable { + #buffer; + #canPush; + + constructor() { + super({ objectMode: true }); + this.#buffer = []; + this.#canPush = true; + } + + _read() { + this.#canPush = true; + + while (this.#buffer.length > 0) { + const obj = ArrayPrototypeShift(this.#buffer); + + if (!this.#tryPush(obj)) { + return; + } + } + } + + fail(nesting, testNumber, name, details, directive) { + this.#emit('test:fail', { __proto__: null, name, nesting, testNumber, details, ...directive }); + } + + ok(nesting, testNumber, name, details, directive) { + this.#emit('test:pass', { __proto__: null, name, nesting, testNumber, details, ...directive }); + } + + plan(nesting, count) { + this.#emit('test:plan', { __proto__: null, nesting, count }); + } + + getSkip(reason = undefined) { + return { __proto__: null, skip: reason ?? true }; + } + + getTodo(reason = undefined) { + return { __proto__: null, todo: reason ?? true }; + } + + start(nesting, name) { + this.#emit('test:start', { __proto__: null, nesting, name }); + } + + diagnostic(nesting, message) { + this.#emit('test:diagnostic', { __proto__: null, nesting, message }); + } + + #emit(type, data) { + this.emit(type, data); + this.#tryPush({ type, data }); + } + + #tryPush(message) { + if (this.#canPush) { + this.#canPush = this.push(message); + } else { + ArrayPrototypePush(this.#buffer, message); + } + + return this.#canPush; + } +} + +module.exports = { TestsStream }; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index ad040f010250e2..9dba00de25719e 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -1,7 +1,18 @@ 'use strict'; -const { RegExp, RegExpPrototypeExec } = primordials; +const { + ArrayPrototypePush, + ObjectGetOwnPropertyDescriptor, + SafePromiseAllReturnArrayLike, + RegExp, + RegExpPrototypeExec, + SafeMap, +} = primordials; const { basename } = require('path'); +const { createWriteStream } = require('fs'); const { createDeferredPromise } = require('internal/util'); +const { getOptionValue } = require('internal/options'); +const { requireOrImport } = require('internal/modules/utils'); + const { codes: { ERR_INVALID_ARG_VALUE, @@ -9,6 +20,7 @@ const { }, kIsNodeError, } = require('internal/errors'); +const { compose } = require('stream'); const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; const kRegExpPattern = /^\/(.*)\/([a-z]*)$/; @@ -74,10 +86,71 @@ function convertStringToRegExp(str, name) { } } +const kBuiltinDestinations = new SafeMap([ + ['stdout', process.stdout], + ['stderr', process.stderr], +]); + +const kBuiltinReporters = new SafeMap([ + ['spec', 'node:test/reporter/spec'], + ['dot', 'node:test/reporter/dot'], + ['tap', 'node:test/reporter/tap'], +]); + +const kDefaultReporter = 'tap'; +const kDefaultDestination = 'stdout'; + +async function getReportersMap(reporters, destinations) { + return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { + const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); + let reporter = await requireOrImport(kBuiltinReporters.get(name) ?? name); + + if (reporter?.default) { + reporter = reporter.default; + } + + if (reporter?.prototype && ObjectGetOwnPropertyDescriptor(reporter.prototype, 'constructor')) { + reporter = new reporter(); + } + + if (!reporter) { + throw new ERR_INVALID_ARG_VALUE('Reporter', name, 'is not a valid reporter'); + } + + return { __proto__: null, reporter, destination }; + }); +} + + +async function setupTestReporters(testsStream) { + const destinations = getOptionValue('--test-reporter-destination'); + const reporters = getOptionValue('--test-reporter'); + + if (reporters.length === 0 && destinations.length === 0) { + ArrayPrototypePush(reporters, kDefaultReporter); + } + + if (reporters.length === 1 && destinations.length === 0) { + ArrayPrototypePush(destinations, kDefaultDestination); + } + + if (destinations.length !== reporters.length) { + throw new ERR_INVALID_ARG_VALUE('--test-reporter', reporters, + 'must match the number of specified \'--test-reporter-destination\''); + } + + const reportersMap = await getReportersMap(reporters, destinations); + for (let i = 0; i < reportersMap.length; i++) { + const { reporter, destination } = reportersMap[i]; + compose(testsStream, reporter).pipe(destination); + } +} + module.exports = { convertStringToRegExp, createDeferredCallback, doesPathMatchFilter, isSupportedFileType, isTestFailureError, + setupTestReporters, }; diff --git a/lib/internal/test_runner/yaml_parser.js b/lib/internal/test_runner/yaml_to_js.js similarity index 100% rename from lib/internal/test_runner/yaml_parser.js rename to lib/internal/test_runner/yaml_to_js.js diff --git a/lib/internal/util/colors.js b/lib/internal/util/colors.js index 5622a88467d038..79021a2bd9825d 100644 --- a/lib/internal/util/colors.js +++ b/lib/internal/util/colors.js @@ -5,6 +5,7 @@ module.exports = { green: '', white: '', red: '', + gray: '', clear: '', hasColors: false, refresh() { @@ -14,6 +15,7 @@ module.exports = { module.exports.green = hasColors ? '\u001b[32m' : ''; module.exports.white = hasColors ? '\u001b[39m' : ''; module.exports.red = hasColors ? '\u001b[31m' : ''; + module.exports.gray = hasColors ? '\u001b[90m' : ''; module.exports.clear = hasColors ? '\u001bc' : ''; module.exports.hasColors = hasColors; } diff --git a/lib/test/reporter/dot.js b/lib/test/reporter/dot.js new file mode 100644 index 00000000000000..7dbba5a957894e --- /dev/null +++ b/lib/test/reporter/dot.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = async function* dot(source) { + let count = 0; + for await (const { type } of source) { + if (type === 'test:pass') { + yield '.'; + } + if (type === 'test:fail') { + yield 'X'; + } + if ((type === 'test:fail' || type === 'test:pass') && ++count % 20 === 0) { + yield '\n'; + } + } + yield '\n'; +}; diff --git a/lib/test/reporter/spec.js b/lib/test/reporter/spec.js new file mode 100644 index 00000000000000..c19d5568d1c5ca --- /dev/null +++ b/lib/test/reporter/spec.js @@ -0,0 +1,107 @@ +'use strict'; + +const { + ArrayPrototypeJoin, + ArrayPrototypePop, + ArrayPrototypeShift, + ArrayPrototypeUnshift, + hardenRegExp, + RegExpPrototypeSymbolSplit, + SafeMap, + StringPrototypeRepeat, +} = primordials; +const assert = require('assert'); +const Transform = require('internal/streams/transform'); +const { inspectWithNoCustomRetry } = require('internal/errors'); +const { green, blue, red, white, gray } = require('internal/util/colors'); + + +const inspectOptions = { __proto__: null, colors: true, breakLength: Infinity }; + +const colors = { + '__proto__': null, + 'test:fail': red, + 'test:pass': green, + 'test:diagnostic': blue, +}; +const symbols = { + '__proto__': null, + 'test:fail': '\u2716 ', + 'test:pass': '\u2714 ', + 'test:diagnostic': '\u2139 ', + 'arrow:right': '\u25B6 ', +}; +class SpecReporter extends Transform { + #stack = []; + #reported = []; + #indentMemo = new SafeMap(); + + constructor() { + super({ writableObjectMode: true }); + } + + #indent(nesting) { + let value = this.#indentMemo.get(nesting); + if (value === undefined) { + value = StringPrototypeRepeat(' ', nesting); + this.#indentMemo.set(nesting, value); + } + + return value; + } + #formatError(error, indent) { + if (!error) return ''; + const err = error.code === 'ERR_TEST_FAILURE' ? error.cause : error; + const message = ArrayPrototypeJoin( + RegExpPrototypeSymbolSplit( + hardenRegExp(/\r?\n/), + inspectWithNoCustomRetry(err, inspectOptions), + ), `\n${indent} `); + return `\n${indent} ${message}\n`; + } + #handleEvent({ type, data }) { + const color = colors[type] ?? white; + const symbol = symbols[type] ?? ' '; + + switch (type) { + case 'test:fail': + case 'test:pass': { + const subtest = ArrayPrototypeShift(this.#stack); // This is the matching `test:start` event + if (subtest) { + assert(subtest.type === 'test:start'); + assert(subtest.data.nesting === data.nesting); + assert(subtest.data.name === data.name); + } + let prefix = ''; + while (this.#stack.length) { + // Report all the parent `test:start` events + const parent = ArrayPrototypePop(this.#stack); + assert(parent.type === 'test:start'); + const msg = parent.data; + ArrayPrototypeUnshift(this.#reported, msg); + prefix += `${this.#indent(msg.nesting)}${symbols['arrow:right']}${msg.name}\n`; + } + const indent = this.#indent(data.nesting); + const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : ''; + const title = `${data.name}${duration_ms}`; + if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) { + // If this test has had children - it was already reporter, so slightly modify the output + ArrayPrototypeShift(this.#reported); + return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n\n`; + } + const error = this.#formatError(data.details?.error, indent); + return `${prefix}${indent}${color}${symbol}${title}${error}${white}\n`; + } + case 'test:start': + ArrayPrototypeUnshift(this.#stack, { __proto__: null, data, type }); + break; + case 'test:diagnostic': + return `${color}${this.#indent(data.nesting)}${symbol}${data.message}${white}\n`; + } + } + _transform({ type, data }, encoding, callback) { + callback(null, this.#handleEvent({ type, data })); + } +} + +module.exports = SpecReporter; diff --git a/lib/internal/test_runner/tap_stream.js b/lib/test/reporter/tap.js similarity index 63% rename from lib/internal/test_runner/tap_stream.js rename to lib/test/reporter/tap.js index 052f8284c8d931..fa5d4684fbb9e3 100644 --- a/lib/internal/test_runner/tap_stream.js +++ b/lib/test/reporter/tap.js @@ -2,18 +2,17 @@ const { ArrayPrototypeForEach, ArrayPrototypeJoin, - ArrayPrototypeMap, ArrayPrototypePush, - ArrayPrototypeShift, ObjectEntries, + RegExpPrototypeSymbolReplace, + SafeMap, StringPrototypeReplaceAll, - StringPrototypeToUpperCase, StringPrototypeSplit, - RegExpPrototypeSymbolReplace, + StringPrototypeRepeat, } = primordials; const { inspectWithNoCustomRetry } = require('internal/errors'); -const Readable = require('internal/streams/readable'); const { isError, kEmptyObject } = require('internal/util'); +const kDefaultIndent = ' '; // 4 spaces const kFrameStartRegExp = /^ {4}at /; const kLineBreakRegExp = /\n|\r\n/; const kDefaultTAPVersion = 13; @@ -22,112 +21,77 @@ let testModule; // Lazy loaded due to circular dependency. function lazyLoadTest() { testModule ??= require('internal/test_runner/test'); - return testModule; } -class TapStream extends Readable { - #buffer; - #canPush; - - constructor() { - super(); - this.#buffer = []; - this.#canPush = true; - } - - _read() { - this.#canPush = true; - while (this.#buffer.length > 0) { - const line = ArrayPrototypeShift(this.#buffer); - - if (!this.#tryPush(line)) { - return; - } +async function * tapReporter(source) { + yield `TAP version ${kDefaultTAPVersion}\n`; + for await (const { type, data } of source) { + switch (type) { + case 'test:fail': + yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo); + yield reportDetails(data.nesting, data.details); + break; + case 'test:pass': + yield reportTest(data.nesting, data.testNumber, 'ok', data.name, data.skip, data.todo); + yield reportDetails(data.nesting, data.details); + break; + case 'test:plan': + yield `${indent(data.nesting)}1..${data.count}\n`; + break; + case 'test:start': + yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n`; + break; + case 'test:diagnostic': + yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; + break; } } +} - bail(message) { - this.#tryPush(`Bail out!${message ? ` ${tapEscape(message)}` : ''}\n`); - } - - fail(indent, testNumber, name, details, directive) { - this.emit('test:fail', { __proto__: null, name, testNumber, details, ...directive }); - this.#test(indent, testNumber, 'not ok', name, directive); - this.#details(indent, details); - } - - ok(indent, testNumber, name, details, directive) { - this.emit('test:pass', { __proto__: null, name, testNumber, details, ...directive }); - this.#test(indent, testNumber, 'ok', name, directive); - this.#details(indent, details); - } - - plan(indent, count, explanation) { - const exp = `${explanation ? ` # ${tapEscape(explanation)}` : ''}`; - - this.#tryPush(`${indent}1..${count}${exp}\n`); - } - - getSkip(reason) { - return { __proto__: null, skip: reason }; - } - - getTodo(reason) { - return { __proto__: null, todo: reason }; - } - - subtest(indent, name) { - this.#tryPush(`${indent}# Subtest: ${tapEscape(name)}\n`); - } - - #details(indent, data = kEmptyObject) { - const { error, duration_ms } = data; - let details = `${indent} ---\n`; +function reportTest(nesting, testNumber, status, name, skip, todo) { + let line = `${indent(nesting)}${status} ${testNumber}`; - details += jsToYaml(indent, 'duration_ms', duration_ms); - details += jsToYaml(indent, null, error); - details += `${indent} ...\n`; - this.#tryPush(details); + if (name) { + line += ` ${tapEscape(`- ${name}`)}`; } - diagnostic(indent, message) { - this.emit('test:diagnostic', message); - this.#tryPush(`${indent}# ${tapEscape(message)}\n`); + if (skip !== undefined) { + line += ` # SKIP${typeof skip === 'string' && skip.length ? ` ${tapEscape(skip)}` : ''}`; + } else if (todo !== undefined) { + line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`; } - version(spec = kDefaultTAPVersion) { - this.#tryPush(`TAP version ${spec}\n`); - } + line += '\n'; - #test(indent, testNumber, status, name, directive = kEmptyObject) { - let line = `${indent}${status} ${testNumber}`; + return line; +} - if (name) { - line += ` ${tapEscape(`- ${name}`)}`; - } - line += ArrayPrototypeJoin(ArrayPrototypeMap(ObjectEntries(directive), ({ 0: key, 1: value }) => ( - ` # ${StringPrototypeToUpperCase(key)}${value ? ` ${tapEscape(value)}` : ''}` - )), ''); +function reportDetails(nesting, data = kEmptyObject) { + const { error, duration_ms } = data; + const _indent = indent(nesting); + let details = `${_indent} ---\n`; - line += '\n'; + details += jsToYaml(_indent, 'duration_ms', duration_ms); + details += jsToYaml(_indent, null, error); + details += `${_indent} ...\n`; + return details; +} - this.#tryPush(line); +const memo = new SafeMap(); +function indent(nesting) { + let value = memo.get(nesting); + if (value === undefined) { + value = StringPrototypeRepeat(kDefaultIndent, nesting); + memo.set(nesting, value); } - #tryPush(message) { - if (this.#canPush) { - this.#canPush = this.push(message); - } else { - ArrayPrototypePush(this.#buffer, message); - } - - return this.#canPush; - } + return value; } + // In certain places, # and \ need to be escaped as \# and \\. function tapEscape(input) { let result = StringPrototypeReplaceAll(input, '\\', '\\\\'); @@ -266,4 +230,4 @@ function isAssertionLike(value) { return value && typeof value === 'object' && 'expected' in value && 'actual' in value; } -module.exports = { TapStream }; +module.exports = tapReporter; diff --git a/src/node_options.cc b/src/node_options.cc index a9b874b519ebcd..604b6006faf2e7 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -558,6 +558,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--test-name-pattern", "run tests whose name matches this regular expression", &EnvironmentOptions::test_name_pattern); + AddOption("--test-reporter", + "report test output using the given reporter", + &EnvironmentOptions::test_reporter); + AddOption("--test-reporter-destination", + "report given reporter to the given destination", + &EnvironmentOptions::test_reporter_destination); AddOption("--test-only", "run tests with 'only' option set", &EnvironmentOptions::test_only, diff --git a/src/node_options.h b/src/node_options.h index 2e4da9decf3b4b..052f57bce6fa6d 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -155,6 +155,8 @@ class EnvironmentOptions : public Options { std::string diagnostic_dir; bool test_runner = false; std::vector test_name_pattern; + std::vector test_reporter; + std::vector test_reporter_destination; bool test_only = false; bool test_udp_no_try_send = false; bool throw_deprecation = false; diff --git a/test/fixtures/test-runner/custom_reporters/custom.cjs b/test/fixtures/test-runner/custom_reporters/custom.cjs new file mode 100644 index 00000000000000..a3f653d11bb981 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/custom.cjs @@ -0,0 +1,17 @@ +const { Transform } = require('node:stream'); + +const customReporter = new Transform({ + writableObjectMode: true, + transform(event, encoding, callback) { + this.counters ??= {}; + this.counters[event.type] = (this.counters[event.type] ?? 0) + 1; + callback(); + }, + flush(callback) { + this.push('custom.cjs ') + this.push(JSON.stringify(this.counters)); + callback(); + } +}); + +module.exports = customReporter; diff --git a/test/fixtures/test-runner/custom_reporters/custom.js b/test/fixtures/test-runner/custom_reporters/custom.js new file mode 100644 index 00000000000000..62690f115b7ae1 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/custom.js @@ -0,0 +1,8 @@ +module.exports = async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "custom.js "; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/custom_reporters/custom.mjs b/test/fixtures/test-runner/custom_reporters/custom.mjs new file mode 100644 index 00000000000000..b202d770c6bf19 --- /dev/null +++ b/test/fixtures/test-runner/custom_reporters/custom.mjs @@ -0,0 +1,8 @@ +export default async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "custom.mjs "; + yield JSON.stringify(counters); +} diff --git a/test/fixtures/test-runner/reporters.js b/test/fixtures/test-runner/reporters.js new file mode 100644 index 00000000000000..ed7066023d1299 --- /dev/null +++ b/test/fixtures/test-runner/reporters.js @@ -0,0 +1,11 @@ +'use strict'; +const test = require('node:test'); + +test('nested', { concurrency: 4 }, async (t) => { + t.test('ok', () => {}); + t.test('failing', () => { + throw new Error('error'); + }); +}); + +test('top level', () => {}); diff --git a/test/message/test_runner_describe_it.out b/test/message/test_runner_describe_it.out index 199e834d6f65ae..87207aca71fafa 100644 --- a/test/message/test_runner_describe_it.out +++ b/test/message/test_runner_describe_it.out @@ -24,7 +24,6 @@ not ok 3 - sync fail todo # TODO * * * - * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -41,7 +40,6 @@ not ok 4 - sync fail todo with message # TODO this is a failing todo * * * - * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP @@ -73,7 +71,6 @@ not ok 8 - sync throw fail * * * - * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -100,7 +97,6 @@ not ok 11 - async throw fail * * * - * ... # Subtest: async skip fail not ok 12 - async skip fail @@ -132,7 +128,6 @@ not ok 13 - async assertion fail * * * - * ... # Subtest: resolve pass ok 14 - resolve pass @@ -154,7 +149,6 @@ not ok 15 - reject fail * * * - * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns @@ -620,7 +614,6 @@ not ok 58 - rejected thenable code: 'ERR_TEST_FAILURE' stack: |- * - * ... # Subtest: invalid subtest fail not ok 59 - invalid subtest fail diff --git a/test/message/test_runner_hooks.out b/test/message/test_runner_hooks.out index 6bb1705967d043..7c82e9ff292ad5 100644 --- a/test/message/test_runner_hooks.out +++ b/test/message/test_runner_hooks.out @@ -64,7 +64,6 @@ not ok 2 - before throws * * * - * ... # Subtest: after throws # Subtest: 1 @@ -93,7 +92,6 @@ not ok 3 - after throws * * * - * ... # Subtest: beforeEach throws # Subtest: 1 @@ -490,7 +488,6 @@ not ok 13 - t.after() is called if test body throws * * * - * ... # - after() called 1..13 diff --git a/test/message/test_runner_output.js b/test/message/test_runner_output.js index f75b11a848e9de..91caca3038e7b8 100644 --- a/test/message/test_runner_output.js +++ b/test/message/test_runner_output.js @@ -113,7 +113,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p1a = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 1000); + }, 100); }); return p1a; @@ -131,7 +131,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p1c = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 2000); + }, 200); }); return p1c; @@ -141,7 +141,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p1c = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 1500); + }, 150); }); return p1c; @@ -150,7 +150,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { const p0a = new Promise((resolve) => { setTimeout(() => { resolve(); - }, 3000); + }, 300); }); return p0a; @@ -159,7 +159,7 @@ test('level 0a', { concurrency: 4 }, async (t) => { test('top level', { concurrency: 2 }, async (t) => { t.test('+long running', async (t) => { return new Promise((resolve, reject) => { - setTimeout(resolve, 3000).unref(); + setTimeout(resolve, 300).unref(); }); }); @@ -331,12 +331,12 @@ test('subtest sync throw fails', async (t) => { test('timed out async test', { timeout: 5 }, async (t) => { return new Promise((resolve) => { - setTimeout(resolve, 1000); + setTimeout(resolve, 100); }); }); test('timed out callback test', { timeout: 5 }, (t, done) => { - setTimeout(done, 1000); + setTimeout(done, 100); }); diff --git a/test/message/test_runner_output.out b/test/message/test_runner_output.out index 14479c773bbc86..42eae979daf6dd 100644 --- a/test/message/test_runner_output.out +++ b/test/message/test_runner_output.out @@ -24,7 +24,6 @@ not ok 3 - sync fail todo # TODO * * * - * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -41,7 +40,6 @@ not ok 4 - sync fail todo with message # TODO this is a failing todo * * * - * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP @@ -74,7 +72,6 @@ not ok 8 - sync throw fail * * * - * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -101,7 +98,6 @@ not ok 11 - async throw fail * * * - * ... # Subtest: async skip fail not ok 12 - async skip fail # SKIP @@ -118,7 +114,6 @@ not ok 12 - async skip fail # SKIP * * * - * ... # Subtest: async assertion fail not ok 13 - async assertion fail @@ -142,7 +137,6 @@ not ok 13 - async assertion fail * * * - * ... # Subtest: resolve pass ok 14 - resolve pass @@ -164,7 +158,6 @@ not ok 15 - reject fail * * * - * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns diff --git a/test/message/test_runner_output_cli.js b/test/message/test_runner_output_cli.js index 1058d903c5fee4..5645f1afb1f3a2 100644 --- a/test/message/test_runner_output_cli.js +++ b/test/message/test_runner_output_cli.js @@ -3,4 +3,5 @@ require('../common'); const spawn = require('node:child_process').spawn; spawn(process.execPath, - ['--no-warnings', '--test', 'test/message/test_runner_output.js'], { stdio: 'inherit' }); + ['--no-warnings', '--test', '--test-reporter', 'tap', 'test/message/test_runner_output.js'], + { stdio: 'inherit' }); diff --git a/test/message/test_runner_output_cli.out b/test/message/test_runner_output_cli.out index b33d3e0fbf50b1..044610905755ca 100644 --- a/test/message/test_runner_output_cli.out +++ b/test/message/test_runner_output_cli.out @@ -25,7 +25,6 @@ TAP version 13 * * * - * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -42,7 +41,6 @@ TAP version 13 * * * - * ... # Subtest: sync skip pass ok 5 - sync skip pass # SKIP @@ -74,7 +72,6 @@ TAP version 13 * * * - * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -101,7 +98,6 @@ TAP version 13 * * * - * ... # Subtest: async skip fail not ok 12 - async skip fail # SKIP @@ -118,7 +114,6 @@ TAP version 13 * * * - * ... # Subtest: async assertion fail not ok 13 - async assertion fail @@ -142,7 +137,6 @@ TAP version 13 * * * - * ... # Subtest: resolve pass ok 14 - resolve pass @@ -164,7 +158,6 @@ TAP version 13 * * * - * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns diff --git a/test/message/test_runner_output_dot_reporter.js b/test/message/test_runner_output_dot_reporter.js new file mode 100644 index 00000000000000..8c36b9ba245425 --- /dev/null +++ b/test/message/test_runner_output_dot_reporter.js @@ -0,0 +1,6 @@ +// Flags: --no-warnings +'use strict'; +require('../common'); +const spawn = require('node:child_process').spawn; +spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'dot', 'test/message/test_runner_output.js'], { stdio: 'inherit' }); diff --git a/test/message/test_runner_output_dot_reporter.out b/test/message/test_runner_output_dot_reporter.out new file mode 100644 index 00000000000000..823ecfb146b991 --- /dev/null +++ b/test/message/test_runner_output_dot_reporter.out @@ -0,0 +1,4 @@ +..XX...X..XXX.X..... +XXX.....X..X...X.... +.........X...XXX.XX. +.....XXXXXXX...XXXX diff --git a/test/message/test_runner_output_spec_reporter.js b/test/message/test_runner_output_spec_reporter.js new file mode 100644 index 00000000000000..49d8d3f2293da1 --- /dev/null +++ b/test/message/test_runner_output_spec_reporter.js @@ -0,0 +1,10 @@ +// Flags: --no-warnings +'use strict'; +require('../common'); +const spawn = require('node:child_process').spawn; +const child = spawn(process.execPath, + ['--no-warnings', '--test-reporter', 'spec', 'test/message/test_runner_output.js'], + { stdio: 'pipe' }); +// eslint-disable-next-line no-control-regex +child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, ''))); +child.stderr.pipe(process.stderr); diff --git a/test/message/test_runner_output_spec_reporter.out b/test/message/test_runner_output_spec_reporter.out new file mode 100644 index 00000000000000..f7e2b7e66d800a --- /dev/null +++ b/test/message/test_runner_output_spec_reporter.out @@ -0,0 +1,280 @@ + sync pass todo (*ms) + sync pass todo with message (*ms) + sync fail todo (*ms) + Error: thrown from sync fail todo + * + * + * + * + * + * + * + + sync fail todo with message (*ms) + Error: thrown from sync fail todo with message + * + * + * + * + * + * + * + + sync skip pass (*ms) + sync skip pass with message (*ms) + sync pass (*ms) + this test should pass + sync throw fail (*ms) + Error: thrown from sync throw fail + * + * + * + * + * + * + * + + async skip pass (*ms) + async pass (*ms) + async throw fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async skip fail (*ms) + Error: thrown from async throw fail + * + * + * + * + * + * + * + + async assertion fail (*ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + * + * + * + * + * + * + * { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual' + } + + resolve pass (*ms) + reject fail (*ms) + Error: rejected from reject fail + * + * + * + * + * + * + * + + unhandled rejection - passes but warns (*ms) + async unhandled rejection - passes but warns (*ms) + immediate throw - passes but warns (*ms) + immediate reject - passes but warns (*ms) + immediate resolve pass (*ms) + subtest sync throw fail + +sync throw fail (*ms) + Error: thrown from subtest sync throw fail + * + * + * + * + * + * + * + * + * + * + + this subtest should make its parent test fail + subtest sync throw fail (*ms) + + sync throw non-error fail (*ms) + Symbol(thrown symbol from sync throw non-error fail) + + level 0a + level 1a (*ms) + level 1b (*ms) + level 1c (*ms) + level 1d (*ms) + level 0a (*ms) + + top level + +long running (*ms) + 'test did not finish before its parent and was cancelled' + + +short running + ++short running (*ms) + +short running (*ms) + + top level (*ms) + + invalid subtest - pass but subtest fails (*ms) + sync skip option (*ms) + sync skip option with message (*ms) + sync skip option is false fail (*ms) + Error: this should be executed + * + * + * + * + * + * + * + + (*ms) + functionOnly (*ms) + (*ms) + test with only a name provided (*ms) + (*ms) + (*ms) + test with a name and options provided (*ms) + functionAndOptions (*ms) + escaped description \ # * + * + (*ms) + escaped skip message (*ms) + escaped todo message (*ms) + escaped diagnostic (*ms) + #diagnostic + callback pass (*ms) + callback fail (*ms) + Error: callback failure + * + * + + sync t is this in test (*ms) + async t is this in test (*ms) + callback t is this in test (*ms) + callback also returns a Promise (*ms) + 'passed a callback but also returned a Promise' + + callback throw (*ms) + Error: thrown from callback throw + * + * + * + * + * + * + * + + callback called twice (*ms) + 'callback invoked multiple times' + + callback called twice in different ticks (*ms) + callback called twice in future tick (*ms) + Error [ERR_TEST_FAILURE]: callback invoked multiple times + * + failureType: 'multipleCallbackInvocations', + cause: 'callback invoked multiple times', + code: 'ERR_TEST_FAILURE' + } + + callback async throw (*ms) + Error: thrown from callback async throw + * + * + + callback async throw after done (*ms) + only is set but not in only mode + running subtest 1 (*ms) + running subtest 2 (*ms) + running subtest 3 (*ms) + running subtest 4 (*ms) + only is set but not in only mode (*ms) + + custom inspect symbol fail (*ms) + customized + + custom inspect symbol that throws fail (*ms) + { foo: 1, [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]] } + + subtest sync throw fails + sync throw fails at first (*ms) + Error: thrown from subtest sync throw fails at first + * + * + * + * + * + * + * + * + * + * + + sync throw fails at second (*ms) + Error: thrown from subtest sync throw fails at second + * + * + * + * + * + * + * + * + * + * + + subtest sync throw fails (*ms) + + timed out async test (*ms) + 'test timed out after *ms' + + timed out callback test (*ms) + 'test timed out after *ms' + + large timeout async test is ok (*ms) + large timeout callback test is ok (*ms) + successful thenable (*ms) + rejected thenable (*ms) + 'custom error' + + unfinished test with uncaughtException (*ms) + Error: foo + * + * + * + + unfinished test with unhandledRejection (*ms) + Error: bar + * + * + * + + invalid subtest fail (*ms) + 'test could not be started because its parent finished' + + Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. + Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. + Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. + Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. + tests 65 + pass 27 + fail 21 + cancelled 2 + skipped 10 + todo 5 + duration_ms * diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 73df0ab053b533..73dce999a5c351 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -75,6 +75,7 @@ const expectedModules = new Set([ 'NativeModule internal/mime', 'NativeModule internal/modules/cjs/helpers', 'NativeModule internal/modules/cjs/loader', + 'NativeModule internal/modules/utils', 'NativeModule internal/modules/esm/assert', 'NativeModule internal/modules/esm/create_dynamic_module', 'NativeModule internal/modules/esm/fetch_module', diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index 1833fa00f7f7ae..1c28c2439050fc 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -20,8 +20,7 @@ async function runAndKill(file) { }); const [code, signal] = await once(child, 'exit'); await finished(child.stdout); - assert.match(stdout, /not ok 1/); - assert.match(stdout, /# cancelled 1\n/); + assert.strictEqual(stdout, 'TAP version 13\n'); assert.strictEqual(signal, null); assert.strictEqual(code, 1); } diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js new file mode 100644 index 00000000000000..74cae3401e2843 --- /dev/null +++ b/test/parallel/test-runner-reporters.js @@ -0,0 +1,95 @@ +'use strict'; + +require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { describe, it } = require('node:test'); +const { spawnSync } = require('node:child_process'); +const assert = require('node:assert'); +const path = require('node:path'); +const fs = require('node:fs'); + +const testFile = fixtures.path('test-runner/reporters.js'); +tmpdir.refresh(); + +let tmpFiles = 0; +describe('node:test reporters', { concurrency: true }, () => { + it('should default to outputing TAP to stdout', async () => { + const child = spawnSync(process.execPath, ['--test', testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.match(child.stdout.toString(), /TAP version 13/); + assert.match(child.stdout.toString(), /ok 1 - ok/); + assert.match(child.stdout.toString(), /not ok 2 - failing/); + assert.match(child.stdout.toString(), /ok 2 - top level/); + }); + + it('should default destination to stdout when passing a single reporter', async () => { + const child = spawnSync(process.execPath, ['--test', '--test-reporter', 'dot', testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), '.XX.X\n'); + }); + + it('should throw when passing reporters without a destination', async () => { + const child = spawnSync(process.execPath, ['--test', '--test-reporter', 'dot', '--test-reporter', 'tap', testFile]); + assert.match(child.stderr.toString(), /The argument '--test-reporter' must match the number of specified '--test-reporter-destination'\. Received \[ 'dot', 'tap' \]/); + assert.strictEqual(child.stdout.toString(), ''); + }); + + it('should throw when passing a destination without a reporter', async () => { + const child = spawnSync(process.execPath, ['--test', '--test-reporter-destination', 'tap', testFile]); + assert.match(child.stderr.toString(), /The argument '--test-reporter' must match the number of specified '--test-reporter-destination'\. Received \[\]/); + assert.strictEqual(child.stdout.toString(), ''); + }); + + it('should support stdout as a destination', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'dot', '--test-reporter-destination', 'stdout', testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), '.XX.X\n'); + }); + + it('should support stderr as a destination', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'dot', '--test-reporter-destination', 'stderr', testFile]); + assert.strictEqual(child.stderr.toString(), '.XX.X\n'); + assert.strictEqual(child.stdout.toString(), ''); + }); + + it('should support a file as a destination', async () => { + const file = path.join(tmpdir.path, `${tmpFiles++}.out`); + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'dot', '--test-reporter-destination', file, testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), ''); + assert.strictEqual(fs.readFileSync(file, 'utf8'), '.XX.X\n'); + }); + + it('should support multiple reporters', async () => { + const file = path.join(tmpdir.path, `${tmpFiles++}.out`); + const file2 = path.join(tmpdir.path, `${tmpFiles++}.out`); + const child = spawnSync(process.execPath, + ['--test', + '--test-reporter', 'dot', '--test-reporter-destination', file, + '--test-reporter', 'spec', '--test-reporter-destination', file2, + '--test-reporter', 'tap', '--test-reporter-destination', 'stdout', + testFile]); + assert.match(child.stdout.toString(), /TAP version 13/); + assert.match(child.stdout.toString(), /# duration_ms/); + assert.strictEqual(fs.readFileSync(file, 'utf8'), '.XX.X\n'); + const file2Contents = fs.readFileSync(file2, 'utf8'); + assert.match(file2Contents, /▶ nested/); + assert.match(file2Contents, /✔ ok/); + assert.match(file2Contents, /✖ failing/); + }); + + ['js', 'cjs', 'mjs'].forEach((ext) => { + it(`should support a '${ext}' file as a custom reporter`, async () => { + const filename = `custom.${ext}`; + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', fixtures.path('test-runner/custom_reporters/', filename), + testFile]); + assert.strictEqual(child.stderr.toString(), ''); + assert.strictEqual(child.stdout.toString(), `${filename} {"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":7}`); + }); + }); +}); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 6ce7c8f5d97c5d..b6e0eee3b259ad 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -10,7 +10,6 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { it('should run with no tests', async () => { const stream = run({ files: [] }); - stream.setEncoding('utf8'); stream.on('test:fail', common.mustNotCall()); stream.on('test:pass', common.mustNotCall()); // eslint-disable-next-line no-unused-vars diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 0e886a4785cb19..146981fe1464d0 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -208,7 +208,7 @@ const customTypesMap = { 'Timeout': 'timers.html#class-timeout', 'Timer': 'timers.html#timers', - 'TapStream': 'test.html#class-tapstream', + 'TestsStream': 'test.html#class-testsstream', 'tls.SecureContext': 'tls.html#tlscreatesecurecontextoptions', 'tls.Server': 'tls.html#class-tlsserver', From 403df210aca8849779304eb81fa4b156cc17d163 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Thu, 22 Dec 2022 13:40:32 -0800 Subject: [PATCH 70/93] module: move test reporter loading Move the logic for handling --test-reporter out of the general module loader and into the test_runner subsystem. PR-URL: https://github.com/nodejs/node/pull/45923 Backport-PR-URL: https://github.com/nodejs/node/pull/46839 Reviewed-By: Moshe Atlow Reviewed-By: Benjamin Gruenbaum Reviewed-By: Colin Ihrig Reviewed-By: Jacob Smith Reviewed-By: Antoine du Hamel --- doc/api/test.md | 5 +- lib/internal/modules/run_main.js | 25 ++++++++- lib/internal/modules/utils.js | 54 ------------------- lib/internal/test_runner/utils.js | 15 +++++- .../node_modules/reporter-cjs/index.js | 8 +++ .../node_modules/reporter-cjs/package.json | 4 ++ .../node_modules/reporter-esm/index.mjs | 8 +++ .../node_modules/reporter-esm/package.json | 4 ++ test/parallel/test-bootstrap-modules.js | 1 - test/parallel/test-runner-reporters.js | 24 ++++++++- 10 files changed, 87 insertions(+), 61 deletions(-) delete mode 100644 lib/internal/modules/utils.js create mode 100644 test/fixtures/test-runner/node_modules/reporter-cjs/index.js create mode 100644 test/fixtures/test-runner/node_modules/reporter-cjs/package.json create mode 100644 test/fixtures/test-runner/node_modules/reporter-esm/index.mjs create mode 100644 test/fixtures/test-runner/node_modules/reporter-esm/package.json diff --git a/doc/api/test.md b/doc/api/test.md index 2f84d89c5a4043..464c73b292c8ec 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -470,7 +470,7 @@ The following built-reporters are supported: The `spec` reporter outputs the test results in a human-readable format. * `dot` - The `dot` reporter outputs the test results in a comact format, + The `dot` reporter outputs the test results in a compact format, where each passing test is represented by a `.`, and each failing test is represented by a `X`. @@ -591,6 +591,9 @@ module.exports = async function * customReporter(source) { }; ``` +The value provided to `--test-reporter` should be a string like one used in an +`import()` in JavaScript code. + ### Multiple reporters The [`--test-reporter`][] flag can be specified multiple times to report test diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index a9ec80b4c92adc..5a50d5d6afab6e 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -2,12 +2,12 @@ const { ObjectCreate, + StringPrototypeEndsWith, } = primordials; const CJSLoader = require('internal/modules/cjs/loader'); -const { Module, toRealPath } = CJSLoader; +const { Module, toRealPath, readPackageScope } = CJSLoader; const { getOptionValue } = require('internal/options'); const path = require('path'); -const { shouldUseESMLoader } = require('internal/modules/utils'); const { handleProcessExit, } = require('internal/modules/esm/handle_process_exit'); @@ -27,6 +27,27 @@ function resolveMainPath(main) { return mainPath; } +function shouldUseESMLoader(mainPath) { + /** + * @type {string[]} userLoaders A list of custom loaders registered by the user + * (or an empty list when none have been registered). + */ + const userLoaders = getOptionValue('--experimental-loader'); + if (userLoaders.length > 0) + return true; + const esModuleSpecifierResolution = + getOptionValue('--experimental-specifier-resolution'); + if (esModuleSpecifierResolution === 'node') + return true; + // Determine the module format of the main + if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) + return true; + if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) + return false; + const pkg = readPackageScope(mainPath); + return pkg && pkg.data.type === 'module'; +} + function runMainESM(mainPath) { const { loadESM } = require('internal/process/esm_loader'); const { pathToFileURL } = require('internal/url'); diff --git a/lib/internal/modules/utils.js b/lib/internal/modules/utils.js deleted file mode 100644 index d2ba0a352f7571..00000000000000 --- a/lib/internal/modules/utils.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const { - ObjectCreate, - StringPrototypeEndsWith, -} = primordials; -const { getOptionValue } = require('internal/options'); - - -function shouldUseESMLoader(mainPath) { - /** - * @type {string[]} userLoaders A list of custom loaders registered by the user - * (or an empty list when none have been registered). - */ - const userLoaders = getOptionValue('--experimental-loader'); - if (userLoaders.length > 0) - return true; - const esModuleSpecifierResolution = - getOptionValue('--experimental-specifier-resolution'); - if (esModuleSpecifierResolution === 'node') - return true; - // Determine the module format of the main - if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) - return true; - if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) - return false; - const { readPackageScope } = require('internal/modules/cjs/loader'); - const pkg = readPackageScope(mainPath); - return pkg && pkg.data.type === 'module'; -} - -/** - * @param {string} filePath - * @returns {any} - * requireOrImport imports a module if the file is an ES module, otherwise it requires it. - */ -function requireOrImport(filePath) { - const useESMLoader = shouldUseESMLoader(filePath); - if (useESMLoader) { - const { esmLoader } = require('internal/process/esm_loader'); - const { pathToFileURL } = require('internal/url'); - const { isAbsolute } = require('path'); - const file = isAbsolute(filePath) ? pathToFileURL(filePath).href : filePath; - return esmLoader.import(file, undefined, ObjectCreate(null)); - } - const { Module } = require('internal/modules/cjs/loader'); - - return new Module._load(filePath, null, false); -} - -module.exports = { - shouldUseESMLoader, - requireOrImport, -}; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 9dba00de25719e..3fc99ce37cc33a 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -1,6 +1,7 @@ 'use strict'; const { ArrayPrototypePush, + ObjectCreate, ObjectGetOwnPropertyDescriptor, SafePromiseAllReturnArrayLike, RegExp, @@ -9,9 +10,9 @@ const { } = primordials; const { basename } = require('path'); const { createWriteStream } = require('fs'); +const { pathToFileURL } = require('internal/url'); const { createDeferredPromise } = require('internal/util'); const { getOptionValue } = require('internal/options'); -const { requireOrImport } = require('internal/modules/utils'); const { codes: { @@ -103,7 +104,17 @@ const kDefaultDestination = 'stdout'; async function getReportersMap(reporters, destinations) { return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); - let reporter = await requireOrImport(kBuiltinReporters.get(name) ?? name); + + // Load the test reporter passed to --test-reporter + const reporterSpecifier = kBuiltinReporters.get(name) ?? name; + let parentURL; + try { + parentURL = pathToFileURL(process.cwd() + '/').href; + } catch { + parentURL = 'file:///'; + } + const { esmLoader } = require('internal/process/esm_loader'); + let reporter = await esmLoader.import(reporterSpecifier, parentURL, ObjectCreate(null)); if (reporter?.default) { reporter = reporter.default; diff --git a/test/fixtures/test-runner/node_modules/reporter-cjs/index.js b/test/fixtures/test-runner/node_modules/reporter-cjs/index.js new file mode 100644 index 00000000000000..d99cd29926e86e --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-cjs/index.js @@ -0,0 +1,8 @@ +module.exports = async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "package: reporter-cjs"; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/node_modules/reporter-cjs/package.json b/test/fixtures/test-runner/node_modules/reporter-cjs/package.json new file mode 100644 index 00000000000000..cf7db2b7eca767 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-cjs/package.json @@ -0,0 +1,4 @@ +{ + "name": "reporter-cjs", + "main": "index.js" +} diff --git a/test/fixtures/test-runner/node_modules/reporter-esm/index.mjs b/test/fixtures/test-runner/node_modules/reporter-esm/index.mjs new file mode 100644 index 00000000000000..0eb82dfe4502d8 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-esm/index.mjs @@ -0,0 +1,8 @@ +export default async function * customReporter(source) { + const counters = {}; + for await (const event of source) { + counters[event.type] = (counters[event.type] ?? 0) + 1; + } + yield "package: reporter-esm"; + yield JSON.stringify(counters); +}; diff --git a/test/fixtures/test-runner/node_modules/reporter-esm/package.json b/test/fixtures/test-runner/node_modules/reporter-esm/package.json new file mode 100644 index 00000000000000..60d6b3a97fd186 --- /dev/null +++ b/test/fixtures/test-runner/node_modules/reporter-esm/package.json @@ -0,0 +1,4 @@ +{ + "name": "reporter-esm", + "exports": "./index.mjs" +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 73dce999a5c351..73df0ab053b533 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -75,7 +75,6 @@ const expectedModules = new Set([ 'NativeModule internal/mime', 'NativeModule internal/modules/cjs/helpers', 'NativeModule internal/modules/cjs/loader', - 'NativeModule internal/modules/utils', 'NativeModule internal/modules/esm/assert', 'NativeModule internal/modules/esm/create_dynamic_module', 'NativeModule internal/modules/esm/fetch_module', diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js index 74cae3401e2843..671b6ac4432167 100644 --- a/test/parallel/test-runner-reporters.js +++ b/test/parallel/test-runner-reporters.js @@ -86,10 +86,32 @@ describe('node:test reporters', { concurrency: true }, () => { it(`should support a '${ext}' file as a custom reporter`, async () => { const filename = `custom.${ext}`; const child = spawnSync(process.execPath, - ['--test', '--test-reporter', fixtures.path('test-runner/custom_reporters/', filename), + ['--test', '--test-reporter', fixtures.fileURL('test-runner/custom_reporters/', filename), testFile]); assert.strictEqual(child.stderr.toString(), ''); assert.strictEqual(child.stdout.toString(), `${filename} {"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":7}`); }); }); + + it('should support a custom reporter from node_modules', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'reporter-cjs', 'reporters.js'], + { cwd: fixtures.path('test-runner') }); + assert.strictEqual(child.stderr.toString(), ''); + assert.match( + child.stdout.toString(), + /^package: reporter-cjs{"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":\d+}$/, + ); + }); + + it('should support a custom ESM reporter from node_modules', async () => { + const child = spawnSync(process.execPath, + ['--test', '--test-reporter', 'reporter-esm', 'reporters.js'], + { cwd: fixtures.path('test-runner') }); + assert.strictEqual(child.stderr.toString(), ''); + assert.match( + child.stdout.toString(), + /^package: reporter-esm{"test:start":5,"test:pass":2,"test:fail":3,"test:plan":3,"test:diagnostic":\d+}$/, + ); + }); }); From 79f4b426fe5e24e9e7a52c234b4e84690c7afee6 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Mon, 2 Jan 2023 23:22:54 +0200 Subject: [PATCH 71/93] test_runner: report `file` in test runner events PR-URL: https://github.com/nodejs/node/pull/46030 Backport-PR-URL: https://github.com/nodejs/node/pull/46839 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig --- doc/api/test.md | 10 +++++++ lib/internal/test_runner/runner.js | 10 ++++--- lib/internal/test_runner/test.js | 29 ++++++++++--------- lib/internal/test_runner/tests_stream.js | 20 ++++++------- .../test-runner/custom_reporters/custom.js | 6 ++++ 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 464c73b292c8ec..d5978408510c34 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1204,6 +1204,8 @@ object, streaming a series of events representing the execution of the tests. ### Event: `'test:diagnostic'` * `data` {Object} + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `message` {string} The diagnostic message. * `nesting` {number} The nesting level of the test. @@ -1215,6 +1217,8 @@ Emitted when [`context.diagnostic`][] is called. * `details` {Object} Additional execution metadata. * `duration` {number} The duration of the test in milliseconds. * `error` {Error} The error thrown by the test. + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. * `testNumber` {number} The ordinal number of the test. @@ -1228,6 +1232,8 @@ Emitted when a test fails. * `data` {Object} * `details` {Object} Additional execution metadata. * `duration` {number} The duration of the test in milliseconds. + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. * `testNumber` {number} The ordinal number of the test. @@ -1239,6 +1245,8 @@ Emitted when a test passes. ### Event: `'test:plan'` * `data` {Object} + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `nesting` {number} The nesting level of the test. * `count` {number} The number of subtests that have ran. @@ -1247,6 +1255,8 @@ Emitted when all subtests have completed for a given test. ### Event: `'test:start'` * `data` {Object} + * `file` {string|undefined} The path of the test file, + undefined if test is not ran through a file. * `name` {string} The test name. * `nesting` {number} The nesting level of the test. diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 4659073bfb61b4..94d7242bb3a433 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -139,11 +139,11 @@ class FileTest extends Test { break; case TokenKind.TAP_PLAN: - this.reporter.plan(nesting, node.end - node.start + 1); + this.reporter.plan(nesting, this.name, node.end - node.start + 1); break; case TokenKind.TAP_SUBTEST_POINT: - this.reporter.start(nesting, node.name); + this.reporter.start(nesting, this.name, node.name); break; case TokenKind.TAP_TEST_POINT: @@ -163,6 +163,7 @@ class FileTest extends Test { if (pass) { this.reporter.ok( nesting, + this.name, node.id, node.description, YAMLToJs(node.diagnostics), @@ -171,6 +172,7 @@ class FileTest extends Test { } else { this.reporter.fail( nesting, + this.name, node.id, node.description, YAMLToJs(node.diagnostics), @@ -184,11 +186,11 @@ class FileTest extends Test { // Ignore file top level diagnostics break; } - this.reporter.diagnostic(nesting, node.comment); + this.reporter.diagnostic(nesting, this.name, node.comment); break; case TokenKind.UNKNOWN: - this.reporter.diagnostic(nesting, node.value); + this.reporter.diagnostic(nesting, this.name, node.value); break; } } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 14ddb96d1155be..984a9a416bba03 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -74,6 +74,7 @@ const testNamePatterns = testNamePatternFlag?.length > 0 ? (re) => convertStringToRegExp(re, '--test-name-pattern') ) : null; const kShouldAbort = Symbol('kShouldAbort'); +const kFilename = process.argv?.[1]; const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']); const kUnwrapErrors = new SafeSet() .add(kTestCodeFailure).add(kHookFailure) @@ -632,19 +633,19 @@ class Test extends AsyncResource { this.parent.processPendingSubtests(); } else if (!this.reported) { this.reported = true; - this.reporter.plan(this.nesting, this.subtests.length); + this.reporter.plan(this.nesting, kFilename, this.subtests.length); for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.nesting, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]); } - this.reporter.diagnostic(this.nesting, `tests ${this.subtests.length}`); - this.reporter.diagnostic(this.nesting, `pass ${counters.passed}`); - this.reporter.diagnostic(this.nesting, `fail ${counters.failed}`); - this.reporter.diagnostic(this.nesting, `cancelled ${counters.cancelled}`); - this.reporter.diagnostic(this.nesting, `skipped ${counters.skipped}`); - this.reporter.diagnostic(this.nesting, `todo ${counters.todo}`); - this.reporter.diagnostic(this.nesting, `duration_ms ${this.#duration()}`); + this.reporter.diagnostic(this.nesting, kFilename, `tests ${this.subtests.length}`); + this.reporter.diagnostic(this.nesting, kFilename, `pass ${counters.passed}`); + this.reporter.diagnostic(this.nesting, kFilename, `fail ${counters.failed}`); + this.reporter.diagnostic(this.nesting, kFilename, `cancelled ${counters.cancelled}`); + this.reporter.diagnostic(this.nesting, kFilename, `skipped ${counters.skipped}`); + this.reporter.diagnostic(this.nesting, kFilename, `todo ${counters.todo}`); + this.reporter.diagnostic(this.nesting, kFilename, `duration_ms ${this.#duration()}`); this.reporter.push(null); } } @@ -680,7 +681,7 @@ class Test extends AsyncResource { report() { if (this.subtests.length > 0) { - this.reporter.plan(this.subtests[0].nesting, this.subtests.length); + this.reporter.plan(this.subtests[0].nesting, kFilename, this.subtests.length); } else { this.reportStarted(); } @@ -694,14 +695,14 @@ class Test extends AsyncResource { } if (this.passed) { - this.reporter.ok(this.nesting, this.testNumber, this.name, details, directive); + this.reporter.ok(this.nesting, kFilename, this.testNumber, this.name, details, directive); } else { details.error = this.error; - this.reporter.fail(this.nesting, this.testNumber, this.name, details, directive); + this.reporter.fail(this.nesting, kFilename, this.testNumber, this.name, details, directive); } for (let i = 0; i < this.diagnostics.length; i++) { - this.reporter.diagnostic(this.nesting, this.diagnostics[i]); + this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]); } } @@ -711,7 +712,7 @@ class Test extends AsyncResource { } this.#reportedSubtest = true; this.parent.reportStarted(); - this.reporter.start(this.nesting, this.name); + this.reporter.start(this.nesting, kFilename, this.name); } } diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index b016d316154807..2614cab16daf64 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -27,16 +27,16 @@ class TestsStream extends Readable { } } - fail(nesting, testNumber, name, details, directive) { - this.#emit('test:fail', { __proto__: null, name, nesting, testNumber, details, ...directive }); + fail(nesting, file, testNumber, name, details, directive) { + this.#emit('test:fail', { __proto__: null, name, nesting, file, testNumber, details, ...directive }); } - ok(nesting, testNumber, name, details, directive) { - this.#emit('test:pass', { __proto__: null, name, nesting, testNumber, details, ...directive }); + ok(nesting, file, testNumber, name, details, directive) { + this.#emit('test:pass', { __proto__: null, name, nesting, file, testNumber, details, ...directive }); } - plan(nesting, count) { - this.#emit('test:plan', { __proto__: null, nesting, count }); + plan(nesting, file, count) { + this.#emit('test:plan', { __proto__: null, nesting, file, count }); } getSkip(reason = undefined) { @@ -47,12 +47,12 @@ class TestsStream extends Readable { return { __proto__: null, todo: reason ?? true }; } - start(nesting, name) { - this.#emit('test:start', { __proto__: null, nesting, name }); + start(nesting, file, name) { + this.#emit('test:start', { __proto__: null, nesting, file, name }); } - diagnostic(nesting, message) { - this.#emit('test:diagnostic', { __proto__: null, nesting, message }); + diagnostic(nesting, file, message) { + this.#emit('test:diagnostic', { __proto__: null, nesting, file, message }); } #emit(type, data) { diff --git a/test/fixtures/test-runner/custom_reporters/custom.js b/test/fixtures/test-runner/custom_reporters/custom.js index 62690f115b7ae1..aa85eab14acff4 100644 --- a/test/fixtures/test-runner/custom_reporters/custom.js +++ b/test/fixtures/test-runner/custom_reporters/custom.js @@ -1,6 +1,12 @@ +const assert = require('assert'); +const path = require('path'); + module.exports = async function * customReporter(source) { const counters = {}; for await (const event of source) { + if (event.data.file) { + assert.strictEqual(event.data.file, path.resolve(__dirname, '../reporters.js')); + } counters[event.type] = (counters[event.type] ?? 0) + 1; } yield "custom.js "; From 0d999e373ace0a721dcc207910648836994c5b51 Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Fri, 6 Jan 2023 14:34:12 -0500 Subject: [PATCH 72/93] test_runner: make built in reporters internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates the test runner to make the built in test reporters internal modules. PR-URL: https://github.com/nodejs/node/pull/46092 Backport-PR-URL: https://github.com/nodejs/node/pull/46839 Reviewed-By: Tobias Nießen Reviewed-By: Antoine du Hamel Reviewed-By: Moshe Atlow --- .../test_runner}/reporter/dot.js | 0 .../test_runner}/reporter/spec.js | 0 .../test_runner}/reporter/tap.js | 0 lib/internal/test_runner/utils.js | 37 +++++++++++++------ 4 files changed, 26 insertions(+), 11 deletions(-) rename lib/{test => internal/test_runner}/reporter/dot.js (100%) rename lib/{test => internal/test_runner}/reporter/spec.js (100%) rename lib/{test => internal/test_runner}/reporter/tap.js (100%) diff --git a/lib/test/reporter/dot.js b/lib/internal/test_runner/reporter/dot.js similarity index 100% rename from lib/test/reporter/dot.js rename to lib/internal/test_runner/reporter/dot.js diff --git a/lib/test/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js similarity index 100% rename from lib/test/reporter/spec.js rename to lib/internal/test_runner/reporter/spec.js diff --git a/lib/test/reporter/tap.js b/lib/internal/test_runner/reporter/tap.js similarity index 100% rename from lib/test/reporter/tap.js rename to lib/internal/test_runner/reporter/tap.js diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 3fc99ce37cc33a..a7476c896648d5 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -93,28 +93,43 @@ const kBuiltinDestinations = new SafeMap([ ]); const kBuiltinReporters = new SafeMap([ - ['spec', 'node:test/reporter/spec'], - ['dot', 'node:test/reporter/dot'], - ['tap', 'node:test/reporter/tap'], + ['spec', 'internal/test_runner/reporter/spec'], + ['dot', 'internal/test_runner/reporter/dot'], + ['tap', 'internal/test_runner/reporter/tap'], ]); const kDefaultReporter = 'tap'; const kDefaultDestination = 'stdout'; +function tryBuiltinReporter(name) { + const builtinPath = kBuiltinReporters.get(name); + + if (builtinPath === undefined) { + return; + } + + return require(builtinPath); +} + async function getReportersMap(reporters, destinations) { return SafePromiseAllReturnArrayLike(reporters, async (name, i) => { const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); // Load the test reporter passed to --test-reporter - const reporterSpecifier = kBuiltinReporters.get(name) ?? name; - let parentURL; - try { - parentURL = pathToFileURL(process.cwd() + '/').href; - } catch { - parentURL = 'file:///'; + let reporter = tryBuiltinReporter(name); + + if (reporter === undefined) { + let parentURL; + + try { + parentURL = pathToFileURL(process.cwd() + '/').href; + } catch { + parentURL = 'file:///'; + } + + const { esmLoader } = require('internal/process/esm_loader'); + reporter = await esmLoader.import(name, parentURL, ObjectCreate(null)); } - const { esmLoader } = require('internal/process/esm_loader'); - let reporter = await esmLoader.import(reporterSpecifier, parentURL, ObjectCreate(null)); if (reporter?.default) { reporter = reporter.default; From 28a775b32fee59f51d39868b556535a0e32aee29 Mon Sep 17 00:00:00 2001 From: cjihrig Date: Wed, 21 Dec 2022 13:26:16 -0500 Subject: [PATCH 73/93] test_runner: add initial code coverage support This commit adds code coverage functionality to the node:test module. When node:test is used in conjunction with the new --test-coverage CLI flag, a coverage report is created when the test runner finishes. The coverage summary is forwarded to any test runner reporters so that the display can be customized as desired. This new functionality is compatible with the existing NODE_V8_COVERAGE environment variable as well. There are still several limitations, which will be addressed in subsequent pull requests: - Coverage is only reported for a single process. It is possible to merge coverage reports together. Once this is done, the --test flag will be supported as well. - Source maps are not currently supported. - Excluding specific files or directories from the coverage report is not currently supported. Node core modules and node_modules/ are excluded though. PR-URL: https://github.com/nodejs/node/pull/46017 Backport-PR-URL: https://github.com/nodejs/node/pull/46839 Reviewed-By: Moshe Atlow Reviewed-By: Geoffrey Booth --- doc/api/cli.md | 12 + doc/api/test.md | 87 ++++++ doc/node.1 | 3 + lib/internal/process/pre_execution.js | 43 +-- lib/internal/test_runner/coverage.js | 371 +++++++++++++++++++++++ lib/internal/test_runner/harness.js | 41 ++- lib/internal/test_runner/reporter/tap.js | 38 +++ lib/internal/test_runner/runner.js | 2 +- lib/internal/test_runner/test.js | 6 + lib/internal/test_runner/tests_stream.js | 4 + lib/internal/util.js | 22 ++ src/inspector_profiler.cc | 3 +- src/node_options.cc | 9 + src/node_options.h | 1 + test/fixtures/test-runner/coverage.js | 89 ++++++ test/parallel/test-runner-coverage.js | 98 ++++++ 16 files changed, 796 insertions(+), 33 deletions(-) create mode 100644 lib/internal/test_runner/coverage.js create mode 100644 test/fixtures/test-runner/coverage.js create mode 100644 test/parallel/test-runner-coverage.js diff --git a/doc/api/cli.md b/doc/api/cli.md index c876f9431b6e89..afa0058e0032d6 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1209,6 +1209,17 @@ Starts the Node.js command line test runner. This flag cannot be combined with See the documentation on [running tests from the command line][] for more details. +### `--test-coverage` + + + +When used in conjunction with the `node:test` module, a code coverage report is +generated as part of the test runner output. If no tests are run, a coverage +report is not generated. See the documentation on +[collecting code coverage from tests][] for more details. + ### `--test-name-pattern` + +When used in conjunction with the `node:test` module, a code coverage report is +generated as part of the test runner output. If no tests are run, a coverage +report is not generated. See the documentation on +[collecting code coverage from tests][] for more details. + ### `--experimental-vm-modules` - -When used in conjunction with the `node:test` module, a code coverage report is -generated as part of the test runner output. If no tests are run, a coverage -report is not generated. See the documentation on -[collecting code coverage from tests][] for more details. - ### `--test-name-pattern` + +Shorthand for marking a suite as `only`, same as +[`describe([name], { only: true }[, fn])`][describe options]. + ## `it([name][, options][, fn])` * `name` {string} The name of the test, which is displayed when reporting test @@ -827,6 +836,15 @@ same as [`it([name], { skip: true }[, fn])`][it options]. Shorthand for marking a test as `TODO`, same as [`it([name], { todo: true }[, fn])`][it options]. +## `it.only([name][, options][, fn])` + + + +Shorthand for marking a test as `only`, +same as [`it([name], { only: true }[, fn])`][it options]. + ## `before([fn][, options])` * input {Buffer | ArrayBuffer | TypedArray} The input to validate. diff --git a/doc/api/cli.md b/doc/api/cli.md index 48fb67adf811f8..4e53192cfb36fc 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -443,7 +443,7 @@ See [customizing ESM specifier resolution][] for example usage. ### `--experimental-test-coverage` When used in conjunction with the `node:test` module, a code coverage report is @@ -1233,7 +1233,7 @@ whose name matches the provided pattern. See the documentation on ### `--test-reporter` A test reporter to use when running tests. See the documentation on @@ -1242,7 +1242,7 @@ A test reporter to use when running tests. See the documentation on ### `--test-reporter-destination` The destination for the corresponding test reporter. See the documentation on diff --git a/doc/api/fs.md b/doc/api/fs.md index c6436b3ce10a19..4486e7eb4364ad 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1528,7 +1528,7 @@ changes: ### `fsPromises.statfs(path[, options])` * `path` {string|Buffer|URL} @@ -4098,7 +4098,7 @@ Stats { ### `fs.statfs(path[, options], callback)` * `path` {string|Buffer|URL} @@ -5858,7 +5858,7 @@ Retrieves the {fs.Stats} for the path. ### `fs.statfsSync(path[, options])` * `path` {string|Buffer|URL} @@ -6961,7 +6961,7 @@ of 0.12, `ctime` is not "creation time", and on Unix systems, it never was. ### Class: `fs.StatFs` Provides information about a mounted file system. @@ -6999,7 +6999,7 @@ StatFs { #### `statfs.bavail` * {number|bigint} @@ -7009,7 +7009,7 @@ Free blocks available to unprivileged users. #### `statfs.bfree` * {number|bigint} @@ -7019,7 +7019,7 @@ Free blocks in file system. #### `statfs.blocks` * {number|bigint} @@ -7029,7 +7029,7 @@ Total data blocks in file system. #### `statfs.bsize` * {number|bigint} @@ -7039,7 +7039,7 @@ Optimal transfer block size. #### `statfs.ffree` * {number|bigint} @@ -7049,7 +7049,7 @@ Free file nodes in file system. #### `statfs.files` * {number|bigint} @@ -7059,7 +7059,7 @@ Total file nodes in file system. #### `statfs.type` * {number|bigint} diff --git a/doc/api/http.md b/doc/api/http.md index 4a3675d83bd7c3..40434891318c2b 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -2953,7 +2953,7 @@ headers with the same name. ### `outgoingMessage.setHeaders(headers)` * `headers` {Headers|Map} diff --git a/doc/api/process.md b/doc/api/process.md index 5b6ae09eda95b1..3b1197b810eb92 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1110,7 +1110,7 @@ over the IPC channel using `process.send()`. ## `process.constrainedMemory()` > Stability: 1 - Experimental diff --git a/doc/api/test.md b/doc/api/test.md index 4c353c88690f75..dbbc4300119112 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -504,7 +504,7 @@ test('spies on an object method', (t) => { ## Test reporters The `node:test` module supports passing [`--test-reporter`][] @@ -806,7 +806,7 @@ Shorthand for marking a suite as `TODO`, same as ## `describe.only([name][, options][, fn])` Shorthand for marking a suite as `only`, same as @@ -839,7 +839,7 @@ same as [`it([name], { todo: true }[, fn])`][it options]. ## `it.only([name][, options][, fn])` Shorthand for marking a test as `only`, diff --git a/doc/api/v8.md b/doc/api/v8.md index b08a203ed3596a..b9f5fda7117c88 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1024,7 +1024,7 @@ Returns true if the Node.js instance is run to build a snapshot. ## Class: `v8.GCProfiler` This API collects GC data in current thread. @@ -1032,7 +1032,7 @@ This API collects GC data in current thread. ### `new v8.GCProfiler()` Create a new instance of the `v8.GCProfiler` class. @@ -1040,7 +1040,7 @@ Create a new instance of the `v8.GCProfiler` class. ### `profiler.start()` Start collecting GC data. @@ -1048,7 +1048,7 @@ Start collecting GC data. ### `profiler.stop()` Stop collecting GC data and return an object.The content of object diff --git a/doc/api/vm.md b/doc/api/vm.md index 481aded758150b..ba45063e1c72c0 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -963,7 +963,7 @@ const vm = require('node:vm'); added: v10.10.0 changes: - version: - - REPLACEME + - v18.15.0 pr-url: https://github.com/nodejs/node/pull/46320 description: The return value now includes `cachedDataRejected` with the same semantics as the `vm.Script` version diff --git a/doc/changelogs/CHANGELOG_V18.md b/doc/changelogs/CHANGELOG_V18.md index f8e8bb529bf5c1..90d54d3cd40e1f 100644 --- a/doc/changelogs/CHANGELOG_V18.md +++ b/doc/changelogs/CHANGELOG_V18.md @@ -9,6 +9,7 @@ +18.15.0
18.14.2
18.14.1
18.14.0
@@ -54,6 +55,116 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2023-03-07, Version 18.15.0 'Hydrogen' (LTS), @BethGriggs prepared by @juanarbol + +### Notable Changes + +* \[[`63563f8a7a`](https://github.com/nodejs/node/commit/63563f8a7a)] - **doc,lib,src,test**: rename --test-coverage (Colin Ihrig) [#46017](https://github.com/nodejs/node/pull/46017) +* \[[`28a775b32f`](https://github.com/nodejs/node/commit/28a775b32f)] - **test\_runner**: add initial code coverage support (Colin Ihrig) [#46017](https://github.com/nodejs/node/pull/46017) +* \[[`4d50db14b3`](https://github.com/nodejs/node/commit/4d50db14b3)] - **(SEMVER-MINOR)** **test\_runner**: add reporters (Moshe Atlow) [#45712](https://github.com/nodejs/node/pull/45712) +* \[[`643545ab79`](https://github.com/nodejs/node/commit/643545ab79)] - **(SEMVER-MINOR)** **fs**: add statfs() functions (Colin Ihrig) [#46358](https://github.com/nodejs/node/pull/46358) +* \[[`110ead9abb`](https://github.com/nodejs/node/commit/110ead9abb)] - **(SEMVER-MINOR)** **vm**: expose cachedDataRejected for vm.compileFunction (Anna Henningsen) [#46320](https://github.com/nodejs/node/pull/46320) +* \[[`02632b42cf`](https://github.com/nodejs/node/commit/02632b42cf)] - **(SEMVER-MINOR)** **v8**: support gc profile (theanarkh) [#46255](https://github.com/nodejs/node/pull/46255) +* \[[`f09b838408`](https://github.com/nodejs/node/commit/f09b838408)] - **(SEMVER-MINOR)** **src,lib**: add constrainedMemory API for process (theanarkh) [#46218](https://github.com/nodejs/node/pull/46218) +* \[[`cb5bb12422`](https://github.com/nodejs/node/commit/cb5bb12422)] - **(SEMVER-MINOR)** **buffer**: add isAscii method (Yagiz Nizipli) [#46046](https://github.com/nodejs/node/pull/46046) + +### Commits + +* \[[`6f91c8e2ae`](https://github.com/nodejs/node/commit/6f91c8e2ae)] - **benchmark**: add trailing commas (Antoine du Hamel) [#46370](https://github.com/nodejs/node/pull/46370) +* \[[`d0b9be21eb`](https://github.com/nodejs/node/commit/d0b9be21eb)] - **benchmark**: remove buffer benchmarks redundancy (Brian White) [#45735](https://github.com/nodejs/node/pull/45735) +* \[[`6468f30d0d`](https://github.com/nodejs/node/commit/6468f30d0d)] - **benchmark**: introduce benchmark combination filtering (Brian White) [#45735](https://github.com/nodejs/node/pull/45735) +* \[[`cb5bb12422`](https://github.com/nodejs/node/commit/cb5bb12422)] - **(SEMVER-MINOR)** **buffer**: add isAscii method (Yagiz Nizipli) [#46046](https://github.com/nodejs/node/pull/46046) +* \[[`ec61bb04c0`](https://github.com/nodejs/node/commit/ec61bb04c0)] - **build**: export more OpenSSL symbols on Windows (Mohamed Akram) [#45486](https://github.com/nodejs/node/pull/45486) +* \[[`7bae4333ce`](https://github.com/nodejs/node/commit/7bae4333ce)] - **build**: fix MSVC 2022 Release compilation (Vladimir Morozov (REDMOND)) [#46228](https://github.com/nodejs/node/pull/46228) +* \[[`0f5f2d4470`](https://github.com/nodejs/node/commit/0f5f2d4470)] - **crypto**: include `hmac.h` in `crypto_util.h` (Adam Langley) [#46279](https://github.com/nodejs/node/pull/46279) +* \[[`91ece4161b`](https://github.com/nodejs/node/commit/91ece4161b)] - **crypto**: avoid hang when no algorithm available (Richard Lau) [#46237](https://github.com/nodejs/node/pull/46237) +* \[[`492fc95bdf`](https://github.com/nodejs/node/commit/492fc95bdf)] - **deps**: V8: cherry-pick 90be99fab31c (Michaël Zasso) [#46646](https://github.com/nodejs/node/pull/46646) +* \[[`732c77e3d9`](https://github.com/nodejs/node/commit/732c77e3d9)] - **deps**: update acorn to 8.8.2 (Node.js GitHub Bot) [#46363](https://github.com/nodejs/node/pull/46363) +* \[[`8582f99ffb`](https://github.com/nodejs/node/commit/8582f99ffb)] - **deps**: update to uvwasi 0.0.15 (Colin Ihrig) [#46253](https://github.com/nodejs/node/pull/46253) +* \[[`5453cd9940`](https://github.com/nodejs/node/commit/5453cd9940)] - **deps**: V8: cherry-pick bf0bd4868dde (Michaël Zasso) [#45908](https://github.com/nodejs/node/pull/45908) +* \[[`3ea53c5dc8`](https://github.com/nodejs/node/commit/3ea53c5dc8)] - **deps**: V8: cherry-pick c875e86df1d7 (sepehrst) [#46501](https://github.com/nodejs/node/pull/46501) +* \[[`c04808de4b`](https://github.com/nodejs/node/commit/c04808de4b)] - **doc**: correct the `sed` command for macOS in release process docs (Juan José) [#46397](https://github.com/nodejs/node/pull/46397) +* \[[`8113220690`](https://github.com/nodejs/node/commit/8113220690)] - **doc**: pass string to `textEncoder.encode` as input (Deokjin Kim) [#46421](https://github.com/nodejs/node/pull/46421) +* \[[`129dccf5d2`](https://github.com/nodejs/node/commit/129dccf5d2)] - **doc**: add tip for session.post function (theanarkh) [#46354](https://github.com/nodejs/node/pull/46354) +* \[[`919e581732`](https://github.com/nodejs/node/commit/919e581732)] - **doc**: add documentation for socket.destroySoon() (Luigi Pinca) [#46337](https://github.com/nodejs/node/pull/46337) +* \[[`fc15ac95a5`](https://github.com/nodejs/node/commit/fc15ac95a5)] - **doc**: fix commit message using test instead of deps (Tony Gorez) [#46313](https://github.com/nodejs/node/pull/46313) +* \[[`d153a93200`](https://github.com/nodejs/node/commit/d153a93200)] - **doc**: add v8 fast api contribution guidelines (Yagiz Nizipli) [#46199](https://github.com/nodejs/node/pull/46199) +* \[[`dbf082d082`](https://github.com/nodejs/node/commit/dbf082d082)] - **doc**: fix small typo error (0xflotus) [#46186](https://github.com/nodejs/node/pull/46186) +* \[[`94421b4cfe`](https://github.com/nodejs/node/commit/94421b4cfe)] - **doc**: mark some parameters as optional in webstreams (Deokjin Kim) [#46269](https://github.com/nodejs/node/pull/46269) +* \[[`5adb743511`](https://github.com/nodejs/node/commit/5adb743511)] - **doc**: update output of example in `events.getEventListeners` (Deokjin Kim) [#46268](https://github.com/nodejs/node/pull/46268) +* \[[`63563f8a7a`](https://github.com/nodejs/node/commit/63563f8a7a)] - **doc,lib,src,test**: rename --test-coverage (Colin Ihrig) [#46017](https://github.com/nodejs/node/pull/46017) +* \[[`4e88c7c813`](https://github.com/nodejs/node/commit/4e88c7c813)] - **esm**: delete preload mock test (Geoffrey Booth) [#46402](https://github.com/nodejs/node/pull/46402) +* \[[`643545ab79`](https://github.com/nodejs/node/commit/643545ab79)] - **(SEMVER-MINOR)** **fs**: add statfs() functions (Colin Ihrig) [#46358](https://github.com/nodejs/node/pull/46358) +* \[[`5019b5473f`](https://github.com/nodejs/node/commit/5019b5473f)] - **http**: res.setHeaders first implementation (Marco Ippolito) [#46109](https://github.com/nodejs/node/pull/46109) +* \[[`76622c4c60`](https://github.com/nodejs/node/commit/76622c4c60)] - **inspector**: allow opening inspector when `NODE_V8_COVERAGE` is set (Moshe Atlow) [#46113](https://github.com/nodejs/node/pull/46113) +* \[[`92f0747e03`](https://github.com/nodejs/node/commit/92f0747e03)] - **meta**: update AUTHORS (Node.js GitHub Bot) [#46399](https://github.com/nodejs/node/pull/46399) +* \[[`795251bc6f`](https://github.com/nodejs/node/commit/795251bc6f)] - **meta**: update AUTHORS (Node.js GitHub Bot) [#46303](https://github.com/nodejs/node/pull/46303) +* \[[`8865424c31`](https://github.com/nodejs/node/commit/8865424c31)] - **meta**: add .mailmap entry (Rich Trott) [#46303](https://github.com/nodejs/node/pull/46303) +* \[[`5ed679407b`](https://github.com/nodejs/node/commit/5ed679407b)] - **meta**: move evanlucas to emeritus (Evan Lucas) [#46274](https://github.com/nodejs/node/pull/46274) +* \[[`403df210ac`](https://github.com/nodejs/node/commit/403df210ac)] - **module**: move test reporter loading (Geoffrey Booth) [#45923](https://github.com/nodejs/node/pull/45923) +* \[[`2f7319e387`](https://github.com/nodejs/node/commit/2f7319e387)] - **readline**: fix detection of carriage return (Antoine du Hamel) [#46306](https://github.com/nodejs/node/pull/46306) +* \[[`73a8f46c4d`](https://github.com/nodejs/node/commit/73a8f46c4d)] - _**Revert**_ "**src**: let http2 streams end after session close" (Santiago Gimeno) [#46721](https://github.com/nodejs/node/pull/46721) +* \[[`30d783f91a`](https://github.com/nodejs/node/commit/30d783f91a)] - **src**: stop tracing agent before shutting down libuv (Santiago Gimeno) [#46380](https://github.com/nodejs/node/pull/46380) +* \[[`1508d90fda`](https://github.com/nodejs/node/commit/1508d90fda)] - **src**: get rid of fp arithmetic in ParseIPv4Host (Tobias Nießen) [#46326](https://github.com/nodejs/node/pull/46326) +* \[[`bdb793a082`](https://github.com/nodejs/node/commit/bdb793a082)] - **src**: use UNREACHABLE instead of CHECK(falsy) (Tobias Nießen) [#46317](https://github.com/nodejs/node/pull/46317) +* \[[`116a33649b`](https://github.com/nodejs/node/commit/116a33649b)] - **src**: add support for ETW stack walking (José Dapena Paz) [#46203](https://github.com/nodejs/node/pull/46203) +* \[[`b06298c98e`](https://github.com/nodejs/node/commit/b06298c98e)] - **src**: refactor EndsInANumber in node\_url.cc and adds IsIPv4NumberValid (Miguel Teixeira) [#46227](https://github.com/nodejs/node/pull/46227) +* \[[`26f41b041c`](https://github.com/nodejs/node/commit/26f41b041c)] - **src**: fix c++ exception on bad command line arg (Ben Noordhuis) [#46290](https://github.com/nodejs/node/pull/46290) +* \[[`14da89f41a`](https://github.com/nodejs/node/commit/14da89f41a)] - **src**: remove unreachable UNREACHABLE (Tobias Nießen) [#46281](https://github.com/nodejs/node/pull/46281) +* \[[`18c4dd004b`](https://github.com/nodejs/node/commit/18c4dd004b)] - **src**: replace custom ASCII validation with simdutf one (Anna Henningsen) [#46271](https://github.com/nodejs/node/pull/46271) +* \[[`cde375510f`](https://github.com/nodejs/node/commit/cde375510f)] - **src**: replace unreachable code with static\_assert (Tobias Nießen) [#46250](https://github.com/nodejs/node/pull/46250) +* \[[`f389b2f3fc`](https://github.com/nodejs/node/commit/f389b2f3fc)] - **src**: use explicit C++17 fallthrough (Tobias Nießen) [#46251](https://github.com/nodejs/node/pull/46251) +* \[[`8adaa1333c`](https://github.com/nodejs/node/commit/8adaa1333c)] - **src**: use CreateEnvironment instead of inlining its code where possible (Anna Henningsen) [#45886](https://github.com/nodejs/node/pull/45886) +* \[[`f09b838408`](https://github.com/nodejs/node/commit/f09b838408)] - **(SEMVER-MINOR)** **src,lib**: add constrainedMemory API for process (theanarkh) [#46218](https://github.com/nodejs/node/pull/46218) +* \[[`63e92eae63`](https://github.com/nodejs/node/commit/63e92eae63)] - **stream**: remove brandchecks from stream duplexify (Debadree Chatterjee) [#46315](https://github.com/nodejs/node/pull/46315) +* \[[`3acfe9bf92`](https://github.com/nodejs/node/commit/3acfe9bf92)] - **stream**: fix readable stream as async iterator function (Erick Wendel) [#46147](https://github.com/nodejs/node/pull/46147) +* \[[`de64315ccb`](https://github.com/nodejs/node/commit/de64315ccb)] - **test**: fix WPT title when no META title is present (Filip Skokan) [#46804](https://github.com/nodejs/node/pull/46804) +* \[[`162e3400ff`](https://github.com/nodejs/node/commit/162e3400ff)] - **test**: fix default WPT titles (Filip Skokan) [#46778](https://github.com/nodejs/node/pull/46778) +* \[[`5f422c4d70`](https://github.com/nodejs/node/commit/5f422c4d70)] - **test**: add WPTRunner support for variants and generating WPT reports (Filip Skokan) [#46498](https://github.com/nodejs/node/pull/46498) +* \[[`4f5aff2557`](https://github.com/nodejs/node/commit/4f5aff2557)] - **test**: fix tap parser fails if a test logs a number (Pulkit Gupta) [#46056](https://github.com/nodejs/node/pull/46056) +* \[[`32b020cf84`](https://github.com/nodejs/node/commit/32b020cf84)] - **test**: fix tap escaping with and without --test (Pulkit Gupta) [#46311](https://github.com/nodejs/node/pull/46311) +* \[[`f2bba1bcdb`](https://github.com/nodejs/node/commit/f2bba1bcdb)] - **test**: add trailing commas in `test/node-api` (Antoine du Hamel) [#46384](https://github.com/nodejs/node/pull/46384) +* \[[`f2ebe66fda`](https://github.com/nodejs/node/commit/f2ebe66fda)] - **test**: add trailing commas in `test/message` (Antoine du Hamel) [#46372](https://github.com/nodejs/node/pull/46372) +* \[[`ed564a9985`](https://github.com/nodejs/node/commit/ed564a9985)] - **test**: add trailing commas in `test/pseudo-tty` (Antoine du Hamel) [#46371](https://github.com/nodejs/node/pull/46371) +* \[[`e4437dd409`](https://github.com/nodejs/node/commit/e4437dd409)] - **test**: set common.bits to 64 for loong64 (Shi Pujin) [#45383](https://github.com/nodejs/node/pull/45383) +* \[[`9d40aef736`](https://github.com/nodejs/node/commit/9d40aef736)] - **test**: s390x zlib test case fixes (Adam Majer) [#46367](https://github.com/nodejs/node/pull/46367) +* \[[`ed3fb52716`](https://github.com/nodejs/node/commit/ed3fb52716)] - **test**: fix logInTimeout is not function (theanarkh) [#46348](https://github.com/nodejs/node/pull/46348) +* \[[`d05b0771be`](https://github.com/nodejs/node/commit/d05b0771be)] - **test**: avoid trying to call sysctl directly (Adam Majer) [#46366](https://github.com/nodejs/node/pull/46366) +* \[[`041aac3bbd`](https://github.com/nodejs/node/commit/041aac3bbd)] - **test**: avoid left behind child processes (Richard Lau) [#46276](https://github.com/nodejs/node/pull/46276) +* \[[`837ddcb322`](https://github.com/nodejs/node/commit/837ddcb322)] - **test**: add failing test for readline with carriage return (Alec Mev) [#46075](https://github.com/nodejs/node/pull/46075) +* \[[`75b8db41c6`](https://github.com/nodejs/node/commit/75b8db41c6)] - **test**: reduce `fs-write-optional-params` flakiness (LiviaMedeiros) [#46238](https://github.com/nodejs/node/pull/46238) +* \[[`c0d3fdaf63`](https://github.com/nodejs/node/commit/c0d3fdaf63)] - **test,crypto**: add CFRG curve vectors to wrap/unwrap tests (Filip Skokan) [#46406](https://github.com/nodejs/node/pull/46406) +* \[[`f328f7b15e`](https://github.com/nodejs/node/commit/f328f7b15e)] - **test,crypto**: update WebCryptoAPI WPT (Filip Skokan) [#46267](https://github.com/nodejs/node/pull/46267) +* \[[`1ef3c53e24`](https://github.com/nodejs/node/commit/1ef3c53e24)] - **test\_runner**: better handle async bootstrap errors (Colin Ihrig) [#46720](https://github.com/nodejs/node/pull/46720) +* \[[`0a690efb76`](https://github.com/nodejs/node/commit/0a690efb76)] - **test\_runner**: add `describe.only` and `it.only` shorthands (Richie McColl) [#46604](https://github.com/nodejs/node/pull/46604) +* \[[`28a1317efe`](https://github.com/nodejs/node/commit/28a1317efe)] - **test\_runner**: bootstrap reporters before running tests (Moshe Atlow) [#46737](https://github.com/nodejs/node/pull/46737) +* \[[`cd3aaa8fac`](https://github.com/nodejs/node/commit/cd3aaa8fac)] - **test\_runner**: emit test-only diagnostic warning (Richie McColl) [#46540](https://github.com/nodejs/node/pull/46540) +* \[[`c19fa45a65`](https://github.com/nodejs/node/commit/c19fa45a65)] - **test\_runner**: centralize CLI option handling (Colin Ihrig) [#46707](https://github.com/nodejs/node/pull/46707) +* \[[`0898145e37`](https://github.com/nodejs/node/commit/0898145e37)] - **test\_runner**: display skipped tests in spec reporter output (Richie McColl) [#46651](https://github.com/nodejs/node/pull/46651) +* \[[`894d7117fa`](https://github.com/nodejs/node/commit/894d7117fa)] - **test\_runner**: parse non-ascii character correctly (Mert Can Altın) [#45736](https://github.com/nodejs/node/pull/45736) +* \[[`5b3c606626`](https://github.com/nodejs/node/commit/5b3c606626)] - **test\_runner**: flatten TAP output when running using `--test` (Moshe Atlow) [#46440](https://github.com/nodejs/node/pull/46440) +* \[[`391ff0dba4`](https://github.com/nodejs/node/commit/391ff0dba4)] - **test\_runner**: allow nesting test within describe (Moshe Atlow) [#46544](https://github.com/nodejs/node/pull/46544) +* \[[`ba784e87b4`](https://github.com/nodejs/node/commit/ba784e87b4)] - **test\_runner**: fix missing test diagnostics (Moshe Atlow) [#46450](https://github.com/nodejs/node/pull/46450) +* \[[`c5f16fb5fb`](https://github.com/nodejs/node/commit/c5f16fb5fb)] - **test\_runner**: top-level diagnostics not ommited when running with --test (Pulkit Gupta) [#46441](https://github.com/nodejs/node/pull/46441) +* \[[`28a775b32f`](https://github.com/nodejs/node/commit/28a775b32f)] - **test\_runner**: add initial code coverage support (Colin Ihrig) [#46017](https://github.com/nodejs/node/pull/46017) +* \[[`0d999e373a`](https://github.com/nodejs/node/commit/0d999e373a)] - **test\_runner**: make built in reporters internal (Colin Ihrig) [#46092](https://github.com/nodejs/node/pull/46092) +* \[[`79f4b426fe`](https://github.com/nodejs/node/commit/79f4b426fe)] - **test\_runner**: report `file` in test runner events (Moshe Atlow) [#46030](https://github.com/nodejs/node/pull/46030) +* \[[`4d50db14b3`](https://github.com/nodejs/node/commit/4d50db14b3)] - **(SEMVER-MINOR)** **test\_runner**: add reporters (Moshe Atlow) [#45712](https://github.com/nodejs/node/pull/45712) +* \[[`5fdf374c74`](https://github.com/nodejs/node/commit/5fdf374c74)] - **test\_runner**: avoid swallowing of asynchronously thrown errors (MURAKAMI Masahiko) [#45264](https://github.com/nodejs/node/pull/45264) +* \[[`23b875806c`](https://github.com/nodejs/node/commit/23b875806c)] - **test\_runner**: update comment to comply with eslint no-fallthrough rule (Antoine du Hamel) [#46258](https://github.com/nodejs/node/pull/46258) +* \[[`00c5495aa3`](https://github.com/nodejs/node/commit/00c5495aa3)] - **tools**: update eslint to 8.33.0 (Node.js GitHub Bot) [#46400](https://github.com/nodejs/node/pull/46400) +* \[[`37a6ce1120`](https://github.com/nodejs/node/commit/37a6ce1120)] - **tools**: update doc to unist-util-select\@4.0.3 unist-util-visit\@4.1.2 (Node.js GitHub Bot) [#46364](https://github.com/nodejs/node/pull/46364) +* \[[`1eaafc7db4`](https://github.com/nodejs/node/commit/1eaafc7db4)] - **tools**: update lint-md-dependencies to rollup\@3.12.0 (Node.js GitHub Bot) [#46398](https://github.com/nodejs/node/pull/46398) +* \[[`a97774603b`](https://github.com/nodejs/node/commit/a97774603b)] - **tools**: require more trailing commas (Antoine du Hamel) [#46346](https://github.com/nodejs/node/pull/46346) +* \[[`03e244a59b`](https://github.com/nodejs/node/commit/03e244a59b)] - **tools**: update lint-md-dependencies (Node.js GitHub Bot) [#46302](https://github.com/nodejs/node/pull/46302) +* \[[`60d714e0c3`](https://github.com/nodejs/node/commit/60d714e0c3)] - **tools**: allow icutrim.py to run on python2 (Michael Dawson) [#46263](https://github.com/nodejs/node/pull/46263) +* \[[`b7950f50de`](https://github.com/nodejs/node/commit/b7950f50de)] - **tools**: update eslint to 8.32.0 (Node.js GitHub Bot) [#46258](https://github.com/nodejs/node/pull/46258) +* \[[`08bafc84f6`](https://github.com/nodejs/node/commit/08bafc84f6)] - **url**: refactor to use more primordials (Antoine du Hamel) [#45966](https://github.com/nodejs/node/pull/45966) +* \[[`02632b42cf`](https://github.com/nodejs/node/commit/02632b42cf)] - **(SEMVER-MINOR)** **v8**: support gc profile (theanarkh) [#46255](https://github.com/nodejs/node/pull/46255) +* \[[`110ead9abb`](https://github.com/nodejs/node/commit/110ead9abb)] - **(SEMVER-MINOR)** **vm**: expose cachedDataRejected for vm.compileFunction (Anna Henningsen) [#46320](https://github.com/nodejs/node/pull/46320) + ## 2023-02-21, Version 18.14.2 'Hydrogen' (LTS), @MylesBorins diff --git a/src/node_version.h b/src/node_version.h index 77cb921a673ad7..ee035efadf1234 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -23,13 +23,13 @@ #define SRC_NODE_VERSION_H_ #define NODE_MAJOR_VERSION 18 -#define NODE_MINOR_VERSION 14 -#define NODE_PATCH_VERSION 3 +#define NODE_MINOR_VERSION 15 +#define NODE_PATCH_VERSION 0 #define NODE_VERSION_IS_LTS 1 #define NODE_VERSION_LTS_CODENAME "Hydrogen" -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_STRINGIFY #define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n)