Fatih'sMG4 — ABRP telemetry for an MG4 EH32 on SWI69
A fork of malys/EVABRPUploader (itself a fork of Leon Kernan's ABRP_Uploader) carrying the changes that make it work on this car. History starts here: the tree is what matters, and the upstream projects are where the earlier work belongs. EVHardware is vendored rather than tracked as a submodule, so one clone builds. Telemetry. SOC, battery power and range read as unavailable on SWI69 because the vendor fallbacks were gated to SWI68 alone, while the property ids they use are the ones this car publishes — the gates now admit SWI69. Tyre pressure, the odometer and charge status come from the vendor block too, since the generic properties for all three are declared here and never publish. Tyre pressure is confirmed on the vehicle; the other two are not yet. Vehicle page. Every signal the car answers, in a table. One that it does not publish shows a dash: an unimplemented VHAL property returns zero, and "0 °C" would read as a measurement. Wi-Fi page. Joins a network the unit already has saved, so a phone hotspot does not need a trip into Settings on every drive. Tried on the upload tick and from a button, skipped while already associated. This leans on the unit being API 28 and the build being platform-signed — getConfiguredNetworks and enableNetwork are closed to ordinary apps from API 29 — and fails quietly where that does not hold. The OTA updater points at this repository. An APK from upstream is signed with a different key, which Android will not install over a platform-signed build. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* [T-912] Credentials must reach ABRP in the request body. A URL is logged by
|
||||
* proxies, CDNs and crash reporters; a body is not.
|
||||
*/
|
||||
public class AbrpApiTest {
|
||||
|
||||
@Test
|
||||
public void endpointUrlCarriesNoCredentials() {
|
||||
assertFalse(AbrpApi.API_URL.contains("?"));
|
||||
assertFalse(AbrpApi.API_URL.contains("api_key"));
|
||||
assertFalse(AbrpApi.API_URL.contains("token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointIsHttps() {
|
||||
assertTrue(AbrpApi.API_URL.startsWith("https://"));
|
||||
assertTrue(AbrpApi.VERIFY_URL.startsWith("https://"));
|
||||
}
|
||||
|
||||
/** [T-913] The Test button must not be able to write telemetry. */
|
||||
@Test
|
||||
public void credentialCheckDoesNotTargetTheTelemetryWriteEndpoint() {
|
||||
assertFalse("le test de connexion ne doit jamais viser /tlm/send",
|
||||
AbrpApi.VERIFY_URL.contains("/tlm/send"));
|
||||
assertTrue(AbrpApi.VERIFY_URL.contains("get_next_charge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyCarriesAllThreeParameters() throws Exception {
|
||||
String body = AbrpApi.formBody("KEY", "TOK", "{\"utc\":1}");
|
||||
assertTrue(body.contains("api_key=KEY"));
|
||||
assertTrue(body.contains("token=TOK"));
|
||||
assertTrue(body.contains("tlm="));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bodyIsFormEncoded() throws Exception {
|
||||
// A token with URL-significant characters must not break the body layout.
|
||||
String body = AbrpApi.formBody("a+b/c", "d&e=f", "{\"utc\":1}");
|
||||
assertEquals("api_key=a%2Bb%2Fc&token=d%26e%3Df&tlm=%7B%22utc%22%3A1%7D", body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Charge detection. The reported regression was a charging car being read as
|
||||
* "parked and not charging", which hands it to {@link UploadCadence#PARKED_INTERVAL_MS}
|
||||
* and costs ABRP the SOC curve it needs to size a charging stop.
|
||||
*/
|
||||
public class ChargingSignalTest {
|
||||
|
||||
private static final long T0 = 1_700_000_000_000L;
|
||||
|
||||
private final ChargingSignal signal = new ChargingSignal();
|
||||
|
||||
// ---- Charge port property, when the VHAL answers it ----
|
||||
|
||||
@Test
|
||||
public void aConnectedPortIsCharging() {
|
||||
assertTrue(signal.evaluate(T0, true, null, true, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnpluggedParkedCarIsNotCharging() {
|
||||
assertFalse(signal.evaluate(T0, false, null, true, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnreadablePortStaysUnknown() {
|
||||
// Unknown must not collapse to "not charging": UploadCadence treats null as active.
|
||||
assertNull(signal.evaluate(T0, null, null, true, 50));
|
||||
}
|
||||
|
||||
// ---- Fallback 1: charge rate ----
|
||||
|
||||
@Test
|
||||
public void aPositiveChargeRateOverridesAPortThatReadsUnplugged() {
|
||||
assertTrue(signal.evaluate(T0, false, 7.4f, true, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aNegativeChargeRateIsDriving() {
|
||||
assertFalse(signal.evaluate(T0, false, -14.5f, false, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noiseAroundZeroIsNotCharging() {
|
||||
assertFalse(signal.evaluate(T0, false, 0.1f, true, 50));
|
||||
}
|
||||
|
||||
// ---- Fallback 2: rising SOC on a parked car ----
|
||||
|
||||
@Test
|
||||
public void aRisingSocOnAParkedCarIsCharging() {
|
||||
// Neither charge property readable — the only evidence left is the battery filling.
|
||||
assertNull(signal.evaluate(T0, null, null, true, 50));
|
||||
assertTrue(signal.evaluate(T0 + 300_000, null, null, true, 51));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theSocRiseIsLatchedAcrossFlatTicks() {
|
||||
// SOC moves a whole percent every few minutes; without the latch is_charging
|
||||
// would flap between every pair of ticks.
|
||||
signal.evaluate(T0, null, null, true, 50);
|
||||
signal.evaluate(T0 + 300_000, null, null, true, 51);
|
||||
assertTrue(signal.evaluate(T0 + 315_000, null, null, true, 51));
|
||||
assertTrue(signal.evaluate(T0 + 1_500_000, null, null, true, 51));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theLatchExpiresSoAParkedCarGoesBackToIdle() {
|
||||
signal.evaluate(T0, null, null, true, 50);
|
||||
signal.evaluate(T0 + 300_000, null, null, true, 51);
|
||||
assertNull(signal.evaluate(T0 + 300_000 + ChargingSignal.SOC_RISE_LATCH_MS,
|
||||
null, null, true, 51));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aFallingSocDropsTheLatchImmediately() {
|
||||
signal.evaluate(T0, null, null, true, 50);
|
||||
assertTrue(signal.evaluate(T0 + 300_000, null, null, true, 51));
|
||||
assertNull(signal.evaluate(T0 + 600_000, null, null, true, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regenWhileDrivingDoesNotLatch() {
|
||||
// SOC climbing on a downhill run, then the car parks: not a charging session.
|
||||
signal.evaluate(T0, false, null, false, 50);
|
||||
signal.evaluate(T0 + 60_000, false, null, false, 52);
|
||||
assertFalse(signal.evaluate(T0 + 120_000, false, null, true, 52));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnreadableSocKeepsThePreviousReadingToCompareAgainst() {
|
||||
signal.evaluate(T0, null, null, true, 50);
|
||||
signal.evaluate(T0 + 15_000, null, null, true, null); // one failed read
|
||||
assertTrue(signal.evaluate(T0 + 30_000, null, null, true, 51));
|
||||
}
|
||||
|
||||
// ---- The regression itself ----
|
||||
|
||||
@Test
|
||||
public void aChargingCarIsNeverHandedToTheParkedCadence() {
|
||||
// Charging at home with a VHAL that does not implement the charge port property:
|
||||
// before the fix this was parked + FALSE, i.e. one upload every 15 minutes.
|
||||
ChargingSignal s = new ChargingSignal();
|
||||
UploadSettings settings = UploadSettings.defaults();
|
||||
|
||||
Boolean charging = s.evaluate(T0, false, null, true, 50);
|
||||
assertEquals(UploadCadence.PARKED_INTERVAL_MS,
|
||||
UploadCadence.intervalFor(true, charging, 50, settings));
|
||||
|
||||
charging = s.evaluate(T0 + 300_000, false, null, true, 51);
|
||||
assertEquals(settings.intervalMs(),
|
||||
UploadCadence.intervalFor(true, charging, 51, settings));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* ABRP config file parsing — pure JVM, no Android needed.
|
||||
*
|
||||
* The file exists so a user does not have to type a long api_key and token on the car's
|
||||
* on-screen keyboard, so the parser must be forgiving about whitespace, case and comments
|
||||
* while never inventing a value the file did not set.
|
||||
*/
|
||||
public class ConfigImportTest {
|
||||
|
||||
@Test
|
||||
public void parsesCredentials() {
|
||||
ConfigImport c = ConfigImport.parse("api_key = abc123\ntoken = def456\n");
|
||||
assertEquals("abc123", c.apiKey);
|
||||
assertEquals("def456", c.token);
|
||||
assertFalse(c.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresCommentsBlankLinesAndCase() {
|
||||
ConfigImport c = ConfigImport.parse(
|
||||
"# my config\n\n API_KEY = key \n# token below\nTOKEN=tok\n");
|
||||
assertEquals("key", c.apiKey);
|
||||
assertEquals("tok", c.token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentKeysStayNullSoNothingIsWiped() {
|
||||
ConfigImport c = ConfigImport.parse("token = only-token\n");
|
||||
assertNull(c.apiKey);
|
||||
assertEquals("only-token", c.token);
|
||||
assertNull(c.intervalSec);
|
||||
assertNull(c.boostLowSoc);
|
||||
assertNull(c.lowSocPercent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyOrJunkFileYieldsEmptyConfig() {
|
||||
assertTrue(ConfigImport.parse("").isEmpty());
|
||||
assertTrue(ConfigImport.parse("just some prose\nno equals here\n").isEmpty());
|
||||
assertTrue(ConfigImport.parse("# only a comment\n").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void intervalSnapsToNearestOfferedChoice() {
|
||||
// 45 is not an offered value; the spinner only has 15/30/60/120/300.
|
||||
assertEquals(Integer.valueOf(30), ConfigImport.parse("interval_sec = 45").intervalSec);
|
||||
assertEquals(Integer.valueOf(60), ConfigImport.parse("interval_sec = 59").intervalSec);
|
||||
assertEquals(Integer.valueOf(300), ConfigImport.parse("interval_sec = 9999").intervalSec);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowSocPercentIsClampedTo1To99() {
|
||||
assertEquals(Integer.valueOf(99), ConfigImport.parse("low_soc_percent = 250").lowSocPercent);
|
||||
assertEquals(Integer.valueOf(1), ConfigImport.parse("low_soc_percent = 0").lowSocPercent);
|
||||
assertEquals(Integer.valueOf(20), ConfigImport.parse("low_soc_percent = 20").lowSocPercent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boostAcceptsCommonBooleanSpellings() {
|
||||
assertEquals(Boolean.TRUE, ConfigImport.parse("boost_low_soc = on").boostLowSoc);
|
||||
assertEquals(Boolean.TRUE, ConfigImport.parse("boost_low_soc = TRUE").boostLowSoc);
|
||||
assertEquals(Boolean.FALSE, ConfigImport.parse("boost_low_soc = 0").boostLowSoc);
|
||||
assertNull(ConfigImport.parse("boost_low_soc = maybe").boostLowSoc);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyFirstEqualsSplitsSoValueMayContainEquals() {
|
||||
ConfigImport c = ConfigImport.parse("token = tok=with=equals\n");
|
||||
assertEquals("tok=with=equals", c.token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasKeysAreAccepted() {
|
||||
ConfigImport c = ConfigImport.parse("apikey = k\nuser_token = t\n");
|
||||
assertEquals("k", c.apiKey);
|
||||
assertEquals("t", c.token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* The broadcast parser, on the JVM.
|
||||
*
|
||||
* What is under test is not the vendor's payload — that shape is unverified and may well be
|
||||
* something else on the car. It is that the parser finds a temperature wherever the payload
|
||||
* puts it, refuses to invent one, and says what it was given when it fails, because the
|
||||
* diagnostic is the only thing that reaches a driver with no adb.
|
||||
*/
|
||||
public class HeadUnitWeatherTest {
|
||||
|
||||
@Test
|
||||
public void readsATemperatureAndThePlaceItIsFor() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload((
|
||||
"{\"code\":0,\"weather\":{\"temperature\":27,\"unit\":\"C\","
|
||||
+ "\"address\":\"Toulouse\",\"conditionType\":1}}"));
|
||||
assertEquals(27f, weather.temperatureC(), 0.01f);
|
||||
assertEquals("Toulouse", weather.place());
|
||||
// The diagnostic carries the reading's age: a broadcast arrives when the weather app
|
||||
// refreshes, so "where from" without "how long ago" does not say whether to trust it.
|
||||
assertEquals("Toulouse/0min", weather.diagnostic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findsTheValueWhereverThePayloadNests_it() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload((
|
||||
"{\"data\":{\"current\":[{\"tempValue\":\"18.5\",\"tempUnit\":\"C\"}]}}"));
|
||||
assertEquals(18.5f, weather.temperatureC(), 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertsAFahrenheitPayload() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload(("{\"temperature\":81,\"temperatureUnit\":\"F\"}"));
|
||||
assertEquals(27.2f, weather.temperatureC(), 0.1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnnamedUnitIsTakenAsCelsius() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload(("{\"temp\":21}"));
|
||||
assertEquals(21f, weather.temperatureC(), 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPayloadWithoutATemperatureReportsWhatItWasGiven() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload(("{\"conditionType\":3,\"address\":\"Toulouse\"}"));
|
||||
assertNull(weather.temperatureC());
|
||||
assertTrue(weather.diagnostic(), weather.diagnostic().startsWith("no temperature in {"));
|
||||
assertTrue(weather.diagnostic(), weather.diagnostic().contains("conditionType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readsThePlaceFromACityKeyToo() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload("{\"temp\":14,\"city\":\"Auzeville\"}");
|
||||
assertEquals(14f, weather.temperatureC(), 0.01f);
|
||||
assertEquals("Auzeville", weather.place());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aBroadcastWithNothingReadableIsReportedNotGuessed() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload(null);
|
||||
assertNull(weather.temperatureC());
|
||||
assertEquals("broadcast carried no JSON", weather.diagnostic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void beforeAnyBroadcastTheDiagnosticSaysSo() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
assertNull(weather.temperatureC());
|
||||
assertEquals("no broadcast yet", weather.diagnostic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedJsonDoesNotEscapeTheReceiver() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.acceptPayload(("{not json at all"));
|
||||
assertNull(weather.temperatureC());
|
||||
assertTrue(weather.diagnostic(), weather.diagnostic().contains("reading broadcast"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aRestoredReadingIsReportedWithItsAge() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.restore(19.5f, "Toulouse", System.currentTimeMillis() - 45 * 60 * 1000L);
|
||||
assertEquals(19.5f, weather.temperatureC(), 0.01f);
|
||||
assertEquals("Toulouse/45min", weather.diagnostic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anAbsentStickyBroadcastIsNotMistakenForAFailedParse() {
|
||||
HeadUnitWeather weather = new HeadUnitWeather();
|
||||
weather.accept(null);
|
||||
assertNull(weather.temperatureC());
|
||||
assertEquals("none held, none sent yet", weather.diagnostic());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* [T-913] Telemetry payload construction — pure JVM, no car and no Android needed.
|
||||
*
|
||||
* The rule under test: a property that could not be read is OMITTED. Emitting 0 instead
|
||||
* tells ABRP the car really is at 0 % / 0 km/h and corrupts the user's route plan.
|
||||
*/
|
||||
public class TelemetryPayloadTest {
|
||||
|
||||
/** Everything readable, no location. */
|
||||
private TelemetryPayload fullCar() {
|
||||
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
|
||||
t.soc = 63;
|
||||
t.speedKmh = 48.4f;
|
||||
t.rangeKm = 210;
|
||||
t.extTemp = 12f;
|
||||
t.powerKw = -14.5f;
|
||||
t.charging = false;
|
||||
t.dcfc = false;
|
||||
t.parked = false;
|
||||
t.cabinTemp = 21.5f;
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Nothing readable at all — the car link is down. */
|
||||
private String noCar() {
|
||||
return new TelemetryPayload(1_700_000_000L).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void utcIsAlwaysPresent() throws Exception {
|
||||
JSONObject json = new JSONObject(noCar());
|
||||
assertEquals(1, json.length());
|
||||
assertEquals(1_700_000_000L, json.getLong("utc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadablePropertiesAreOmittedNotZeroed() {
|
||||
String json = noCar();
|
||||
for (String field : new String[]{
|
||||
"soc", "speed", "est_battery_range", "ext_temp", "power",
|
||||
"is_charging", "is_dcfc", "is_parked", "cabin_temp",
|
||||
"batt_temp", "capacity", "odometer", "hvac_setpoint",
|
||||
"tire_pressure_fl", "tire_pressure_fr",
|
||||
"tire_pressure_rl", "tire_pressure_rr"}) {
|
||||
assertFalse("le champ " + field + " aurait dû être omis", json.contains(field));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSingleUnreadablePropertyDoesNotSuppressTheOthers() throws Exception {
|
||||
// SOC throws (null) but speed reads fine: speed must still be sent.
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.speedKmh = 48.4f;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertFalse("soc illisible aurait dû être omis", json.has("soc"));
|
||||
assertEquals(48, json.getInt("speed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aGenuineZeroIsStillSent() throws Exception {
|
||||
// 0 % battery is a real reading and must not be confused with "unreadable".
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.soc = 0;
|
||||
t.speedKmh = 0f;
|
||||
t.rangeKm = 0;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertEquals(0, json.getInt("soc"));
|
||||
assertEquals(0, json.getInt("speed"));
|
||||
assertEquals(0, json.getInt("est_battery_range"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readablePropertiesAreEmitted() throws Exception {
|
||||
JSONObject json = new JSONObject(fullCar().build());
|
||||
assertEquals(63, json.getInt("soc"));
|
||||
assertEquals(48, json.getInt("speed")); // rounded
|
||||
assertEquals(210, json.getInt("est_battery_range"));
|
||||
assertEquals(12, json.getInt("ext_temp"));
|
||||
assertEquals(-14.5, json.getDouble("power"), 0.001);
|
||||
assertEquals(0, json.getInt("is_charging"));
|
||||
assertEquals(0, json.getInt("is_dcfc"));
|
||||
assertEquals(0, json.getInt("is_parked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void powerUsesDotDecimalSeparatorRegardlessOfDefaultLocale() throws Exception {
|
||||
java.util.Locale previous = java.util.Locale.getDefault();
|
||||
try {
|
||||
java.util.Locale.setDefault(java.util.Locale.FRANCE); // comma locale
|
||||
assertTrue(fullCar().build().contains("-14.5"));
|
||||
} finally {
|
||||
java.util.Locale.setDefault(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implausibleCabinTempIsOmitted() {
|
||||
// 0.0 means the property is unsupported on this VHAL.
|
||||
TelemetryPayload zero = new TelemetryPayload(1L);
|
||||
zero.cabinTemp = 0f;
|
||||
assertFalse(zero.build().contains("cabin_temp"));
|
||||
|
||||
TelemetryPayload hot = new TelemetryPayload(1L);
|
||||
hot.cabinTemp = 120f;
|
||||
assertFalse(hot.build().contains("cabin_temp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plausibleCabinTempIsSent() throws Exception {
|
||||
assertEquals(22, new JSONObject(fullCar().build()).getInt("cabin_temp")); // 21.5 rounds up
|
||||
}
|
||||
|
||||
// ---------- Fields added for the ABRP lower-priority set ----------
|
||||
|
||||
@Test
|
||||
public void batteryTemperatureIsSentWhenPlausible() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.battTempC = 24.6f;
|
||||
assertEquals(25, new JSONObject(t.build()).getInt("batt_temp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedBatteryTemperatureIsOmitted() {
|
||||
// A VHAL without the property answers 0.0; "pack at 0 °C" changes ABRP's plan.
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.battTempC = 0f;
|
||||
assertFalse(t.build().contains("batt_temp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capacityIsSentInKwhWithTwoDecimals() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.capacityKwh = 61.734f;
|
||||
assertEquals(61.73, new JSONObject(t.build()).getDouble("capacity"), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroCapacityIsOmitted() {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.capacityKwh = 0f;
|
||||
assertFalse(t.build().contains("capacity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void odometerIsRoundedToWholeKm() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.odometerKm = 24518.7f;
|
||||
assertEquals(24519, new JSONObject(t.build()).getInt("odometer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroOdometerIsOmitted() {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.odometerKm = 0f;
|
||||
assertFalse(t.build().contains("odometer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hvacSetpointIsSentWhenSet() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.hvacSetpointC = 21f;
|
||||
assertEquals(21, new JSONObject(t.build()).getInt("hvac_setpoint"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedHvacSetpointIsOmitted() {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.hvacSetpointC = 0f;
|
||||
assertFalse(t.build().contains("hvac_setpoint"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stateOfEnergyIsSentInKwh() throws Exception {
|
||||
TelemetryPayload t = fullCar();
|
||||
t.soeKwh = 41.256f;
|
||||
assertEquals(41.26, new JSONObject(t.build()).getDouble("soe"), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedStateOfEnergyIsOmitted() {
|
||||
TelemetryPayload t = fullCar();
|
||||
t.soeKwh = 0f;
|
||||
assertFalse(t.build().contains("soe"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chargedEnergyIsSentIncludingZeroAtSessionStart() throws Exception {
|
||||
TelemetryPayload started = fullCar();
|
||||
started.kwhCharged = 0.0;
|
||||
assertEquals(0.0, new JSONObject(started.build()).getDouble("kwh_charged"), 0.001);
|
||||
|
||||
TelemetryPayload running = fullCar();
|
||||
running.kwhCharged = 12.34;
|
||||
assertEquals(12.34, new JSONObject(running.build()).getDouble("kwh_charged"), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chargedEnergyIsOmittedOutsideACharge() {
|
||||
assertFalse(fullCar().build().contains("kwh_charged"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tyrePressuresAreSentPerWheelInKpa() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.tirePressureFlKpa = 240f;
|
||||
t.tirePressureFrKpa = 241.4f;
|
||||
t.tirePressureRlKpa = 250.6f;
|
||||
t.tirePressureRrKpa = 249f;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertEquals(240, json.getInt("tire_pressure_fl"));
|
||||
assertEquals(241, json.getInt("tire_pressure_fr"));
|
||||
assertEquals(251, json.getInt("tire_pressure_rl"));
|
||||
assertEquals(249, json.getInt("tire_pressure_rr"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aWheelWithoutASensorIsOmittedWithoutHidingTheOthers() throws Exception {
|
||||
// 0 kPa is not a flat tyre, it is a missing sensor: a flat still reads well above 0.
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.tirePressureFlKpa = 0f;
|
||||
t.tirePressureFrKpa = 240f;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertFalse(json.has("tire_pressure_fl"));
|
||||
assertEquals(240, json.getInt("tire_pressure_fr"));
|
||||
}
|
||||
|
||||
// ---------- Location ----------
|
||||
|
||||
@Test
|
||||
public void locationIsIndependentOfTheCarLink() throws Exception {
|
||||
// No car data at all, but a GPS fix: ABRP should still see the vehicle online.
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.lat = 48.85;
|
||||
t.lon = 2.35;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertEquals(48.85, json.getDouble("lat"), 0.0001);
|
||||
assertEquals(2.35, json.getDouble("lon"), 0.0001);
|
||||
assertFalse(json.has("soc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingLocationOmitsAllLocationFields() {
|
||||
String json = fullCar().build();
|
||||
assertFalse(json.contains("lat"));
|
||||
assertFalse(json.contains("lon"));
|
||||
assertFalse(json.contains("elevation"));
|
||||
assertFalse(json.contains("heading"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void elevationAndHeadingAreOptional() throws Exception {
|
||||
TelemetryPayload flat = new TelemetryPayload(1L);
|
||||
flat.lat = 48.85;
|
||||
flat.lon = 2.35;
|
||||
JSONObject flatJson = new JSONObject(flat.build());
|
||||
assertFalse(flatJson.has("elevation"));
|
||||
assertFalse(flatJson.has("heading"));
|
||||
|
||||
TelemetryPayload full = new TelemetryPayload(1L);
|
||||
full.lat = 48.85;
|
||||
full.lon = 2.35;
|
||||
full.elevation = 35.4;
|
||||
full.heading = 271.6f;
|
||||
JSONObject fullJson = new JSONObject(full.build());
|
||||
assertEquals(35, fullJson.getInt("elevation"));
|
||||
assertEquals(272, fullJson.getInt("heading"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void booleanFlagsAreEmittedAsIntegers() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1L);
|
||||
t.soc = 50;
|
||||
t.powerKw = 62f;
|
||||
t.charging = true;
|
||||
t.dcfc = true;
|
||||
t.parked = true;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertEquals(1, json.getInt("is_charging"));
|
||||
assertEquals(1, json.getInt("is_dcfc"));
|
||||
assertEquals(1, json.getInt("is_parked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thePayloadIsValidJson() throws Exception {
|
||||
// The hand-built string could not guarantee this; JSONObject can.
|
||||
new JSONObject(fullCar().build());
|
||||
new JSONObject(noCar());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unimplementedOutsideTempIsOmittedNotSentAsZero() {
|
||||
// A VHAL without ENV_OUTSIDE_TEMPERATURE answers 0.0. Sent as ext_temp:0 it is a
|
||||
// freezing day as far as ABRP's consumption model is concerned.
|
||||
TelemetryPayload t = fullCar();
|
||||
t.extTemp = 0f;
|
||||
assertFalse(t.build().contains("ext_temp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void impossibleOutsideTempIsOmitted() {
|
||||
TelemetryPayload t = fullCar();
|
||||
t.extTemp = -273f;
|
||||
assertFalse(t.build().contains("ext_temp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void summaryListsSentValuesAndOmittedFields() {
|
||||
String summary = TelemetryPayload.summarize(fullCar().build());
|
||||
assertTrue(summary, summary.contains("soc=63"));
|
||||
assertTrue(summary, summary.contains("ext_temp=12"));
|
||||
assertTrue(summary, summary.contains("cabin_temp=22"));
|
||||
// Nothing gave us a position, so lat/lon must show up as omitted, not as 0.
|
||||
assertFalse(summary, summary.contains("lat="));
|
||||
assertTrue(summary, summary.contains("| omitted: "));
|
||||
assertTrue(summary, summary.substring(summary.indexOf("| omitted: ")).contains("lat"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void summaryOfAnEmptyPayloadReportsEverythingOmitted() {
|
||||
String summary = TelemetryPayload.summarize(noCar());
|
||||
assertTrue(summary, summary.startsWith("utc=1700000000"));
|
||||
assertTrue(summary, summary.contains("| omitted: soc,speed,"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Cadence policy. The head unit is awake for as long as the car is on, so every avoidable
|
||||
* upload and every avoidable GPS fix costs power — but nothing may be lost while driving,
|
||||
* charging, or running low on battery.
|
||||
*/
|
||||
public class UploadCadenceTest {
|
||||
|
||||
private static final long T0 = 1_700_000_000_000L;
|
||||
|
||||
private final UploadSettings defaults = UploadSettings.defaults();
|
||||
|
||||
private UploadSettings settings(int intervalSec, boolean boost, int lowSoc) {
|
||||
return new UploadSettings(intervalSec, boost, lowSoc);
|
||||
}
|
||||
|
||||
// ---- Basic cadence ----
|
||||
|
||||
@Test
|
||||
public void theFirstUploadIsImmediate() {
|
||||
assertTrue(UploadCadence.shouldUpload(T0, 0L, true, false, 80, false, defaults));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drivingUsesTheConfiguredInterval() {
|
||||
UploadSettings s = settings(60, false, 20);
|
||||
assertFalse(UploadCadence.shouldUpload(T0 + 59_000, T0, false, false, 80, false, s));
|
||||
assertTrue(UploadCadence.shouldUpload(T0 + 60_000, T0, false, false, 80, false, s));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theDefaultIsOneMinute() {
|
||||
assertEquals(60, UploadSettings.DEFAULT_INTERVAL_SEC);
|
||||
assertEquals(60_000L, UploadCadence.intervalFor(false, false, 80, defaults));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chargingKeepsTheActiveIntervalEvenWhenParked() {
|
||||
// Charging at a stop is exactly when ABRP needs a tight SOC curve.
|
||||
assertEquals(defaults.intervalMs(),
|
||||
UploadCadence.intervalFor(true, true, 50, defaults));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parkedAndIdleBacksOffToFifteenMinutes() {
|
||||
assertEquals(900_000L, UploadCadence.intervalFor(true, false, 50, defaults));
|
||||
assertFalse(UploadCadence.shouldUpload(T0 + 899_000, T0, true, false, 50, false, defaults));
|
||||
assertTrue(UploadCadence.shouldUpload(T0 + 900_000, T0, true, false, 50, false, defaults));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aStateChangeUploadsImmediately() {
|
||||
// Just parked, plugged in or unplugged: ABRP must not wait for the slow interval.
|
||||
assertTrue(UploadCadence.shouldUpload(T0 + 1_000, T0, true, false, 50, true, defaults));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownStateCountsAsActive() {
|
||||
// An unreadable gear or charge port does not prove the car is idle.
|
||||
assertEquals(defaults.intervalMs(), UploadCadence.intervalFor(null, null, 80, defaults));
|
||||
assertEquals(defaults.intervalMs(), UploadCadence.intervalFor(true, null, 80, defaults));
|
||||
assertEquals(defaults.intervalMs(), UploadCadence.intervalFor(null, false, 80, defaults));
|
||||
}
|
||||
|
||||
// ---- Low-SOC boost ----
|
||||
|
||||
@Test
|
||||
public void lowSocBoostsTheCadence() {
|
||||
UploadSettings s = settings(300, true, 20);
|
||||
assertEquals(15_000L, UploadCadence.intervalFor(false, false, 19, s));
|
||||
assertEquals(15_000L, UploadCadence.intervalFor(false, false, 20, s)); // inclusive
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aboveTheThresholdThereIsNoBoost() {
|
||||
UploadSettings s = settings(300, true, 20);
|
||||
assertEquals(300_000L, UploadCadence.intervalFor(false, false, 21, s));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theThresholdIsConfigurable() {
|
||||
UploadSettings s = settings(300, true, 35);
|
||||
assertEquals(15_000L, UploadCadence.intervalFor(false, false, 30, s));
|
||||
assertEquals(300_000L, UploadCadence.intervalFor(false, false, 40, s));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theBoostCanBeDisabled() {
|
||||
UploadSettings s = settings(300, false, 20);
|
||||
assertEquals(300_000L, UploadCadence.intervalFor(false, false, 5, s));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theBoostNeverSlowsAFasterConfiguredInterval() {
|
||||
// Configured 15 s with the boost on must not become slower at low SOC.
|
||||
UploadSettings s = settings(15, true, 20);
|
||||
assertEquals(15_000L, UploadCadence.intervalFor(false, false, 10, s));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnreadableSocDoesNotBoost() {
|
||||
// No reading is not a low reading; boosting on unknown would poll fast forever.
|
||||
UploadSettings s = settings(300, true, 20);
|
||||
assertEquals(300_000L, UploadCadence.intervalFor(false, false, null, s));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aParkedCarDoesNotBoostEvenAtLowSoc() {
|
||||
// Parked at 5 % and unplugged: still parked, still nothing to report.
|
||||
UploadSettings s = settings(60, true, 20);
|
||||
assertEquals(900_000L, UploadCadence.intervalFor(true, false, 5, s));
|
||||
}
|
||||
|
||||
// ---- Measured effect ----
|
||||
|
||||
@Test
|
||||
public void requestCountOverAnHourMatchesThePolicy() {
|
||||
// Counts exclude the baseline first upload, hence one short of the round number.
|
||||
assertEquals("parked and idle: one upload every 15 min",
|
||||
3, countUploadsOverAnHour(true, false, 50, defaults));
|
||||
assertEquals("driving at the default 60 s",
|
||||
59, countUploadsOverAnHour(false, false, 50, defaults));
|
||||
assertEquals("driving at low SOC: boosted to 15 s",
|
||||
239, countUploadsOverAnHour(false, false, 10, defaults));
|
||||
|
||||
// The point of the exercise: the old fixed 15 s cadence sent 239 uploads an hour
|
||||
// in every state, including parked overnight.
|
||||
assertTrue(countUploadsOverAnHour(true, false, 50, defaults)
|
||||
< countUploadsOverAnHour(false, false, 50, defaults));
|
||||
}
|
||||
|
||||
/** Simulates one hour of 15 s scheduler ticks with no state change. */
|
||||
private long countUploadsOverAnHour(Boolean parked, Boolean charging,
|
||||
Integer soc, UploadSettings settings) {
|
||||
long uploads = 0;
|
||||
long lastUpload = 0L;
|
||||
for (long tick = 0; tick < 3_600_000L; tick += 15_000L) {
|
||||
long now = T0 + tick;
|
||||
if (lastUpload == 0L) { // ignore the immediate first upload
|
||||
lastUpload = now;
|
||||
continue;
|
||||
}
|
||||
if (UploadCadence.shouldUpload(now, lastUpload, parked, charging, soc,
|
||||
false, settings)) {
|
||||
uploads++;
|
||||
lastUpload = now;
|
||||
}
|
||||
}
|
||||
return uploads;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Upload history and the service state derived from it. A single failed upload is normal
|
||||
* in a car (tunnel, dead spot); three in a row means something is actually wrong.
|
||||
*/
|
||||
public class UploadLogTest {
|
||||
|
||||
private UploadLog log;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
log = new UploadLog();
|
||||
}
|
||||
|
||||
private UploadLog.Entry ok() {
|
||||
return new UploadLog.Entry(1L, 200, true, "OK");
|
||||
}
|
||||
|
||||
private UploadLog.Entry fail(int status, String detail) {
|
||||
return new UploadLog.Entry(1L, status, false, detail);
|
||||
}
|
||||
|
||||
// ---- Ring buffer ----
|
||||
|
||||
@Test
|
||||
public void keepsOnlyTheLastTwentyCalls() {
|
||||
for (int i = 0; i < 25; i++) {
|
||||
log.record(new UploadLog.Entry(i, 200, true, "call" + i));
|
||||
}
|
||||
List<UploadLog.Entry> recent = log.recent();
|
||||
assertEquals(UploadLog.MAX_ENTRIES, recent.size());
|
||||
// Most recent first.
|
||||
assertEquals("call24", recent.get(0).detail);
|
||||
assertEquals("call5", recent.get(recent.size() - 1).detail);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anEmptyLogHasNoLatestEntry() {
|
||||
assertNull(log.latest());
|
||||
assertTrue(log.recent().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recordsTheHttpStatusAndDetail() {
|
||||
log.record(fail(401, "HTTP 401"));
|
||||
UploadLog.Entry latest = log.latest();
|
||||
assertEquals(401, latest.httpStatus);
|
||||
assertEquals("HTTP 401", latest.detail);
|
||||
assertEquals(false, latest.success);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aRequestThatNeverReachedAServerHasNoStatus() {
|
||||
// Status 0 is how the UI knows to show "---" rather than a bogus code.
|
||||
log.record(fail(0, "No internet"));
|
||||
assertEquals(0, log.latest().httpStatus);
|
||||
}
|
||||
|
||||
// ---- Error state ----
|
||||
|
||||
@Test
|
||||
public void stoppedWhenTheServiceIsNotRunning() {
|
||||
log.record(ok());
|
||||
assertEquals(UploadLog.State.STOPPED, log.state(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startingWhileRunningWithNoAttemptYet() {
|
||||
assertEquals(UploadLog.State.STARTING, log.state(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runningAfterASuccess() {
|
||||
log.record(ok());
|
||||
assertEquals(UploadLog.State.RUNNING, log.state(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oneOrTwoFailuresAreNotAnError() {
|
||||
// A tunnel or a dead spot must not alarm the driver.
|
||||
log.record(fail(0, "No internet"));
|
||||
assertEquals(UploadLog.State.RUNNING, log.state(true));
|
||||
log.record(fail(0, "No internet"));
|
||||
assertEquals(UploadLog.State.RUNNING, log.state(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void threeConsecutiveFailuresAreAnError() {
|
||||
for (int i = 0; i < 3; i++) log.record(fail(500, "HTTP 500"));
|
||||
assertEquals(UploadLog.State.ERROR, log.state(true));
|
||||
assertEquals(3, log.consecutiveFailures());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSuccessClearsTheErrorState() {
|
||||
for (int i = 0; i < 5; i++) log.record(fail(500, "HTTP 500"));
|
||||
assertEquals(UploadLog.State.ERROR, log.state(true));
|
||||
|
||||
log.record(ok());
|
||||
assertEquals(UploadLog.State.RUNNING, log.state(true));
|
||||
assertEquals(0, log.consecutiveFailures());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failuresMustBeConsecutiveToCount() {
|
||||
log.record(fail(500, "a"));
|
||||
log.record(fail(500, "b"));
|
||||
log.record(ok());
|
||||
log.record(fail(500, "c"));
|
||||
// 2 failures, then a success, then 1: not three in a row.
|
||||
assertEquals(1, log.consecutiveFailures());
|
||||
assertEquals(UploadLog.State.RUNNING, log.state(true));
|
||||
}
|
||||
|
||||
// ---- Threading ----
|
||||
|
||||
@Test
|
||||
public void concurrentWritesAndReadsStayConsistent() throws Exception {
|
||||
// Written from the scheduler thread, read from the UI thread.
|
||||
int threads = 6;
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(threads);
|
||||
for (int t = 0; t < threads; t++) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
for (int i = 0; i < 200; i++) {
|
||||
log.record(ok());
|
||||
log.recent();
|
||||
log.state(true);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
} finally {
|
||||
done.countDown();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
start.countDown();
|
||||
assertTrue(done.await(30, TimeUnit.SECONDS));
|
||||
assertEquals(UploadLog.MAX_ENTRIES, log.recent().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryCarriesThePayloadSummaryAndDefaultsItToNull() {
|
||||
UploadLog log = new UploadLog();
|
||||
log.record(new UploadLog.Entry(1L, 200, true, "OK", "soc=63 | omitted: lat,lon"));
|
||||
log.record(new UploadLog.Entry(2L, 0, false, "No internet"));
|
||||
java.util.List<UploadLog.Entry> recent = log.recent();
|
||||
assertNull(recent.get(0).summary);
|
||||
assertEquals("soc=63 | omitted: lat,lon", recent.get(1).summary);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user