Skip to content

Commit 29ffea3

Browse files
committed
[ZEPPELIN-6666] Add notebook transport fixtures
1 parent 50b0f40 commit 29ffea3

8 files changed

Lines changed: 2572 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
# Notebook transport contract fixtures
19+
20+
This directory defines the versioned REST and WebSocket fixture format used by
21+
Notebook adapter tests. It is an in-repository contract test, not a Pact
22+
consumer/provider contract and not a replacement for live-server E2E tests.
23+
24+
## Fixture ownership
25+
26+
Every committed fixture includes these required fields. Both
27+
`createNotebookTransportRecorder` and `validateFixture` enforce them:
28+
29+
```json
30+
{
31+
"version": 1,
32+
"metadata": {
33+
"scenario": "Open a notebook",
34+
"owner": "zeppelin-web-angular",
35+
"coveredOperations": ["GET_NOTE"],
36+
"knownExclusions": ["Live interpreter execution is covered by a separate E2E scenario"]
37+
},
38+
"records": []
39+
}
40+
```
41+
42+
- `scenario` describes the user-visible flow.
43+
- `owner` identifies the component that maintains the fixture.
44+
- `coveredOperations` lists the REST or WebSocket operations represented by the
45+
fixture.
46+
- `knownExclusions` records intentionally uncovered behavior. An empty array
47+
is valid when there are no exclusions.
48+
49+
Add a fixture when a Notebook operation is moved into the shared adapter
50+
contract. If that operation cannot yet be represented, add its explicit reason
51+
to the scenario's `knownExclusions`; do not silently rely on another fixture.
52+
53+
## Test layers
54+
55+
Run the fast format, redaction, ordering, and replay checks with:
56+
57+
```bash
58+
npm run check:core-contract-fixtures
59+
```
60+
61+
Run the Playwright adapter checks with:
62+
63+
```bash
64+
npm run e2e:core-contract
65+
```
66+
67+
The live capture scenario is intentionally separate because it requires a
68+
running Zeppelin test server with a usable Notebook session:
69+
70+
```bash
71+
npm run e2e:core-contract:live
72+
```
73+
74+
The format check and adapter replay command use strict replay: unexpected
75+
traffic, out-of-order traffic, or unconsumed records fail the test. The Maven
76+
test phase runs the format check.
77+
78+
These checks prove fixture shape and adapter transport behavior. Separate E2E
79+
scenarios must cover a running Zeppelin server, authorization, collaboration,
80+
reconnection, interpreter execution, streaming output, performance, and
81+
accessibility.
82+
83+
## Capturing safely
84+
85+
`createNotebookTransportRecorder(metadata)` records only `/api/notebook` REST
86+
traffic and `/ws` frames. It redacts configured sensitive and volatile fields
87+
before it writes a fixture. JSON WebSocket frames are normalized and redacted;
88+
binary frames are rejected during capture until a binary redaction policy is
89+
implemented. Replay still supports deliberately authored binary fixtures for
90+
protocol-level tests.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one or more
4+
# contributor license agreements. See the NOTICE file distributed with
5+
# this work for additional information regarding copyright ownership.
6+
# The ASF licenses this file to You under the Apache License, Version 2.0
7+
# (the "License"); you may not use this file except in compliance with
8+
# the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
set -euo pipefail
19+
20+
usage() {
21+
echo "usage: $0 start|stop --root <dir> [--mode anonymous|auth] [--port <port>]" >&2
22+
}
23+
24+
command="${1:-}"
25+
shift || true
26+
capture_root=""
27+
capture_mode="anonymous"
28+
zeppelin_port="8080"
29+
30+
while [[ $# -gt 0 ]]; do
31+
case "$1" in
32+
--root)
33+
capture_root="${2:-}"
34+
shift 2
35+
;;
36+
--mode)
37+
capture_mode="${2:-}"
38+
shift 2
39+
;;
40+
--port)
41+
zeppelin_port="${2:-}"
42+
shift 2
43+
;;
44+
*)
45+
usage
46+
exit 2
47+
;;
48+
esac
49+
done
50+
51+
if [[ -z "${command}" || -z "${capture_root}" ]]; then
52+
usage
53+
exit 2
54+
fi
55+
56+
repo_root="$(cd "$(dirname "$0")/../../.." && pwd)"
57+
capture_root="$(mkdir -p "${capture_root}" && cd "${capture_root}" && pwd)"
58+
marker_file="${capture_root}/.zeppelin-capture-root"
59+
zeppelin_pid_file="${capture_root}/zeppelin.pid"
60+
61+
port_in_use() {
62+
lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
63+
}
64+
65+
write_marker() {
66+
{
67+
echo "root=${capture_root}"
68+
echo "repo=${repo_root}"
69+
} > "${marker_file}"
70+
}
71+
72+
verify_root_marker() {
73+
[[ -f "${marker_file}" ]] && grep -qx "root=${capture_root}" "${marker_file}"
74+
}
75+
76+
verify_pid_identity() {
77+
local pid="$1"
78+
local expected="$2"
79+
[[ "${pid}" =~ ^[0-9]+$ ]] || return 1
80+
ps -p "${pid}" -o command= | grep -F -- "${expected}" >/dev/null 2>&1
81+
}
82+
83+
stop_pid() {
84+
local pid_file="$1"
85+
local expected="$2"
86+
[[ -f "${pid_file}" ]] || return 0
87+
local pid
88+
pid="$(cat "${pid_file}")"
89+
if ps -p "${pid}" >/dev/null 2>&1; then
90+
if ! verify_pid_identity "${pid}" "${expected}"; then
91+
echo "refusing to stop ${pid}: command does not match ${expected}" >&2
92+
exit 1
93+
fi
94+
kill "${pid}"
95+
for _ in {1..20}; do
96+
ps -p "${pid}" >/dev/null 2>&1 || break
97+
sleep 1
98+
done
99+
fi
100+
rm -f "${pid_file}"
101+
}
102+
103+
start_zeppelin() {
104+
mkdir -p "${capture_root}/conf" "${capture_root}/notebook" "${capture_root}/index" \
105+
"${capture_root}/logs" "${capture_root}/run" "${capture_root}/recovery" "${capture_root}/webapps"
106+
cp "${repo_root}/conf/log4j2.properties" "${capture_root}/conf/log4j2.properties"
107+
cp "${repo_root}/conf/zeppelin-site.xml.template" "${capture_root}/conf/zeppelin-site.xml"
108+
if [[ "${capture_mode}" == "auth" ]]; then
109+
cp "${repo_root}/conf/shiro.ini.template" "${capture_root}/conf/shiro.ini"
110+
else
111+
rm -f "${capture_root}/conf/shiro.ini"
112+
fi
113+
114+
export ZEPPELIN_CONF_DIR="${capture_root}/conf"
115+
export ZEPPELIN_NOTEBOOK_DIR="${capture_root}/notebook"
116+
export ZEPPELIN_LOG_DIR="${capture_root}/logs"
117+
export ZEPPELIN_PID_DIR="${capture_root}/run"
118+
export ZEPPELIN_WAR_TEMPDIR="${capture_root}/webapps"
119+
export ZEPPELIN_JAVA_OPTS="${ZEPPELIN_JAVA_OPTS:-} -Dzeppelin.server.port=${zeppelin_port} -Dzeppelin.notebook.dir=${capture_root}/notebook -Dzeppelin.search.index.path=${capture_root}/index -Dzeppelin.recovery.dir=${capture_root}/recovery -Dzeppelin.capture.root=${capture_root}"
120+
export ZEPPELIN_CAPTURE_ROOT="${capture_root}"
121+
export ZEPPELIN_PORT="${zeppelin_port}"
122+
# The fixture server has no Hadoop configuration; do not inherit a developer shell setting.
123+
export USE_HADOOP=false
124+
125+
if [[ -n "${CAPTURE_ZEPPELIN_COMMAND:-}" ]]; then
126+
bash -c "${CAPTURE_ZEPPELIN_COMMAND}" >"${capture_root}/logs/zeppelin-stdout.log" 2>"${capture_root}/logs/zeppelin-stderr.log" &
127+
echo "$!" > "${zeppelin_pid_file}"
128+
else
129+
"${repo_root}/bin/zeppelin.sh" >"${capture_root}/logs/zeppelin-stdout.log" 2>"${capture_root}/logs/zeppelin-stderr.log" &
130+
echo "$!" > "${zeppelin_pid_file}"
131+
fi
132+
}
133+
134+
wait_for_http() {
135+
local url="$1"
136+
for _ in {1..120}; do
137+
if curl -fsS "${url}" >/dev/null 2>&1; then
138+
return 0
139+
fi
140+
sleep 1
141+
done
142+
return 1
143+
}
144+
145+
start_server() {
146+
if [[ "${capture_mode}" != "anonymous" && "${capture_mode}" != "auth" ]]; then
147+
echo "mode must be anonymous or auth" >&2
148+
exit 2
149+
fi
150+
if port_in_use "${zeppelin_port}"; then
151+
echo "port ${zeppelin_port} is already in use" >&2
152+
exit 1
153+
fi
154+
155+
write_marker
156+
start_zeppelin
157+
if ! wait_for_http "http://127.0.0.1:${zeppelin_port}/api/version"; then
158+
echo "zeppelin did not become ready on port ${zeppelin_port}" >&2
159+
stop_server
160+
exit 1
161+
fi
162+
}
163+
164+
stop_server() {
165+
if ! verify_root_marker; then
166+
echo "refusing to stop without matching capture root marker: ${marker_file}" >&2
167+
exit 1
168+
fi
169+
stop_pid "${zeppelin_pid_file}" "${capture_root}"
170+
}
171+
172+
case "${command}" in
173+
start)
174+
start_server
175+
;;
176+
stop)
177+
stop_server
178+
;;
179+
*)
180+
usage
181+
exit 2
182+
;;
183+
esac

0 commit comments

Comments
 (0)