Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions acceptance/cmd/workspace/apps/run-local-node/app/.npmrc
Original file line number Diff line number Diff line change
@@ -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
41 changes: 20 additions & 21 deletions acceptance/cmd/workspace/apps/run-local-node/app/app.js
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 2 additions & 3 deletions acceptance/cmd/workspace/apps/run-local-node/app/app.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
command:
- npm
- run
- run-app
- node
- app.js
3 changes: 3 additions & 0 deletions acceptance/cmd/workspace/apps/run-local-node/app/build.js
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion acceptance/cmd/workspace/apps/run-local-node/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 24 additions & 8 deletions acceptance/cmd/workspace/apps/run-local-node/output.txt
Original file line number Diff line number Diff line change
@@ -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
75 changes: 34 additions & 41 deletions acceptance/cmd/workspace/apps/run-local-node/script
Original file line number Diff line number Diff line change
@@ -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
23 changes: 5 additions & 18 deletions acceptance/cmd/workspace/apps/run-local-node/test.toml
Original file line number Diff line number Diff line change
@@ -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"]
Loading