| | |
| | | private final FakeIngestLanding landing = new FakeIngestLanding(); |
| | | private final TDeviceAdapterMapper adapters = Mockito.mock(TDeviceAdapterMapper.class); |
| | | private static final Instant NOW = Instant.parse("2026-09-27T09:00:00Z"); |
| | | /** |
| | | * 环境量的确认结论落在这里:厂商列名只在配置里出现一次,代码里没有任何厂商列名。 |
| | | * 22 所的列语义与量纲仍未现场确认,所以生产配置里默认没有这一项。 |
| | | */ |
| | | private static final Map<String, String> AUXILIARY_METRICS = Map.of("auxiliaryMetrics", |
| | | "[{\"vendorChannel\":26,\"channelCode\":\"S-01\",\"column\":\"Tempbox\"," |
| | | + "\"metricCode\":\"TEMPERATURE\",\"unit\":\"℃\"}]"); |
| | | |
| | | @Test |
| | | void topologyPushMakesChannelsCandidatesWithTheirOwnLatestDataTime() |
| | |
| | | assertTrue(adapter.inquire(Set.of(channel())).isEmpty()); |
| | | } |
| | | |
| | | /** 采集器辅助测量按 ADR-0064 逐来源确认;22 所环境量的列语义未确认,因此不交出测量值。 */ |
| | | /** 未声明辅助测量列就不交出测量值:确认之前不按列名推断语义,也不写零占位。 */ |
| | | @Test |
| | | void collectorAndStepCycleObservationsStayEmptyForThisStorageForm() |
| | | void collectorAndStepCycleObservationsStayEmptyWithoutDeclaredAuxiliaryMetrics() |
| | | { |
| | | ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}"); |
| | | ingest(BATCH); |
| | |
| | | assertEquals(5000L, adapter.persistenceIntervalMillis(channel())); |
| | | AdapterCapabilities capabilities = adapter.capabilities(); |
| | | assertTrue(capabilities.discovery() && capabilities.realtimeObservation() && !capabilities.control()); |
| | | } |
| | | |
| | | /** 环境量与量纲确认后落到配置:列名、指标编码、单位、采集器通道都由声明给出,代码里没有厂商列名。 */ |
| | | @Test |
| | | void declaredAuxiliaryMetricIsProjectedWithItsOwnSourceIdentityValueUnitAndDeviceTime() |
| | | { |
| | | ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}"); |
| | | ingest(""" |
| | | {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[ |
| | | {"record_time":"134349692000000000","Data_Point":420,"Voltage":1.26778,"eCurrent":0.5,"Tempbox":26.5}]} |
| | | """); |
| | | PushIngestAdapter adapter = adapter(NOW, AUXILIARY_METRICS); |
| | | |
| | | var collector = adapter.latestCollector(ADAPTER, "collector-1"); |
| | | |
| | | assertTrue(collector.online()); |
| | | var measurement = collector.measurements().get(0); |
| | | assertEquals("S-01", measurement.channelCode()); |
| | | assertEquals("TEMPERATURE", measurement.metricCode()); |
| | | assertEquals(0, new java.math.BigDecimal("26.5").compareTo(measurement.value())); |
| | | assertEquals("℃", measurement.unit()); |
| | | // 设备时间与来源身份来自这一行自己:不与检测柜记录共用时间,也不伪造配对。 |
| | | assertEquals(Instant.parse("2026-09-27T07:53:20Z"), measurement.deviceTime()); |
| | | assertEquals("134349692000000000", measurement.deviceTimeRaw()); |
| | | } |
| | | |
| | | /** 落地行里没有这一列(设备没给)就不写这一项,也不写零占位。 */ |
| | | @Test |
| | | void declaredAuxiliaryMetricAbsentFromTheRowIsNotFabricated() |
| | | { |
| | | ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}"); |
| | | ingest(BATCH); |
| | | PushIngestAdapter adapter = adapter(NOW, AUXILIARY_METRICS); |
| | | |
| | | var collector = adapter.latestCollector(ADAPTER, "collector-1"); |
| | | |
| | | assertTrue(collector.online()); |
| | | assertTrue(collector.measurements().isEmpty()); |
| | | } |
| | | |
| | | /** 来源离线(心跳超时)时不交出任何测量值:离线不伪造测量值,业务侧据此也不落采集器行。 */ |
| | | @Test |
| | | void staleHeartbeatReportsCollectorSourceOfflineWithoutMeasurements() |
| | | { |
| | | ingest("{\"kind\":\"TOPOLOGY\",\"rows\":[{\"channel\":26}]}"); |
| | | ingest(""" |
| | | {"kind":"SAMPLE","database":"DB_20260927_155209","table":"Ws26_IV_0","channel":26,"rows":[ |
| | | {"record_time":"134349692000000000","Data_Point":420,"Voltage":1.26778,"eCurrent":0.5,"Tempbox":26.5}]} |
| | | """); |
| | | PushIngestAdapter adapter = adapter(NOW.plus(Duration.ofMinutes(10)), AUXILIARY_METRICS); |
| | | |
| | | var collector = adapter.latestCollector(ADAPTER, "collector-1"); |
| | | |
| | | assertFalse(collector.online()); |
| | | assertTrue(collector.measurements().isEmpty()); |
| | | } |
| | | |
| | | @Test |
| | |
| | | |
| | | private PushIngestAdapter adapter(Instant now) |
| | | { |
| | | return new PushIngestAdapter(config(), landing, Clock.fixed(now, ZoneOffset.UTC)); |
| | | return adapter(now, Map.of("tokenEnv", "BATTERY_INGEST_TOKEN")); |
| | | } |
| | | |
| | | private static AdapterInstanceConfig config() |
| | | private PushIngestAdapter adapter(Instant now, Map<String, String> sourceOptions) |
| | | { |
| | | return new PushIngestAdapter(config(sourceOptions), landing, Clock.fixed(now, ZoneOffset.UTC)); |
| | | } |
| | | |
| | | private static AdapterInstanceConfig config(Map<String, String> sourceOptions) |
| | | { |
| | | return new AdapterInstanceConfig(ADAPTER, PushIngestAdapter.ADAPTER_TYPE, "127.0.0.1", 8080, 60000, |
| | | "Asia/Shanghai", true, AdapterCapabilities.observeOnly(), |
| | | Map.of("tokenEnv", "BATTERY_INGEST_TOKEN")); |
| | | "Asia/Shanghai", true, AdapterCapabilities.observeOnly(), sourceOptions); |
| | | } |
| | | |
| | | private void ingest(String body) |