mirror of
https://github.com/logos-co/logos-chat-ui.git
synced 2026-08-29 18:21:13 +00:00
* feat: hold the run's failures and hand over the logs beside them A failure lived on a toast for four seconds and then left with the only copy of its message, so two in quick succession meant the first went unread. The account of the run it belonged to was no easier to reach: the log files sit in the session directory under names only the host assigns, and nothing in the app named them. Failures are retained instead, newest first, with consecutive repeats collapsed into one row and a count. The strip across the foot holds the newest and counts the ones behind it, and opening the dialog marks them seen, which quiets the strip without emptying the list. Each precondition guard names the action it refused rather than sharing one context-free sentence between call sites, and a failure the module gave no reason for says that much, because an empty reason is what a call that never reached the module leaves behind. The logs are listed one writer at a time, because each keeps its own wherever the platform gave it somewhere to write: the chat module in the instance directory it was assigned, and this view in that same directory, borrowed because a view module is assigned none of its own. Runs are grouped by the stem of the file their writer announced, so neither list picks up the other's files, and nothing reads what is in them. The fix for the borrowed folder is an instance path on LogosUiPluginContext. chat_module moves to a rev that installs a `tracing` subscriber, without which the chat core's account of the run is empty: libchat logs exclusively through `tracing`, and an event raised in a process with no subscriber is dropped before it reaches stderr. That rev also names the file it opens through get_log_path(), which is what lets this list the chat core's runs at all. * feat: notice when the chat module is gone The view found out that the module had died only by trying to use it, twenty seconds later, when a call it had already made ran out its own timeout. Left alone it never found out: the status bar went on saying Online, the composer went on accepting messages, and the failure that mattered arrived as "the chat module gave no reason". Ask instead. A probe every ten seconds calls health(), whose answer is whether the call arrived, and two unanswered in a row put the app in Error: the composer closes, the account card stops claiming a connection, and the failure names the module and points at its log, which is where the panic and its backtrace are. One unanswered probe is not enough, because a loaded machine can miss one and being wrong about this is worse than being slow. The probe passes a two-second timeout and stops once it has concluded. Acquiring the remote object blocks the caller for as long as it is given, so the default twenty would freeze the UI on every tick for the rest of the run, which is a worse fault than the one being fixed.
120 lines
4.0 KiB
C++
120 lines
4.0 KiB
C++
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QTemporaryDir>
|
|
#include <QTest>
|
|
|
|
#include "RunLog.h"
|
|
#include "SessionLogFiles.h"
|
|
|
|
class TestRunLog : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void namesItsFileAfterItsStem();
|
|
void refusesADirectoryItCannotMake();
|
|
void movesAFullFileAsideAndKeepsTheAnnouncedPath();
|
|
void prunesToTheNewestRunsAndLeavesAnotherWritersAlone();
|
|
|
|
private:
|
|
static void writeLines(RunLog& log, int count);
|
|
static QStringList namesIn(const QString& directory);
|
|
// A file of a run that is over, so pruning has something to count.
|
|
static void placeRun(const QString& directory, const QString& stem, const QString& stamp);
|
|
};
|
|
|
|
void TestRunLog::writeLines(RunLog& log, int count)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
log.write(QStringLiteral("WARNING: default: something happened"));
|
|
}
|
|
|
|
QStringList TestRunLog::namesIn(const QString& directory)
|
|
{
|
|
QStringList names = QDir(directory).entryList(QDir::Files, QDir::Name);
|
|
names.sort();
|
|
return names;
|
|
}
|
|
|
|
void TestRunLog::placeRun(const QString& directory, const QString& stem, const QString& stamp)
|
|
{
|
|
QFile file(QDir(directory).filePath(QStringLiteral("%1_%2.log").arg(stem, stamp)));
|
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
|
file.write("x");
|
|
}
|
|
|
|
void TestRunLog::namesItsFileAfterItsStem()
|
|
{
|
|
QTemporaryDir dir;
|
|
RunLog log;
|
|
|
|
QVERIFY(log.open(dir.path(), QStringLiteral("chat_ui")));
|
|
|
|
QVERIFY(log.isOpen());
|
|
const QString name = QFileInfo(log.path()).fileName();
|
|
QVERIFY2(name.startsWith(QStringLiteral("chat_ui_")), qPrintable(name));
|
|
QVERIFY2(name.endsWith(QStringLiteral(".log")), qPrintable(name));
|
|
// The naming a reader groups a directory's runs by, so a run it opened is a
|
|
// run the dialog can list.
|
|
QCOMPARE(listSessionLogRuns(log.path()).size(), 1);
|
|
}
|
|
|
|
void TestRunLog::refusesADirectoryItCannotMake()
|
|
{
|
|
RunLog log;
|
|
|
|
QVERIFY(!log.open(QString(), QStringLiteral("chat_ui")));
|
|
|
|
QVERIFY(!log.isOpen());
|
|
QVERIFY(log.path().isEmpty());
|
|
// Writing anyway is a no-op rather than a crash: the caller reports the
|
|
// failure and carries on.
|
|
log.write(QStringLiteral("a line with nowhere to go"));
|
|
}
|
|
|
|
void TestRunLog::movesAFullFileAsideAndKeepsTheAnnouncedPath()
|
|
{
|
|
QTemporaryDir dir;
|
|
RunLog log;
|
|
QVERIFY(log.open(dir.path(), QStringLiteral("chat_ui")));
|
|
const QString announced = log.path();
|
|
|
|
writeLines(log, RunLog::kRotateAfterLines);
|
|
|
|
QCOMPARE(log.path(), announced);
|
|
const QStringList names = namesIn(dir.path());
|
|
QCOMPARE(names.size(), 2);
|
|
QVERIFY2(names.at(0).endsWith(QStringLiteral(".001.log")), qPrintable(names.at(0)));
|
|
QCOMPARE(QFileInfo(announced).size(), 0);
|
|
// Both files are the one run, which is what makes a rotation invisible to a
|
|
// reader who only wanted this launch.
|
|
const QList<SessionLogRun> runs = listSessionLogRuns(announced);
|
|
QCOMPARE(runs.size(), 1);
|
|
QCOMPARE(runs.first().paths.size(), 2);
|
|
}
|
|
|
|
void TestRunLog::prunesToTheNewestRunsAndLeavesAnotherWritersAlone()
|
|
{
|
|
QTemporaryDir dir;
|
|
for (int day = 1; day <= RunLog::kKeepRuns + 2; ++day)
|
|
placeRun(dir.path(), QStringLiteral("chat_ui"), QStringLiteral("202001%1_120000").arg(day, 2, 10, QLatin1Char('0')));
|
|
// The chat module's own log, in the directory this writer borrows.
|
|
placeRun(dir.path(), QStringLiteral("chat_module"), QStringLiteral("20200101_120000"));
|
|
|
|
RunLog log;
|
|
QVERIFY(log.open(dir.path(), QStringLiteral("chat_ui")));
|
|
|
|
const QStringList names = namesIn(dir.path());
|
|
// The runs kept are this writer's newest, the run just opened included, and
|
|
// the other writer's file is not this one's to delete.
|
|
QCOMPARE(names.size(), RunLog::kKeepRuns + 1);
|
|
QVERIFY(names.contains(QStringLiteral("chat_module_20200101_120000.log")));
|
|
QVERIFY(!names.contains(QStringLiteral("chat_ui_20200101_120000.log")));
|
|
QVERIFY(!names.contains(QStringLiteral("chat_ui_20200102_120000.log")));
|
|
QVERIFY(names.contains(QStringLiteral("chat_ui_20200112_120000.log")));
|
|
}
|
|
|
|
QTEST_MAIN(TestRunLog)
|
|
#include "tst_runlog.moc"
|