diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/.npmrc b/acceptance/cmd/workspace/apps/run-local-node/app/.npmrc new file mode 100644 index 00000000000..c43162960a8 --- /dev/null +++ b/acceptance/cmd/workspace/apps/run-local-node/app/.npmrc @@ -0,0 +1,3 @@ +; With nothing to install, the one thing npm still reaches the registry for is its check for +; a newer npm, and the acceptance proxy rejects that. The install and the audit stay local. +update-notifier=false diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/app.js b/acceptance/cmd/workspace/apps/run-local-node/app/app.js index 74686d09a82..4e47e5c9cda 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/app/app.js +++ b/acceptance/cmd/workspace/apps/run-local-node/app/app.js @@ -1,27 +1,26 @@ -const express = require('express'); -const app = express(); -const port = process.env.PORT || 8000; +const http = require('node:http'); -// Root route -app.get('/', (req, res) => { - res.json({ - message: 'Hello From App', - timestamp: new Date().toISOString(), - status: 'running' - }); -}); +// Standard library only: acceptance tests run with the network disabled, so the +// app cannot install anything from the NPM registry. + +// The CLI sets PORT alongside DATABRICKS_APP_PORT. Unset, listen() would pick a random port +// and the run would fail as a proxy timeout instead. +const port = process.env.PORT; +if (!port) throw new Error('PORT is not set'); -app.get('/shutdown', (req, res) => { - console.log('Server closed') - // Add a small delay to ensure response is sent before exit - setTimeout(() => { - process.exit(0); - }, 1000); +const server = http.createServer((req, res) => { + if (req.url === '/shutdown') { + // close() waits for open connections, so the response must end this one or the proxy holds it. + res.setHeader('Connection', 'close'); + res.end(); + server.close(); + return; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ message: 'Hello From App' }) + '\n'); }); -// Start the server -app.listen(port, () => { +server.listen(port, '127.0.0.1', () => { console.log(`Server is running on port ${port}`); }); - -module.exports = app; diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/app.yml b/acceptance/cmd/workspace/apps/run-local-node/app/app.yml index bc02e198203..8acf26d0927 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/app/app.yml +++ b/acceptance/cmd/workspace/apps/run-local-node/app/app.yml @@ -1,4 +1,3 @@ command: - - npm - - run - - run-app + - node + - app.js diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/build.js b/acceptance/cmd/workspace/apps/run-local-node/app/build.js new file mode 100644 index 00000000000..58421926421 --- /dev/null +++ b/acceptance/cmd/workspace/apps/run-local-node/app/build.js @@ -0,0 +1,3 @@ +// A file rather than an inline command in package.json, so the asserted output does +// not depend on how npm's shell quotes it, which differs on Windows. +console.log('Build script ran'); diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/package.json b/acceptance/cmd/workspace/apps/run-local-node/app/package.json index 15e06e17ffb..e3b250f18dc 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/app/package.json +++ b/acceptance/cmd/workspace/apps/run-local-node/app/package.json @@ -2,12 +2,7 @@ "name": "app", "version": "1.0.0", "description": "A simple Node.js app", - "main": "app.js", "scripts": { - "run-app": "node app.js", - "build": "echo 'Building app...'" - }, - "dependencies": { - "express": "^5.1.0" + "build": "node build.js" } } diff --git a/acceptance/cmd/workspace/apps/run-local-node/out.test.toml b/acceptance/cmd/workspace/apps/run-local-node/out.test.toml index 3ef9121aba6..e90b6d5d1ba 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/out.test.toml +++ b/acceptance/cmd/workspace/apps/run-local-node/out.test.toml @@ -1,3 +1,3 @@ -Local = false +Local = true Cloud = false EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/cmd/workspace/apps/run-local-node/output.txt b/acceptance/cmd/workspace/apps/run-local-node/output.txt index 0185dbe5234..e6b0bd0a642 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/output.txt +++ b/acceptance/cmd/workspace/apps/run-local-node/output.txt @@ -1,12 +1,28 @@ + +=== Prepare environment runs the build script, and the entry point overrides app.yml + +>>> grep -x Build script ran ../LOG.prepare +Build script ran + +>>> grep -x Running command: node -e console.log('Hello, world') ../LOG.prepare Running command: node -e console.log('Hello, world') + +>>> grep -x Hello, world ../LOG.prepare Hello, world -=== Starting the app in background... -=== Waiting -=== Checking app is running... ->>> curl -s -o - http://127.0.0.1:$(port) -{"message":"Hello From App","timestamp":"[TIMESTAMP]","status":"running"} +=== Proxy forwards to the app + +>>> retry --until Hello From App curl -sS http://127.0.0.1:[PROXY_PORT] +{"message":"Hello From App"} + +=== Debug mode reports the debugger port, and node listens on it + +>>> retry grep -x To debug your app, attach a debugger to port [DEBUG_PORT] LOG.run +To debug your app, attach a debugger to port [DEBUG_PORT] + +>>> grep Debugger listening on ws://127.0.0.1:[DEBUG_PORT]/ LOG.run +Debugger listening on ws://127.0.0.1:[DEBUG_PORT]/[UUID] + +=== Shutting the app down -=== Sending shutdown request... ->>> curl -s -o /dev/null http://127.0.0.1:$(port)/shutdown -Process terminated +>>> curl -sS -o /dev/null http://127.0.0.1:[PROXY_PORT]/shutdown diff --git a/acceptance/cmd/workspace/apps/run-local-node/script b/acceptance/cmd/workspace/apps/run-local-node/script index 3701e6fd697..58a139975b5 100755 --- a/acceptance/cmd/workspace/apps/run-local-node/script +++ b/acceptance/cmd/workspace/apps/run-local-node/script @@ -1,52 +1,45 @@ cd app -# We first run the command with different entry point which starts unblocking script -# so we don't need to start it in background. It will install the dependencies as part of the command -trace $CLI apps run-local --prepare-environment --entry-point test.yml 2>&1 | grep -w "Hello, world" - -read -r PORT DEBUG_PORT PROXY_PORT <<< "$(free_port.py 3)" +# Ports are allocated per run so that tests in parallel worktrees do not collide. +read -r APP_PORT PROXY_PORT DEBUG_PORT PREPARE_PORT <<< "$(free_port.py 4)" +add_repl.py "$PROXY_PORT" PROXY_PORT +add_repl.py "$DEBUG_PORT" DEBUG_PORT + +# The app and the proxy start asynchronously; allow up to 15s for them to come up. +export RETRY_MAX_ATTEMPTS=30 + +title "Prepare environment runs the build script, and the entry point overrides app.yml\n" +# npm reaches no registry here: nothing to install, and app/.npmrc turns off its update check. +# The build script runs only if the install succeeded, so the first grep covers both halves. +trace $CLI apps run-local --prepare-environment --entry-point test.yml --port "$PREPARE_PORT" &> ../LOG.prepare +trace grep -x "Build script ran" ../LOG.prepare +trace grep -x "Running command: node -e console.log('Hello, world')" ../LOG.prepare +trace grep -x "Hello, world" ../LOG.prepare cleanup() { - # Kill any still running processes on these ports when the script exits - kill_port.py $PORT $DEBUG_PORT $PROXY_PORT + local status=$? + # Reap by port rather than by $PID, and only on failure, for the reasons in + # ../run-local/script. The debug port is here too because node binds the inspector before + # it runs app.js, so it is the only handle on an app that died before it listened. + [ "$status" -eq 0 ] || kill_port.py "$APP_PORT" "$PROXY_PORT" "$DEBUG_PORT" } -trap cleanup EXIT - -title "Starting the app in background..." -trace $CLI apps run-local --prepare-environment --debug --port "$PROXY_PORT" --debug-port "$DEBUG_PORT" --app-port "$PORT" > ../out.run.txt 2>&1 & +# app/package.json selects the CLI's Node code path (runlocal.NewApp); without it the app is +# treated as Python and --debug no longer sets NODE_OPTIONS. +trace $CLI apps run-local --debug --port "$PROXY_PORT" --debug-port "$DEBUG_PORT" --app-port "$APP_PORT" &> ../LOG.run & PID=$! -# Ensure background process is killed on script exit -trap '(kill $PID >/dev/null 2>&1) 2>/dev/null || true' EXIT +trap cleanup EXIT cd .. -title Waiting for the app to start... -# Use a loop to check for the startup message instead of tail/sed which can be unreliable on Windows -# due to file locking, buffering issues, and different text processing behavior across Windows versions. -# A simple grep loop is more robust across platforms. -while [ -z "$(grep -o "Server is running on port " out.run.txt 2>/dev/null)" ]; do - sleep 1 -done - -# Make sure the proxy is ready to serve requests -while [ -z "$(grep -o "To access your app go to " out.run.txt 2>/dev/null)" ]; do - sleep 1 -done - -title "Checking app is running..." -trace curl -s -o - http://127.0.0.1:$PROXY_PORT | grep -w "Hello From App" - -title "Sending shutdown request..." -trace curl -s -o /dev/null http://127.0.0.1:$PROXY_PORT/shutdown || true - -# We need to wait for the app to shutdown before we can exit the test meaning wait until the -# server is closed. We need to poll because the server is closed asynchronously. -while [ -z "$(grep -o "Server closed" out.run.txt 2>/dev/null)" ]; do - sleep 1 -done +title "Proxy forwards to the app\n" +trace retry --until "Hello From App" curl -sS "http://127.0.0.1:$PROXY_PORT" -# Wait for the background process to actually terminate -wait $PID 2>/dev/null || true -echo "Process terminated" +title "Debug mode reports the debugger port, and node listens on it\n" +# The CLI prints this only after the proxy accepts connections, so the curl above can beat it. +# node's line below needs no retry: it is written before the app listens, so the curl proves it. +trace retry grep -x "To debug your app, attach a debugger to port $DEBUG_PORT" LOG.run +trace grep "Debugger listening on ws://127.0.0.1:$DEBUG_PORT/" LOG.run -rm out.run.txt +title "Shutting the app down\n" +trace curl -sS -o /dev/null "http://127.0.0.1:$PROXY_PORT/shutdown" +wait $PID diff --git a/acceptance/cmd/workspace/apps/run-local-node/test.toml b/acceptance/cmd/workspace/apps/run-local-node/test.toml index 2b6bb4bdda4..023a62e4f8c 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/test.toml +++ b/acceptance/cmd/workspace/apps/run-local-node/test.toml @@ -1,26 +1,13 @@ -Badness = "need to enable NPM registry access" Cloud = false -Local = false +Local = true RecordRequests = false -Timeout = '2m' -TimeoutWindows = '10m' +# npm install writes package-lock.json even with nothing to install, and older npm versions +# also create node_modules for their own copy of the lockfile. Ignore = [ 'node_modules', 'package-lock.json' ] -[[Repls]] -Old='curl/[0-9]+\.[0-9]+\.[0-9]+' -New='curl/(version)' - -[[Repls]] -Old='127.0.0.1:[0-9]+' -New='127.0.0.1:$(port)' - -[[Repls]] -Old='To debug your app, attach a debugger to port [0-9]+' -New='To debug your app, attach a debugger to port $(debug_port)' - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["direct"] +# The command is unrelated to bundle deployment, so run it once rather than per engine. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]