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
@@ -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