#include "lib/Snapshot.hpp" #include "common/Literals.hpp" #include #include #include #include #include namespace { using namespace chatterino::literals; bool compareJson(const QJsonValue &expected, const QJsonValue &got, const QString &context) { if (expected == got) { return true; } if (expected.type() != got.type()) { qWarning() << context << "- mismatching type - expected:" << expected.type() << "got:" << got.type(); return false; } switch (expected.type()) { case QJsonValue::Array: { auto expArr = expected.toArray(); auto gotArr = got.toArray(); if (expArr.size() != gotArr.size()) { qWarning() << context << "- Mismatching array size - expected:" << expArr.size() << "got:" << gotArr.size(); return false; } for (QJsonArray::size_type i = 0; i < expArr.size(); i++) { if (!compareJson(expArr[i], gotArr[i], context % '[' % QString::number(i) % ']')) { return false; } } } break; // unreachable case QJsonValue::Object: { auto expObj = expected.toObject(); auto gotObj = got.toObject(); if (expObj.size() != gotObj.size()) { qWarning() << context << "- Mismatching object size - expected:" << expObj.size() << "got:" << gotObj.size(); return false; } for (auto it = expObj.constBegin(); it != expObj.constEnd(); it++) { if (!gotObj.contains(it.key())) { qWarning() << context << "- Object doesn't contain key" << it.key(); return false; } if (!compareJson(it.value(), gotObj[it.key()], context % '.' % it.key())) { return false; } } } break; case QJsonValue::Null: case QJsonValue::Bool: case QJsonValue::Double: case QJsonValue::String: case QJsonValue::Undefined: break; } qWarning() << context << "- expected:" << expected << "got:" << got; return false; } void mergeJson(QJsonObject &base, const QJsonObject &additional) { for (auto it = additional.begin(); it != additional.end(); it++) { auto ref = base[it.key()]; if (ref.isArray()) { // there's no way of pushing to the array without detaching first auto arr = ref.toArray(); if (!it->isArray()) { throw std::runtime_error("Mismatched types"); } // append all additional values auto addArr = it->toArray(); for (auto v : addArr) { arr.append(v); } ref = arr; continue; } if (ref.isObject()) { // same here, detach first and overwrite auto obj = ref.toObject(); if (!it->isObject()) { throw std::runtime_error("Mismatched types"); } mergeJson(obj, it->toObject()); ref = obj; continue; } ref = it.value(); // overwrite for simple types/non-existent keys } } QDir baseDir(const QString &category) { QDir snapshotDir(QStringLiteral(__FILE__)); snapshotDir.cd("../../../snapshots/"); snapshotDir.cd(category); return snapshotDir; } QString filePath(const QString &category, const QString &name) { return baseDir(category).filePath(name); } } // namespace namespace chatterino::testlib { std::unique_ptr Snapshot::read(QString category, QString name) { if (!name.endsWith(u".json")) { name.append(u".json"); } QFile file(filePath(category, name)); if (!file.open(QFile::ReadOnly)) { throw std::runtime_error("Failed to open file"); } auto content = file.readAll(); file.close(); const auto doc = QJsonDocument::fromJson(content).object(); return std::unique_ptr( new Snapshot(std::move(category), std::move(name), doc)); } QStringList Snapshot::discover(const QString &category) { auto files = baseDir(category).entryList(QDir::NoDotAndDotDot | QDir::Files); for (auto &file : files) { file.remove(".json"); } return files; } QStringList Snapshot::discoverNested(const QString &category) { auto directories = baseDir(category).entryList(QDir::NoDotAndDotDot | QDir::Dirs); QStringList all; for (const auto &dir : directories) { auto d = baseDir(category); d.cd(dir); auto files = d.entryList(QDir::NoDotAndDotDot | QDir::Files); for (const auto &file : files) { QStringView view(file); if (view.endsWith(u".json")) { view = view.sliced(0, view.size() - 5); } all.append(dir % u'/' % view); } } return all; } bool Snapshot::run(const QJsonValue &got, bool updateSnapshots) const { if (updateSnapshots) { this->write(got); return true; } return compareJson(this->output_, got, QStringLiteral("output")); } Snapshot::Snapshot(QString category, QString name, const QJsonObject &root) : category_(std::move(category)) , name_(std::move(name)) , input_(root["input"_L1]) , params_(root["params"_L1].toObject()) , settings_(root["settings"_L1].toObject()) , output_(root["output"_L1]) { } void Snapshot::write(const QJsonValue &got) const { QFile file(filePath(this->category_, this->name_)); if (!file.open(QFile::WriteOnly)) { throw std::runtime_error("Failed to open file"); } QJsonObject obj{ {"input"_L1, this->input_}, {"output"_L1, got}, }; if (!this->params_.isEmpty()) { obj.insert("params"_L1, this->params_); } if (!this->settings_.isEmpty()) { obj.insert("settings"_L1, this->settings_); } file.write(QJsonDocument{obj}.toJson()); file.close(); } QByteArray Snapshot::mergedSettings(const QByteArray &base) const { auto baseDoc = QJsonDocument::fromJson(base); if (!baseDoc.isObject()) { throw std::runtime_error("Invalid base settings"); } auto baseObj = baseDoc.object(); mergeJson(baseObj, this->settings_); baseDoc.setObject(baseObj); return baseDoc.toJson(QJsonDocument::Compact); } } // namespace chatterino::testlib