Skip to content

Commit 8259031

Browse files
committed
feat(examples): websocket autobahn test suit integration
1 parent 318bca1 commit 8259031

20 files changed

+2075
-3
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
name: "autobahn: build/target-tests"
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
types: [opened, synchronize, reopened, labeled]
9+
10+
jobs:
11+
build_autobahn:
12+
# TODO - websocket label should be switch to autobahn
13+
#if: contains(github.event.pull_request.labels.*.name, 'autobahn') || github.event_name == 'push'
14+
if: contains(github.event.pull_request.labels.*.name, 'websocket') || github.event_name == 'push'
15+
name: Build
16+
strategy:
17+
matrix:
18+
#idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "latest"]
19+
idf_ver: [ "latest"]
20+
idf_target: ["esp32"]
21+
runs-on: ubuntu-22.04
22+
container: espressif/idf:${{ matrix.idf_ver }}
23+
env:
24+
TEST_DIR: components/esp_websocket_client/examples/autobahn-testsuite/testee
25+
steps:
26+
- name: Checkout esp-protocols
27+
uses: actions/checkout@v4
28+
with:
29+
submodules: recursive
30+
- name: Build autobahn testee with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }}
31+
working-directory: ${{ env.TEST_DIR }}
32+
env:
33+
IDF_TARGET: ${{ matrix.idf_target }}
34+
shell: bash
35+
run: |
36+
. ${IDF_PATH}/export.sh
37+
test -f sdkconfig.ci.plain_tcp && cat sdkconfig.ci.plain_tcp >> sdkconfig.defaults || echo "No sdkconfig.ci.plain_tcp"
38+
idf.py set-target ${{ matrix.idf_target }}
39+
idf.py build
40+
- name: Merge binaries with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }}
41+
working-directory: ${{ env.TEST_DIR }}/build
42+
env:
43+
IDF_TARGET: ${{ matrix.idf_target }}
44+
shell: bash
45+
run: |
46+
. ${IDF_PATH}/export.sh
47+
esptool.py --chip ${{ matrix.idf_target }} merge_bin --fill-flash-size 4MB -o flash_image.bin @flash_args
48+
- uses: actions/upload-artifact@v4
49+
with:
50+
name: autobahn_testee_bin_${{ matrix.idf_target }}_${{ matrix.idf_ver }}
51+
path: |
52+
${{ env.TEST_DIR }}/build/bootloader/bootloader.bin
53+
${{ env.TEST_DIR }}/build/partition_table/partition-table.bin
54+
${{ env.TEST_DIR }}/build/*.bin
55+
${{ env.TEST_DIR }}/build/*.elf
56+
${{ env.TEST_DIR }}/build/flasher_args.json
57+
${{ env.TEST_DIR }}/build/config/sdkconfig.h
58+
${{ env.TEST_DIR }}/build/config/sdkconfig.json
59+
if-no-files-found: error
60+
61+
run-target-autobahn:
62+
# Skip running on forks since it won't have access to secrets
63+
if: |
64+
github.repository == 'espressif/esp-protocols' &&
65+
( contains(github.event.pull_request.labels.*.name, 'websocket') || github.event_name == 'push' )
66+
name: Target test
67+
needs: build_autobahn
68+
strategy:
69+
fail-fast: false
70+
matrix:
71+
idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "latest"]
72+
idf_target: ["esp32"]
73+
runs-on:
74+
- self-hosted
75+
- ESP32-ETHERNET-KIT
76+
env:
77+
TEST_DIR: components/esp_websocket_client/examples/autobahn-testsuite
78+
TESTEE_DIR: components/esp_websocket_client/examples/autobahn-testsuite/testee
79+
steps:
80+
- uses: actions/checkout@v4
81+
with:
82+
submodules: recursive
83+
- uses: actions/download-artifact@v4
84+
with:
85+
name: autobahn_testee_bin_${{ matrix.idf_target }}_${{ matrix.idf_ver }}
86+
path: ${{ env.TESTEE_DIR }}/build
87+
- name: Install Docker Compose
88+
run: |
89+
sudo apt-get update
90+
sudo apt-get install -y docker-compose-plugin || sudo apt-get install -y docker-compose
91+
# Ensure user has permission to use Docker (if not already in docker group)
92+
sudo usermod -aG docker $USER || true
93+
# Start Docker service if not running
94+
sudo systemctl start docker || true
95+
- name: Start Autobahn Fuzzing Server
96+
working-directory: ${{ env.TEST_DIR }}
97+
run: |
98+
# Get host IP address for ESP32 to connect to
99+
HOST_IP=$(hostname -I | awk '{print $1}')
100+
echo "HOST_IP=$HOST_IP" >> $GITHUB_ENV
101+
echo "Autobahn server will be accessible at ws://$HOST_IP:9001"
102+
103+
# Start the fuzzing server
104+
docker compose up -d || docker-compose up -d
105+
106+
# Wait for server to be ready
107+
echo "Waiting for fuzzing server to start..."
108+
sleep 10
109+
110+
# Check if container is running
111+
if ! docker ps | grep -q ws-fuzzing-server; then
112+
echo "Error: Fuzzing server failed to start"
113+
docker compose logs || docker-compose logs
114+
exit 1
115+
fi
116+
117+
echo "✓ Fuzzing server started successfully"
118+
- name: Flash ESP32 Testee
119+
working-directory: ${{ env.TESTEE_DIR }}/build
120+
env:
121+
IDF_TARGET: ${{ matrix.idf_target }}
122+
run: |
123+
python -m esptool --chip ${{ matrix.idf_target }} write_flash 0x0 flash_image.bin
124+
- name: Run Autobahn Tests
125+
working-directory: ${{ env.TESTEE_DIR }}
126+
env:
127+
PIP_EXTRA_INDEX_URL: "https://www.piwheels.org/simple"
128+
run: |
129+
# Detect ESP32 port if not set in environment
130+
if [ -z "${ESP_PORT:-}" ]; then
131+
for port in /dev/ttyUSB* /dev/ttyACM*; do
132+
if [ -e "$port" ]; then
133+
export ESP_PORT="$port"
134+
echo "Detected ESP32 port: $ESP_PORT"
135+
break
136+
fi
137+
done
138+
fi
139+
140+
# Default to /dev/ttyUSB0 if still not found
141+
export ESP_PORT="${ESP_PORT:-/dev/ttyUSB0}"
142+
143+
if [ ! -e "$ESP_PORT" ]; then
144+
echo "Error: ESP32 port not found. Please set ESP_PORT environment variable."
145+
echo "Available ports:"
146+
ls -la /dev/tty* || true
147+
exit 1
148+
fi
149+
150+
echo "Using ESP32 port: $ESP_PORT"
151+
export PYENV_ROOT="$HOME/.pyenv"
152+
export PATH="$PYENV_ROOT/bin:$PATH"
153+
eval "$(pyenv init --path)"
154+
eval "$(pyenv init -)"
155+
if ! pyenv versions --bare | grep -q '^3\.12\.6$'; then
156+
echo "Installing Python 3.12.6..."
157+
pyenv install -s 3.12.6
158+
fi
159+
if ! pyenv virtualenvs --bare | grep -q '^myenv$'; then
160+
echo "Creating pyenv virtualenv 'myenv'..."
161+
pyenv virtualenv 3.12.6 myenv
162+
fi
163+
pyenv activate myenv
164+
python --version
165+
pip install --prefer-binary pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code esptool pyserial
166+
pip install --extra-index-url https://dl.espressif.com/pypi/ -r $GITHUB_WORKSPACE/ci/requirements.txt
167+
168+
echo "Starting Autobahn test suite on ESP32..."
169+
echo "Tests may take 15-30 minutes to complete..."
170+
171+
# Send server URI via serial (stdin) and monitor for completion
172+
# Script is in the parent directory (TEST_DIR) from TESTEE_DIR
173+
SERVER_URI="ws://$HOST_IP:9001"
174+
echo "Sending server URI to ESP32: $SERVER_URI"
175+
python3 ../scripts/monitor_serial.py --port "$ESP_PORT" --uri "$SERVER_URI" --timeout 2400
176+
- name: Collect Test Reports
177+
working-directory: ${{ env.TEST_DIR }}
178+
if: always()
179+
run: |
180+
# Stop the fuzzing server
181+
docker compose down || docker-compose down
182+
183+
# Check if reports were generated
184+
if [ -d "reports/clients" ]; then
185+
echo "✓ Test reports found"
186+
ls -la reports/clients/
187+
else
188+
echo "⚠ No test reports found in reports/clients/"
189+
fi
190+
- name: Generate Test Summary
191+
working-directory: ${{ env.TEST_DIR }}
192+
if: always()
193+
run: |
194+
# Generate summary from test results
195+
# Check for JSON files in both reports/ and reports/clients/
196+
if [ -d "reports" ] && ( [ -n "$(ls -A reports/*.json 2>/dev/null)" ] || [ -n "$(ls -A reports/clients/*.json 2>/dev/null)" ] ); then
197+
echo "Generating test summary..."
198+
python3 scripts/generate_summary.py
199+
echo ""
200+
echo "Summary generated successfully!"
201+
if [ -f "reports/summary.html" ]; then
202+
echo "HTML summary available at: reports/summary.html"
203+
fi
204+
else
205+
echo "⚠ No JSON test results found, skipping summary generation"
206+
fi
207+
- uses: actions/upload-artifact@v4
208+
if: always()
209+
with:
210+
name: autobahn_reports_${{ matrix.idf_target }}_${{ matrix.idf_ver }}
211+
path: |
212+
${{ env.TEST_DIR }}/reports/**
213+
if-no-files-found: warn
214+

components/esp_websocket_client/esp_websocket_client.c

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,10 +664,19 @@ static int esp_websocket_client_send_with_exact_opcode(esp_websocket_client_hand
664664
} else if (contained_fin) {
665665
opcode = opcode | WS_TRANSPORT_OPCODES_FIN;
666666
}
667-
memcpy(client->tx_buffer, data + widx, need_write);
667+
668+
// Prepare buffer pointer for transport layer
669+
// NOTE: Always pass a valid buffer pointer (even for 0-length), never NULL
670+
// Control frames (PING/PONG) can use NULL, but data frames (TEXT/BINARY) need a valid pointer
671+
if (need_write > 0 && data != NULL) {
672+
// Copy data if there's something to copy (avoid undefined behavior with NULL pointer)
673+
memcpy(client->tx_buffer, data + widx, need_write);
674+
}
675+
// Always use tx_buffer (never NULL) - transport layer needs valid pointer for data frames
668676
// send with ws specific way and specific opcode
669677
wlen = esp_transport_ws_send_raw(client->transport, opcode, (char *)client->tx_buffer, need_write,
670678
(timeout == portMAX_DELAY) ? -1 : timeout * portTICK_PERIOD_MS);
679+
671680
if (wlen < 0 || (wlen == 0 && need_write != 0)) {
672681
ret = wlen;
673682
esp_websocket_free_buf(client, true);
@@ -818,6 +827,8 @@ esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_clie
818827
ESP_WS_CLIENT_MEM_CHECK(TAG, client->tx_buffer, {
819828
goto _websocket_init_fail;
820829
});
830+
// Initialize tx_buffer to avoid undefined behavior when sending 0-length frames
831+
memset(client->tx_buffer, 0, buffer_size);
821832
#endif
822833
client->status_bits = xEventGroupCreate();
823834
ESP_WS_CLIENT_MEM_CHECK(TAG, client->status_bits, {
@@ -1137,7 +1148,26 @@ static void esp_websocket_client_task(void *pv)
11371148
client->wait_for_pong_resp = false;
11381149
client->error_handle.error_type = WEBSOCKET_ERROR_TYPE_NONE;
11391150
esp_websocket_client_dispatch_event(client, WEBSOCKET_EVENT_CONNECTED, NULL, 0);
1140-
break;
1151+
// When falling through from INIT, try immediate read to catch buffered data from handshake
1152+
// Poll first to avoid blocking if there's no data on socket
1153+
// Note: poll_read only checks socket, not buffered data, so we still try read if poll==0
1154+
int immediate_poll = esp_transport_poll_read(client->transport, 0); // Non-blocking
1155+
if (immediate_poll >= 0) {
1156+
// Poll didn't return error - either socket has data (poll > 0) or might have buffered data (poll == 0)
1157+
// Try reading - esp_transport_read_internal() checks buffer first, so if buffered
1158+
// data exists, it will read immediately without blocking. If no buffered data and poll==0,
1159+
// we'll block with network_timeout_ms, but that's acceptable for this immediate read attempt.
1160+
esp_err_t recv_result = esp_websocket_client_recv(client);
1161+
if (recv_result == ESP_OK) {
1162+
// Data was read and event dispatched - process it immediately
1163+
esp_event_loop_run(client->event_handle, 0);
1164+
}
1165+
// Note: If recv_result == ESP_OK but no data was read (timeout), that's fine
1166+
// The normal polling loop will handle subsequent data
1167+
}
1168+
// Set read_select so normal flow continues (use poll result, or 0 if poll failed)
1169+
read_select = (immediate_poll > 0) ? 1 : 0;
1170+
//fallthrough
11411171
case WEBSOCKET_STATE_CONNECTED:
11421172
if ((CLOSE_FRAME_SENT_BIT & xEventGroupGetBits(client->status_bits)) == 0) { // only send and check for PING
11431173
// if closing hasn't been initiated
@@ -1169,7 +1199,8 @@ static void esp_websocket_client_task(void *pv)
11691199
}
11701200
}
11711201

1172-
1202+
// Only check read_select if it was set in this iteration (not from fall-through)
1203+
// If we fell through from INIT, read_select was reset to -1, so skip this check
11731204
if (read_select == 0) {
11741205
ESP_LOGV(TAG, "Read poll timeout: skipping esp_transport_read()...");
11751206
break;
@@ -1220,6 +1251,7 @@ static void esp_websocket_client_task(void *pv)
12201251
esp_websocket_client_abort_connection(client, WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT);
12211252
xSemaphoreGiveRecursive(client->lock);
12221253
} else if (read_select > 0) {
1254+
// Data available - read and process immediately
12231255
if (esp_websocket_client_recv(client) == ESP_FAIL) {
12241256
ESP_LOGE(TAG, "Error receive data");
12251257
xSemaphoreTakeRecursive(client->lock, lock_timeout);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Autobahn testsuite generated files
2+
reports/
3+
4+
# ESP-IDF build artifacts
5+
testee/build/
6+
testee/sdkconfig
7+
testee/sdkconfig.old
8+
testee/dependencies.lock
9+
10+
# Python
11+
__pycache__/
12+
*.pyc
13+

0 commit comments

Comments
 (0)