mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-17 06:24:19 +00:00
Rework the /mqtt_prefs load/save path so preference migrations are
crash-safe and, for the first time, unit-testable on the host.
Most of this is extraction. The multi-format migration that previously
lived inline in CommonCLI.cpp (and could only run on-device) is moved
into three dependency-free headers so it can be exercised without
Arduino, a filesystem, or the radio stack:
- MQTTPrefsStorage.h frozen layout structs for every /mqtt_prefs
format ever shipped, with static_asserts that
fail the build if any on-flash offset changes.
- MQTTPrefsCodec.h pure format classification, field-copy
migration, and plausibility validation.
- MQTTPrefsAtomicStore.h transactional writer plus the power-cut
upgrade gate, both host-testable.
New behavior, beyond the refactor:
- Atomic writes: /mqtt_prefs is written to /mqtt_prefs.tmp, verified,
then published with an atomic rename; the writer never removes the
existing file. A failed or interrupted save leaves the current
config intact.
- Power-cut ordering: LegacyUpgradeGate guarantees /mqtt_prefs is
durably committed before the legacy /com_prefs (or /node_prefs)
carrying the observer tail is compacted or removed, so an
interrupted two-file upgrade retries on the next boot without
losing settings.
- Corrupt, unsupported-version, and newer-than-known files are
preserved and the device boots on in-RAM defaults, rather than
overwriting a file this firmware cannot fully decode.
- Headerless legacy formats are validated for plausibility before
they are trusted and rewritten (raw prefs carry no checksum).
The full historical format matrix is migrated forward to the versioned
v1 layout: pre-slot (including pre-wifi-power), 3-slot (base and
token/topic tails), and headerless 6-slot (base, audience, rx, ntp).
Scope note: only /mqtt_prefs and the one-time /node_prefs -> /com_prefs
name migration use the atomic path. Ordinary /com_prefs saves remain a
direct rewrite, unchanged by this commit.
Tests: adds two host GoogleTest suites (pio test -e native).
- test_mqtt_prefs_codec: format classification, migration fixtures,
v1 header integrity, downgrade preservation.
- test_mqtt_prefs_atomic_store: transactional writes, short-write
detection, begin/finish/rename failure cleanup, original-file
preservation.
360 lines
15 KiB
C++
360 lines
15 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#define WITH_MQTT_BRIDGE 1
|
|
#include "helpers/MQTTPrefsCodec.h"
|
|
|
|
namespace Codec = MQTTPrefsCodec;
|
|
|
|
namespace {
|
|
|
|
MQTTPrefs defaults() {
|
|
MQTTPrefs prefs = {};
|
|
prefs.mqtt_status_enabled = 1;
|
|
prefs.mqtt_packets_enabled = 1;
|
|
prefs.mqtt_tx_enabled = 2;
|
|
prefs.mqtt_rx_enabled = 1;
|
|
prefs.mqtt_status_interval = 300000;
|
|
prefs.wifi_power_save = 1;
|
|
for (int i = 0; i < MQTT_PREFS_SLOT_COUNT; ++i) {
|
|
strncpy(prefs.mqtt_slot_preset[i], "none", sizeof(prefs.mqtt_slot_preset[i]) - 1);
|
|
}
|
|
strncpy(prefs.snmp_community, "public", sizeof(prefs.snmp_community) - 1);
|
|
prefs.radio_watchdog_minutes = 5;
|
|
prefs.alert_wifi_minutes = 30;
|
|
prefs.alert_mqtt_minutes = 240;
|
|
prefs.alert_min_interval_min = 60;
|
|
return prefs;
|
|
}
|
|
|
|
void writeText(std::vector<uint8_t>* bytes, size_t offset, const char* value) {
|
|
const size_t length = strlen(value);
|
|
ASSERT_LE(offset + length, bytes->size());
|
|
memcpy(bytes->data() + offset, value, length);
|
|
}
|
|
|
|
void writeLe16(std::vector<uint8_t>* bytes, size_t offset, uint16_t value) {
|
|
ASSERT_LE(offset + 2, bytes->size());
|
|
(*bytes)[offset] = static_cast<uint8_t>(value & 0xff);
|
|
(*bytes)[offset + 1] = static_cast<uint8_t>(value >> 8);
|
|
}
|
|
|
|
void writeHeader(std::vector<uint8_t>* bytes, uint16_t version, uint16_t payload_len) {
|
|
ASSERT_GE(bytes->size(), sizeof(MQTTPrefsHeader));
|
|
(*bytes)[0] = MQTT_PREFS_MAGIC[0];
|
|
(*bytes)[1] = MQTT_PREFS_MAGIC[1];
|
|
(*bytes)[2] = MQTT_PREFS_MAGIC[2];
|
|
(*bytes)[3] = MQTT_PREFS_MAGIC[3];
|
|
writeLe16(bytes, 4, version);
|
|
writeLe16(bytes, 6, payload_len);
|
|
}
|
|
|
|
Codec::DecodePlan classify(const std::vector<uint8_t>& bytes) {
|
|
const size_t prefix_size = bytes.size() < sizeof(MQTTPrefsHeader)
|
|
? bytes.size() : sizeof(MQTTPrefsHeader);
|
|
return Codec::classify(bytes.data(), prefix_size, bytes.size());
|
|
}
|
|
|
|
void fillHighEntropy(std::vector<uint8_t>* bytes) {
|
|
uint32_t state = 0x89abcdef;
|
|
for (size_t i = 0; i < bytes->size(); ++i) {
|
|
state = state * 1664525u + 1013904223u;
|
|
// Keep every byte non-NUL so this is a useful corruption fixture rather
|
|
// than a sparse/default-like legacy file.
|
|
(*bytes)[i] = static_cast<uint8_t>((state >> 24) | 0x80);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(MQTTPrefsCodec, MigratesPostWifiPowerPreSlotFixture) {
|
|
// Frozen post-WiFi-power pre-slot offsets: status=44, ssid=48, power=144,
|
|
// timezone=145, server=178, port=242. Do not derive this fixture from structs.
|
|
std::vector<uint8_t> bytes(472, 0);
|
|
writeText(&bytes, 0, "legacy-node");
|
|
writeText(&bytes, 32, "YYZ");
|
|
bytes[40] = 1;
|
|
bytes[41] = 1;
|
|
bytes[43] = 2;
|
|
bytes[144] = 2;
|
|
writeText(&bytes, 145, "UTC");
|
|
writeText(&bytes, 178, "broker.example");
|
|
writeLe16(&bytes, 242, 1883);
|
|
writeText(&bytes, 244, "alice");
|
|
writeText(&bytes, 276, "secret");
|
|
bytes[340] = 1;
|
|
|
|
const Codec::DecodePlan plan = classify(bytes);
|
|
ASSERT_EQ(Codec::Source::LegacyPreSlot, plan.source);
|
|
ASSERT_TRUE(plan.rewrite_legacy);
|
|
ASSERT_FALSE(Codec::looksLikePreWifiPower(bytes.data(), bytes.size()));
|
|
ASSERT_TRUE(Codec::isPlausibleLegacy(plan.source, bytes.data(), bytes.size()));
|
|
|
|
OldMQTTPrefs old_prefs = {};
|
|
memcpy(&old_prefs, bytes.data(), sizeof(old_prefs));
|
|
MQTTPrefs prefs = defaults();
|
|
Codec::migratePreSlot(old_prefs, &prefs);
|
|
|
|
EXPECT_STREQ("legacy-node", prefs.mqtt_origin);
|
|
EXPECT_STREQ("analyzer-us", prefs.mqtt_slot_preset[0]);
|
|
EXPECT_STREQ("custom", prefs.mqtt_slot_preset[2]);
|
|
EXPECT_STREQ("broker.example", prefs.mqtt_slot_host[2]);
|
|
EXPECT_EQ(1883, prefs.mqtt_slot_port[2]);
|
|
EXPECT_STREQ("secret", prefs.mqtt_slot_password[2]);
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, MigratesPreWifiPowerFixtureWithConservativeHeuristic) {
|
|
// In the pre-WiFi-power variant timezone starts at 144 and server at 177;
|
|
// port still aligns at 242. A non-empty timezone makes the layout unambiguous.
|
|
std::vector<uint8_t> bytes(472, 0);
|
|
writeText(&bytes, 0, "pre-power");
|
|
writeText(&bytes, 144, "PST8PDT");
|
|
writeText(&bytes, 177, "old-broker");
|
|
writeLe16(&bytes, 242, 8883);
|
|
|
|
ASSERT_TRUE(Codec::looksLikePreWifiPower(bytes.data(), bytes.size()));
|
|
ASSERT_TRUE(Codec::isPlausibleLegacy(Codec::Source::LegacyPreSlot,
|
|
bytes.data(), bytes.size()));
|
|
PreWifiPowerOldMQTTPrefs old_prefs = {};
|
|
memcpy(&old_prefs, bytes.data(), sizeof(old_prefs));
|
|
MQTTPrefs prefs = defaults();
|
|
Codec::migratePreWifiPower(old_prefs, &prefs);
|
|
|
|
EXPECT_STREQ("PST8PDT", prefs.timezone_string);
|
|
EXPECT_STREQ("old-broker", prefs.mqtt_slot_host[2]);
|
|
EXPECT_EQ(8883, prefs.mqtt_slot_port[2]);
|
|
EXPECT_EQ(1, prefs.wifi_power_save); // retained current default; old layout had none
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, DetectsPreWifiPowerFixtureWithOnlyUtcOffsetConfigured) {
|
|
std::vector<uint8_t> bytes(472, 0);
|
|
bytes[176] = static_cast<uint8_t>(-8);
|
|
|
|
EXPECT_TRUE(Codec::looksLikePreWifiPower(bytes.data(), bytes.size()));
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, MigratesThreeSlotBaseAndExtendedFixtures) {
|
|
// Frozen 3-slot offsets: presets=178, hosts=250, ports=442, users=448,
|
|
// passwords=544, owner=736, tokens=1030, topics=1174. The base file has
|
|
// two tail-padding bytes, so its frozen size is 1032.
|
|
std::vector<uint8_t> base(1032, 0);
|
|
writeText(&base, 0, "three-base");
|
|
writeText(&base, 178, "meshmapper");
|
|
writeText(&base, 250, "three.example");
|
|
writeLe16(&base, 442, 1884);
|
|
const Codec::DecodePlan base_plan = classify(base);
|
|
ASSERT_EQ(Codec::Source::LegacyThreeSlotBase, base_plan.source);
|
|
ASSERT_TRUE(Codec::isPlausibleLegacy(base_plan.source, base.data(), base.size()));
|
|
ThreeSlotBaseMQTTPrefs old_base = {};
|
|
memcpy(&old_base, base.data(), sizeof(old_base));
|
|
MQTTPrefs base_prefs = defaults();
|
|
Codec::migrateThreeSlot(old_base, &base_prefs);
|
|
EXPECT_STREQ("three-base", base_prefs.mqtt_origin);
|
|
EXPECT_STREQ("meshmapper", base_prefs.mqtt_slot_preset[0]);
|
|
EXPECT_EQ(1884, base_prefs.mqtt_slot_port[0]);
|
|
EXPECT_STREQ("none", base_prefs.mqtt_slot_preset[3]);
|
|
|
|
std::vector<uint8_t> extended(1464, 0);
|
|
writeText(&extended, 0, "three-extended");
|
|
writeText(&extended, 178 + 24, "custom");
|
|
writeText(&extended, 250 + 64, "slot-two.example");
|
|
writeText(&extended, 1030 + 48, "token-two");
|
|
writeText(&extended, 1174 + 96, "custom/{type}");
|
|
const Codec::DecodePlan extended_plan = classify(extended);
|
|
ASSERT_EQ(Codec::Source::LegacyThreeSlot, extended_plan.source);
|
|
ASSERT_TRUE(Codec::isPlausibleLegacy(extended_plan.source, extended.data(), extended.size()));
|
|
ThreeSlotMQTTPrefs old_extended = {};
|
|
memcpy(&old_extended, extended.data(), sizeof(old_extended));
|
|
MQTTPrefs extended_prefs = defaults();
|
|
Codec::migrateThreeSlot(old_extended, &extended_prefs);
|
|
EXPECT_STREQ("custom", extended_prefs.mqtt_slot_preset[1]);
|
|
EXPECT_STREQ("token-two", extended_prefs.mqtt_slot_token[1]);
|
|
EXPECT_STREQ("custom/{type}", extended_prefs.mqtt_slot_topic[1]);
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, MigratesAllLegacySixSlotPrefixesWithoutClobberingDefaults) {
|
|
// Frozen 6-slot offsets: presets=178, hosts=322, ports=706, tokens=1588,
|
|
// topics=1876, audience=2452, rx=2836, ntp=2837.
|
|
for (const size_t size : {size_t(2452), size_t(2836), size_t(2840), size_t(2904)}) {
|
|
std::vector<uint8_t> bytes(size, 0);
|
|
writeText(&bytes, 0, "six-slot");
|
|
writeText(&bytes, 178 + 5 * 24, "custom");
|
|
writeText(&bytes, 322 + 5 * 64, "six.example");
|
|
writeText(&bytes, 1588 + 5 * 48, "token-six");
|
|
writeText(&bytes, 1876 + 5 * 96, "six/{type}");
|
|
if (size >= 2836) writeText(&bytes, 2452 + 5 * 64, "audience-six");
|
|
if (size >= 2840) bytes[2836] = 0;
|
|
if (size >= 2904) writeText(&bytes, 2837, "time.example");
|
|
|
|
const Codec::DecodePlan plan = classify(bytes);
|
|
const Codec::Source expected_source = size == 2452 ? Codec::Source::LegacySixSlotBase
|
|
: size == 2836 ? Codec::Source::LegacySixSlotAudience
|
|
: size == 2840 ? Codec::Source::LegacySixSlotAudienceRx
|
|
: Codec::Source::LegacySixSlot;
|
|
EXPECT_EQ(expected_source, plan.source) << size;
|
|
ASSERT_TRUE(plan.rewrite_legacy);
|
|
ASSERT_TRUE(Codec::isPlausibleLegacy(plan.source, bytes.data(), bytes.size())) << size;
|
|
Legacy6SlotMQTTPrefs old_prefs = {};
|
|
memcpy(&old_prefs, bytes.data(), bytes.size());
|
|
MQTTPrefs prefs = defaults();
|
|
Codec::migrateLegacySixSlot(old_prefs, plan.source, &prefs);
|
|
|
|
EXPECT_STREQ("custom", prefs.mqtt_slot_preset[5]);
|
|
EXPECT_STREQ("token-six", prefs.mqtt_slot_token[5]);
|
|
if (size < 2836) {
|
|
EXPECT_EQ('\0', prefs.mqtt_slot_audience[5][0]);
|
|
} else {
|
|
EXPECT_STREQ("audience-six", prefs.mqtt_slot_audience[5]);
|
|
}
|
|
if (size < 2840) {
|
|
EXPECT_EQ(1, prefs.mqtt_rx_enabled) << size;
|
|
} else {
|
|
EXPECT_EQ(0, prefs.mqtt_rx_enabled);
|
|
}
|
|
if (size < 2904) {
|
|
EXPECT_EQ('\0', prefs.mqtt_ntp_server[0]);
|
|
} else {
|
|
EXPECT_STREQ("time.example", prefs.mqtt_ntp_server);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, CurrentVersionedPayloadRoundTripsExactly) {
|
|
MQTTPrefs source = defaults();
|
|
strncpy(source.mqtt_origin, "current-node", sizeof(source.mqtt_origin) - 1);
|
|
strncpy(source.mqtt_slot_password[2], "preserve-me", sizeof(source.mqtt_slot_password[2]) - 1);
|
|
strncpy(source.alert_region, "PNW", sizeof(source.alert_region) - 1);
|
|
std::vector<uint8_t> bytes(Codec::kEncodedSize);
|
|
ASSERT_EQ(Codec::kEncodedSize, Codec::encode(source, bytes.data(), bytes.size()));
|
|
|
|
const Codec::DecodePlan plan = classify(bytes);
|
|
ASSERT_EQ(Codec::Source::Current, plan.source);
|
|
ASSERT_FALSE(plan.preserve_file);
|
|
ASSERT_TRUE(plan.observer_fields_present);
|
|
MQTTPrefs loaded = defaults();
|
|
memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);
|
|
EXPECT_EQ(0, memcmp(&source, &loaded, sizeof(source)));
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, CompatibleShortV1PayloadPreservesDefaultsBeyondObserverBoundary) {
|
|
MQTTPrefs source = defaults();
|
|
strncpy(source.mqtt_origin, "short-v1-node", sizeof(source.mqtt_origin) - 1);
|
|
strncpy(source.mqtt_ntp_server, "ntp.short.example", sizeof(source.mqtt_ntp_server) - 1);
|
|
source.snmp_enabled = 1;
|
|
strncpy(source.snmp_community, "do-not-copy", sizeof(source.snmp_community) - 1);
|
|
source.radio_watchdog_minutes = 99;
|
|
|
|
std::vector<uint8_t> bytes(sizeof(MQTTPrefsHeader) + Codec::kV1PreObserverPayloadSize, 0);
|
|
writeHeader(&bytes, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1PreObserverPayloadSize));
|
|
memcpy(bytes.data() + sizeof(MQTTPrefsHeader), &source, Codec::kV1PreObserverPayloadSize);
|
|
|
|
const Codec::DecodePlan plan = classify(bytes);
|
|
ASSERT_EQ(Codec::Source::Current, plan.source);
|
|
ASSERT_EQ(Codec::kV1PreObserverPayloadSize, plan.payload_len);
|
|
ASSERT_FALSE(plan.preserve_file);
|
|
ASSERT_FALSE(plan.observer_fields_present);
|
|
|
|
MQTTPrefs loaded = defaults();
|
|
memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);
|
|
EXPECT_STREQ("short-v1-node", loaded.mqtt_origin);
|
|
EXPECT_STREQ("ntp.short.example", loaded.mqtt_ntp_server);
|
|
EXPECT_EQ(0, loaded.snmp_enabled);
|
|
EXPECT_STREQ("public", loaded.snmp_community);
|
|
EXPECT_EQ(5, loaded.radio_watchdog_minutes);
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, CorruptOrShortVersionedInputsArePreserved) {
|
|
Codec::DecodePlan plan = Codec::classify(nullptr, 0, 0);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> partial_magic = {MQTT_PREFS_MAGIC[0], MQTT_PREFS_MAGIC[1], MQTT_PREFS_MAGIC[2]};
|
|
plan = classify(partial_magic);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> short_payload(sizeof(MQTTPrefsHeader) + 4, 0);
|
|
writeHeader(&short_payload, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1BaselinePayloadSize - 1));
|
|
plan = classify(short_payload);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> declared_short_with_full_body(
|
|
sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize, 0);
|
|
writeHeader(&declared_short_with_full_body, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1PreObserverPayloadSize));
|
|
plan = classify(declared_short_with_full_body);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> declared_full_with_short_body(
|
|
sizeof(MQTTPrefsHeader) + Codec::kV1PreObserverPayloadSize, 0);
|
|
writeHeader(&declared_full_with_short_body, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1BaselinePayloadSize));
|
|
plan = classify(declared_full_with_short_body);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> truncated(sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize - 1, 0);
|
|
writeHeader(&truncated, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1BaselinePayloadSize));
|
|
plan = classify(truncated);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> trailing(sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize + 1, 0);
|
|
writeHeader(&trailing, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1BaselinePayloadSize));
|
|
plan = classify(trailing);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, LegacyPlausibilityRejectsHighEntropyBytesAtEveryWhitelistedSize) {
|
|
// A headerless raw struct has no checksum, so this only reduces false
|
|
// migrations; it cannot prove that a plausible-looking file is authentic.
|
|
for (const size_t size : {size_t(472), size_t(1032), size_t(1464), size_t(2452),
|
|
size_t(2836), size_t(2840), size_t(2904)}) {
|
|
std::vector<uint8_t> bytes(size, 0);
|
|
fillHighEntropy(&bytes);
|
|
const Codec::DecodePlan plan = classify(bytes);
|
|
ASSERT_TRUE(plan.rewrite_legacy) << size;
|
|
EXPECT_FALSE(Codec::isPlausibleLegacy(plan.source, bytes.data(), bytes.size())) << size;
|
|
}
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, UnsupportedHeaderlessSizesArePreserved) {
|
|
// 3024 was produced briefly before versioning, but repository history says
|
|
// that raw observer-tail form was not shipped. Preserve it rather than guess.
|
|
for (const size_t size : {size_t(471), size_t(473), size_t(1465), size_t(2905), size_t(3024)}) {
|
|
std::vector<uint8_t> bytes(size, 0);
|
|
const Codec::DecodePlan plan = classify(bytes);
|
|
EXPECT_EQ(Codec::Source::Corrupt, plan.source) << size;
|
|
EXPECT_TRUE(plan.preserve_file) << size;
|
|
}
|
|
}
|
|
|
|
TEST(MQTTPrefsCodec, NewerAndSameVersionExtendedPayloadsAreHeld) {
|
|
std::vector<uint8_t> newer(sizeof(MQTTPrefsHeader), 0);
|
|
writeHeader(&newer, MQTT_PREFS_VERSION + 1, 0);
|
|
Codec::DecodePlan plan = classify(newer);
|
|
EXPECT_EQ(Codec::Source::UnsupportedVersion, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
|
|
std::vector<uint8_t> extended(sizeof(MQTTPrefsHeader) + Codec::kV1BaselinePayloadSize + 1, 0);
|
|
writeHeader(&extended, MQTT_PREFS_VERSION,
|
|
static_cast<uint16_t>(Codec::kV1BaselinePayloadSize + 1));
|
|
plan = classify(extended);
|
|
EXPECT_EQ(Codec::Source::UnsupportedVersion, plan.source);
|
|
EXPECT_TRUE(plan.preserve_file);
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|