Skip to content

Commit acfeeea

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

20 files changed

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

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)