fix: correct STEP-057B weather runtime state semantics
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -25,3 +25,13 @@ OFF 会停止 Timeline、clear Host ownership,并在 Render 线程的空 Sourc
|
||||
- timestamp 测试改为 epoch milliseconds(2024-01-01 UTC 起的 64-bit ms),覆盖 exact T0、midpoint、exact T1、上下界 out-of-range 与 clear 后 fail-closed。
|
||||
|
||||
验证:output-throttle 下 shader generator、Linux OpenGL `mbgl-core`、`mbgl-render`、`mbgl-test-runner`、相关精确 Source/Style compile 和定向非 Jest tests 均 PASS;`git diff --check` PASS。Android Gradle/APK/ADB/设备/Jest 未运行。
|
||||
|
||||
## STEP-057B 状态语义修正(2026-09-07)
|
||||
|
||||
状态:`IMPLEMENTED / WAITING_CODE_REVIEW`。
|
||||
|
||||
基于 `dad6876c72f29e2d5671677ee15e13cd25e653a1`,本轮只修改 `WeatherRuntime` 与状态测试/记录:普通 `REJECTED_ADAPTER`、`REJECTED_INCOMPATIBLE`、`REJECTED_NON_MONOTONIC`、`REJECTED_CAPACITY` 和空 metadata/payload 保持当前 state;`REJECTED_SOURCE_MODE`、`UNKNOWN` 才进入 ERROR。pending T0、initial window、next snapshot、duplicate 的成功/保持语义不变。
|
||||
|
||||
`READY + enable(true)`、`WAITING_FOR_FIELDS + enable(true)` 为幂等 no-op;`DISABLED + enable(true)` 成功恢复到 WAITING;ERROR enable 明确返回 REJECTED 并保持 ERROR。clear/disable 成功与失败转换保持原有契约,DISABLED 重复 disable 仍为成功 no-op。
|
||||
|
||||
`WeatherRuntimeStateTest` 已覆盖普通 rejection 保持 READY/WAITING、真正错误、所有 enable 分支、clear/disable,以及 public flow 的 invalid submit 和 ERROR enable。public flow 测试源码已加入现有 Mockito/JUnit 体系,但本轮未运行 Android Gradle;因此仍需代码审计后统一 Android 验收。
|
||||
|
||||
39
docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md
Normal file
39
docs/报告/STEP-057_AndroidHost天气运行时接口基础_Codex完成报告_v3.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# STEP-057B WeatherRuntime 状态语义修正 — Codex 完成报告
|
||||
|
||||
状态:`IMPLEMENTED / WAITING_CODE_REVIEW`。
|
||||
|
||||
## 实际修改
|
||||
|
||||
本轮基于 `dad6876c72f29e2d5671677ee15e13cd25e653a1`,只修改:
|
||||
|
||||
- `platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/maps/WeatherRuntime.java`
|
||||
- `platform/android/MapLibreAndroid/src/test/java/org/maplibre/android/maps/WeatherRuntimeStateTest.java`
|
||||
- STEP-057 Step 记录、`粒子开发记录.md` 与本报告
|
||||
|
||||
没有修改 renderer、RenderOrchestrator、JNI、shader/UBO、WeatherColor ramp、timestamp Core、Runtime Field Window、Test App UI 或 React Native bridge。
|
||||
|
||||
## 最终状态语义
|
||||
|
||||
普通数据质量拒绝 `REJECTED_ADAPTER`、`REJECTED_INCOMPATIBLE`、`REJECTED_NON_MONOTONIC`、`REJECTED_CAPACITY` 以及空 metadata/payload 只表示本次提交失败,保留当前 `READY` 或 `WAITING_FOR_FIELDS`。`ACCEPTED_PENDING_INITIAL_WINDOW` → WAITING,`INITIAL_WINDOW_READY`/`ACCEPTED_NEXT_SNAPSHOT` → READY,`DUPLICATE` 保持 current。只有 `REJECTED_SOURCE_MODE` 和 `UNKNOWN` 进入 ERROR。
|
||||
|
||||
`READY` 或 `WAITING_FOR_FIELDS` 重复 enable 是 ACCEPTED no-op;`DISABLED` enable 成功恢复 visibility 并进入 WAITING,失败进入 ERROR;ERROR enable 返回 REJECTED 并保持 ERROR。clear 成功 → WAITING、失败保持 current;disable 成功 → DISABLED、失败 → ERROR;DISABLED 重复 disable 为 ACCEPTED no-op。
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
`WeatherRuntimeStateTest` 使用现有 JUnit/Mockito,覆盖所有普通 rejection 在 READY/WAITING 下保持状态、SOURCE_MODE/UNKNOWN 错误、enable 分支、clear/disable,以及两条实际 public flow:READY 下 invalid submit 保持 READY,ERROR 下 enable 返回 REJECTED 且不调用 visibility recovery。该源码未单独引入测试框架。
|
||||
|
||||
## 验证
|
||||
|
||||
本轮执行 `git diff --check`:PASS。未执行编译或测试命令;因此 public flow 测试尚未运行。明确未执行:Android Gradle、APK build、ADB、设备/虚拟机测试、Jest。此前 STEP-057A 的 Linux/OpenGL 与非 Jest Core 验证不受本轮代码修改影响。
|
||||
|
||||
## 风险与审计状态
|
||||
|
||||
已知剩余风险是 Android Java/Kotlin 编译和 Mockito final-class 配置需在后续统一 Android 验收中确认;本轮没有改变 Core field/timeline 生命周期。当前提交具备再次独立代码审计条件,等待 code review 后再进入 Android T1–T13 综合验证。
|
||||
|
||||
## Git
|
||||
|
||||
- 基线:`dad6876c72f29e2d5671677ee15e13cd25e653a1`
|
||||
- 功能 commit:待本轮提交后记录
|
||||
- Push:待本轮提交后记录
|
||||
- Remote branch:`origin/particle/main`
|
||||
- Final HEAD:待本轮提交后记录
|
||||
@@ -55,7 +55,7 @@ public final class WeatherRuntime {
|
||||
|
||||
public FieldSubmitResult submitWindField(@NonNull String metadataJson, @NonNull byte[] payloadBytes) {
|
||||
requireEnabled();
|
||||
if (metadataJson.isEmpty() || payloadBytes.length == 0) { state = State.ERROR; return FieldSubmitResult.REJECTED_ADAPTER; }
|
||||
if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER;
|
||||
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes));
|
||||
state = stateAfterSubmit(state, result);
|
||||
return result;
|
||||
@@ -71,6 +71,7 @@ public final class WeatherRuntime {
|
||||
state = stateAfterDisable(color && particle && clear);
|
||||
return result(color && particle && clear);
|
||||
}
|
||||
if (state == State.ERROR) return CommandResult.REJECTED;
|
||||
if (state != State.DISABLED) return CommandResult.ACCEPTED;
|
||||
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible);
|
||||
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible);
|
||||
@@ -105,13 +106,14 @@ public final class WeatherRuntime {
|
||||
if (submit == FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW) return State.WAITING_FOR_FIELDS;
|
||||
if (submit == FieldSubmitResult.INITIAL_WINDOW_READY || submit == FieldSubmitResult.ACCEPTED_NEXT_SNAPSHOT) return State.READY;
|
||||
if (submit == FieldSubmitResult.DUPLICATE) return current;
|
||||
return State.ERROR;
|
||||
return submit == FieldSubmitResult.REJECTED_SOURCE_MODE || submit == FieldSubmitResult.UNKNOWN ? State.ERROR : current;
|
||||
}
|
||||
static State stateAfterClear(State current, boolean accepted) { return accepted ? State.WAITING_FOR_FIELDS : current; }
|
||||
static State stateAfterDisable(boolean accepted) { return accepted ? State.DISABLED : State.ERROR; }
|
||||
static State stateAfterEnable(State current, boolean accepted) {
|
||||
if (!accepted) return State.ERROR;
|
||||
return current == State.DISABLED ? State.WAITING_FOR_FIELDS : current;
|
||||
if (current == State.DISABLED) return State.WAITING_FOR_FIELDS;
|
||||
return current;
|
||||
}
|
||||
private void requireEnabled() { if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.maplibre.android.maps;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -15,9 +16,51 @@ public class WeatherRuntimeStateTest {
|
||||
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY));
|
||||
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.DUPLICATE));
|
||||
}
|
||||
@Test public void ordinarySubmitRejectionsPreserveRuntimeState() {
|
||||
WeatherRuntime.FieldSubmitResult[] rejected = {
|
||||
WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER,
|
||||
WeatherRuntime.FieldSubmitResult.REJECTED_INCOMPATIBLE,
|
||||
WeatherRuntime.FieldSubmitResult.REJECTED_NON_MONOTONIC,
|
||||
WeatherRuntime.FieldSubmitResult.REJECTED_CAPACITY};
|
||||
for (WeatherRuntime.FieldSubmitResult result : rejected) {
|
||||
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, result));
|
||||
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, result));
|
||||
}
|
||||
}
|
||||
@Test public void onlyFacadeContractFailuresEnterError() {
|
||||
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.REJECTED_SOURCE_MODE));
|
||||
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.UNKNOWN));
|
||||
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.ERROR, false));
|
||||
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.ERROR, true));
|
||||
}
|
||||
@Test public void clearAndDisableUseSuccessfulCoreResult() {
|
||||
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterClear(WeatherRuntime.State.READY, true));
|
||||
assertEquals(WeatherRuntime.State.DISABLED, WeatherRuntime.stateAfterDisable(true));
|
||||
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterDisable(false));
|
||||
}
|
||||
|
||||
@Test public void publicFlowKeepsReadyOnInvalidSubmission() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
Mockito.when(map.submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class)))
|
||||
.thenReturn(MapLibreMap.HOST_FIELD_INITIAL_WINDOW_READY)
|
||||
.thenReturn(MapLibreMap.HOST_FIELD_REJECTED_ADAPTER);
|
||||
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
|
||||
assertEquals(WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY, runtime.submitWindField("{}", new byte[] {1}));
|
||||
assertEquals(WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, runtime.submitWindField("{}", new byte[] {1}));
|
||||
assertEquals(WeatherRuntime.State.READY, runtime.getState());
|
||||
assertEquals(WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, runtime.submitWindField("", new byte[] {1}));
|
||||
assertEquals(WeatherRuntime.State.READY, runtime.getState());
|
||||
}
|
||||
|
||||
@Test public void publicFlowRejectsEnableFromErrorWithoutVisibilityRecovery() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
|
||||
assertEquals(WeatherRuntime.FieldSubmitResult.UNKNOWN, runtime.submitWindField("{}", new byte[] {1}));
|
||||
assertEquals(WeatherRuntime.State.ERROR, runtime.getState());
|
||||
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherEnabled(true));
|
||||
assertEquals(WeatherRuntime.State.ERROR, runtime.getState());
|
||||
Mockito.verify(map, Mockito.never()).setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,3 +463,9 @@ WeatherColorLayer 的 `ParticleGpuValidationUBO` designated initializer 补齐 `
|
||||
- enable/clear/disable/submit 状态转换已幂等并可恢复,另增加既有 Android JUnit state transition 测试源码;未运行 Gradle。
|
||||
- hidden unload 改成 RasterParticle/WeatherColor renderer 的 Host-empty-source 显式 opt-in cleanup,移除对所有 hidden RasterArray layer 的全局 prepare/update。
|
||||
- timestamp 测试改用真实 epoch milliseconds,覆盖 T0、midpoint、T1、上下边界和 clear 后拒绝;WeatherColor capacity 由 Java 通过 JNI 读取 C++ `MaxWeatherColorStops`。
|
||||
|
||||
## 2026-09-07 — STEP-057B WeatherRuntime 状态语义修正
|
||||
|
||||
- 普通 field rejection 与 facade/runtime failure 分离:Core 保留有效 timeline 时,READY/WAITING 状态不被坏提交改成 ERROR;空 metadata/payload 同样只拒绝本次提交。
|
||||
- `REJECTED_SOURCE_MODE`/`UNKNOWN` 才进入 ERROR;ERROR + enable(true) 返回 REJECTED 并保持 ERROR;READY/WAITING enable 保持幂等,DISABLED enable 成功后等待新 fields。
|
||||
- 扩展 `WeatherRuntimeStateTest`,覆盖 static state transition 与 Mockito public control flow;未执行 Android Gradle、设备测试或 Jest。
|
||||
|
||||
Reference in New Issue
Block a user