feat(system/esp_stdio): Add test coverage for esp_stdio refactor

This commit is contained in:
Tomáš Rohlínek
2026-05-27 11:37:56 +02:00
parent e08bed82c6
commit 8605931afc
6 changed files with 225 additions and 4 deletions
+6 -2
View File
@@ -189,12 +189,16 @@ menu "ESP-STDIO"
int "Max logical stdio file descriptors"
depends on VFS_SUPPORT_IO
range 0 64
default 3
default 16
help
Maximum number of logical /dev/console descriptors tracked by esp_stdio VFS.
- 0 enables basic mode with a shared internal descriptor.
- 1..2 is not allowed
- Values >3 enable descriptor-table mode.
- Values >= 3 select descriptor-table mode (minimum table size is 3).
stdin, stdout, and stderr each occupy one slot. The default leaves room for additional
opens of /dev/console (e.g. dup2, reopen). If set to exactly 3, only the three standard
streams fit and further opens fail with ENFILE unless one is closed first.
Enabling basic mode decreases RAM usage, but disables checks for per-fd behaviour, such as read-only.
+1 -1
View File
@@ -84,7 +84,7 @@ static const char *TAG = "esp_stdio";
static inline bool is_fd_valid(int fd)
{
#if ESP_STDIO_IS_BASIC
return (fd == 0) && (s_ctx.fd_count > 0);
return (fd >= 0) && (s_ctx.fd_count > 0);
#else
return (fd >= 0) && (fd < CONFIG_ESP_STDIO_MAX_FDS) && s_ctx.fds[fd].in_use;
#endif
@@ -1,11 +1,12 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
#include "sdkconfig.h"
#include "esp_rom_serial_output.h"
@@ -50,6 +51,7 @@ static void console_none_print(void)
#endif
#if CONFIG_VFS_SUPPORT_IO
#if CONFIG_ESP_STDIO_MAX_FDS <= 0 || CONFIG_ESP_STDIO_MAX_FDS > 3
static void console_open_close_check(void)
{
printf("Opening /dev/console\n");
@@ -65,6 +67,110 @@ static void console_open_close_check(void)
printf("This should be printed to stdout\n");
}
#endif
static void stdio_fd_mode_behavior_check(void)
{
#if CONFIG_ESP_STDIO_MAX_FDS <= 0
printf("STDIO_TEST:MODE=BASIC\n");
int fd0 = open("/dev/console", O_RDWR);
int fd1 = open("/dev/console", O_RDWR);
int fd2 = open("/dev/console", O_RDWR);
assert(fd0 >= 0);
assert(fd1 >= 0);
assert(fd2 >= 0);
printf("STDIO_TEST:BASIC:OPEN_OK\n");
const char *msg = "STDIO_TEST:BASIC:WRITE_OK\n";
ssize_t wr = write(fd0, msg, strlen(msg));
assert(wr == (ssize_t) strlen(msg));
assert(close(fd0) == 0);
assert(close(fd1) == 0);
assert(close(fd2) == 0);
printf("STDIO_TEST:BASIC:CLOSE_OK\n");
errno = 0;
assert(close(-1) < 0);
assert(errno == EBADF);
errno = 0;
assert(write(fd0, msg, strlen(msg)) < 0);
assert(errno == EBADF);
printf("STDIO_TEST:BASIC:EBADF_OK\n");
#else
printf("STDIO_TEST:MODE=NON_BASIC\n");
errno = 0;
int fd0 = open("/dev/console", O_RDWR);
if (fd0 < 0) {
/* Small fd limits can be fully consumed by stdin/stdout/stderr. */
assert(errno == ENFILE);
printf("STDIO_TEST:NON_BASIC:SATURATED_OK\n");
return;
}
int fd1 = open("/dev/console", O_RDWR);
int fd2 = open("/dev/console", O_RDWR);
assert(fd0 >= 0);
assert(fd1 >= 0);
assert(fd2 >= 0);
assert(fd0 != fd1 && fd1 != fd2 && fd0 != fd2);
printf("STDIO_TEST:NON_BASIC:UNIQUE_OK\n");
assert(close(fd1) == 0);
int fd_reused = open("/dev/console", O_RDWR);
assert(fd_reused == fd1);
printf("STDIO_TEST:NON_BASIC:REUSE_OK\n");
assert(close(fd0) == 0);
assert(close(fd2) == 0);
assert(close(fd_reused) == 0);
int fds[CONFIG_ESP_STDIO_MAX_FDS + 2];
int opened = 0;
while (opened < (int)(sizeof(fds) / sizeof(fds[0]))) {
int fd = open("/dev/console", O_RDWR);
if (fd < 0) {
break;
}
fds[opened++] = fd;
}
assert(opened >= 0);
assert(opened <= CONFIG_ESP_STDIO_MAX_FDS);
errno = 0;
assert(open("/dev/console", O_RDWR) < 0);
assert(errno == ENFILE);
printf("STDIO_TEST:NON_BASIC:LIMIT_OK\n");
for (int i = 0; i < opened; ++i) {
assert(close(fds[i]) == 0);
}
const char *msg = "x";
errno = 0;
assert(write(-1, msg, 1) < 0);
assert(errno == EBADF);
printf("STDIO_TEST:NON_BASIC:EBADF_OK\n");
#endif
}
static void stdio_fd_init_check(void)
{
int in_fd = fileno(stdin);
int out_fd = fileno(stdout);
int err_fd = fileno(stderr);
assert(in_fd == 0);
assert(out_fd == 1);
assert(err_fd == 2);
printf("STDIO_TEST:FD_INIT:IN=%d,OUT=%d,ERR=%d\n", in_fd, out_fd, err_fd);
fprintf(stdout, "STDIO_TEST:STREAM:STDOUT\n");
fflush(stdout);
fprintf(stderr, "STDIO_TEST:STREAM:STDERR\n");
fflush(stderr);
}
#endif // CONFIG_VFS_SUPPORT_IO
void app_main(void)
@@ -80,6 +186,8 @@ void app_main(void)
assert(fd > 2 && "Incorrect file descriptor returned, stdin, stdout, stderr were not correctly assigned");
close(fd);
stdio_fd_init_check();
#endif // CONFIG_VFS_SUPPORT_IO
#if CONFIG_ESP_CONSOLE_NONE
@@ -87,7 +195,12 @@ void app_main(void)
#endif // CONFIG_ESP_CONSOLE_NONE
#if CONFIG_VFS_SUPPORT_IO
#if CONFIG_ESP_STDIO_MAX_FDS <= 0 || CONFIG_ESP_STDIO_MAX_FDS > 3
console_open_close_check();
#else
printf("STDIO_TEST:SKIP:OPEN_CLOSE_CHECK\n");
#endif
stdio_fd_mode_behavior_check();
#endif // CONFIG_VFS_SUPPORT_IO
}
@@ -1,5 +1,9 @@
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
#
# HW runs: use --embedded-services esp,idf (not idf alone) so serial is attached.
# See README.md in this directory for QEMU vs hardware commands and common failures.
#
import pexpect
import pytest
from pytest_embedded import Dut
@@ -7,6 +11,11 @@ from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
def _expect_app_main_banner(dut: Dut) -> None:
"""Sync to the latest app boot (avoids matching a stale 'Hello World' left in the serial buffer)."""
dut.expect('Calling app_main()')
def not_expect(dut: Dut, output_regex: str) -> None:
try:
dut.expect(output_regex, timeout=5)
@@ -34,7 +43,11 @@ def test_esp_system_stdio_no_output_uart(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_system_stdio_only_serial_jtag(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:STREAM:STDOUT')
dut.expect('STDIO_TEST:STREAM:STDERR')
dut.expect('Opening /dev/console')
dut.expect('This should be printed to stdout')
dut.expect('Closing /dev/console')
@@ -50,6 +63,7 @@ def test_esp_system_stdio_only_serial_jtag(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_system_stdio_only_serial_jtag_no_vfs(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
@@ -62,6 +76,7 @@ def test_esp_system_stdio_only_serial_jtag_no_vfs(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_system_stdio_secondary_serial_jtag(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
@@ -71,8 +86,95 @@ def test_esp_system_stdio_secondary_serial_jtag(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration')
def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
# Order matches test_app_main.c: stdio_fd_init_check() runs before console_open_close_check()
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:STREAM:STDOUT')
dut.expect('STDIO_TEST:STREAM:STDERR')
dut.expect('Opening /dev/console')
dut.expect('This should be printed to stdout')
dut.expect('Closing /dev/console')
dut.expect('This should be printed to stdout')
dut.expect('STDIO_TEST:MODE=NON_BASIC')
dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK')
dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK')
dut.expect('STDIO_TEST:NON_BASIC:LIMIT_OK')
dut.expect('STDIO_TEST:NON_BASIC:EBADF_OK')
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'config, embedded_services',
[('simple', 'idf,qemu')],
)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None:
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:MODE=NON_BASIC')
dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK')
dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK')
dut.expect('STDIO_TEST:NON_BASIC:LIMIT_OK')
dut.expect('STDIO_TEST:NON_BASIC:EBADF_OK')
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'config, embedded_services',
[('stdio_non_basic_small_fd', 'idf,qemu')],
)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_esp_stdio_non_basic_small_fd_mode_qemu(dut: Dut) -> None:
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:MODE=NON_BASIC')
dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK')
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'config, embedded_services',
[('stdio_basic_mode', 'idf,qemu')],
)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_esp_stdio_basic_mode_qemu(dut: Dut) -> None:
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:MODE=BASIC')
dut.expect('STDIO_TEST:BASIC:OPEN_OK')
dut.expect('STDIO_TEST:BASIC:WRITE_OK')
dut.expect('STDIO_TEST:BASIC:CLOSE_OK')
dut.expect('STDIO_TEST:BASIC:EBADF_OK')
@pytest.mark.generic
@idf_parametrize('config', ['stdio_non_basic_small_fd'], indirect=['config'])
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration')
def test_esp_stdio_non_basic_small_fd_mode(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:MODE=NON_BASIC')
dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK')
@pytest.mark.generic
@idf_parametrize('config', ['stdio_basic_mode'], indirect=['config'])
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration')
def test_esp_stdio_basic_mode(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:MODE=BASIC')
dut.expect('STDIO_TEST:BASIC:OPEN_OK')
dut.expect('STDIO_TEST:BASIC:WRITE_OK')
dut.expect('STDIO_TEST:BASIC:CLOSE_OK')
dut.expect('STDIO_TEST:BASIC:EBADF_OK')
@@ -0,0 +1 @@
CONFIG_ESP_STDIO_MAX_FDS=0
@@ -0,0 +1 @@
CONFIG_ESP_STDIO_MAX_FDS=3