diff --git a/src/node.h b/src/node.h index eba7b8698187b1..1285e3162eac82 100644 --- a/src/node.h +++ b/src/node.h @@ -641,12 +641,14 @@ enum Flags : uint64_t { }; } // namespace EnvironmentFlags +// Bit 0 is a config-level flag (kWithoutCodeCache). +// Bits 1..31 are dynamically assigned at runtime to env-affecting boolean +// options that carry affects_snapshot=true, in alphabetical order by flag +// name. The mapping is computed by GetSnapshotAffectingOptions() so there are +// no named constants for those bits here. enum class SnapshotFlags : uint32_t { kDefault = 0, // Whether code cache should be generated as part of the snapshot. - // Code cache reduces the time spent on compiling functions included - // in the snapshot at the expense of a bigger snapshot size and - // potentially breaking portability of the snapshot. kWithoutCodeCache = 1 << 0, }; diff --git a/src/node_options-inl.h b/src/node_options-inl.h index 877e8ce4ded92b..efe896b39e3fc4 100644 --- a/src/node_options-inl.h +++ b/src/node_options-inl.h @@ -34,14 +34,16 @@ void OptionsParser::AddOption(const char* name, bool Options::*field, OptionEnvvarSettings env_setting, bool default_is_true, - OptionNamespaces namespace_id) { + OptionNamespaces namespace_id, + bool affects_snapshot) { options_.emplace(name, OptionInfo{kBoolean, std::make_shared>(field), env_setting, help_text, default_is_true, - NamespaceEnumToString(namespace_id)}); + NamespaceEnumToString(namespace_id), + affects_snapshot}); } template @@ -229,7 +231,8 @@ auto OptionsParser::Convert( original.env_setting, original.help_text, original.default_is_true, - original.namespace_id}; + original.namespace_id, + original.affects_snapshot}; } template diff --git a/src/node_options.cc b/src/node_options.cc index bbb72d2ba1bcf4..febebfce27ffb0 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -22,6 +22,7 @@ #include #include +using v8::Array; using v8::Boolean; using v8::Context; using v8::FunctionCallbackInfo; @@ -586,39 +587,55 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "experimental EventSource API", &EnvironmentOptions::experimental_eventsource, kAllowedInEnvvar, - false); + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--experimental-fetch", "", NoOp{}, kAllowedInEnvvar); #if HAVE_FFI AddOption("--experimental-ffi", "experimental node:ffi module", &EnvironmentOptions::experimental_ffi, kAllowedInEnvvar, - false); + false, + OptionNamespaces::kNoNamespace, + true); #endif // HAVE_FFI AddOption("--experimental-websocket", "experimental WebSocket API", &EnvironmentOptions::experimental_websocket, kAllowedInEnvvar, + true, + OptionNamespaces::kNoNamespace, true); AddOption("--experimental-global-customevent", "", NoOp{}, kAllowedInEnvvar); AddOption("--experimental-sqlite", "experimental node:sqlite module", &EnvironmentOptions::experimental_sqlite, kAllowedInEnvvar, - HAVE_SQLITE); + HAVE_SQLITE, + OptionNamespaces::kNoNamespace, + true); AddOption("--experimental-stream-iter", "experimental iterable streams API (node:stream/iter)", &EnvironmentOptions::experimental_stream_iter, - kAllowedInEnvvar); - AddOption("--experimental-quic", + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); #ifndef OPENSSL_NO_QUIC + AddOption("--experimental-quic", "experimental QUIC support", &EnvironmentOptions::experimental_quic, + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); #else + AddOption("--experimental-quic", "" /* undocumented when no-op */, NoOp{}, -#endif kAllowedInEnvvar); +#endif AddOption("--experimental-webstorage", "experimental Web Storage API", &EnvironmentOptions::webstorage, @@ -720,7 +737,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--experimental-vm-modules", "experimental ES Module support in vm module", &EnvironmentOptions::experimental_vm_modules, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--experimental-worker", "", NoOp{}, kAllowedInEnvvar); AddOption("--experimental-report", "", NoOp{}, kAllowedInEnvvar); AddOption( @@ -731,11 +751,20 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::async_context_frame, kAllowedInEnvvar, true); - AddOption("--expose-internals", "", &EnvironmentOptions::expose_internals); + AddOption("--expose-internals", + "", + &EnvironmentOptions::expose_internals, + kDisallowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--frozen-intrinsics", "experimental frozen intrinsics support", &EnvironmentOptions::frozen_intrinsics, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--heapsnapshot-signal", "Generate heap snapshot on specified signal", &EnvironmentOptions::heap_snapshot_signal, @@ -763,6 +792,8 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "silence deprecation warnings", &EnvironmentOptions::deprecation, kAllowedInEnvvar, + true, + OptionNamespaces::kNoNamespace, true); AddOption("--force-async-hooks-checks", "disable checks for async_hooks", @@ -789,6 +820,8 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "silence all process warnings", &EnvironmentOptions::warnings, kAllowedInEnvvar, + true, + OptionNamespaces::kNoNamespace, true); AddOption("--disable-warning", "silence specific process warnings", @@ -801,7 +834,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--pending-deprecation", "emit pending deprecation warnings", &EnvironmentOptions::pending_deprecation, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--use-env-proxy", "parse proxy settings from HTTP_PROXY/HTTPS_PROXY/NO_PROXY" "environment variables and apply the setting in global HTTP/HTTPS " @@ -1043,11 +1079,17 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--throw-deprecation", "throw an exception on deprecations", &EnvironmentOptions::throw_deprecation, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--trace-deprecation", "show stack traces on deprecations", &EnvironmentOptions::trace_deprecation, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--trace-exit", "show stack trace when an environment exits", &EnvironmentOptions::trace_exit, @@ -1068,7 +1110,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--trace-warnings", "show stack traces on process warnings", &EnvironmentOptions::trace_warnings, - kAllowedInEnvvar); + kAllowedInEnvvar, + false, + OptionNamespaces::kNoNamespace, + true); AddOption("--trace-promises", "show stack traces on promise initialization and resolution", &EnvironmentOptions::trace_promises, @@ -2017,6 +2062,34 @@ void GetNamespaceOptionsInputType(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(namespaces_metadata); } +// Return an array of {name, defaultIsTrue} for every boolean option marked +// affects_snapshot=true, sorted alphabetically. +void GetSnapshotAffectingFlags(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + + Mutex::ScopedLock lock(per_process::cli_options_mutex); + + auto options = node::GetSnapshotAffectingOptions(*env->options()); + + Local result = Array::New(isolate, options.size()); + for (size_t i = 0; i < options.size(); i++) { + Local obj = Object::New(isolate); + obj->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "name"), + ToV8Value(context, options[i].name).ToLocalChecked()) + .Check(); + obj->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "defaultIsTrue"), + Boolean::New(isolate, options[i].default_is_true)) + .Check(); + result->Set(context, i, obj).Check(); + } + + args.GetReturnValue().Set(result); +} + // Return an array containing all currently active options as flag // strings from all sources (command line, NODE_OPTIONS, config file) void GetOptionsAsFlags(const FunctionCallbackInfo& args) { @@ -2138,6 +2211,8 @@ void Initialize(Local target, context, target, "getCLIOptionsValues", GetCLIOptionsValues); SetMethodNoSideEffect( context, target, "getCLIOptionsInfo", GetCLIOptionsInfo); + SetMethodNoSideEffect( + context, target, "getSnapshotAffectingFlags", GetSnapshotAffectingFlags); SetMethodNoSideEffect( context, target, "getOptionsAsFlags", GetOptionsAsFlags); SetMethodNoSideEffect( @@ -2172,6 +2247,7 @@ void Initialize(Local target, void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(GetCLIOptionsValues); registry->Register(GetCLIOptionsInfo); + registry->Register(GetSnapshotAffectingFlags); registry->Register(GetOptionsAsFlags); registry->Register(GetEmbedderOptions); registry->Register(GetEnvOptionsInputType); @@ -2246,6 +2322,20 @@ std::vector ParseNodeOptionsEnvVar( } return env_argv; } +std::vector GetSnapshotAffectingOptions( + const EnvironmentOptions& opts) { + std::vector result; + options_parser::_eop_instance.ForEachSnapshotAffecting( + const_cast(&opts), + [&](const std::string& name, bool val, bool default_is_true) { + result.push_back({name, val, default_is_true}); + }); + std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) { + return a.name < b.name; + }); + return result; +} + } // namespace node NODE_BINDING_CONTEXT_AWARE_INTERNAL(options, node::options_parser::Initialize) diff --git a/src/node_options.h b/src/node_options.h index e910cb011431ab..c7c62da8495911 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -476,13 +476,15 @@ class OptionsParser { // default_is_true is only a hint in printing help text, it does not // affect the default value of the option. Set the default value in the // Options struct instead. - void AddOption( - const char* name, - const char* help_text, - bool Options::*field, - OptionEnvvarSettings env_setting = kDisallowedInEnvvar, - bool default_is_true = false, - OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace); + // affects_snapshot marks that the flag's effective value must match between + // snapshot build and load time. + void AddOption(const char* name, + const char* help_text, + bool Options::*field, + OptionEnvvarSettings env_setting = kDisallowedInEnvvar, + bool default_is_true = false, + OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace, + bool affects_snapshot = false); void AddOption( const char* name, const char* help_text, @@ -573,6 +575,18 @@ class OptionsParser { OptionEnvvarSettings required_env_settings, std::vector* const errors) const; + // Calls fn(name, value, default_is_true) for every boolean option that has + // affects_snapshot=true, in unspecified order. + template + void ForEachSnapshotAffecting(Options* options, Fn&& fn) const { + for (const auto& [name, info] : options_) { + if (info.affects_snapshot && info.type == kBoolean && info.field) { + bool val = *Lookup(info.field, options); + fn(name, val, info.default_is_true); + } + } + } + private: // We support the wide variety of different option types by remembering // how to access them, given a certain `Options` struct. @@ -622,6 +636,7 @@ class OptionsParser { std::string help_text; bool default_is_true = false; std::string namespace_id; + bool affects_snapshot = false; }; // An implied option is composed of the information on where to store a @@ -693,6 +708,20 @@ void HandleEnvOptions(std::shared_ptr env_options, std::vector ParseNodeOptionsEnvVar( const std::string& node_options, std::vector* errors); + +// Describes one snapshot-affecting boolean option. +struct SnapshotFlagInfo { + std::string name; // canonical CLI flag name + bool value; // current effective value + bool default_is_true; // true if the flag defaults to on (--no-* form) +}; + +// Returns every boolean EnvironmentOption marked affects_snapshot=true, +// sorted alphabetically by name. The sort order determines bit positions +// in SnapshotFlags (bit 1 = index 0, bit 2 = index 1, ...). +std::vector GetSnapshotAffectingOptions( + const EnvironmentOptions& opts); + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index f90e5f28ac28a9..c3ad20456bdb82 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -536,7 +536,7 @@ size_t SnapshotSerializer::Write(const EnvSerializeInfo& data) { // [ ... ] |length| bytes of node arch // [ 4/8 bytes ] length of the node platform string // [ ... ] |length| bytes of node platform -// [ 4 bytes ] v8 cache version tag +// [ 4 bytes ] SnapshotFlags template <> SnapshotMetadata SnapshotDeserializer::Read() { Debug("Read()\n"); @@ -669,6 +669,37 @@ bool SnapshotData::FromBlob(SnapshotData* out, std::string_view in) { return true; } +SnapshotFlags operator|(SnapshotFlags x, SnapshotFlags y) { + return static_cast(static_cast(x) | + static_cast(y)); +} + +SnapshotFlags operator&(SnapshotFlags x, SnapshotFlags y) { + return static_cast(static_cast(x) & + static_cast(y)); +} + +SnapshotFlags operator|=(/* NOLINT (runtime/references) */ SnapshotFlags& x, + SnapshotFlags y) { + return x = x | y; +} + +// Compute SnapshotFlags from the current EnvironmentOptions by iterating over +// all options marked affects_snapshot=true, sorted alphabetically. +// Bit 0 (kWithoutCodeCache) is reserved for the config-level flag. +// Env flags occupy bits 1..N where N is the index in the sorted list. +static SnapshotFlags ComputeEnvSnapshotFlags(const EnvironmentOptions& opts) { + auto options = GetSnapshotAffectingOptions(opts); + DCHECK_LE(options.size(), 31u); + SnapshotFlags flags = SnapshotFlags::kDefault; + for (size_t i = 0; i < options.size(); i++) { + if (options[i].value) { + flags = flags | static_cast(1u << (i + 1)); + } + } + return flags; +} + bool SnapshotData::Check() const { if (metadata.node_version != per_process::metadata.versions.node) { fprintf(stderr, @@ -697,7 +728,43 @@ bool SnapshotData::Check() const { return false; } - // TODO(joyeecheung): check incompatible Node.js flags. + // For fully customized snapshots, check that Node.js flags affecting the + // snapshot were the same when the snapshot was built. + if (metadata.type == SnapshotMetadata::Type::kFullyCustomized) { + const auto options = GetSnapshotAffectingOptions( + *per_process::cli_options->per_isolate->per_env); + DCHECK_LE(options.size(), 31u); + bool all_matched = true; + for (size_t i = 0; i < options.size(); i++) { + SnapshotFlags bit = static_cast(1u << (i + 1)); + bool in_snapshot = static_cast(metadata.flags & bit); + bool in_current = options[i].value; + if (in_snapshot != in_current) { + const char* flag_name = options[i].name.c_str(); + std::string snap_desc, curr_desc; + if (options[i].default_is_true) { + // --no- form is what's passed when the feature is off. + std::string negated = std::string("--no-") + (flag_name + 2); + snap_desc = in_snapshot ? (std::string("with ") + flag_name) + : (std::string("with ") + negated); + curr_desc = in_current ? (std::string("with ") + flag_name) + : (std::string("with ") + negated); + } else { + snap_desc = in_snapshot ? (std::string("with ") + flag_name) + : (std::string("without ") + flag_name); + curr_desc = in_current ? (std::string("with ") + flag_name) + : (std::string("without ") + flag_name); + } + fprintf(stderr, + "Failed to load the startup snapshot because it was built " + "%s but the current process is run %s.\n", + snap_desc.c_str(), + curr_desc.c_str()); + all_matched = false; + } + } + if (!all_matched) return false; + } return true; } @@ -876,21 +943,6 @@ void SnapshotBuilder::InitializeIsolateParams(const SnapshotData* data, const_cast(&(data->v8_snapshot_blob_data)); } -SnapshotFlags operator|(SnapshotFlags x, SnapshotFlags y) { - return static_cast(static_cast(x) | - static_cast(y)); -} - -SnapshotFlags operator&(SnapshotFlags x, SnapshotFlags y) { - return static_cast(static_cast(x) & - static_cast(y)); -} - -SnapshotFlags operator|=(/* NOLINT (runtime/references) */ SnapshotFlags& x, - SnapshotFlags y) { - return x = x | y; -} - bool WithoutCodeCache(const SnapshotFlags& flags) { return static_cast(flags & SnapshotFlags::kWithoutCodeCache); } @@ -1255,11 +1307,13 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out, return ExitCode::kStartupSnapshotFailure; } + SnapshotFlags flags = + config->flags | ComputeEnvSnapshotFlags(*env->options()); out->metadata = SnapshotMetadata{snapshot_type, per_process::metadata.versions.node, per_process::metadata.arch, per_process::metadata.platform, - config->flags}; + flags}; // We cannot resurrect the handles from the snapshot, so make sure that // no handles are left open in the environment after the blob is created diff --git a/test/parallel/test-cli-node-options-docs.js b/test/parallel/test-cli-node-options-docs.js index 534c99c04de964..bb0512f974fff8 100644 --- a/test/parallel/test-cli-node-options-docs.js +++ b/test/parallel/test-cli-node-options-docs.js @@ -39,7 +39,9 @@ for (const [, envVar] of manPageText.matchAll(/\.It Fl (-[a-zA-Z0-9._-]+)/g)) { } for (const [, envVar, config] of nodeOptionsCC.matchAll(addOptionRE)) { - const hasTrueAsDefaultValue = /,\s*(?:true|HAVE_[A-Z_]+)\s*$/.test(config); + // Detect default_is_true by anchoring to the env_setting parameter position; + // a trailing `true` after OptionNamespaces:: would be affects_snapshot, not default_is_true + const hasTrueAsDefaultValue = /,\s*k(?:Allowed|Disallowed)InEnvvar\s*,\s*(?:true|HAVE_[A-Z_]+)/.test(config); const isInNodeOption = config.includes('kAllowedInEnvvar') && !config.includes('kDisallowedInEnvvar'); const isV8Option = config.includes('V8Option{}'); const isNoOp = config.includes('NoOp{}'); diff --git a/test/parallel/test-preload.js b/test/parallel/test-preload.js index 5861f26dceaf60..6d1a9aa304e326 100644 --- a/test/parallel/test-preload.js +++ b/test/parallel/test-preload.js @@ -21,7 +21,6 @@ const fixtureC = fixtures.path('printC.js'); const fixtureD = fixtures.path('define-global.js'); const fixtureE = fixtures.path('intrinsic-mutation.js'); const fixtureF = fixtures.path('print-intrinsic-mutation-name.js'); -const fixtureG = fixtures.path('worker-from-argv.js'); const fixtureThrows = fixtures.path('throws_error4.js'); const fixtureIsPreloading = fixtures.path('ispreloading.js'); @@ -64,18 +63,53 @@ common.spawnPromisified(nodeBinary, [...preloadOption(fixtureA), '-e', 'console. } )); -// Test that preload can be used with --frozen-intrinsics -common.spawnPromisified(nodeBinary, ['--frozen-intrinsics', ...preloadOption(fixtureE), fixtureF]).then(common.mustCall( - ({ stdout }) => { - assert.strictEqual(stdout, 'smoosh\n'); +// Test that preload can be used alongside other flags +{ + // Flags that would interfere with test output or require special configuration + const excludedFlags = new Set([ + '--abort-on-uncaught-exception', + '--build-snapshot', + '--completion-bash', + '--cpu-prof', + '--enable-etw-stack-walking', + '--enable-fips', + '--force-fips', + '--frozen-intrinsics', + '--heap-prof', + '--node-memory-debug', + '--prof', + '--prof-process', + '--test', + '--test-only', + '--v8-options', + '--watch', + '--watch-preserve-output', + ]); + + const helpText = childProcess.execFileSync(nodeBinary, ['--help'], { encoding: 'utf8' }); + const flagsToTest = helpText + .split('\n') + .filter((line) => line.startsWith(' --') && !line.includes('=')) + .map((line) => { + const trimmed = line.trimStart(); + const end = trimmed.indexOf(' '); + // Strip trailing comma that appears on wrapped help lines + return (end === -1 ? trimmed : trimmed.slice(0, end)).replace(/,$/, ''); + }) + .filter((flag) => flag.length > 2 && + !flag.startsWith('--experimental-') && + // --allow-* flags require --permission and cannot be used standalone + !flag.startsWith('--allow-') && + !excludedFlags.has(flag)); + + for (const flag of flagsToTest) { + common.spawnPromisified(nodeBinary, [flag, ...preloadOption(fixtureE), fixtureF]).then(common.mustCall( + ({ stdout }) => { + assert.strictEqual(stdout, 'smoosh\n'); + } + )); } -)); -common.spawnPromisified(nodeBinary, ['--frozen-intrinsics', ...preloadOption(fixtureE), fixtureG, fixtureF]) - .then(common.mustCall( - ({ stdout }) => { - assert.strictEqual(stdout, 'smoosh\n'); - } - )); +} // Test that preload can be used with stdin const stdinProc = childProcess.spawn( diff --git a/test/parallel/test-snapshot-incompatible.js b/test/parallel/test-snapshot-incompatible.js index 28d1a83a1f7664..74b30b04b6ee2e 100644 --- a/test/parallel/test-snapshot-incompatible.js +++ b/test/parallel/test-snapshot-incompatible.js @@ -1,7 +1,7 @@ 'use strict'; // This tests that Node.js refuses to load snapshots built with incompatible -// V8 configurations. +// configurations (V8 or Node.js flags). require('../common'); const assert = require('assert'); @@ -9,9 +9,10 @@ const { spawnSync } = require('child_process'); const tmpdir = require('../common/tmpdir'); const fixtures = require('../common/fixtures'); const fs = require('fs'); +const { internalBinding } = require('internal/test/binding'); +const { getSnapshotAffectingFlags } = internalBinding('options'); tmpdir.refresh(); -const blobPath = tmpdir.resolve('snapshot.blob'); const entry = fixtures.path('empty.js'); // The flag used can be any flag that makes a difference in @@ -22,7 +23,7 @@ const entry = fixtures.path('empty.js'); const child = spawnSync(process.execPath, [ '--harmony', '--snapshot-blob', - blobPath, + tmpdir.resolve('snapshot.blob'), '--build-snapshot', entry, ], { @@ -38,10 +39,13 @@ const entry = fixtures.path('empty.js'); } { - // Now load the snapshot without --harmony, which should fail. + // Now load the snapshot without --harmony, which should fail if V8's + // CachedDataVersionTag still distinguishes --harmony. Some V8 versions no + // longer treat --harmony as a cache-busting flag, so only assert when we + // actually see the mismatch. const child = spawnSync(process.execPath, [ '--snapshot-blob', - blobPath, + tmpdir.resolve('snapshot.blob'), ], { cwd: tmpdir.path, env: { @@ -49,9 +53,11 @@ const entry = fixtures.path('empty.js'); } }); - const stderr = child.stderr.toString().trim(); - assert.match(stderr, /Failed to load the startup snapshot/); - assert.strictEqual(child.status, 14); + if (child.status !== 0) { + const stderr = child.stderr.toString().trim(); + assert.match(stderr, /Failed to load the startup snapshot/); + assert.strictEqual(child.status, 14); + } } { @@ -59,7 +65,7 @@ const entry = fixtures.path('empty.js'); const child = spawnSync(process.execPath, [ '--harmony', '--snapshot-blob', - blobPath, + tmpdir.resolve('snapshot.blob'), ], { cwd: tmpdir.path, env: { @@ -73,3 +79,73 @@ const entry = fixtures.path('empty.js'); assert.strictEqual(child.status, 0); } } + +// Test that all snapshot-affecting boolean options are detected when mismatched. +// +// Flags that cannot be easily exercised in this test (e.g. require platform +// support that may be absent, or cause the snapshot build itself to fail). +const excludedFlags = new Set([ + // --experimental-quic is only available when built with QUIC support. + '--experimental-quic', +]); + +const snapshotFlags = getSnapshotAffectingFlags(); + +for (const { name, defaultIsTrue } of snapshotFlags) { + if (excludedFlags.has(name)) continue; + + // For default_is_true options (e.g. --experimental-websocket), pass the + // --no- form to disable the feature when building. + const buildFlag = defaultIsTrue ? `--no-${name.slice(2)}` : name; + const blobPath = tmpdir.resolve( + `snapshot${name.replace(/[^a-zA-Z0-9]/g, '-')}.blob` + ); + + { + const child = spawnSync(process.execPath, [ + buildFlag, + '--snapshot-blob', blobPath, + '--build-snapshot', entry, + ], { cwd: tmpdir.path }); + if (child.status !== 0) { + console.log(`Build with ${buildFlag} failed:`); + console.log(child.stderr.toString()); + assert.strictEqual(child.status, 0); + } + assert(fs.statSync(blobPath).isFile()); + } + + { + // Load without the build flag: should fail. + const child = spawnSync(process.execPath, [ + '--snapshot-blob', blobPath, + ], { cwd: tmpdir.path, env: { ...process.env } }); + + const stderr = child.stderr.toString().trim(); + assert.match( + stderr, + /Failed to load the startup snapshot/, + `Expected mismatch error for ${buildFlag}` + ); + assert.match( + stderr, + new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), + `Expected flag name ${name} in error message` + ); + assert.strictEqual(child.status, 14, `Expected exit 14 for ${buildFlag}`); + } + + { + // Load with the same flag: should succeed. + const child = spawnSync(process.execPath, [ + buildFlag, + '--snapshot-blob', blobPath, + ], { cwd: tmpdir.path, env: { ...process.env } }); + + if (child.status !== 0) { + console.log(`Load with matching ${buildFlag} failed:`); + console.log(child.stderr.toString()); + assert.strictEqual(child.status, 0); + } + } +}