Public Access
Updates: a newer release installs by itself, after a question #99
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace roro::release {
|
||||||
|
|
||||||
|
// Installing a release without being asked to (issue #52): once a newer one is known, wait for a
|
||||||
|
// quiet moment (nothing recording, nothing written, no key for a while), then say so on the
|
||||||
|
// screen for 30 seconds. Unless someone answers no, it is installed. Host-tested; the caller says
|
||||||
|
// what "quiet" is and does the installing.
|
||||||
|
class AutoUpdate {
|
||||||
|
public:
|
||||||
|
static constexpr uint32_t kIdleMs = 120000; // no key for this long: nobody is using the device
|
||||||
|
static constexpr uint32_t kAskMs = 30000; // how long the question stays before it answers itself
|
||||||
|
|
||||||
|
enum class Action : uint8_t {
|
||||||
|
None,
|
||||||
|
Ask, // the question goes up
|
||||||
|
Dismiss, // the question comes down, nothing is installed
|
||||||
|
Install, // the question comes down, install tag()
|
||||||
|
};
|
||||||
|
|
||||||
|
// A newer release is known. A second offer while one is waiting or asked replaces its tag.
|
||||||
|
void offer(const std::string& tag) {
|
||||||
|
tag_ = tag;
|
||||||
|
if (state_ == State::Idle) state_ = State::Waiting;
|
||||||
|
}
|
||||||
|
// The setting went off, or the release is no longer wanted.
|
||||||
|
void withdraw() { withdrawn_ = true; }
|
||||||
|
// The user's answer to the question.
|
||||||
|
void answer(bool install) {
|
||||||
|
if (state_ != State::Asking) return;
|
||||||
|
answered_ = true;
|
||||||
|
accepted_ = install;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call often. `busy`: something is recording or being written, and a restart would cut it.
|
||||||
|
// `idleMs`: how long since the last key.
|
||||||
|
Action step(uint32_t nowMs, bool busy, uint32_t idleMs) {
|
||||||
|
bool withdrawn = withdrawn_;
|
||||||
|
withdrawn_ = false;
|
||||||
|
switch (state_) {
|
||||||
|
case State::Idle: return Action::None;
|
||||||
|
case State::Waiting:
|
||||||
|
if (withdrawn) return state_ = State::Idle, Action::None;
|
||||||
|
if (busy || idleMs < kIdleMs) return Action::None;
|
||||||
|
state_ = State::Asking;
|
||||||
|
askedMs_ = nowMs;
|
||||||
|
answered_ = false;
|
||||||
|
return Action::Ask;
|
||||||
|
case State::Asking:
|
||||||
|
if (withdrawn) return state_ = State::Idle, Action::Dismiss;
|
||||||
|
if (answered_) return state_ = State::Idle, accepted_ ? Action::Install : Action::Dismiss;
|
||||||
|
// Something started meanwhile (a Track from a console): the question goes, and comes back later.
|
||||||
|
if (busy) return state_ = State::Waiting, Action::Dismiss;
|
||||||
|
if (nowMs - askedMs_ >= kAskMs) return state_ = State::Idle, Action::Install;
|
||||||
|
return Action::None;
|
||||||
|
}
|
||||||
|
return Action::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool waiting() const { return state_ == State::Waiting; }
|
||||||
|
bool asking() const { return state_ == State::Asking; }
|
||||||
|
const std::string& tag() const { return tag_; }
|
||||||
|
// Whole seconds until the question answers itself, rounded up.
|
||||||
|
int secondsLeft(uint32_t nowMs) const {
|
||||||
|
uint32_t gone = nowMs - askedMs_;
|
||||||
|
return !asking() || gone >= kAskMs ? 0 : static_cast<int>((kAskMs - gone + 999) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class State : uint8_t { Idle, Waiting, Asking };
|
||||||
|
State state_ = State::Idle;
|
||||||
|
std::string tag_;
|
||||||
|
uint32_t askedMs_ = 0;
|
||||||
|
bool answered_ = false, accepted_ = false, withdrawn_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro::release
|
||||||
@@ -24,6 +24,12 @@ inline bool shouldAnnounce(const Release& latest, const std::string& running, co
|
|||||||
return isNewer(latest, running) && latest.tag != failed && latest.tag != announced;
|
return isNewer(latest, running) && latest.tag != failed && latest.tag != announced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should the background check offer to install it by itself (issue #52)? Any newer release, each
|
||||||
|
// day it is still the newest; never a version that rolled back on this device.
|
||||||
|
inline bool shouldInstall(const Release& latest, const std::string& running, const std::string& failed) {
|
||||||
|
return isNewer(latest, running) && latest.tag != failed;
|
||||||
|
}
|
||||||
|
|
||||||
// Is `url` a download this device takes from the project's server, for this repository? Whatever
|
// Is `url` a download this device takes from the project's server, for this repository? Whatever
|
||||||
// the API says, the device only fetches from where it asked (a redirected or rewritten answer
|
// the API says, the device only fetches from where it asked (a redirected or rewritten answer
|
||||||
// can't send it elsewhere).
|
// can't send it elsewhere).
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include "auto_update.h"
|
||||||
|
#include "update_check.h"
|
||||||
|
|
||||||
|
using namespace roro::release;
|
||||||
|
using Action = AutoUpdate::Action;
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint32_t kIdle = AutoUpdate::kIdleMs, kAsk = AutoUpdate::kAskMs;
|
||||||
|
Release release(const char* tag) {
|
||||||
|
Release r;
|
||||||
|
r.tag = tag;
|
||||||
|
r.otaUrl = "https://git.twis.la/twisla/roro9stack/releases/download/x/x.ota";
|
||||||
|
r.otaSize = 1;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void test_nothing_happens_without_an_offer() {
|
||||||
|
AutoUpdate a;
|
||||||
|
TEST_ASSERT_TRUE(a.step(1000, false, kIdle) == Action::None);
|
||||||
|
TEST_ASSERT_FALSE(a.waiting());
|
||||||
|
TEST_ASSERT_FALSE(a.asking());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_it_waits_for_a_quiet_moment() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
TEST_ASSERT_TRUE(a.waiting());
|
||||||
|
TEST_ASSERT_TRUE(a.step(1000, false, kIdle - 1) == Action::None); // a key not long ago
|
||||||
|
TEST_ASSERT_TRUE(a.step(2000, true, kIdle * 10) == Action::None); // a Track is recording
|
||||||
|
TEST_ASSERT_TRUE(a.step(3000, false, kIdle) == Action::Ask);
|
||||||
|
TEST_ASSERT_TRUE(a.asking());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("v1.2.0", a.tag().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_unanswered_it_installs_after_30_seconds() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.step(5000, false, kIdle);
|
||||||
|
TEST_ASSERT_EQUAL(30, a.secondsLeft(5000));
|
||||||
|
TEST_ASSERT_EQUAL(30, a.secondsLeft(5001));
|
||||||
|
TEST_ASSERT_EQUAL(1, a.secondsLeft(5000 + kAsk - 1));
|
||||||
|
TEST_ASSERT_TRUE(a.step(5000 + kAsk - 1, false, kIdle) == Action::None);
|
||||||
|
TEST_ASSERT_TRUE(a.step(5000 + kAsk, false, kIdle) == Action::Install);
|
||||||
|
TEST_ASSERT_FALSE(a.asking());
|
||||||
|
TEST_ASSERT_TRUE(a.step(5000 + kAsk + 1, false, kIdle) == Action::None); // once
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_accept_installs_at_once() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.step(0, false, kIdle);
|
||||||
|
a.answer(true);
|
||||||
|
TEST_ASSERT_TRUE(a.step(100, false, 0) == Action::Install);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_cancel_drops_it_until_the_next_offer() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.step(0, false, kIdle);
|
||||||
|
a.answer(false);
|
||||||
|
TEST_ASSERT_TRUE(a.step(100, false, 0) == Action::Dismiss);
|
||||||
|
TEST_ASSERT_TRUE(a.step(kAsk * 2, false, kIdle * 2) == Action::None);
|
||||||
|
a.offer("v1.2.0"); // the next day's check
|
||||||
|
TEST_ASSERT_TRUE(a.step(kAsk * 3, false, kIdle) == Action::Ask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_something_starting_takes_the_question_down() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.step(0, false, kIdle);
|
||||||
|
TEST_ASSERT_TRUE(a.step(1000, true, kIdle) == Action::Dismiss);
|
||||||
|
TEST_ASSERT_TRUE(a.waiting());
|
||||||
|
TEST_ASSERT_TRUE(a.step(2000, true, kIdle) == Action::None);
|
||||||
|
TEST_ASSERT_TRUE(a.step(90000, false, kIdle) == Action::Ask); // and it comes back, with a full count
|
||||||
|
TEST_ASSERT_EQUAL(30, a.secondsLeft(90000));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_setting_going_off_withdraws_it() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.withdraw();
|
||||||
|
TEST_ASSERT_TRUE(a.step(0, false, kIdle) == Action::None);
|
||||||
|
TEST_ASSERT_FALSE(a.waiting());
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.step(0, false, kIdle);
|
||||||
|
a.withdraw();
|
||||||
|
TEST_ASSERT_TRUE(a.step(10, false, kIdle) == Action::Dismiss);
|
||||||
|
TEST_ASSERT_FALSE(a.asking());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_an_answer_is_only_for_a_question() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.answer(true); // nothing was asked yet
|
||||||
|
TEST_ASSERT_TRUE(a.step(0, false, kIdle) == Action::Ask);
|
||||||
|
TEST_ASSERT_TRUE(a.step(1, false, kIdle) == Action::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_a_newer_offer_replaces_the_tag() {
|
||||||
|
AutoUpdate a;
|
||||||
|
a.offer("v1.2.0");
|
||||||
|
a.offer("v1.3.0");
|
||||||
|
a.step(0, false, kIdle);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("v1.3.0", a.tag().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_what_is_offered() {
|
||||||
|
// Newer than what runs, and not a version that rolled back here.
|
||||||
|
TEST_ASSERT_TRUE(shouldInstall(release("v1.2.0"), "v1.1.0", ""));
|
||||||
|
TEST_ASSERT_FALSE(shouldInstall(release("v1.1.0"), "v1.1.0", ""));
|
||||||
|
TEST_ASSERT_FALSE(shouldInstall(release("v1.0.0"), "v1.1.0", ""));
|
||||||
|
TEST_ASSERT_FALSE(shouldInstall(release("v1.2.0"), "v1.1.0", "v1.2.0"));
|
||||||
|
Release noFile = release("v1.2.0");
|
||||||
|
noFile.otaUrl.clear();
|
||||||
|
TEST_ASSERT_FALSE(shouldInstall(noFile, "v1.1.0", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_nothing_happens_without_an_offer);
|
||||||
|
RUN_TEST(test_it_waits_for_a_quiet_moment);
|
||||||
|
RUN_TEST(test_unanswered_it_installs_after_30_seconds);
|
||||||
|
RUN_TEST(test_accept_installs_at_once);
|
||||||
|
RUN_TEST(test_cancel_drops_it_until_the_next_offer);
|
||||||
|
RUN_TEST(test_something_starting_takes_the_question_down);
|
||||||
|
RUN_TEST(test_the_setting_going_off_withdraws_it);
|
||||||
|
RUN_TEST(test_an_answer_is_only_for_a_question);
|
||||||
|
RUN_TEST(test_a_newer_offer_replaces_the_tag);
|
||||||
|
RUN_TEST(test_what_is_offered);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user