feat(javaTest): publish historical simulation evidence
This commit is contained in:
@@ -60,7 +60,7 @@ mvn -Dtest=HistoricalQueueDatabaseReplayTest test
|
||||
生成真实历史离线回放证据:
|
||||
|
||||
```bash
|
||||
mvn exec:java \
|
||||
mvn compile exec:java \
|
||||
-Dexec.mainClass=com.ft546.wcs.shadow.history.HistoricalReplayApplication \
|
||||
-Dexec.args="outputs/wcs-shadow-history-replay-local"
|
||||
```
|
||||
@@ -71,6 +71,27 @@ mvn exec:java \
|
||||
> 这些结果是“真实历史队列离线回放”,不是现场 SHADOW 验收。历史队列表没有保存
|
||||
> 当时每一轮 PLC 快照、资源账本或 LEGACY 决策,因此不能把最终完成状态当作算法正确率。
|
||||
|
||||
## 运行真实历史负载全链路调度仿真
|
||||
|
||||
240 条历史记录按 `create_date` 生成到达事件,默认按 `100x` 加速。调度中心重新选车和
|
||||
规划路径,经过 SHADOW 交通裁决、模拟 PLC/ACK、逐节点移动、资源释放、完成归档和
|
||||
幂等 WMS 回调。历史 `agv_id/path_info/finish_date/完成状态` 仅用于对照。
|
||||
|
||||
```bash
|
||||
mvn compile exec:java \
|
||||
-Dexec.mainClass=com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationApplication \
|
||||
-Dexec.args="outputs/wcs-history-driven-dispatch-simulation-local 100"
|
||||
```
|
||||
|
||||
第二个参数只允许 `10` 或 `100`。输出包括 `summary.json`、`task-results.csv`、
|
||||
`tick-events.csv`、`invariant-results.json` 和 `manifest.sha256`,回答完成/等待/失败、
|
||||
每车完成数、车辆和路径差异、资源安全、平均/P95/P99、HOLD/重试原因、历史耗时差异、
|
||||
归档和 WMS 幂等情况。
|
||||
|
||||
> 这是“真实历史负载驱动纯内存仿真”,不是现场 SHADOW、HIL 或 ACTIVE 验收。
|
||||
> 历史表不含独立的仿真起始时刻车辆位置/状态快照,因此每辆车以其最早历史任务的
|
||||
> 起点初始化;选车距离、冲突与吞吐指标用于算法仿真对比,不能解释为现场轨迹复现。
|
||||
|
||||
## 运行控制台模拟
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public final class HistoricalDispatchSimulationApplication {
|
||||
private HistoricalDispatchSimulationApplication() { }
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length < 1 || args.length > 2) {
|
||||
throw new IllegalArgumentException(
|
||||
"usage: HistoricalDispatchSimulationApplication <outputDir> [10|100]");
|
||||
}
|
||||
int acceleration = args.length == 2 ? Integer.parseInt(args[1]) : 100;
|
||||
SimulationConfig defaults = SimulationConfig.defaults();
|
||||
SimulationConfig config = new SimulationConfig(acceleration,
|
||||
defaults.getAgingIntervalMillis(), defaults.getRetryLimit(),
|
||||
defaults.getNoProgressLimit(), defaults.getTickLimit());
|
||||
ClassLoader classLoader = HistoricalDispatchSimulationApplication.class.getClassLoader();
|
||||
HistoricalQueueLoader loader = new HistoricalQueueLoader();
|
||||
HistoricalQueueDataset dataset = classLoader.getResource("shadow/history/manifest.json") != null
|
||||
? loader.load(classLoader, "shadow/history")
|
||||
: loader.load(Paths.get("src/test/resources/shadow/history"));
|
||||
HistoricalSimulationInput input = new HistoricalSimulationInputFactory().create(dataset);
|
||||
DeterministicFaultPlan.Builder faults = DeterministicFaultPlan.builder();
|
||||
for (int i = 0; i < input.getTasks().size(); i += 37) {
|
||||
faults.plcFailure(input.getTasks().get(i).getSimulationTaskId(), 1,
|
||||
SimulatedPlcGateway.PlcReason.SEND_FAILURE);
|
||||
}
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
input, config, faults.build());
|
||||
Path output = Paths.get(args[0]);
|
||||
new HistoricalDispatchSimulationReportWriter().write(output, dataset, result);
|
||||
System.out.println("HISTORY-DISPATCH-SIMULATION input=" + result.getTaskResults().size()
|
||||
+ " completed=" + result.getCompletedCount()
|
||||
+ " waiting=" + result.getWaitingCount()
|
||||
+ " failed=" + result.getFailedCount()
|
||||
+ " deadlocks=" + result.getInvariants().getDeadlocks()
|
||||
+ " output=" + output.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class HistoricalDispatchSimulationReportWriter {
|
||||
private static final DateTimeFormatter TIME =
|
||||
DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
|
||||
public void write(Path outputDir, HistoricalQueueDataset dataset,
|
||||
HistoricalDispatchSimulationResult result) throws IOException {
|
||||
if (outputDir == null || dataset == null || result == null) {
|
||||
throw new IllegalArgumentException("output, dataset and result are required");
|
||||
}
|
||||
Files.createDirectories(outputDir);
|
||||
MAPPER.writeValue(outputDir.resolve("summary.json").toFile(), summary(dataset, result));
|
||||
writeTaskCsv(outputDir.resolve("task-results.csv"), result);
|
||||
writeEventCsv(outputDir.resolve("tick-events.csv"), result);
|
||||
MAPPER.writeValue(outputDir.resolve("invariant-results.json").toFile(),
|
||||
invariantSummary(result));
|
||||
writeManifest(outputDir, Arrays.asList("invariant-results.json", "summary.json",
|
||||
"task-results.csv", "tick-events.csv"));
|
||||
}
|
||||
|
||||
private Map<String, Object> summary(HistoricalQueueDataset dataset,
|
||||
HistoricalDispatchSimulationResult result) {
|
||||
Map<String, Object> value = new LinkedHashMap<String, Object>();
|
||||
value.put("mode", "REAL_HISTORY_DRIVEN_DISPATCH_SIMULATION");
|
||||
value.put("boundary", "真实历史负载驱动纯内存仿真,不是现场 SHADOW/HIL/ACTIVE 验收");
|
||||
value.put("database", dataset.getDatabase());
|
||||
value.put("sourceTable", dataset.getSourceTable());
|
||||
value.put("sourceRowCount", dataset.getFullScanTotalRows());
|
||||
value.put("inputCount", result.getTaskResults().size());
|
||||
value.put("acceleration", result.getAcceleration());
|
||||
value.put("completedCount", result.getCompletedCount());
|
||||
value.put("waitingCount", result.getWaitingCount());
|
||||
value.put("failedCount", result.getFailedCount());
|
||||
value.put("completedByVehicle", result.completedByVehicle());
|
||||
value.put("reassignmentCount", result.getReassignmentCount());
|
||||
value.put("replannedPathDifferenceCount", result.getReplannedCount());
|
||||
value.put("tickCount", result.getTickCount());
|
||||
value.put("waitingTime", timeStats(waitingSamples(result)));
|
||||
value.put("completionTime", timeStats(completionSamples(result)));
|
||||
value.put("historicalDuration", timeStats(historicalSamples(result)));
|
||||
value.put("simulationMinusHistoricalDuration",
|
||||
timeStats(durationDifferenceSamples(result)));
|
||||
value.put("holdReasons", aggregateReasons(result, true));
|
||||
value.put("retryReasons", aggregateReasons(result, false));
|
||||
value.put("failureReasons", failureReasons(result));
|
||||
value.put("archiveCount", result.getArchiveCount());
|
||||
value.put("wmsDeliveryAttempts", result.getWmsDeliveryAttempts());
|
||||
value.put("wmsBusinessEffects", result.getWmsBusinessEffects());
|
||||
value.put("archiveComplete", result.getArchiveCount() == result.getCompletedCount());
|
||||
value.put("wmsIdempotent", result.getWmsBusinessEffects() == result.getCompletedCount()
|
||||
&& result.getWmsDeliveryAttempts() == result.getCompletedCount() * 2);
|
||||
value.put("invariants", invariantSummary(result));
|
||||
return value;
|
||||
}
|
||||
|
||||
private Map<String, Object> timeStats(List<Long> samples) {
|
||||
SimulationMetrics metrics = new SimulationMetrics();
|
||||
Map<String, Object> stats = new LinkedHashMap<String, Object>();
|
||||
stats.put("sampleCount", samples.size());
|
||||
stats.put("averageMillis", metrics.average(samples));
|
||||
stats.put("p95Millis", metrics.percentile(samples, 0.95));
|
||||
stats.put("p99Millis", metrics.percentile(samples, 0.99));
|
||||
return stats;
|
||||
}
|
||||
|
||||
private List<Long> waitingSamples(HistoricalDispatchSimulationResult result) {
|
||||
List<Long> values = new ArrayList<Long>();
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
if (task.getWaitingMillis() != null) values.add(task.getWaitingMillis());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private List<Long> completionSamples(HistoricalDispatchSimulationResult result) {
|
||||
List<Long> values = new ArrayList<Long>();
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
if (task.getCompletionMillis() != null) values.add(task.getCompletionMillis());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private List<Long> historicalSamples(HistoricalDispatchSimulationResult result) {
|
||||
List<Long> values = new ArrayList<Long>();
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
Long duration = historicalDuration(task);
|
||||
if (duration != null) values.add(duration);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private List<Long> durationDifferenceSamples(HistoricalDispatchSimulationResult result) {
|
||||
List<Long> values = new ArrayList<Long>();
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
Long historical = historicalDuration(task);
|
||||
Long simulated = task.getCompletionMillis();
|
||||
if (historical != null && simulated != null) {
|
||||
values.add(simulated * result.getAcceleration() - historical);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private Map<String, Integer> aggregateReasons(HistoricalDispatchSimulationResult result,
|
||||
boolean hold) {
|
||||
Map<String, Integer> values = new LinkedHashMap<String, Integer>();
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
Map<String, Integer> source = hold ? task.getHoldReasons() : task.getRetryReasons();
|
||||
for (Map.Entry<String, Integer> entry : source.entrySet()) {
|
||||
values.put(entry.getKey(), values.containsKey(entry.getKey())
|
||||
? values.get(entry.getKey()) + entry.getValue() : entry.getValue());
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private Map<String, Integer> failureReasons(HistoricalDispatchSimulationResult result) {
|
||||
Map<String, Integer> values = new LinkedHashMap<String, Integer>();
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
if (task.getStatus() != SimulationTaskStatus.FAILED) continue;
|
||||
String reason = task.getReason();
|
||||
values.put(reason, values.containsKey(reason) ? values.get(reason) + 1 : 1);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private Map<String, Object> invariantSummary(HistoricalDispatchSimulationResult result) {
|
||||
SimulationInvariantSummary source = result.getInvariants();
|
||||
Map<String, Object> values = new LinkedHashMap<String, Object>();
|
||||
values.put("capacityViolations", source.getCapacityViolations());
|
||||
values.put("nodeDoubleOccupancyViolations", source.getNodeDoubleOccupancyViolations());
|
||||
values.put("oppositeEdgeViolations", source.getOppositeEdgeViolations());
|
||||
values.put("duplicateArchiveAttempts", source.getDuplicateArchiveAttempts());
|
||||
values.put("duplicateWmsBusinessEffects", source.getDuplicateWmsBusinessEffects());
|
||||
values.put("deadlocks", source.getDeadlocks());
|
||||
values.put("totalSafetyViolations", source.totalSafetyViolations());
|
||||
return values;
|
||||
}
|
||||
|
||||
private void writeTaskCsv(Path path, HistoricalDispatchSimulationResult result)
|
||||
throws IOException {
|
||||
StringBuilder csv = new StringBuilder("\ufeff");
|
||||
row(csv, "simulation_task_id", "task_no", "guid", "task_type", "priority",
|
||||
"original_start_site", "original_end_site", "historical_agv", "simulated_agv",
|
||||
"historical_path", "planned_path", "assigned_vehicles", "status", "reason",
|
||||
"arrival_ms", "first_assigned_ms", "completed_ms", "waiting_ms",
|
||||
"historical_duration_ms", "simulation_duration_ms", "scaled_source_duration_ms",
|
||||
"duration_difference_ms", "hold_count", "protocol_attempts", "retry_count",
|
||||
"hold_reasons", "retry_reasons", "vehicle_changed", "path_changed");
|
||||
for (SimulationTaskResult task : result.getTaskResults()) {
|
||||
Long historical = historicalDuration(task);
|
||||
Long simulated = task.getCompletionMillis();
|
||||
Long scaled = simulated == null ? null : simulated * result.getAcceleration();
|
||||
Long difference = historical == null || scaled == null ? null : scaled - historical;
|
||||
String historicalPath = trimTrailingCommas(text(task.getRecord().getPathInfo()));
|
||||
String plannedPath = String.join(",", task.getPlannedPath());
|
||||
row(csv, task.getSimulationTaskId(), task.getRecord().getTaskNo(),
|
||||
task.getRecord().getGuid(), task.getRecord().getTaskType(),
|
||||
task.getRecord().getPriority(), task.getRecord().getStartSite(),
|
||||
task.getRecord().getEndSite(), task.getRecord().getAgvId(),
|
||||
task.getSimulatedVehicleId(), historicalPath, plannedPath,
|
||||
String.join("|", task.getAssignedVehicles()), task.getStatus(), task.getReason(),
|
||||
task.getArrivalMillis(), task.getFirstAssignedMillis(), task.getCompletedMillis(),
|
||||
task.getWaitingMillis(), historical, simulated, scaled, difference,
|
||||
task.getHoldCount(), task.getProtocolAttempts(), task.getRetryCount(),
|
||||
task.getHoldReasons(), task.getRetryReasons(),
|
||||
task.getSimulatedVehicleId() != null
|
||||
&& !task.getSimulatedVehicleId().equals(task.getRecord().getAgvId()),
|
||||
!historicalPath.equals(plannedPath));
|
||||
}
|
||||
Files.write(path, csv.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void writeEventCsv(Path path, HistoricalDispatchSimulationResult result)
|
||||
throws IOException {
|
||||
StringBuilder csv = new StringBuilder("\ufeff");
|
||||
row(csv, "tick", "simulation_ms", "event_type", "task_id", "vehicle_id",
|
||||
"reason", "from_site", "to_site");
|
||||
for (SimulationTickEvent event : result.getEvents()) {
|
||||
row(csv, event.getTick(), event.getSimulationMillis(), event.getType(),
|
||||
event.getTaskId(), event.getVehicleId(), event.getReason(),
|
||||
event.getFromSite(), event.getToSite());
|
||||
}
|
||||
Files.write(path, csv.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void row(StringBuilder target, Object... values) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (i > 0) target.append(',');
|
||||
target.append(csv(values[i]));
|
||||
}
|
||||
target.append("\r\n");
|
||||
}
|
||||
|
||||
private String csv(Object value) {
|
||||
if (value == null) return "";
|
||||
String text = String.valueOf(value);
|
||||
if (text.contains(",") || text.contains("\"") || text.contains("\r") || text.contains("\n")) {
|
||||
return "\"" + text.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private Long historicalDuration(SimulationTaskResult task) {
|
||||
try {
|
||||
LocalDateTime created = LocalDateTime.parse(task.getRecord().getCreateDate(), TIME);
|
||||
LocalDateTime finished = LocalDateTime.parse(task.getRecord().getFinishDate(), TIME);
|
||||
return Duration.between(created, finished).toMillis();
|
||||
} catch (DateTimeParseException | NullPointerException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeManifest(Path outputDir, List<String> names) throws IOException {
|
||||
List<String> ordered = new ArrayList<String>(names);
|
||||
Collections.sort(ordered);
|
||||
StringBuilder manifest = new StringBuilder();
|
||||
for (String name : ordered) {
|
||||
manifest.append(sha256(Files.readAllBytes(outputDir.resolve(name))))
|
||||
.append(" ").append(name).append('\n');
|
||||
}
|
||||
Files.write(outputDir.resolve("manifest.sha256"),
|
||||
manifest.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String sha256(byte[] bytes) {
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(bytes);
|
||||
StringBuilder value = new StringBuilder();
|
||||
for (byte item : digest) value.append(String.format("%02x", item & 0xff));
|
||||
return value.toString();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String trimTrailingCommas(String value) {
|
||||
int end = value.length();
|
||||
while (end > 0 && value.charAt(end - 1) == ',') end--;
|
||||
return value.substring(0, end);
|
||||
}
|
||||
|
||||
private String text(String value) { return value == null ? "" : value; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class SimulationMetrics {
|
||||
public Double average(List<Long> samples) {
|
||||
validate(samples);
|
||||
if (samples.isEmpty()) return null;
|
||||
double total = 0.0;
|
||||
for (Long sample : samples) total += sample;
|
||||
return total / samples.size();
|
||||
}
|
||||
|
||||
public Long percentile(List<Long> samples, double percentile) {
|
||||
validate(samples);
|
||||
if (percentile <= 0.0 || percentile > 1.0) {
|
||||
throw new IllegalArgumentException("percentile must be in (0, 1]");
|
||||
}
|
||||
if (samples.isEmpty()) return null;
|
||||
List<Long> ordered = new ArrayList<Long>(samples);
|
||||
Collections.sort(ordered);
|
||||
int index = (int) Math.ceil(percentile * ordered.size()) - 1;
|
||||
return ordered.get(index);
|
||||
}
|
||||
|
||||
private void validate(List<Long> samples) {
|
||||
if (samples == null || samples.contains(null)) {
|
||||
throw new IllegalArgumentException("non-null samples are required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.SEND_FAILURE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HistoricalDispatchSimulationReportWriterTest {
|
||||
@TempDir Path output;
|
||||
|
||||
@Test
|
||||
void writesAuditableMetricsTaskEventsInvariantsAndHashes() throws Exception {
|
||||
HistoricalQueueDataset dataset = new HistoricalQueueLoader().load(
|
||||
getClass().getClassLoader(), "shadow/history");
|
||||
HistoricalSimulationInput input = new HistoricalSimulationInputFactory().create(dataset);
|
||||
DeterministicFaultPlan.Builder faults = DeterministicFaultPlan.builder();
|
||||
for (int i = 0; i < input.getTasks().size(); i += 37) {
|
||||
faults.plcFailure(input.getTasks().get(i).getSimulationTaskId(), 1, SEND_FAILURE);
|
||||
}
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
input, SimulationConfig.defaults(), faults.build());
|
||||
|
||||
new HistoricalDispatchSimulationReportWriter().write(output, dataset, result);
|
||||
|
||||
for (String name : new String[]{"summary.json", "task-results.csv",
|
||||
"tick-events.csv", "invariant-results.json", "manifest.sha256"}) {
|
||||
assertTrue(Files.isRegularFile(output.resolve(name)), name);
|
||||
}
|
||||
JsonNode summary = new ObjectMapper().readTree(output.resolve("summary.json").toFile());
|
||||
assertEquals("REAL_HISTORY_DRIVEN_DISPATCH_SIMULATION", summary.path("mode").asText());
|
||||
assertEquals(240, summary.path("inputCount").asInt());
|
||||
assertEquals(240, summary.path("completedCount").asInt()
|
||||
+ summary.path("waitingCount").asInt() + summary.path("failedCount").asInt());
|
||||
assertEquals(result.getArchiveCount(), summary.path("archiveCount").asInt());
|
||||
assertEquals(result.getWmsBusinessEffects(), summary.path("wmsBusinessEffects").asInt());
|
||||
assertTrue(summary.has("waitingTime"));
|
||||
assertTrue(summary.has("completionTime"));
|
||||
assertTrue(summary.has("historicalDuration"));
|
||||
assertTrue(summary.has("holdReasons"));
|
||||
assertTrue(summary.has("retryReasons"));
|
||||
|
||||
String taskCsv = new String(Files.readAllBytes(output.resolve("task-results.csv")),
|
||||
StandardCharsets.UTF_8);
|
||||
assertTrue(taskCsv.startsWith("\ufeffsimulation_task_id,task_no,guid"));
|
||||
assertTrue(taskCsv.contains("historical_agv,simulated_agv"));
|
||||
assertTrue(taskCsv.contains("historical_path,planned_path"));
|
||||
assertTrue(taskCsv.contains("historical_duration_ms,simulation_duration_ms"));
|
||||
assertTrue(taskCsv.contains("hold_reasons,retry_reasons"));
|
||||
List<String> manifest = Files.readAllLines(output.resolve("manifest.sha256"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertEquals(4, manifest.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class SimulationMetricsTest {
|
||||
@Test
|
||||
void calculatesAverageAndNearestRankPercentilesFromLiteralSamples() {
|
||||
SimulationMetrics metrics = new SimulationMetrics();
|
||||
|
||||
assertEquals(3.0, metrics.average(Arrays.asList(1L, 3L, 5L)));
|
||||
assertEquals(5L, metrics.percentile(Arrays.asList(1L, 3L, 5L), 0.95));
|
||||
assertEquals(5L, metrics.percentile(Arrays.asList(1L, 3L, 5L), 0.99));
|
||||
assertEquals(3L, metrics.percentile(Arrays.asList(1L, 3L, 5L), 0.50));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNullForEmptySamplesInsteadOfInventingZero() {
|
||||
SimulationMetrics metrics = new SimulationMetrics();
|
||||
|
||||
assertNull(metrics.average(Collections.emptyList()));
|
||||
assertNull(metrics.percentile(Collections.emptyList(), 0.95));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user