"""Exercise real diagnostic modules with SDK headers and fake modem/USB APIs. No live network or physical serial port is used. Host size_t adaptation is restricted to temporary SDK header copies; firmware headers remain untouched. """ from pathlib import Path import re import shutil import subprocess import tempfile ROOT = Path(__file__).resolve().parents[1] BASE = ROOT / 'ql-application/threadx/interface/custom_app' SDK = ROOT / 'ql-application/threadx/common/include' SAMPLE = r''' #include #include #include "net_diag.c" static int creates, fail_create, mode, hold, pdp = 1, sinr_calls; static unsigned long clock_ms; static int sample_steps, step_limit = 1; static jmp_buf loop_exit; uint64_t hal_os_up_time_ms(void) { return clock_ms; } uint8_t get_sleep_mode(void) { return hold; } int get_mqtt_connect_status(void) { return 1; } unsigned char get_xiaoice_connected_status(void) { return 1; } int fota_start; u8 power_down_status; static s32 api_lookup(char *name) { (void)name; return 1; } _get_api_ptr_t m_get_api_ptr = api_lookup; QlOSStatus ql_rtos_task_create(ql_task_t *task, u32 stack, u8 priority, char *name, void (*entry)(void *), void *arg, ...) { (void)name; (void)entry; (void)arg; assert(stack == 4096 && priority == 120); ++creates; if (fail_create == creates) return -9; *task = (void *)1; return 0; } void ql_rtos_task_sleep_ms(u32 ms) { clock_ms += ms; if (step_limit > 1) { ++sample_steps; if (sample_steps == 1) pdp = 0; if (sample_steps == 2) pdp = 1; if (sample_steps < step_limit) return; } longjmp(loop_exit, 1); } QL_NW_ERROR_CODE ql_nw_get_signal_strength(QL_NW_SIGNAL_STRENGTH_INFO_T *s) { s->LTE_SignalStrength.rsrp = -97; s->LTE_SignalStrength.cqi = 255; return QL_NW_SUCCESS; } QL_NW_ERROR_CODE ql_nw_get_reg_status(QL_NW_REG_STATUS_INFO_T *r) { r->data_reg.state = 1; r->data_reg.rat = QL_NW_ACCESS_TECH_E_UTRAN; return QL_NW_SUCCESS; } int ql_get_data_call_info(int profile, int version, struct ql_data_call_info *d) { assert(profile == 1 && version == 0); d->v4.state = pdp; return 0; } QL_NW_ERROR_CODE ql_nw_get_sinr(int *s) { (void)s; ++sinr_calls; return (QL_NW_ERROR_CODE)255; } QL_NW_ERROR_CODE ql_nw_get_cell_info(QL_NW_CELL_INFO_T *c) { c->lte_info_valid = 1; c->lte_info_num = 1; c->lte_info[0].cid = 123; c->lte_info[0].pci = 42; return QL_NW_SUCCESS; } void net_diag_probe(const char *host, unsigned short port, net_probe_result *r) { assert(host && (port == 53 || port == 443)); memset(r, 0, sizeof(*r)); r->outcome = mode; r->tcp_started = mode == PROBE_OK || mode == PROBE_TIMEOUT; strcpy(r->address, "192.0.2.1"); r->tcp_ms = 123; } int main(void) { fail_create = 2; net_diag_start(); assert(!enabled && sample_task && !probe_task); fail_create = 0; net_diag_start(); assert(enabled && creates == 3); net_diag_start(); assert(creates == 3); mode = PROBE_OK; probe_once(0); assert(attempts[0] == 1 && received[0] == 1 && !streak[0]); mode = PROBE_TIMEOUT; probe_once(0); assert(attempts[0] == 2 && received[0] == 1 && streak[0] == 1); mode = PROBE_DNS; probe_once(0); assert(attempts[0] == 2 && errors[0] == 1); mode = PROBE_LOCAL; probe_once(0); assert(attempts[0] == 2 && errors[0] == 2); mode = PROBE_CANCELLED; probe_once(0); assert(attempts[0] == 2 && errors[0] == 2); mode = PROBE_OK; probe_once(0); assert(!streak[0]); step_limit = 3; if (!setjmp(loop_exit)) sample_worker(NULL); step_limit = 1; if (!setjmp(loop_exit)) sample_worker(NULL); assert(sinr_calls == 1 && sinr_unavailable); hold = 1; if (!setjmp(loop_exit)) probe_worker(NULL); assert(attempts[1] == 0); puts("PASS: diagnostic activation, statistics, invalid metrics, recovery and pause"); return 0; } ''' PROBE = r''' #include #include "net_diag_probe.c" static int mode, closes, frees, sys_error, permitted = 1, selects, socket_calls; static unsigned long clock_ms; static struct sockaddr_in remote; static struct addrinfo address; uint64_t hal_os_up_time_ms(void) { return clock_ms; } int net_diag_can_probe(void) { return permitted; } int getaddrinfo_with_pcid(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result, u8_t cid) { assert(host && service && hints->ai_family == AF_INET && cid == 1); clock_ms += 35; if (mode == 1) return 202; if (mode == 2) { *result = NULL; return 0; } memset(&address, 0, sizeof(address)); memset(&remote, 0, sizeof(remote)); remote.sin_family = AF_INET; address.ai_family = AF_INET; address.ai_addr = (struct sockaddr *)&remote; address.ai_addrlen = sizeof(remote); *result = &address; if (mode == 12) permitted = 0; return 0; } void freeaddrinfo(struct addrinfo *p) { assert(p == &address); ++frees; } int ql_get_data_call_info(int cid, int version, struct ql_data_call_info *d) { assert(cid == 1 && version == 0); d->v4.state = mode == 14 ? 0 : 1; return 0; } int socket(int family, int type, int protocol) { assert(family == AF_INET && type == SOCK_STREAM && protocol == 0); ++socket_calls; return mode == 3 ? -1 : 2; } int ioctl(int fd, long cmd, void *value) { assert(fd == 2 && cmd == FIONBIO && *(int *)value == 1); return mode == 4 ? -1 : 0; } u16 ql_soc_generate_port(void) { return 12345; } u16 ql_get_soc_errno(void) { return sys_error; } int bind(int fd, const struct sockaddr *addr, socklen_t len) { assert(fd == 2 && addr && len == sizeof(struct sockaddr_in)); return mode == 5 ? -1 : 0; } int connect(int fd, const struct sockaddr *addr, socklen_t len) { assert(fd == 2 && addr && len == sizeof(struct sockaddr_in)); if (mode == 7) return 0; sys_error = mode == 6 ? 111 : EINPROGRESS; if (mode == 13) permitted = 0; return -1; } int select(int nfds, fd_set *readable, fd_set *writable, fd_set *exceptional, struct timeval *wait) { assert(nfds == 3 && !readable && writable && exceptional && wait->tv_usec <= 200000); ++selects; if (mode == 9) { clock_ms += wait->tv_usec / 1000; return 0; } clock_ms += 120; if (mode == 10) { sys_error = 5; return -1; } FD_ZERO(exceptional); FD_SET(2, writable); return 1; } int getsockopt(int fd, int level, int option, void *value, socklen_t *len) { assert(fd == 2 && level == SOL_SOCKET && option == SO_ERROR && *len == sizeof(int)); if (mode == 11) { sys_error = 5; return -1; } *(int *)value = mode == 8 ? 111 : 0; return 0; } int close(int fd) { assert(fd == 2); ++closes; return 0; } int main(void) { net_probe_result r; int expected[] = {PROBE_OK, PROBE_DNS, PROBE_DNS, PROBE_LOCAL, PROBE_LOCAL, PROBE_LOCAL, PROBE_CONNECT, PROBE_OK, PROBE_CONNECT, PROBE_TIMEOUT, PROBE_LOCAL, PROBE_LOCAL, PROBE_CANCELLED, PROBE_CANCELLED, PROBE_LOCAL}; for (mode = 0; mode < 15; ++mode) { closes = frees = selects = socket_calls = sys_error = 0; clock_ms = 0; permitted = 1; net_diag_probe("test.example", 443, &r); assert(r.outcome == expected[mode]); assert(r.dns_ms == 35); assert(frees == (mode != 1 && mode != 2)); assert(closes == (mode != 1 && mode != 2 && mode != 3 && mode != 12 && mode != 14)); if (mode == 0) assert(r.tcp_ms == 120 && r.error == 0); if (mode == 8) assert(r.error == 111); if (mode == 9) assert(r.tcp_ms == 3000 && selects == 15); } permitted = 0; socket_calls = 0; net_diag_probe("test.example", 443, &r); assert(r.outcome == PROBE_CANCELLED && !socket_calls && r.dns_ms == 0); puts("PASS: real probe DNS/connect success, refusal, timeout, local errors, cancellation and cleanup"); return 0; } ''' USB = r''' #include #include #include "field_log.c" static int queue_mode, queue_deletes, task_error, writes, write_mode, close_count; static int worker_run, worker_reads, open_count; static jmp_buf usb_exit; static struct log_record last_record; static char sent_text[4096]; QlOSStatus ql_rtos_task_sched_lock(void) { return 0; } QlOSStatus ql_rtos_task_sched_unlock(void) { return 0; } QlOSStatus ql_rtos_queue_create(ql_queue_t *q, u32 size, u32 count) { assert(size == sizeof(struct log_record) && size % 4 == 0 && count == 16); *q = (void *)1; return queue_mode == 1 ? -1 : 0; } QlOSStatus ql_rtos_queue_delete(ql_queue_t q) { assert(q); ++queue_deletes; return 0; } QlOSStatus ql_rtos_queue_release(ql_queue_t q, u32 size, u8 *record, u32 timeout) { assert(q && size == sizeof(struct log_record) && timeout == 0); last_record = *(struct log_record *)record; return queue_mode == 2 ? -1 : 0; } QlOSStatus ql_rtos_queue_wait(ql_queue_t q, u8 *record, u32 size, u32 timeout) { (void)q; (void)size; (void)timeout; if (worker_run) { if (++worker_reads > 1) longjmp(usb_exit, 1); strcpy(((struct log_record *)record)->text, "test"); ((struct log_record *)record)->length = 4; return 0; } return -1; } QlOSStatus ql_rtos_task_create(ql_task_t *t, u32 size, u8 priority, char *name, void (*fn)(void *), void *arg, ...) { assert(size == 4096 && priority == 130); (void)name; (void)fn; (void)arg; *t = (void *)1; return task_error; } void ql_rtos_task_sleep_ms(u32 ms) { (void)ms; } u32 ql_rtos_get_systicks(void) { return 0; } int ql_uart_open(QL_UART_PORT_NUMBER_E port, QL_UART_BAUD_E baud, QL_UART_FLOWCTRL_E flow) { assert(port == QL_USB_CDC_PORT && baud == QL_UART_BAUD_115200 && flow == QL_FC_NONE); ++open_count; return QL_UART_ALREAD_OPEN; } int ql_uart_close(QL_UART_PORT_NUMBER_E port) { assert(port == QL_USB_CDC_PORT); ++close_count; return 0; } int ql_uart_write(QL_UART_PORT_NUMBER_E port, unsigned char *data, int len) { int n = write_mode == 1 && len > 3 ? 3 : len; assert(port == QL_USB_CDC_PORT); ++writes; if (write_mode == 2 || (write_mode == 3 && len == 4)) return -1; strncat(sent_text, (char *)data, write_mode == 0 ? len : n); return write_mode == 0 ? 0 : n; } int main(void) { char large[1001]; queue_mode = 1; assert(field_log_init() == -1 && !ready); queue_mode = 0; task_error = -2; assert(field_log_init() == -2 && !ready && queue_deletes == 1); task_error = 0; assert(field_log_init() == 0 && ready); assert(field_log_init() == 0); field_log_printf("[现场诊断] 中文 value=%d\r\n", 42); assert(strcmp(last_record.text, "[现场诊断] 中文 value=42\r\n") == 0); assert(last_record.length == strlen(last_record.text)); queue_mode = 2; field_log_printf("queue full\n"); assert(dropped == 1); queue_mode = 0; memset(large, 'A', 1000); large[1000] = 0; field_log_printf("%s", large); assert(strstr(last_record.text, "[TRUNCATED]") && last_record.length < 512); write_mode = 0; assert(usb_write_all("abcdef", 6) == 0 && writes == 1 && strcmp(sent_text, "abcdef") == 0); sent_text[0] = 0; writes = 0; write_mode = 1; assert(usb_write_all("abcdef", 6) == 0 && writes == 2 && strcmp(sent_text, "abcdef") == 0); write_mode = 2; assert(usb_write_all("abcdef", 6) == -1); worker_run = 1; write_mode = 3; writes = 0; sent_text[0] = 0; if (!setjmp(usb_exit)) usb_worker(NULL); assert(open_count == 2 && close_count == 1 && dropped == 2); puts("PASS: USB init rollback, no-wait queue, mirror, truncation, writes, disconnect/reopen"); return 0; } ''' VOICE = r''' #include #include "net_diag_voice.c" static unsigned long clock_ms; static int active = 1; uint64_t hal_os_up_time_ms(void) { return clock_ms; } int net_diag_is_enabled(void) { return active; } QlOSStatus ql_rtos_task_sched_lock(void) { return 0; } QlOSStatus ql_rtos_task_sched_unlock(void) { return 0; } int main(void) { unsigned int token; clock_ms = 100; net_diag_voice_begin(3); clock_ms = 500; net_diag_voice_mark(3, VOICE_SPEECH); clock_ms = 1000; net_diag_voice_mark(3, VOICE_RECORD_STOP); clock_ms = 1100; net_diag_voice_mark(3, VOICE_END_QUEUED); clock_ms = 1200; net_diag_voice_mark(3, VOICE_END_ACK); clock_ms = 1500; net_diag_voice_mark(3, VOICE_TTS_START); clock_ms = 3000; net_diag_voice_url(3, "http://test/first.mp3"); net_diag_voice_url(3, "http://test/second.mp3"); assert(net_diag_audio_begin("http://test/unrelated.mp3") == 0); clock_ms = 3100; token = net_diag_audio_begin("http://test/first.mp3"); assert(token == 3); clock_ms = 3300; net_diag_audio_done(token, 8001, 0); clock_ms = 3350; net_diag_voice_mark(token, VOICE_FIRST_PCM); clock_ms = 3500; net_diag_voice_mark(token, VOICE_FIRST_PCM); assert(voice.stamp[VOICE_FIRST_PCM] == 3350); net_diag_voice_begin(4); net_diag_voice_mark(3, VOICE_FIRST_PCM); assert(!voice.seen); net_diag_voice_url(3, "http://old.mp3"); assert(!voice.first_url[0]); active = 0; net_diag_voice_begin(5); assert(voice.round == 4); puts("PASS: voice stage timing, first URL matching, duplicate/stale events, disabled mode"); return 0; } ''' def run_c(tmp, name, source, flags=()): path = tmp / f'{name}.c' path.write_text(source) command = [shutil.which('clang') or 'cc', '-std=c99', '-Werror=implicit-function-declaration', '-Wno-typedef-redefinition', *flags] for include in [tmp, BASE, BASE / 'inc', BASE / 'lib/hal', SDK, SDK / 'lwipv4v6']: command.extend(['-I', str(include)]) subprocess.run(command + [str(path), '-o', str(tmp / name)], check=True) result = subprocess.run([str(tmp / name)], capture_output=True, text=True) if result.returncode: print(result.stdout, result.stderr) result.check_returncode() for line in result.stdout.splitlines(): if line.startswith('PASS:'): print(line) return result.stdout def main(): with tempfile.TemporaryDirectory(prefix='banban-netdiag-') as directory: tmp = Path(directory) for name in ['ql_type.h', 'ql_rtos.h', 'ql_nw.h', 'ql_data_call.h']: content = (SDK / name).read_bytes() if name == 'ql_type.h': content, count = re.subn(rb'typedef unsigned int\s+size_t;', b'/* host size_t */', content) assert count == 1 (tmp / name).write_bytes(content) sample = run_c(tmp, 'sample', SAMPLE) assert '本次网络不可用观测时长=2000 ms' in sample assert '失败率=50%' in sample and 'CQI=不可用(raw=255)' in sample run_c(tmp, 'probe', PROBE, flags=['-include', 'sys/time.h', '-DTIME_VAL_DEF', '-Wno-macro-redefined']) run_c(tmp, 'usb', USB, flags=['-Dprintf=field_log_printf']) voice = run_c(tmp, 'voice', VOICE) assert '录音结束到首PCM=2350 ms' in voice assert '收尾入队=100 等TTS通知=400 等首URL=1500 播放队列=100 下载=200 解码提交=50' in voice assert voice.count('[耗时汇总]') == 1 # Exercise the actual quiet macro: muting must still execute side effects. gnss = (BASE / 'gnss.c').read_text() config = gnss[gnss.index('#ifndef BANBAN_GNSS_LOG_ENABLED'):gnss.index('// 新增宏定义')] run_c(tmp, 'gnss', '#include \n#include \n' + config + r''' int main(void) { int closed = 0; GNSS_LOG("close=%d", ++closed); assert(closed == 1); puts("PASS: GNSS mute preserves side effects"); return 0; } ''') key = (BASE / 'hal_key.c').read_text() alarm = key[key.index('if(key_press_alarm_counter!=0'):key.index('ql_rtos_task_sleep_ms(10);')] assert 'net_diag_start();' in alarm for forbidden in ['build_alarm(', 'build_leave_to_parent(', 'stop_player_func(', 'app_play_tips_file(']: assert forbidden not in alarm, forbidden print('PASS: diagnostic key retains no former alarm/message/audio actions') if __name__ == '__main__': main()