mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
[host] Fix handling of directory for preferences (#11160)
This commit is contained in:
@@ -10,8 +10,8 @@ class HostPreferenceBackend final {
|
||||
public:
|
||||
explicit HostPreferenceBackend(uint32_t key) : key_(key) {}
|
||||
|
||||
bool save(const uint8_t *data, size_t len);
|
||||
bool load(uint8_t *data, size_t len);
|
||||
bool save(const uint8_t *data, size_t len) const;
|
||||
bool load(uint8_t *data, size_t len) const;
|
||||
|
||||
protected:
|
||||
uint32_t key_{};
|
||||
|
||||
@@ -14,21 +14,31 @@ static const char *const TAG = "preferences";
|
||||
void HostPreferences::setup_() {
|
||||
if (this->setup_complete_)
|
||||
return;
|
||||
const char *home = getenv("HOME");
|
||||
if (home == nullptr) {
|
||||
ESP_LOGE(TAG, "HOME environment variable is not set");
|
||||
abort();
|
||||
const char *prefdir = getenv("ESPHOME_PREFDIR");
|
||||
std::string pref_path;
|
||||
if (prefdir != nullptr) {
|
||||
pref_path = prefdir;
|
||||
} else {
|
||||
const char *home = getenv("HOME");
|
||||
if (home == nullptr) {
|
||||
ESP_LOGE(TAG, "ESPHOME_PREFDIR and HOME environment variables not set, unable to save preferences");
|
||||
return;
|
||||
}
|
||||
pref_path = std::string(home) + "/.esphome/prefs";
|
||||
}
|
||||
this->filename_.append(home);
|
||||
this->filename_.append("/.esphome");
|
||||
this->filename_.append("/prefs");
|
||||
fs::create_directories(this->filename_);
|
||||
std::error_code ec;
|
||||
fs::create_directories(pref_path, ec);
|
||||
if (ec) {
|
||||
ESP_LOGE(TAG, "Failed to create preferences directory: %s (%s)", pref_path.c_str(), ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
this->filename_ = pref_path;
|
||||
this->filename_.append("/");
|
||||
this->filename_.append(App.get_name());
|
||||
this->filename_.append(".prefs");
|
||||
FILE *fp = fopen(this->filename_.c_str(), "rb");
|
||||
if (fp != nullptr) {
|
||||
while (!feof((fp))) {
|
||||
while (!feof(fp)) {
|
||||
uint32_t key;
|
||||
uint8_t len;
|
||||
if (fread(&key, sizeof(key), 1, fp) != 1)
|
||||
@@ -39,7 +49,7 @@ void HostPreferences::setup_() {
|
||||
if (fread(data, sizeof(uint8_t), len, fp) != len)
|
||||
break;
|
||||
std::vector vec(data, data + len);
|
||||
this->data[key] = vec;
|
||||
this->data_[key] = vec;
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
@@ -48,29 +58,33 @@ void HostPreferences::setup_() {
|
||||
|
||||
bool HostPreferences::sync() {
|
||||
this->setup_();
|
||||
if (this->filename_.empty()) {
|
||||
ESP_LOGE(TAG, "Preferences filename not set, unable to save preferences");
|
||||
return false;
|
||||
}
|
||||
FILE *fp = fopen(this->filename_.c_str(), "wb");
|
||||
if (fp == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to open preferences file for writing: %s", this->filename_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = this->data.begin(); it != this->data.end(); ++it) {
|
||||
fwrite(&it->first, sizeof(uint32_t), 1, fp);
|
||||
uint8_t len = it->second.size();
|
||||
for (auto &it : this->data_) {
|
||||
fwrite(&it.first, sizeof(uint32_t), 1, fp);
|
||||
uint8_t len = it.second.size();
|
||||
fwrite(&len, sizeof(len), 1, fp);
|
||||
fwrite(it->second.data(), sizeof(uint8_t), it->second.size(), fp);
|
||||
fwrite(it.second.data(), sizeof(uint8_t), it.second.size(), fp);
|
||||
}
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HostPreferences::reset() {
|
||||
host_preferences->data.clear();
|
||||
host_preferences->data_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
|
||||
auto backend = new HostPreferenceBackend(type);
|
||||
auto *backend = new HostPreferenceBackend(type);
|
||||
return ESPPreferenceObject(backend);
|
||||
};
|
||||
|
||||
@@ -83,11 +97,13 @@ void setup_preferences() {
|
||||
global_preferences = &s_preferences;
|
||||
}
|
||||
|
||||
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) const {
|
||||
return host_preferences->save(this->key_, data, len);
|
||||
}
|
||||
|
||||
bool HostPreferenceBackend::load(uint8_t *data, size_t len) { return host_preferences->load(this->key_, data, len); }
|
||||
bool HostPreferenceBackend::load(uint8_t *data, size_t len) const {
|
||||
return host_preferences->load(this->key_, data, len);
|
||||
}
|
||||
|
||||
HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class HostPreferences final : public PreferencesMixin<HostPreferences> {
|
||||
return false;
|
||||
this->setup_();
|
||||
std::vector vec(data, data + len);
|
||||
this->data[key] = vec;
|
||||
this->data_[key] = vec;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ class HostPreferences final : public PreferencesMixin<HostPreferences> {
|
||||
if (len > 255)
|
||||
return false;
|
||||
this->setup_();
|
||||
auto it = this->data.find(key);
|
||||
if (it == this->data.end())
|
||||
auto it = this->data_.find(key);
|
||||
if (it == this->data_.end())
|
||||
return false;
|
||||
const auto &vec = it->second;
|
||||
if (vec.size() != len)
|
||||
@@ -45,7 +45,7 @@ class HostPreferences final : public PreferencesMixin<HostPreferences> {
|
||||
void setup_();
|
||||
bool setup_complete_{};
|
||||
std::string filename_{};
|
||||
std::map<uint32_t, std::vector<uint8_t>> data{};
|
||||
std::map<uint32_t, std::vector<uint8_t>> data_{};
|
||||
};
|
||||
|
||||
void setup_preferences();
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#ifdef USE_HOST
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include "esphome/components/host/preferences.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::host::testing {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
/// RAII helper to save and restore an environment variable.
|
||||
class ScopedEnvVar {
|
||||
public:
|
||||
explicit ScopedEnvVar(const char *name) : name_(name) {
|
||||
const char *val = getenv(name);
|
||||
if (val != nullptr) {
|
||||
saved_value_ = val;
|
||||
was_set_ = true;
|
||||
}
|
||||
}
|
||||
~ScopedEnvVar() {
|
||||
if (this->was_set_) {
|
||||
setenv(this->name_.c_str(), this->saved_value_.c_str(), 1);
|
||||
} else {
|
||||
unsetenv(this->name_.c_str());
|
||||
}
|
||||
}
|
||||
ScopedEnvVar(const ScopedEnvVar &) = delete;
|
||||
ScopedEnvVar &operator=(const ScopedEnvVar &) = delete;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string saved_value_;
|
||||
bool was_set_{false};
|
||||
};
|
||||
|
||||
class HostPreferencesTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a unique temp directory for this test
|
||||
this->temp_dir_ = fs::temp_directory_path() / "esphome_prefs_test";
|
||||
fs::create_directories(this->temp_dir_);
|
||||
|
||||
// Set up App name — string literal has static storage so StringRef is safe
|
||||
App.pre_setup("test_prefs", 10, "", 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
std::error_code ec;
|
||||
fs::remove_all(this->temp_dir_, ec);
|
||||
}
|
||||
|
||||
fs::path temp_dir_;
|
||||
};
|
||||
|
||||
TEST_F(HostPreferencesTest, BothVarsUnset_SyncReturnsFalse) {
|
||||
ScopedEnvVar home_guard("HOME");
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
unsetenv("HOME");
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
|
||||
HostPreferences prefs;
|
||||
EXPECT_FALSE(prefs.sync());
|
||||
}
|
||||
|
||||
TEST_F(HostPreferencesTest, BothVarsUnset_SaveSucceedsInMemory) {
|
||||
ScopedEnvVar home_guard("HOME");
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
unsetenv("HOME");
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
|
||||
HostPreferences prefs;
|
||||
uint32_t value = 42;
|
||||
// save() stores in memory even without a valid file path
|
||||
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
|
||||
|
||||
// But sync to disk should fail
|
||||
EXPECT_FALSE(prefs.sync());
|
||||
}
|
||||
|
||||
TEST_F(HostPreferencesTest, PrefDirSet_SaveAndSync) {
|
||||
ScopedEnvVar home_guard("HOME");
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
|
||||
auto prefdir = this->temp_dir_ / "prefdir";
|
||||
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
|
||||
unsetenv("HOME");
|
||||
|
||||
HostPreferences prefs;
|
||||
uint32_t value = 42;
|
||||
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
|
||||
EXPECT_TRUE(prefs.sync());
|
||||
|
||||
// Verify file was created in ESPHOME_PREFDIR
|
||||
auto expected_file = prefdir / "test_prefs.prefs";
|
||||
EXPECT_TRUE(fs::exists(expected_file));
|
||||
}
|
||||
|
||||
TEST_F(HostPreferencesTest, HomeSet_SaveAndSync) {
|
||||
ScopedEnvVar home_guard("HOME");
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
|
||||
auto home = this->temp_dir_ / "home";
|
||||
setenv("HOME", home.c_str(), 1);
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
|
||||
HostPreferences prefs;
|
||||
uint32_t value = 42;
|
||||
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
|
||||
EXPECT_TRUE(prefs.sync());
|
||||
|
||||
// Verify file was created in HOME/.esphome/prefs
|
||||
auto expected_file = home / ".esphome" / "prefs" / "test_prefs.prefs";
|
||||
EXPECT_TRUE(fs::exists(expected_file));
|
||||
}
|
||||
|
||||
TEST_F(HostPreferencesTest, PrefDirTakesPrecedenceOverHome) {
|
||||
ScopedEnvVar home_guard("HOME");
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
|
||||
auto prefdir = this->temp_dir_ / "prefdir";
|
||||
auto home = this->temp_dir_ / "home";
|
||||
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
|
||||
setenv("HOME", home.c_str(), 1);
|
||||
|
||||
HostPreferences prefs;
|
||||
uint32_t value = 42;
|
||||
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
|
||||
EXPECT_TRUE(prefs.sync());
|
||||
|
||||
// File should be in ESPHOME_PREFDIR, not HOME
|
||||
auto prefdir_file = prefdir / "test_prefs.prefs";
|
||||
auto home_file = home / ".esphome" / "prefs" / "test_prefs.prefs";
|
||||
EXPECT_TRUE(fs::exists(prefdir_file));
|
||||
EXPECT_FALSE(fs::exists(home_file));
|
||||
}
|
||||
|
||||
TEST_F(HostPreferencesTest, SaveAndLoadRoundTrip) {
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
|
||||
auto prefdir = this->temp_dir_ / "roundtrip";
|
||||
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
|
||||
|
||||
// Save data with one instance
|
||||
{
|
||||
HostPreferences prefs;
|
||||
uint32_t value = 0xDEADBEEF;
|
||||
EXPECT_TRUE(prefs.save(0xABCD, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
|
||||
EXPECT_TRUE(prefs.sync());
|
||||
}
|
||||
|
||||
// Load with a fresh instance (reads from file)
|
||||
{
|
||||
HostPreferences prefs;
|
||||
uint32_t loaded = 0;
|
||||
EXPECT_TRUE(prefs.load(0xABCD, reinterpret_cast<uint8_t *>(&loaded), sizeof(loaded)));
|
||||
EXPECT_EQ(loaded, 0xDEADBEEFu);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(HostPreferencesTest, LoadNonExistentKeyReturnsFalse) {
|
||||
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
|
||||
|
||||
auto prefdir = this->temp_dir_ / "nokey";
|
||||
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
|
||||
|
||||
HostPreferences prefs;
|
||||
uint32_t loaded = 0;
|
||||
EXPECT_FALSE(prefs.load(0x9999, reinterpret_cast<uint8_t *>(&loaded), sizeof(loaded)));
|
||||
}
|
||||
|
||||
} // namespace esphome::host::testing
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user