From c7756c9325fc93dcf2bd013dc6df5ff6865ecb63 Mon Sep 17 00:00:00 2001 From: chenjintao Date: Mon, 26 Oct 2026 18:09:25 +0800 Subject: [PATCH] fix: calibrate APP clock from NITZ after PDP activation --- docs/app_time_calibrate.md | 9 + .../threadx/interface/custom_app/Makefile | 1 + .../interface/custom_app/app_time_calibrate.c | 226 ++++++++++++++++++ .../custom_app/inc/app_time_calibrate.h | 6 + .../threadx/interface/custom_app/net_mgt.c | 2 + tools/test_app_time_calibrate.py | 181 ++++++++++++++ 6 files changed, 425 insertions(+) create mode 100644 docs/app_time_calibrate.md create mode 100644 ql-application/threadx/interface/custom_app/app_time_calibrate.c create mode 100644 ql-application/threadx/interface/custom_app/inc/app_time_calibrate.h create mode 100644 tools/test_app_time_calibrate.py diff --git a/docs/app_time_calibrate.md b/docs/app_time_calibrate.md new file mode 100644 index 0000000..d2adb1f --- /dev/null +++ b/docs/app_time_calibrate.md @@ -0,0 +1,9 @@ +# APP 启动校时 + +PDP 首次激活后,APP 通过现有 SDK 的 `ql_nw_get_nitz_time_info` 获取运营商 NITZ 快照。按 UTC 解析日期,用 `ql_localtime_r` 转换到当前 PMIC 时区,再调用 `ql_rtc_set_time` 写入 APP RTC。读取 RTC 和 `time(NULL)`,两项误差都不超过 3 秒才打印 `[APP-TIME] PASS`。 + +每次开机只启动一次;读取失败、日期无效或查询耗时超过 2 秒时,间隔 5 秒重试,最多 3 次。选中首个有效快照后最多写入一次,即使读回失败也不重复写入。保留原有 NITZ 开关,不增加网络域名或端口,不修改内核,不依赖 NTP、AT 命令、SYS 探测或额外内核诊断。支持现有内核导出接口下的纯 APP OTA。 + +限制:SDK 返回快照未提供接收时间,读回验证只证明 APP 时钟与快照一致,不能证明快照新鲜度。本方案保留实机验证通过的首次 PDP 后快照策略,不扩展为持续校时。首次 WSS 建立仍需等待实际 TLS 握手验证;本次修复不修改连接调度或对话回复超时。 + +验证:`DEVELOPER_DIR=/Library/Developer/CommandLineTools python3 tools/test_app_time_calibrate.py`,实际 C 源码通过 26 组行为场景和日期解析验证(ASan/UBSan),覆盖 UTC/时区、无效和静态快照、重试、tick 回绕、RTC 写入/读回失败。迁移依据为 test/time-fix 的 884ab54;仅调整模块命名与日志,不改变已验证校时算法。2026-10-26 17:29:27 完整串口日志中,原版本校时读回误差为 0 秒,WSS 证书验证与 HTTP 101 成功,第二次对话收到 TTS。 diff --git a/ql-application/threadx/interface/custom_app/Makefile b/ql-application/threadx/interface/custom_app/Makefile index b79cab6..198d717 100644 --- a/ql-application/threadx/interface/custom_app/Makefile +++ b/ql-application/threadx/interface/custom_app/Makefile @@ -82,6 +82,7 @@ SRC_FILES+= \ DP1312EA.c \ mqtt.c \ mqtt_connect_diag.c \ + app_time_calibrate.c \ vad_det.c \ sleep_wake.c \ battery.c \ diff --git a/ql-application/threadx/interface/custom_app/app_time_calibrate.c b/ql-application/threadx/interface/custom_app/app_time_calibrate.c new file mode 100644 index 0000000..51a967e --- /dev/null +++ b/ql-application/threadx/interface/custom_app/app_time_calibrate.c @@ -0,0 +1,226 @@ +#include +#include +#include +#include +#include "ql_rtos.h" +#include "ql_func.h" +#include "ql_nw.h" +#include "ql_rtc.h" +#include "app_time_calibrate.h" + +/* SDK wrapper uses the same PMIC timezone as time(); no libc TZ assumptions. */ +extern struct tm *ql_localtime_r(const time_t *, struct tm *); +#define MIN_EPOCH 1704067200LL +#define MAX_EPOCH 2147483647LL +#define ATTEMPTS 3 + +static ql_task_t calibration_task; +static int calibration_started; + +typedef struct { + time_t utc; + unsigned int ticks; + int zone_quarters; +} nitz_sample; + +static int leap(int year) +{ + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +/* Explicit Gregorian conversion: no mktime() or APP clock dependency. */ +static int calendar_epoch(int year, int month, int day, int hour, int minute, + int second, long long *result) +{ + static const int days[] = {31,28,31,30,31,30,31,31,30,31,30,31}; + long long total = 0; + int y, m; + if (year < 1970 || year > 2038 || month < 1 || month > 12 || + day < 1 || day > days[month - 1] + (month == 2 && leap(year)) || + hour < 0 || hour > 23 || minute < 0 || minute > 59 || + second < 0 || second > 59) return -1; + for (y = 1970; y < year; ++y) total += 365 + leap(y); + for (m = 1; m < month; ++m) total += days[m - 1] + (m == 2 && leap(year)); + *result = ((total + day - 1) * 24 + hour) * 3600 + minute * 60 + second; + return 0; +} + +static int two_digits(const char *s) +{ + if (s[0] < '0' || s[0] > '9' || s[1] < '0' || s[1] > '9') return -1; + return (s[0] - '0') * 10 + s[1] - '0'; +} + +/* Accept actual "YY/MM/DD HH:MM:SS 32 0", documented signed format, and compact + * "YY/MM/DD,HH:MM:SS+32". NITZ UT is UTC; zone is metadata, not subtracted. + * abs_time/leap_sec have insufficient documented semantics to use as epoch. */ +static int parse_nitz(const char *s, time_t *epoch, int *zone) +{ + size_t n = strlen(s), pos = 17; + int yy, mo, dd, hh, mm, ss, q, dst, negative = 0; + size_t start; + long long value; + if (n < 20 || s[2] != '/' || s[5] != '/' || + (s[8] != ' ' && s[8] != ',') || s[11] != ':' || s[14] != ':') return -1; + yy = two_digits(s); mo = two_digits(s + 3); dd = two_digits(s + 6); + hh = two_digits(s + 9); mm = two_digits(s + 12); ss = two_digits(s + 15); + if (yy < 0 || calendar_epoch(2000 + yy, mo, dd, hh, mm, ss, &value) != 0 || + value < MIN_EPOCH || value > MAX_EPOCH) return -2; + /* Actual SDK output is also "26/10/26 08:00:10 32 0". */ + if (s[pos] == ' ') ++pos; + else if (s[pos] != '+' && s[pos] != '-') return -3; + if (s[pos] == '+' || s[pos] == '-') negative = s[pos++] == '-'; + start = pos; + q = 0; + while (pos < n && s[pos] >= '0' && s[pos] <= '9' && pos - start < 2) + q = q * 10 + s[pos++] - '0'; + if (pos == start || q > 56 || (negative && q > 48)) return -3; + *zone = negative ? -q : q; + if (pos < n) { + if (s[pos++] != ' ') return -4; + start = pos; + dst = 0; + while (pos < n && s[pos] >= '0' && s[pos] <= '9' && pos - start < 2) + dst = dst * 10 + s[pos++] - '0'; + if (pos == start || pos != n || dst > 2) return -4; + } + *epoch = (time_t)value; + return 0; +} + +static int get_sample(int attempt, int index, nitz_sample *sample) +{ + QL_NW_NITZ_TIME_INFO_T info; + unsigned int began = ql_rtos_get_systicks(); + int rc, parsed; + memset(&info, 0, sizeof(info)); + rc = ql_nw_get_nitz_time_info(&info); + sample->ticks = ql_rtos_get_systicks(); + printf("[APP-TIME] sample attempt=%d index=%d rc=%d raw=\"%.*s\" " + "abs_time=%lu leap_sec=%u query_ms=%lu\r\n", attempt, index, rc, + (int)sizeof(info.nitz_time), info.nitz_time, info.abs_time, + (unsigned int)info.leap_sec, + (unsigned long)(sample->ticks - began) * 5UL); + if (rc != QL_NW_SUCCESS || + memchr(info.nitz_time, '\0', sizeof(info.nitz_time)) == NULL) return -1; + parsed = parse_nitz(info.nitz_time, &sample->utc, &sample->zone_quarters); + if (parsed != 0) { + printf("[APP-TIME] rejected stage=parse reason=%d\r\n", parsed); + return -1; + } + /* Bound query delay; this does not establish the age of a cached NITZ. */ + if (sample->ticks - began > 400U) { + printf("[APP-TIME] rejected stage=slow_query rtc_not_written=1\r\n"); + return -1; + } + printf("[APP-TIME] parsed utc=%ld zone_quarters=%d zone_minutes=%d\r\n", + (long)sample->utc, sample->zone_quarters, sample->zone_quarters * 15); + return 0; +} + +static int write_and_verify(const nitz_sample *reference) +{ + struct tm local; + ql_rtc_time_t target, actual; + time_t epoch, before = time(NULL), after; + long long expected, local_epoch, delta, actual_local = 0; + int rc, read_rc, calendar_ok; + expected = (long long)reference->utc + + (ql_rtos_get_systicks() - reference->ticks) / 200U; + if (expected > MAX_EPOCH) { + printf("[APP-TIME] FAIL stage=epoch_range\r\n"); + return -1; + } + epoch = (time_t)expected; + memset(&local, 0, sizeof(local)); + if (ql_localtime_r(&epoch, &local) == NULL || + calendar_epoch(local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, + local.tm_hour, local.tm_min, local.tm_sec, &local_epoch) != 0 || + local_epoch < MIN_EPOCH || local_epoch > MAX_EPOCH || + local_epoch - expected < -43200 || local_epoch - expected > 50400) { + printf("[APP-TIME] FAIL stage=local_calendar\r\n"); + return -1; + } + memset(&target, 0, sizeof(target)); + target.tm_year = local.tm_year + 1900; + target.tm_mon = local.tm_mon + 1; + target.tm_mday = local.tm_mday; + target.tm_hour = local.tm_hour; + target.tm_min = local.tm_min; + target.tm_sec = local.tm_sec; + target.tm_wday = local.tm_wday; + printf("[APP-TIME] set_begin app_before=%ld expected_utc=%ld " + "pmic_zone_minutes=%ld app_calendar=%04d-%02d-%02dT%02d:%02d:%02d\r\n", + (long)before, (long)epoch, (long)((local_epoch - expected) / 60), + target.tm_year, target.tm_mon, target.tm_mday, + target.tm_hour, target.tm_min, target.tm_sec); + rc = ql_rtc_set_time(&target); + memset(&actual, 0, sizeof(actual)); + read_rc = ql_rtc_get_time(&actual); + after = time(NULL); + expected = (long long)reference->utc + + (ql_rtos_get_systicks() - reference->ticks) / 200U; + delta = (long long)after - expected; + calendar_ok = calendar_epoch(actual.tm_year, actual.tm_mon, actual.tm_mday, + actual.tm_hour, actual.tm_min, actual.tm_sec, + &actual_local) == 0; + actual_local -= local_epoch + (expected - (long long)epoch); + printf("[APP-TIME] readback set_rc=%d rtc_rc=%d app_after=%ld " + "expected_utc=%lld delta_s=%lld calendar=%04d-%02d-%02dT%02d:%02d:%02d\r\n", + rc, read_rc, (long)after, expected, delta, actual.tm_year, actual.tm_mon, + actual.tm_mday, actual.tm_hour, actual.tm_min, actual.tm_sec); + if (rc != 0 || read_rc != 0 || after < MIN_EPOCH || + delta < -3 || delta > 3 || !calendar_ok || + actual_local < -3 || actual_local > 3) { + printf("[APP-TIME] FAIL stage=app_clock_verification\r\n"); + return -1; + } + printf("[APP-TIME] PASS source=nitz app_utc=verified " + "wss_requires_separate_handshake_check=1\r\n"); + return 0; +} + +static void time_calibrate_worker(void *arg) +{ + nitz_sample sample; + int attempt; + (void)arg; + printf("[APP-TIME] begin version=1 source=operator_nitz ntp=off " + "policy=first_valid_after_pdp sys_probe=off\r\n"); + if (!m_get_api_ptr || !m_get_api_ptr("ql_nw_get_nitz_time_info") || + !m_get_api_ptr("ql_localtime_r") || !m_get_api_ptr("ql_rtc_set_time") || + !m_get_api_ptr("ql_rtc_get_time")) { + printf("[APP-TIME] FAIL stage=sdk_api_unavailable\r\n"); + goto done; + } + /* Use the first valid operator snapshot promptly after + * PDP activation. A snapshot need not advance between API reads. Its actual + * network reception time is unknown; readback alone cannot establish age. */ + for (attempt = 1; attempt <= ATTEMPTS; ++attempt) { + if (get_sample(attempt, 1, &sample) == 0) { + printf("[APP-TIME] snapshot_selected attempt=%d utc=%ld " + "age=unknown first_valid_after_pdp=1 max_rtc_writes=1\r\n", + attempt, (long)sample.utc); + write_and_verify(&sample); + goto done; /* No repeated write, even on setter/readback failure. */ + } + if (attempt < ATTEMPTS) ql_rtos_task_sleep_ms(5000); + } + printf("[APP-TIME] FAIL stage=no_valid_nitz rtc_not_written=1\r\n"); +done: + printf("[APP-TIME] finished once_per_boot=1\r\n"); + ql_rtos_task_delete(NULL); +} + +void app_time_calibrate_start(void) +{ + int rc; + if (calibration_started) return; /* PDP management is the sole caller. */ + calibration_started = 1; + rc = ql_rtos_task_create(&calibration_task, 6 * 1024, 110, "app_time_calibrate", + time_calibrate_worker, NULL); + if (rc != 0) { + calibration_started = 0; + printf("[APP-TIME] FAIL stage=create_task rc=%d\r\n", rc); + } +} diff --git a/ql-application/threadx/interface/custom_app/inc/app_time_calibrate.h b/ql-application/threadx/interface/custom_app/inc/app_time_calibrate.h new file mode 100644 index 0000000..6a6800a --- /dev/null +++ b/ql-application/threadx/interface/custom_app/inc/app_time_calibrate.h @@ -0,0 +1,6 @@ +#ifndef APP_TIME_CALIBRATE_H +#define APP_TIME_CALIBRATE_H + +void app_time_calibrate_start(void); + +#endif diff --git a/ql-application/threadx/interface/custom_app/net_mgt.c b/ql-application/threadx/interface/custom_app/net_mgt.c index 8987699..e231604 100644 --- a/ql-application/threadx/interface/custom_app/net_mgt.c +++ b/ql-application/threadx/interface/custom_app/net_mgt.c @@ -21,6 +21,7 @@ #include "ql_atcmd.h" #include "http_downfile.h" #include "ota_post_update.h" +#include "app_time_calibrate.h" //#define NTP_SERVER_DOMAIN "ntp.aliyun.com" @@ -141,6 +142,7 @@ void ql_datacall_demo_thread(void *arvg) if(ret == 0) { + app_time_calibrate_start(); ql_get_data_call_info(1, 0, &info); printf("info.profile_idx: %d\r\n", info.profile_idx); printf("info.ip_version: %d\r\n", info.ip_version); diff --git a/tools/test_app_time_calibrate.py b/tools/test_app_time_calibrate.py new file mode 100644 index 0000000..19320b0 --- /dev/null +++ b/tools/test_app_time_calibrate.py @@ -0,0 +1,181 @@ +"""Host behavioral tests of the actual APP module; no modem traffic is sent.""" +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +APP = ROOT / 'ql-application/threadx/interface/custom_app' +with tempfile.TemporaryDirectory(prefix='app-nitz-test-') as directory: + work = Path(directory) + headers = { + 'ql_rtos.h': '''#include +typedef void *ql_task_t; +unsigned int ql_rtos_get_systicks(void); +void ql_rtos_task_sleep_ms(unsigned int); +int ql_rtos_task_create(ql_task_t *, unsigned int, unsigned char, char *, void (*)(void *), void *, ...); +int ql_rtos_task_delete(ql_task_t); +''', + 'ql_func.h': '#include \nextern intptr_t (*m_get_api_ptr)(char *);\n', + 'ql_nw.h': '''enum { QL_NW_SUCCESS=0 }; +typedef struct {char nitz_time[32]; unsigned long abs_time; unsigned char leap_sec;} QL_NW_NITZ_TIME_INFO_T; +int ql_nw_get_nitz_time_info(QL_NW_NITZ_TIME_INFO_T *); +''', + 'ql_rtc.h': '''typedef struct {int tm_sec,tm_min,tm_hour,tm_mday,tm_mon,tm_year,tm_wday;} ql_rtc_time_t; +int ql_rtc_set_time(ql_rtc_time_t *); +int ql_rtc_get_time(ql_rtc_time_t *); +''', + } + for name, content in headers.items(): + (work / name).write_text(content) + harness = r''' +#include +#include +#include "APP_SOURCE" +static int mode, creates, queries, sets, sleeps; +static unsigned int ticks=100; +static time_t app_epoch=100; +static int zone_minutes=480; +static ql_rtc_time_t saved; +unsigned int ql_rtos_get_systicks(void) {return ticks;} +void ql_rtos_task_sleep_ms(unsigned int ms) {assert(ms==5000); ++sleeps; ticks+=ms/5;} +static intptr_t lookup(char *name) { + assert(!strcmp(name,"ql_nw_get_nitz_time_info") || !strcmp(name,"ql_localtime_r") || + !strcmp(name,"ql_rtc_set_time") || !strcmp(name,"ql_rtc_get_time")); + return mode==1 ? 0 : 1; +} +intptr_t (*m_get_api_ptr)(char *)=lookup; +int ql_nw_get_nitz_time_info(QL_NW_NITZ_TIME_INFO_T *info) { + time_t value=1793019600; /* 2026-10-26 13:00:00 UTC */ + struct tm *utc; + ++queries; + if (mode==2) return 1; + if (mode==3) return 0; + if (mode==4) {memset(info->nitz_time,'X',32); return 0;} + if (mode==5) {strcpy(info->nitz_time,"26/02/30 13:00:00 +32 00"); return 0;} + if (mode==6 || mode==22) { + strcpy(info->nitz_time,mode==22 ? "26/10/26 08:00:10 32 0" : "26/10/26 13:00:00 +32 00"); + return 0; + } + if (mode==16 && queries==1) return 1; + if (mode==23 && queries<3) return 1; + if (mode==17) ticks+=600; /* slow query must be rejected */ + if (mode==14) value=1793059198; /* near midnight UTC */ + if (mode==15) value=2147483646; /* PMIC +8 local calendar exceeds signed32 range */ + if (mode==18) value=1704067200; /* Old but valid snapshot: age cannot be proven. */ + else value+=(ticks-100U)/200U; + /* Compute relative elapsed correctly in wrap scenario. */ + if (mode==13) value=1793019600+(unsigned int)(ticks-(UINT32_MAX-199U))/200U; + utc=gmtime(&value); assert(utc); + snprintf(info->nitz_time,32,mode==21 ? "%02d/%02d/%02d %02d:%02d:%02d 32 0" : "%02d/%02d/%02d %02d:%02d:%02d +32 00", + utc->tm_year%100,utc->tm_mon+1,utc->tm_mday,utc->tm_hour,utc->tm_min,utc->tm_sec); + return 0; +} +struct tm *ql_localtime_r(const time_t *epoch,struct tm *out) { + time_t local=*epoch+zone_minutes*60; + struct tm *value; + if (mode==7) return NULL; + if (mode==25) local+=28800; /* Invalid PMIC conversion (+16 hours total). */ + value=gmtime(&local); assert(value); *out=*value; return out; +} +time_t time(time_t *out) {(void)out; return app_epoch;} +int ql_rtc_set_time(ql_rtc_time_t *calendar) { + long long value; + ++sets; saved=*calendar; + if (mode==8) return -1; + assert(calendar_epoch(calendar->tm_year,calendar->tm_mon,calendar->tm_mday, + calendar->tm_hour,calendar->tm_min,calendar->tm_sec,&value)==0); + if (mode!=9) app_epoch=(time_t)(value-zone_minutes*60); + if (mode==10) app_epoch+=28800; /* catches double timezone conversion */ + return 0; +} +int ql_rtc_get_time(ql_rtc_time_t *out) {*out=saved; if (mode==20) out->tm_year=1970; return mode==11 ? -1 : 0;} +int ql_rtos_task_create(ql_task_t *task,unsigned int size,unsigned char priority,char *name, + void (*entry)(void *),void *arg,...) { + assert(size>=4096 && priority==110 && !strcmp(name,"app_time_calibrate")); + ++creates; + if (mode==19) return -1; + *task=(void *)1; entry(arg); return 0; +} +int ql_rtos_task_delete(ql_task_t task) {assert(task==NULL); return 0;} +static void check_parser(void) { + time_t epoch; int zone; long long value; + assert(parse_nitz("26/10/26 13:00:00 +32 00",&epoch,&zone)==0 && + epoch==1793019600 && zone==32); + assert(parse_nitz("26/10/26,13:00:00-22",&epoch,&zone)==0 && + epoch==1793019600 && zone==-22); + assert(parse_nitz("26/10/26 13:00:00 +56 02",&epoch,&zone)==0); + assert(parse_nitz("26/10/26 08:00:10 32 0",&epoch,&zone)==0 && epoch==1793001610 && zone==32); + assert(parse_nitz("26/10/26 13:00:00 -22 0",&epoch,&zone)==0 && zone==-22); + assert(parse_nitz("26/10/26 13:00:00 0 0",&epoch,&zone)==0 && zone==0); + assert(parse_nitz("26/10/26 13:00:00 0",&epoch,&zone)!=0); + assert(parse_nitz("26/10/26 13:00:00 320 0",&epoch,&zone)!=0); + assert(parse_nitz("24/02/29 23:59:59 +00 00",&epoch,&zone)==0); + assert(parse_nitz("26/02/29 00:00:00 +00 00",&epoch,&zone)!=0); + assert(parse_nitz("38/01/19 03:14:07 +00 00",&epoch,&zone)==0 && epoch==2147483647); + assert(parse_nitz("38/01/19 03:14:08 +00 00",&epoch,&zone)!=0); + const char *bad[]={"", "26/10/26 13:00:00", "23/12/31 23:59:59 +00 00", + "26/00/26 13:00:00 +00 00", "26/13/26 13:00:00 +00 00", + "26/10/00 13:00:00 +00 00", "26/04/31 13:00:00 +00 00", + "26/10/26 24:00:00 +00 00", "26/10/26 13:60:00 +00 00", + "26/10/26 13:00:60 +00 00", "26/10/26 13:00:00 +57 00", + "26/10/26 13:00:00 -49 00", "26/10/26 13:00:00 +32 03", + "26/10/26 13:00:00 +32 junk", + "26/10/26 13:00:00 +32 00junk", "x6/10/26 13:00:00 +32 00"}; + for (unsigned int i=0;i=1 && mode<=5) || mode==7 || mode==15 || mode==17 || mode==19 || mode==25) + assert(sets==0); + else assert(sets==1); + if (mode==1 || mode==19) {assert(queries==0 && sleeps==0);} + else if ((mode>=2 && mode<=5) || mode==17 || mode==23) {assert(queries==3 && sleeps==2);} + else if (mode==16) {assert(queries==2 && sleeps==1);} + else {assert(queries==1 && sleeps==0);} + if (mode==22) { + assert(saved.tm_year==2026 && saved.tm_mon==10 && saved.tm_mday==26); + assert(saved.tm_hour==16 && saved.tm_min==0 && saved.tm_sec==10); + assert(app_epoch==1793001610); /* Local RTC +8, UTC remains unshifted. */ + } + return 0; +} +'''.replace('APP_SOURCE', str(APP / 'app_time_calibrate.c')) + (work / 'test.c').write_text(harness) + compiler = shutil.which('clang') or shutil.which('cc') + if not compiler: + raise SystemExit('A native C compiler is required') + subprocess.run([compiler, '-std=c99', '-Wall', '-Wextra', '-Werror', + '-fsanitize=address,undefined', '-I', str(work), '-I', str(APP / 'inc'), + str(work / 'test.c'), '-o', str(work / 'test')], check=True) + cases = {0: 'PASS source=nitz', 1: 'FAIL stage=sdk_api_unavailable', + 2: 'FAIL stage=no_valid_nitz', 3: 'FAIL stage=no_valid_nitz', + 4: 'FAIL stage=no_valid_nitz', 5: 'FAIL stage=no_valid_nitz', + 6: 'PASS source=nitz', 7: 'FAIL stage=local_calendar', + 8: 'FAIL stage=app_clock_verification', 9: 'FAIL stage=app_clock_verification', + 10: 'FAIL stage=app_clock_verification', 11: 'FAIL stage=app_clock_verification', + 12: 'PASS source=nitz', 13: 'PASS source=nitz', 14: 'PASS source=nitz', + 15: 'FAIL stage=local_calendar', 16: 'PASS source=nitz', + 17: 'FAIL stage=no_valid_nitz', 18: 'PASS source=nitz', + 19: 'FAIL stage=create_task', 20: 'FAIL stage=app_clock_verification', + 21: 'PASS source=nitz', 22: 'PASS source=nitz', 23: 'PASS source=nitz', + 24: 'PASS source=nitz', 25: 'FAIL stage=local_calendar'} + for mode, expected in cases.items(): + log = subprocess.check_output([str(work / 'test'), str(mode)], text=True) + assert expected in log, log + assert '[APP-SYS-PROBE]' not in log, log + assert 'stage=freshness' not in log, log + if expected.startswith('PASS'): + assert 'age=unknown first_valid_after_pdp=1 max_rtc_writes=1' in log, log + else: + assert '[APP-TIME] PASS ' not in log, log + print('PASS: parser validation and 26 first-snapshot/retry/timezone/RTC scenarios (ASan/UBSan)')