mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
This has been causing a lot of confusion when contributors have been debugging tree shaking, eg. https://github.com/vercel/next.js/issues/95698 and the linked issues. Turbopack's standard tree-shaking features are controlled through the `turbopackRemoveUnusedExports` and `turbopackRemoveUnusedImports` options which default to true.
812 lines
32 KiB
Rust
812 lines
32 KiB
Rust
#![allow(clippy::items_after_test_module)]
|
|
#![feature(arbitrary_self_types)]
|
|
#![feature(arbitrary_self_types_pointers)]
|
|
|
|
mod helpers;
|
|
use std::{
|
|
collections::HashSet,
|
|
env::temp_dir,
|
|
fmt::Display,
|
|
fs::{
|
|
remove_dir_all, {self},
|
|
},
|
|
io::{ErrorKind, Write as _},
|
|
path::{Path, PathBuf},
|
|
sync::{Arc, LazyLock, Mutex},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use anyhow::{Context, Result};
|
|
use difference::Changeset;
|
|
use helpers::print_changeset;
|
|
use regex::Regex;
|
|
use rstest::*;
|
|
use rstest_reuse::{
|
|
*, {self},
|
|
};
|
|
use serde::Serialize;
|
|
use tokio::{process::Command, time::timeout};
|
|
use turbo_rcstr::{RcStr, rcstr};
|
|
use turbo_tasks::{
|
|
Effects, ResolvedVc, TurboTasks, ValueToString, Vc, backend::Backend,
|
|
read_strongly_consistent_and_apply_effects, take_effects, trace::TraceRawVcs,
|
|
};
|
|
use turbo_tasks_backend::TurboTasksBackend;
|
|
use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath};
|
|
use turbopack::{
|
|
ModuleAssetContext, emit_assets_into_dir_operation,
|
|
module_options::{
|
|
CssOptionsContext, EcmascriptOptionsContext, ModuleOptionsContext,
|
|
TypescriptTransformOptions,
|
|
},
|
|
};
|
|
use turbopack_core::{
|
|
chunk::SourceMapsType,
|
|
compile_time_info::CompileTimeInfo,
|
|
context::AssetContext,
|
|
environment::{Environment, ExecutionEnvironment, NodeJsEnvironment},
|
|
file_source::FileSource,
|
|
ident::Layer,
|
|
output::{OutputAsset, OutputAssetsReference},
|
|
rebase::RebasedAsset,
|
|
reference::all_assets_from_entry,
|
|
reference_type::ReferenceType,
|
|
resolve::options::ConditionValue,
|
|
};
|
|
use turbopack_ecmascript::AnalyzeMode;
|
|
use turbopack_resolve::resolve_options_context::ResolveOptionsContext;
|
|
|
|
#[global_allocator]
|
|
static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc;
|
|
|
|
#[template]
|
|
#[rstest]
|
|
#[case::analytics_node("integration/analytics-node.js")]
|
|
#[case::array_map_require("integration/array-map-require/index.js")]
|
|
#[case::apollo("integration/apollo.js")]
|
|
#[case::argon2("integration/argon2.js")]
|
|
#[case::auth0("integration/auth0.js")]
|
|
#[case::aws_sdk_old("integration/aws-sdk-old.js")]
|
|
#[case::aws_sdk("integration/aws-sdk.js")]
|
|
#[case::axios("integration/axios.js")]
|
|
#[case::azure_cosmos("integration/azure-cosmos.js")]
|
|
#[case::azure_storage("integration/azure-storage.js")]
|
|
#[case::bcrypt("integration/bcrypt.js")]
|
|
#[case::better_sqlite3("integration/better-sqlite3.js")]
|
|
#[cfg_attr(
|
|
not(feature = "bench_against_node_nft"),
|
|
should_panic(expected = "Error: Could not locate the bindings file."),
|
|
case::bindings_failure("integration/bindings-failure.js")
|
|
)]
|
|
#[case::browserify_middleware("integration/browserify-middleware.js")]
|
|
#[case::bugsnag_js("integration/bugsnag-js.js")]
|
|
#[case::bull("integration/bull.js")]
|
|
#[case::bull_mq("integration/bullmq.js")]
|
|
#[case::camaro("integration/camaro.js")]
|
|
#[case::canvas("integration/canvas.js")]
|
|
#[case::chromeless("integration/chromeless.js")]
|
|
#[case::core_js("integration/core-js.js")]
|
|
#[case::cosmosdb_query("integration/cosmosdb-query.js")]
|
|
#[case::cowsay("integration/cowsay.js")]
|
|
#[cfg_attr(
|
|
not(feature = "bench_against_node_nft"),
|
|
should_panic(expected = "Error: Cannot find module '../../out/node-file-trace'"),
|
|
case::dogfood("integration/dogfood.js")
|
|
)]
|
|
#[case::datadog_pprof("integration/datadog-pprof.js")]
|
|
#[case::dynamic_in_package("integration/dynamic-in-package.js")]
|
|
#[case::empty("integration/empty.js")]
|
|
#[case::env_var("integration/env-var.js")]
|
|
#[case::es_get_iterator("integration/es-get-iterator.js")]
|
|
// This is flakey on Windows. Disable for now.
|
|
#[cfg_attr(not(target_os = "windows"), case::esbuild("integration/esbuild.js"))]
|
|
#[case::esm("integration/esm.js")]
|
|
#[case::express_consolidate("integration/express-consolidate.js")]
|
|
#[case::express_template_engine("integration/express-template-engine.js")]
|
|
#[case::express_template_pug("integration/express-template.js")]
|
|
#[case::express("integration/express.js")]
|
|
#[case::fast_glob("integration/fast-glob.js")]
|
|
#[case::fetch_h2("integration/fetch-h2.js")]
|
|
// Could not find ffmpeg executable
|
|
#[cfg_attr(target_arch = "x86_64", case::ffmpeg_js("integration/ffmpeg.js"))]
|
|
#[case::ffmpeg_static("integration/ffmpeg-static.js")]
|
|
#[case::firebase_admin("integration/firebase-admin.js")]
|
|
#[case::firebase("integration/firebase.js")]
|
|
#[case::firestore("integration/firestore.js")]
|
|
#[case::fluent_ffmpeg("integration/fluent-ffmpeg.js")]
|
|
#[case::geo_tz("integration/geo-tz.js")]
|
|
#[case::geoip_lite("integration/geoip-lite.js")]
|
|
#[case::google_bigquery("integration/google-bigquery.js")]
|
|
#[case::got("integration/got.js")]
|
|
#[case::highlights("integration/highlights.js")]
|
|
#[case::hot_shots("integration/hot-shots.js")]
|
|
#[case::ioredis("integration/ioredis.js")]
|
|
#[case::isomorphic_unfetch("integration/isomorphic-unfetch.js")]
|
|
#[case::jimp("integration/jimp.js")]
|
|
#[case::jugglingdb("integration/jugglingdb.js")]
|
|
#[case::koa("integration/koa.js")]
|
|
#[case::leveldown("integration/leveldown.js")]
|
|
#[case::lighthouse("integration/lighthouse.js")]
|
|
#[case::loopback("integration/loopback.js")]
|
|
#[case::mailgun("integration/mailgun.js")]
|
|
#[case::mariadb("integration/mariadb.js")]
|
|
#[case::memcached("integration/memcached.js")]
|
|
#[cfg_attr(
|
|
not(feature = "bench_against_node_nft"),
|
|
should_panic(expected = "Error [ERR_MODULE_NOT_FOUND]: Cannot find module"),
|
|
case::mdx("integration/mdx/index.cjs")
|
|
)]
|
|
#[case::mongoose("integration/mongoose.js")]
|
|
#[case::mysql("integration/mysql.js")]
|
|
#[case::npm("integration/npm.js")]
|
|
// unable to resolve esm request module 'spdx-license-ids' in
|
|
// node-file-trace/node_modules/npm/node_modules/spdx-correct oracledb doesn't support non x86
|
|
// architectures
|
|
#[cfg_attr(target_arch = "x86_64", case::oracledb("integration/oracledb.js"))]
|
|
#[case::otel_api("integration/otel-api.js")]
|
|
#[case::paraphrase("integration/paraphrase.js")]
|
|
#[case::passport_trakt("integration/passport-trakt.js")]
|
|
#[case::passport("integration/passport.js")]
|
|
#[case::path_platform("integration/path-platform.js")]
|
|
#[case::pixelmatch("integration/pixelmatch.js")]
|
|
#[case::pdf2json("integration/pdf2json.js")]
|
|
#[case::pdfkit("integration/pdfkit.js")]
|
|
#[case::pg("integration/pg.js")]
|
|
#[case::pino("integration/pino.js")]
|
|
#[case::playwright_core("integration/playwright-core.js")]
|
|
#[case::pnpm_like("integration/pnpm/pnpm-like.js")]
|
|
#[case::polyfill_library("integration/polyfill-library.js")]
|
|
#[case::pug("integration/pug.js")]
|
|
#[case::react("integration/react.js")]
|
|
#[case::redis("integration/redis.js")]
|
|
#[case::remark_prism("integration/remark-prism.mjs")]
|
|
#[case::request("integration/request.js")]
|
|
#[case::rxjs("integration/rxjs.js")]
|
|
#[case::saslprep("integration/saslprep.js")]
|
|
#[case::semver("integration/semver.js")]
|
|
#[case::sentry("integration/sentry.js")]
|
|
#[case::sequelize("integration/sequelize.js")]
|
|
#[case::serialport("integration/serialport.js")]
|
|
#[cfg_attr(
|
|
target_os = "windows",
|
|
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
|
|
case::sharp030("integration/sharp030.js")
|
|
)]
|
|
#[cfg_attr(not(target_os = "windows"), case::sharp030("integration/sharp030.js"))]
|
|
#[cfg_attr(
|
|
target_os = "windows",
|
|
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
|
|
case::sharp033("integration/sharp033.js")
|
|
)]
|
|
#[cfg_attr(not(target_os = "windows"), case::sharp033("integration/sharp033.js"))]
|
|
#[cfg_attr(
|
|
target_os = "windows",
|
|
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
|
|
case::sharp034("integration/sharp034.js")
|
|
)]
|
|
#[cfg_attr(not(target_os = "windows"), case::sharp034("integration/sharp034.js"))]
|
|
#[cfg_attr(
|
|
target_os = "windows",
|
|
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
|
|
case::sharp("integration/sharp.js")
|
|
)]
|
|
#[cfg_attr(not(target_os = "windows"), case::sharp("integration/sharp.js"))]
|
|
#[cfg_attr(
|
|
target_os = "windows",
|
|
should_panic(expected = "Something went wrong installing the \"sharp\" module"),
|
|
case::sharp_pnpm("integration/sharp-pnpm.js")
|
|
)]
|
|
#[cfg_attr(
|
|
not(target_os = "windows"),
|
|
case::sharp_pnpm("integration/sharp-pnpm.js")
|
|
)]
|
|
#[case::shiki("integration/shiki.js")]
|
|
#[case::simple("integration/simple.js")]
|
|
#[case::socket_io("integration/socket.io.js")]
|
|
#[case::source_map("integration/source-map/index.js")]
|
|
#[case::sparql_builder("integration/sparql-builder.js")]
|
|
#[case::sqlite("integration/sqlite.js")]
|
|
#[case::stripe("integration/stripe.js")]
|
|
#[case::strong_error_handler("integration/strong-error-handler.js")]
|
|
#[case::symlink_to_file("integration/symlink-to-file/index.js")]
|
|
#[case::tiny_json_http("integration/tiny-json-http.js")]
|
|
#[case::twilio("integration/twilio.js")]
|
|
#[case::ts_morph("integration/ts-morph.js")]
|
|
#[case::typescript("integration/typescript.js")]
|
|
#[case::uglify("integration/uglify.js")]
|
|
#[case::underscore("integration/underscore.js")]
|
|
#[case::vm2("integration/vm2.js")]
|
|
#[case::vue("integration/vue.js")]
|
|
#[case::webpack_target_node("integration/webpack-target-node/index.js")]
|
|
#[case::whatwg_url("integration/whatwg-url.js")]
|
|
#[case::when("integration/when.js")]
|
|
// TODO PACK-4987
|
|
// #[case::zeromq("integration/zeromq.js")]
|
|
#[case::package_exports_alt_folders_base(
|
|
CaseInput::new("integration/package-exports/pass/alt-folders.js").expected_stderr("Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath")
|
|
)]
|
|
#[case::package_exports_folder(
|
|
CaseInput::new("integration/package-exports/pass/folder.js").expected_stderr("Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath")
|
|
)]
|
|
#[case::package_exports_alt_base("integration/package-exports/pass/alt.js")]
|
|
#[case::package_exports_catch_all("integration/package-exports/pass/catch-all.js")]
|
|
#[case::package_exports_direct("integration/package-exports/pass/direct.js")]
|
|
#[case::package_exports_double("integration/package-exports/pass/double.js")]
|
|
#[case::package_exports_nested("integration/package-exports/pass/nested.js")]
|
|
#[case::package_exports_package_root("integration/package-exports/pass/root.js")]
|
|
#[case::package_exports_package_single_export_root(
|
|
"integration/package-exports/pass/single-export-root.js"
|
|
)]
|
|
#[case::package_exports_package_sub_infix_sep("integration/package-exports/pass/sub-infix-sep.js")]
|
|
#[case::package_exports_package_sub_infix_base("integration/package-exports/pass/sub-infix.js")]
|
|
#[case::package_exports_package_sub_prefix_sep(
|
|
"integration/package-exports/pass/sub-prefix-sep.js"
|
|
)]
|
|
#[case::package_exports_package_sub_prefix("integration/package-exports/pass/sub-prefix.js")]
|
|
#[case::package_exports_package_sub_suffix_sep(
|
|
"integration/package-exports/pass/sub-suffix-sep.js"
|
|
)]
|
|
#[case::package_exports_package_sub_suffix_base("integration/package-exports/pass/sub-suffix.js")]
|
|
#[case::package_exports_alt_folders_multiple(
|
|
CaseInput::new("integration/package-exports/fail/alt-folders-multiple.js")
|
|
.expected_stderr("Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath")
|
|
)]
|
|
#[case::package_exports_alt_multiple(
|
|
CaseInput::new("integration/package-exports/fail/alt-multiple.js")
|
|
.expected_stderr("Error [ERR_MODULE_NOT_FOUND]: Cannot find module")
|
|
)]
|
|
#[case::read_file("integration/read-file.mjs")]
|
|
#[cfg_attr(
|
|
not(feature = "bench_against_node_nft"),
|
|
//[TODO]: WEB-1188 reenable once fixed.
|
|
//case::ts_package_base("integration/ts-package/index.ts"),
|
|
//case::ts_package_extends("integration/ts-package-extends/index.ts"),
|
|
//case::ts_package_from_js("integration/ts-package-from-js/index.js"),
|
|
case::ts_paths_alt_base("integration/ts-paths/pass/alt.ts"),
|
|
case::ts_paths_catch_all("integration/ts-paths/pass/catch-all.ts"),
|
|
case::ts_paths_direct("integration/ts-paths/pass/direct.ts"),
|
|
case::ts_paths_nested("integration/ts-paths/pass/nested.ts"),
|
|
case::ts_paths_package_sub_prefix("integration/ts-paths/pass/sub-prefix.ts"),
|
|
case::ts_paths_package_sub_suffix_sep_base("integration/ts-paths/pass/sub-suffix-sep.ts"),
|
|
case::ts_paths_package_sub_suffix_base("integration/ts-paths/pass/sub-suffix.ts"),
|
|
case::ts_paths_alt_folders(
|
|
CaseInput::new("integration/ts-paths/fail/alt-folders.ts")
|
|
.expected_stderr("Cannot find module 'fixtures/alt-folders/alt1.js'")
|
|
),
|
|
case::ts_paths_double(
|
|
CaseInput::new("integration/ts-paths/fail/double.ts")
|
|
.expected_stderr("Cannot find module 'double/sub' or its corresponding type declarations.")
|
|
),
|
|
case::ts_paths_folder(
|
|
CaseInput::new("integration/ts-paths/fail/folder.ts")
|
|
.expected_stderr("Cannot find module 'folder/alt1' or its corresponding type declarations.")
|
|
),
|
|
// TODO(alexkirsz) I expect the two following infix tests are only failing when using `tsconfig-paths`, since
|
|
// VSCode's TS language server can resolve them properly. We should pre-compile the TS files to JS and run Node.js
|
|
// on them directly instead.
|
|
case::ts_paths_package_sub_infix_sep(
|
|
CaseInput::new("integration/ts-paths/fail/sub-infix-sep.ts")
|
|
.expected_stderr("Cannot find module '@/sub/@'")
|
|
),
|
|
case::ts_paths_package_sub_infix_base(
|
|
CaseInput::new("integration/ts-paths/fail/sub-infix.ts")
|
|
.expected_stderr("Cannot find module '@sub@'")
|
|
),
|
|
case::ts_paths_sub_prefix_sep(
|
|
CaseInput::new("integration/ts-paths/fail/sub-prefix-sep.ts")
|
|
.expected_stderr("Cannot find module 'sub/@' or its corresponding type declarations")
|
|
),
|
|
)]
|
|
#[cfg_attr(
|
|
//[TODO]: WEB-1188 reenable windows once fixed.
|
|
not(any(feature = "bench_against_node_nft", target_os = "windows")),
|
|
case::ts_package_base("integration/ts-package/index.ts"),
|
|
case::ts_package_extends("integration/ts-package-extends/index.ts"),
|
|
case::ts_package_from_js("integration/ts-package-from-js/index.js"),
|
|
)]
|
|
fn test_cases() {}
|
|
|
|
#[apply(test_cases)]
|
|
fn node_file_trace_noop_backing_storage(#[case] input: CaseInput) {
|
|
node_file_trace(input, "noop_backing_storage", 1, 120, |_| {
|
|
TurboTasks::new(TurboTasksBackend::new(
|
|
turbo_tasks_backend::BackendOptions {
|
|
storage_mode: None,
|
|
dependency_tracking: false,
|
|
..Default::default()
|
|
},
|
|
turbo_tasks_backend::noop_backing_storage(),
|
|
))
|
|
});
|
|
}
|
|
|
|
#[apply(test_cases)]
|
|
fn node_file_trace_persistent(#[case] input: CaseInput) {
|
|
node_file_trace(input, "persistent_cache", 2, 240, |directory_path| {
|
|
TurboTasks::new(TurboTasksBackend::new(
|
|
turbo_tasks_backend::BackendOptions::default(),
|
|
turbo_tasks_backend::turbo_backing_storage(
|
|
&directory_path.join(".cache"),
|
|
&turbo_tasks_backend::GitVersionInfo {
|
|
describe: "test-unversioned",
|
|
dirty: false,
|
|
},
|
|
false,
|
|
true,
|
|
false,
|
|
)
|
|
.unwrap()
|
|
.0,
|
|
))
|
|
});
|
|
}
|
|
|
|
#[cfg(feature = "bench_against_node_nft")]
|
|
#[apply(test_cases)]
|
|
fn bench_against_node_nft(#[case] input: CaseInput) {
|
|
bench_against_node_nft_inner(input);
|
|
}
|
|
|
|
#[cfg(feature = "bench_against_node_nft")]
|
|
fn bench_against_node_nft_inner(input: CaseInput) {
|
|
node_file_trace(input, "noop_backing_storage", 1, 120, |_| {
|
|
TurboTasks::new(TurboTasksBackend::new(
|
|
turbo_tasks_backend::BackendOptions::default(),
|
|
turbo_tasks_backend::noop_backing_storage(),
|
|
))
|
|
});
|
|
}
|
|
|
|
#[turbo_tasks::value(serialization = "skip", evict = "never")]
|
|
struct NodeFileTraceResult {
|
|
rebased: ResolvedVc<RebasedAsset>,
|
|
effects: Effects,
|
|
}
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn node_file_trace_operation(
|
|
package_root: RcStr,
|
|
input: RcStr,
|
|
directory: RcStr,
|
|
) -> Result<Vc<NodeFileTraceResult>> {
|
|
let workspace_fs: Vc<Box<dyn FileSystem>> = Vc::upcast(DiskFileSystem::new(
|
|
rcstr!("workspace"),
|
|
Vc::cell(package_root.clone()),
|
|
));
|
|
let input_dir = workspace_fs.root().owned().await?;
|
|
let input = input_dir.join(&format!("tests/{input}"))?;
|
|
|
|
let output_fs = DiskFileSystem::new(rcstr!("output"), Vc::cell(directory.clone()));
|
|
let output_dir = output_fs.root().owned().await?;
|
|
|
|
let source = FileSource::new(input);
|
|
let environment = Environment::new(ExecutionEnvironment::NodeJsLambda(
|
|
NodeJsEnvironment {
|
|
cwd: ResolvedVc::cell(Some(input_dir.join("tests/node-file-trace/integration")?)),
|
|
..Default::default()
|
|
}
|
|
.resolved_cell(),
|
|
));
|
|
let module_asset_context = ModuleAssetContext::new_without_replace_externals(
|
|
Default::default(),
|
|
// TODO These test cases should move into the `node-file-trace` crate and use the same
|
|
// config.
|
|
// This config should be kept in sync with
|
|
// turbopack/crates/turbopack-tracing/tests/node-file-trace.rs and
|
|
// turbopack/crates/turbopack-tracing/tests/unit.rs and
|
|
// turbopack/crates/turbopack/src/lib.rs and
|
|
// turbopack/crates/turbopack-nft/src/nft.rs
|
|
CompileTimeInfo::new(environment),
|
|
ModuleOptionsContext {
|
|
ecmascript: EcmascriptOptionsContext {
|
|
enable_typescript_transform: Some(
|
|
TypescriptTransformOptions::default().resolved_cell(),
|
|
),
|
|
// enable_types is required here to ensure .d.ts files are collected.
|
|
enable_types: true,
|
|
..Default::default()
|
|
},
|
|
css: CssOptionsContext {
|
|
source_maps: SourceMapsType::None,
|
|
enable_raw_css: true,
|
|
..Default::default()
|
|
},
|
|
// Environment is not passed in order to avoid downleveling JS / CSS for
|
|
// node-file-trace.
|
|
environment: None,
|
|
analyze_mode: AnalyzeMode::Tracing,
|
|
// Disable tree shaking. Even side-effect-free imports need to be traced, as they will
|
|
// execute at runtime.
|
|
follow_reexports: false,
|
|
module_fragments_enabled: false,
|
|
..Default::default()
|
|
}
|
|
.cell(),
|
|
ResolveOptionsContext {
|
|
enable_node_native_modules: true,
|
|
enable_node_modules: Some(input_dir.clone()),
|
|
custom_conditions: vec![rcstr!("node")],
|
|
module_sync: ConditionValue::Unknown,
|
|
collect_affecting_sources: true,
|
|
..Default::default()
|
|
}
|
|
.cell(),
|
|
Layer::new(rcstr!("test")),
|
|
);
|
|
let module = module_asset_context
|
|
.process(Vc::upcast(source), ReferenceType::Undefined)
|
|
.module();
|
|
|
|
let rebased = RebasedAsset::new(module, input_dir.clone(), output_dir.clone())
|
|
.to_resolved()
|
|
.await?;
|
|
let assets = all_assets_from_entry(Vc::upcast(*rebased))
|
|
.to_resolved()
|
|
.await?;
|
|
|
|
let emit_op = emit_assets_into_dir_operation(assets, output_dir.clone());
|
|
emit_op.read_strongly_consistent().await?;
|
|
let effects = take_effects(emit_op).await?;
|
|
|
|
Ok(NodeFileTraceResult { rebased, effects }.cell())
|
|
}
|
|
|
|
fn node_file_trace<B: Backend + 'static>(
|
|
CaseInput {
|
|
path: input_path,
|
|
#[allow(unused)]
|
|
expected_stderr,
|
|
}: CaseInput,
|
|
mode: &str,
|
|
run_count: i32,
|
|
timeout_len: u64,
|
|
create_turbo_tasks: impl Fn(&Path) -> Arc<TurboTasks<B>>,
|
|
) {
|
|
static BENCH_SUITES: LazyLock<Arc<Mutex<Vec<BenchSuite>>>> =
|
|
LazyLock::new(|| Arc::new(Mutex::new(Vec::new())));
|
|
|
|
let r = &mut {
|
|
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
builder.enable_all();
|
|
builder.max_blocking_threads(20);
|
|
builder.build().unwrap()
|
|
};
|
|
r.block_on(async move {
|
|
let bench_suites = BENCH_SUITES.clone();
|
|
let package_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
let mut tests_output_root = temp_dir();
|
|
tests_output_root.push("tests_output");
|
|
let package_root: RcStr = package_root.to_string_lossy().into();
|
|
let input: RcStr = format!("node-file-trace/{input_path}").into();
|
|
let directory_path = tests_output_root.join(format!("{mode}_{input}"));
|
|
let directory: RcStr = directory_path.to_string_lossy().into();
|
|
|
|
remove_dir_all(&directory)
|
|
.or_else(|err| {
|
|
if err.kind() == ErrorKind::NotFound {
|
|
Ok(())
|
|
} else {
|
|
Err(err)
|
|
}
|
|
})
|
|
.with_context(|| format!("Failed to remove directory: {directory}"))
|
|
.unwrap();
|
|
|
|
for _ in 0..run_count {
|
|
let bench_suites = bench_suites.clone();
|
|
let expected_stderr = expected_stderr.clone();
|
|
let package_root = package_root.clone();
|
|
let input = input.clone();
|
|
let directory = directory.clone();
|
|
let task = async move {
|
|
let before_start = Instant::now();
|
|
let trace_op = node_file_trace_operation(
|
|
package_root.clone(),
|
|
input.clone(),
|
|
directory.clone(),
|
|
);
|
|
let trace_result =
|
|
read_strongly_consistent_and_apply_effects(trace_op, |v| &v.effects).await?;
|
|
let rebased = trace_result.rebased;
|
|
let duration = before_start.elapsed();
|
|
|
|
print_graph_operation(ResolvedVc::upcast(rebased))
|
|
.read_strongly_consistent()
|
|
.await?;
|
|
|
|
if cfg!(feature = "bench_against_node_nft") {
|
|
let node_start = Instant::now();
|
|
exec_node(&package_root, &input).await?;
|
|
let node_duration = node_start.elapsed();
|
|
let is_faster = node_duration > duration;
|
|
{
|
|
let mut bench_suites_lock = bench_suites.lock().unwrap();
|
|
let rust_speedup =
|
|
node_duration.as_millis() as f32 / duration.as_millis() as f32 - 1.0;
|
|
let rust_duration = format!("{duration:?}");
|
|
let node_duration = format!("{node_duration:?}");
|
|
let rust_speedup = if rust_speedup > 1.0 {
|
|
format!("+{rust_speedup:.2}x")
|
|
} else if rust_speedup > 0.0 {
|
|
format!("+{:.0}%", rust_speedup * 100.0)
|
|
} else {
|
|
format!("-{:.0}%", -rust_speedup * 100.0)
|
|
};
|
|
bench_suites_lock.push(BenchSuite {
|
|
suite: input
|
|
.trim_start_matches("node-file-trace/integration/")
|
|
.to_string(),
|
|
is_faster,
|
|
rust_duration,
|
|
node_duration,
|
|
rust_speedup,
|
|
});
|
|
}
|
|
Ok(CommandOutput {
|
|
stdout: String::new(),
|
|
stderr: String::new(),
|
|
})
|
|
} else {
|
|
let output_path = &asset_path_operation(ResolvedVc::upcast(rebased))
|
|
.read_strongly_consistent()
|
|
.await?
|
|
.path;
|
|
let original_output =
|
|
exec_node(&package_root, &format!("{package_root}/tests/{input}")).await?;
|
|
let output = exec_node(&directory, output_path).await?;
|
|
assert_output(original_output, output, expected_stderr.map(From::from))
|
|
}
|
|
};
|
|
|
|
fn handle_result(result: Result<CommandOutput>) {
|
|
match result {
|
|
Ok(output) => {
|
|
if !cfg!(feature = "bench_against_node_nft") {
|
|
assert!(
|
|
output.is_empty(),
|
|
"emitted files behave differently when executed via \
|
|
node.js\n{output}"
|
|
);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
panic!("Execution failed: {err:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
let tt = create_turbo_tasks(directory_path.as_path());
|
|
let output = timeout(Duration::from_secs(timeout_len), tt.run_once(task)).await;
|
|
let _ = timeout(Duration::from_secs(2), tt.wait_background_done()).await;
|
|
let stop = timeout(Duration::from_secs(60), tt.stop_and_wait()).await;
|
|
match (output, stop) {
|
|
(Ok(result), Ok(_)) => handle_result(result),
|
|
(Err(err), _) => {
|
|
panic!("Execution is hanging (for > {timeout_len}s): {err}");
|
|
}
|
|
(_, Err(err)) => {
|
|
panic!("Stopping is hanging (for > 60s): {err}");
|
|
}
|
|
}
|
|
}
|
|
let bench_suites_lock = BENCH_SUITES.lock().unwrap();
|
|
if !bench_suites_lock.is_empty() {
|
|
static BENCH_FILE_NAME: &str = "bench.json";
|
|
let mut bench_result = fs::File::options()
|
|
.append(true)
|
|
.open(BENCH_FILE_NAME)
|
|
.unwrap_or_else(|_| fs::File::create(BENCH_FILE_NAME).unwrap());
|
|
bench_result
|
|
.write_all(
|
|
{ serde_json::to_string(bench_suites_lock.as_slice()).unwrap() + "\n" }
|
|
.as_bytes(),
|
|
)
|
|
.unwrap();
|
|
drop(bench_result);
|
|
}
|
|
})
|
|
}
|
|
|
|
#[derive(TraceRawVcs)]
|
|
struct CommandOutput {
|
|
stdout: String,
|
|
stderr: String,
|
|
}
|
|
|
|
impl CommandOutput {
|
|
fn is_empty(&self) -> bool {
|
|
self.stderr.is_empty() && self.stdout.is_empty()
|
|
}
|
|
}
|
|
|
|
impl Display for CommandOutput {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"---------- Stdout ----------\n{}\n---------- Stderr ----------\n{}",
|
|
self.stdout, self.stderr,
|
|
)
|
|
}
|
|
}
|
|
|
|
async fn exec_node(directory: &str, path: &str) -> Result<CommandOutput> {
|
|
let mut cmd = Command::new("node");
|
|
|
|
let f = Path::new(&directory).join(path);
|
|
let dir = f.parent().unwrap();
|
|
println!("[CWD]: {}", dir.display());
|
|
|
|
// See https://github.com/nodejs/node/issues/51555
|
|
// The compile cache is causing flaky crashes when run on github actions.
|
|
cmd.env("DISABLE_V8_COMPILE_CACHE", "1");
|
|
|
|
if path.contains("mdx") {
|
|
cmd.arg("--experimental-loader=@mdx-js/node-loader")
|
|
.arg("--no-warnings");
|
|
}
|
|
|
|
if cfg!(feature = "bench_against_node_nft") {
|
|
let current_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
cmd.current_dir(¤t_dir);
|
|
|
|
let mut node_nft = current_dir;
|
|
node_nft.push("tests");
|
|
node_nft.push("node-file-trace");
|
|
node_nft.push("node_modules");
|
|
node_nft.push("@vercel");
|
|
node_nft.push("nft");
|
|
node_nft.push("out");
|
|
node_nft.push("cli.js");
|
|
cmd.arg(&node_nft).arg("build");
|
|
cmd.arg(path);
|
|
} else {
|
|
cmd.current_dir(dir);
|
|
if path.ends_with(".ts") {
|
|
let mut ts_node = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
ts_node.push("tests");
|
|
ts_node.push("node-file-trace");
|
|
ts_node.push("node_modules");
|
|
ts_node.push("ts-node");
|
|
ts_node.push("dist");
|
|
ts_node.push("bin.js");
|
|
cmd.arg(&ts_node);
|
|
}
|
|
cmd.arg(&f);
|
|
}
|
|
|
|
println!("[CMD]: {cmd:#?}");
|
|
|
|
let output = timeout(Duration::from_secs(100), cmd.output())
|
|
.await
|
|
.with_context(|| format!("node execution of {path} is hanging"))?
|
|
.with_context(|| format!("failed to spawn node process of {path}"))?;
|
|
|
|
let output = CommandOutput {
|
|
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
|
stderr: clean_stderr(String::from_utf8_lossy(&output.stderr).as_ref()),
|
|
};
|
|
|
|
println!("File: {}\n{}", f.display(), output,);
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
fn clean_stderr(str: &str) -> String {
|
|
static EXPERIMENTAL_WARNING: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"\(node:\d+\) ExperimentalWarning:").unwrap());
|
|
static DEPRECATION_WARNING: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"\(node:\d+\) \[DEP\d+] DeprecationWarning:").unwrap());
|
|
let str = EXPERIMENTAL_WARNING.replace_all(str, "(node:XXXX) ExperimentalWarning:");
|
|
let str = DEPRECATION_WARNING.replace_all(&str, "(node:XXXX) [DEPXXXX] DeprecationWarning:");
|
|
str.to_string()
|
|
}
|
|
|
|
fn diff(expected: &str, actual: &str) -> String {
|
|
static JAVASCRIPT_TIMESTAMP: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z").unwrap());
|
|
static JAVASCRIPT_DATE_TIME: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{6}").unwrap());
|
|
// Remove timestamps from the output.
|
|
if JAVASCRIPT_DATE_TIME.replace_all(JAVASCRIPT_TIMESTAMP.replace_all(actual, "").as_ref(), "")
|
|
== JAVASCRIPT_DATE_TIME
|
|
.replace_all(JAVASCRIPT_TIMESTAMP.replace_all(expected, "").as_ref(), "")
|
|
{
|
|
return String::new();
|
|
}
|
|
print_changeset(&Changeset::new(expected.trim(), actual.trim(), "\n"))
|
|
}
|
|
|
|
fn assert_output(
|
|
expected: CommandOutput,
|
|
actual: CommandOutput,
|
|
expected_stderr: Option<RcStr>,
|
|
) -> Result<CommandOutput> {
|
|
Ok(CommandOutput {
|
|
stdout: diff(&expected.stdout, &actual.stdout),
|
|
stderr: if let Some(expected_stderr) = expected_stderr {
|
|
if actual.stderr.contains(&*expected_stderr)
|
|
&& expected.stderr.contains(&*expected_stderr)
|
|
{
|
|
String::new()
|
|
} else {
|
|
let stderr_diff = diff(&expected.stderr, &actual.stderr);
|
|
format!("could not find `{expected_stderr}` in stderr\n{stderr_diff}")
|
|
}
|
|
} else {
|
|
diff(&expected.stderr, &actual.stderr)
|
|
},
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct BenchSuite {
|
|
suite: String,
|
|
node_duration: String,
|
|
rust_duration: String,
|
|
rust_speedup: String,
|
|
is_faster: bool,
|
|
}
|
|
|
|
/// rstest's #[case] attribute does not allow for specifying default values.
|
|
/// However, it can automatically convert between types, so we can use a
|
|
/// custom input struct with the Builder pattern instead.
|
|
struct CaseInput {
|
|
/// The path to the JS or TS test file.
|
|
path: String,
|
|
/// The test will pass if the provided error message is included in both
|
|
/// stderr outputs.
|
|
expected_stderr: Option<String>,
|
|
}
|
|
|
|
impl CaseInput {
|
|
fn new(path: &str) -> Self {
|
|
Self {
|
|
path: path.to_owned(),
|
|
expected_stderr: None,
|
|
}
|
|
}
|
|
|
|
fn expected_stderr(mut self, msg: &str) -> Self {
|
|
self.expected_stderr = Some(msg.to_owned());
|
|
self
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for CaseInput {
|
|
type Err = std::convert::Infallible;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(Self::new(s))
|
|
}
|
|
}
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn asset_path_operation(asset: ResolvedVc<Box<dyn OutputAsset>>) -> Vc<FileSystemPath> {
|
|
asset.path()
|
|
}
|
|
|
|
#[turbo_tasks::function(operation, root)]
|
|
async fn print_graph_operation(asset: ResolvedVc<Box<dyn OutputAsset>>) -> Result<()> {
|
|
let mut visited = HashSet::new();
|
|
let mut queue = Vec::new();
|
|
queue.push((0, asset));
|
|
while let Some((depth, asset)) = queue.pop() {
|
|
let references = asset.references().all_assets().await?;
|
|
let mut indent = String::new();
|
|
for _ in 0..depth {
|
|
indent.push_str(" ");
|
|
}
|
|
if visited.insert(asset) {
|
|
for &asset in references.iter().rev() {
|
|
queue.push((depth + 1, asset));
|
|
}
|
|
println!("{}{}", indent, asset.path().to_string().await?);
|
|
} else if references.is_empty() {
|
|
println!("{}{} *", indent, asset.path().to_string().await?);
|
|
} else {
|
|
println!("{}{} *...", indent, asset.path().to_string().await?);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|