// SPDX-License-Identifier: GPL-2.0-or-later // Lazy folios (QET_LAZY_FOLIOS), // linked against the application's objects: a folio shown in a project // opened without building its folios draws and links as in the project // opened as usual, building only it and the folios its links and tables // reach, and the project saves the same. #include #include #include #include #include #include #include #include #include #include #include #include #include "../../sources/diagram.h" #include "../../sources/qetproject.h" #include "../../sources/qetresult.h" #include "../../sources/qetmessagebox.h" #include "../../sources/qetgraphicsitem/element.h" #include "../../sources/autoNum/elementautonumschemecommand.h" #include "../../sources/elementprovider.h" #include "../../sources/container/qetzip.h" #include #include #include "../../sources/TerminalStrip/terminalstrip.h" #include "../../sources/ui/elementpropertieswidget.h" #include "../../sources/conductorautonumerotation.h" #include "../../sources/conductornumexport.h" #include "../../sources/wiringlistexport.h" #include "../../sources/ui/terminalnumberingdialog.h" #include "../../sources/diagramcontent.h" #include "../../sources/diagramview.h" #include "../../sources/diagramevent/diagrameventaddpaste.h" #include #include "../../sources/qetgraphicsitem/conductor.h" #include "../../sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "../../sources/qetgraphicsitem/terminal.h" #include "../../sources/undocommand/changeelementinformationcommand.h" #include "../../sources/undocommand/changetitleblockcommand.h" #include "../../sources/undocommand/deleteqgraphicsitemcommand.h" #include "../../sources/factory/elementfactory.h" #include "../../sources/undocommand/addgraphicsobjectcommand.h" #include "../../sources/dataBase/projectdatabase.h" #include static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); } //Every symbol's numbering, in a stable order static QStringList numbering(const QETProject &project) { QStringList list; for (const auto &symbol : project.symbolNumbering()) list << QStringList{symbol.label, symbol.formula, symbol.scheme.toString(), symbol.sequence.unit.join(';'), symbol.sequence.ten.join(';'), symbol.sequence.hundred.join(';'), QString::number(symbol.numbered)}.join('|'); list.sort(); return list; } //What the scheme @p title tells of the numbers its symbols carry static QString gaps(const QETProject &project, const QString &title) { QStringList list; for (const auto &gap : ElementAutoNumSchemeCommand::gapRanges(&project, title)) list << QString("%1-%2").arg(gap.from).arg(gap.to); return list.join(','); } //A symbol of @p location placed on folio @p i as the folio's editor //places one (DiagramEventAddElement::addElement()), numbered with the //scheme @p title: its label, and the numbers then free for it static QString place(QETProject &project, int i, const QString &location, const QString &title) { Diagram *folio = project.folios().at(i); project.buildFolioToShow(folio); project.setCurrrentElementAutonum(title); int state = 0; Element *element = ElementFactory::Instance()->createElement( ElementsLocation(location, &project), nullptr, &state); check(!state, "symbol made"); element->setPos(QPointF(-500, -500)); folio->addItem(element); auto *undo = new QUndoCommand("insert"); new AddGraphicsObjectCommand(element, folio, element->pos(), undo); element->setUpFormula(true, undo); folio->undoStack().push(undo); element->freezeNewAddedElement(); QStringList free; for (int n : ElementAutoNumSchemeCommand::freeNumbers(&project, title, element)) free << QString::number(n); return element->actualLabel() + " free " + free.join(','); } //The symbols a search of a project finds, by uuid static QStringList found(const QVector> &elements) { QStringList list; for (const auto &element : elements) list << element->uuid().toString(); list.sort(); return list; } //Everyday edits on folio @p i, as its editor makes them: a symbol's //information, a wire, the folio's title block, a linked symbol //deleted, then one undo and one redo static void edit(QETProject &project, int i) { Diagram *folio = project.folios().at(i); project.buildFolioToShow(folio); QList elements = folio->elements(); std::sort(elements.begin(), elements.end(), [](Element *a, Element *b) { return a->uuid() < b->uuid(); }); check(elements.size() > 2, "a folio with symbols to edit"); const DiagramContext old_info = elements.first()->elementInformations(); DiagramContext new_info = old_info; new_info.addValue(QStringLiteral("comment"), QStringLiteral("edited lazily")); folio->undoStack().push(new ChangeElementInformationCommand(elements.first(), old_info, new_info)); Terminal *ends[2] = {nullptr, nullptr}; for (Element *element : elements) { for (Terminal *terminal : element->terminals()) { if (terminal->conductors().isEmpty() && (!ends[0] || ends[0]->parentElement() != element)) { ends[ends[0] ? 1 : 0] = terminal; break; } } if (ends[1]) break; } if (ends[1]) { auto *undo = new QUndoCommand(QStringLiteral("wire")); auto *wire = new Conductor(ends[0], ends[1]); new AddGraphicsObjectCommand(wire, folio, QPointF(), undo); ConductorAutoNumerotation numbering(wire, folio, undo); numbering.numerate(); folio->undoStack().push(undo); } const TitleBlockProperties old_block = folio->border_and_titleblock.exportTitleBlock(); TitleBlockProperties new_block = old_block; new_block.title += QStringLiteral(" (edited)"); folio->undoStack().push(new ChangeTitleBlockCommand(folio, old_block, new_block)); //Copy three symbols and paste them, as Ctrl+C then Ctrl+Shift+V //and Return do { DiagramView view(folio); folio->clearSelection(); for (int n = 0 ; n < 3 ; ++n) elements.at(n)->setSelected(true); view.copy(); auto *paste = new DiagramEventAddPaste(folio, QPointF(), DiagramEventAddPaste::AtOrigin); folio->setEventInterface(paste); QKeyEvent drop(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); paste->keyPressEvent(&drop); folio->clearEventInterface(); } Element *linked = nullptr; for (Element *element : elements) if (!element->linkedElements().isEmpty()) { linked = element; break; } folio->clearSelection(); (linked ? linked : elements.last())->setSelected(true); folio->undoStack().push(new DeleteQGraphicsItemCommand(folio, DiagramContent(folio, true))); project.undoStack()->undo(); project.undoStack()->redo(); } //The project as saving writes it: folios not built written as read //when building them would write the same (QETProject::toXml()) static QString saved(QETProject &project) { QHash kept; const QDomDocument document = project.toXml(&kept); return QETProject::spliceKeptFolios(document.toString(4), kept); } //@p xml's lines in order, any uuid not in the file @p path "new": a //pasted copy's and a new wire's are random, and a folio's symbols are //saved in uuid order static QStringList sortedLines(QString xml, const QString &path) { QFile file(path); file.open(QIODevice::ReadOnly); const QString before = QString::fromUtf8(file.readAll()); static const QRegularExpression uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}")); for (auto it = uuid_re.globalMatch(QString(xml)) ; it.hasNext() ; ) { const QString uuid = it.next().captured(0); if (!before.contains(uuid)) xml.replace(uuid, QStringLiteral("new")); } QStringList list = xml.split(QLatin1Char('\n')); list.sort(); return list; } //The saved links of a .qetz, symbol by symbol in their order: what a //symbol saves of its partners, as its file holds it static QStringList savedLinks(const QString &qetz, const QString &path) { QFile file(qetz); file.open(QIODevice::ReadOnly); QList entries; QString error; check(QetZip::fromBytes(file.readAll(), &entries, &error), "a .qetz read"); QTemporaryDir dir; const QString db_path = dir.filePath(QStringLiteral("project.sqlite")); for (const QetZip::Entry &entry : entries) if (entry.name == QLatin1String("project.sqlite")) { QFile db(db_path); db.open(QIODevice::WriteOnly); db.write(entry.data); } QMap links; //symbol -> its partners in order { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("saved_links")); db.setDatabaseName(db_path); check(db.open(), "a .qetz's database opened"); QSqlQuery query(db); check(query.exec(QStringLiteral("SELECT element_key, linked_uuid FROM doc_link ORDER BY element_key, ord")), "saved links read"); while (query.next()) links[query.value(0).toString()] << query.value(1).toString(); } QSqlDatabase::removeDatabase(QStringLiteral("saved_links")); QStringList rows; for (auto it = links.cbegin() ; it != links.cend() ; ++it) rows << it.key() + QStringLiteral(": ") + it.value().join(QLatin1Char(',')); return sortedLines(rows.join(QLatin1Char('\n')), path); } //The rows of the project database's tables, in their order, read as //every caller reads them (newQuery(): brought up to date first) static QStringList tables(QETProject &project) { QStringList rows; for (const char *table : {"diagram", "diagram_info", "element", "element_info", "terminal", "conductor", "link"}) { QSqlQuery query = project.dataBase()->newQuery( QStringLiteral("SELECT * FROM %1 ORDER BY rowid").arg(QLatin1String(table))); check(query.exec(), "a table read"); QStringList table_rows; while (query.next()) { QStringList row; for (int i = 0 ; i < query.record().count() ; ++i) row << query.value(i).toString(); table_rows << QLatin1String(table) + ": " + row.join('|'); } //Rows are written in the order the scene gives a folio's items, //which is not the same from one opening to the next: two usual //opens given the same edits differ in it (637 rows of 3 394 on //industrial.qet). What a table holds is compared, not its order table_rows.sort(); rows << table_rows; } return rows; } static QImage render(Diagram *folio) { const QRectF rect = folio->border_and_titleblock.borderAndTitleBlockRect().adjusted(-5, -5, 5, 5); QImage image(rect.size().toSize(), QImage::Format_RGB32); image.fill(Qt::white); QPainter painter(&image); folio->render(&painter, QRectF(QPointF(0, 0), rect.size()), rect); return image; } //Each symbol's partners, in their order static QStringList links(Diagram *folio) { QStringList list; for (Element *element : folio->elements()) { QStringList partners; for (Element *partner : element->linkedElements()) partners << partner->uuid().toString(); list << element->uuid().toString() + ' ' + partners.join(','); } list.sort(); return list; } int main(int argc, char **argv) { QApplication app(argc, argv); std::freopen(argv[2], "w", stdout); QTemporaryDir settings; QSettings::setDefaultFormat(QSettings::IniFormat); QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path()); QCoreApplication::setOrganizationName("QETLazyFoliosRegression"); QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true); QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf"); //Folios not built saved as read (QETProject::toXml()): this //fixture is written by this version's own save qputenv("QET_LAZY_SAVE_KEPT", "1"); try { const QString output = QString::fromLocal8Bit(argv[3]); qunsetenv("QET_LAZY_FOLIOS"); //A file saved by this version: its folios carry their uuids, //which opening without building them needs const QString path = output + "/lazy_folios.qet"; QSet strip_folios; { QETProject source(QString::fromLocal8Bit(argv[1])); check(source.state() == QETProject::Ok, "fixture opens"); //A terminal strip of terminals on several folios: it is //read with the project, before any folio is built TerminalStrip *strip = source.newTerminalStrip(QStringLiteral("=A1"), QStringLiteral("+B1"), QStringLiteral("X99")); int added = 0; for (Diagram *folio : source.folios()) for (Element *element : folio->elements()) if (element->elementData().m_type == ElementData::Terminal && added < 12 && strip->addTerminal(element)) { ++added; strip_folios.insert(source.folioIndex(folio)); } check(added > 3, "a terminal strip with terminals"); source.setFilePath(path); check(source.write().isOk(), "fixture saved by this version"); } QETProject eager(path); check(eager.state() == QETProject::Ok, "project opens"); { qputenv("QET_LAZY_FOLIOS", "1"); QETProject lazy_strips(path); qunsetenv("QET_LAZY_FOLIOS"); auto strip_of = [](const QETProject &project) { for (TerminalStrip *strip : project.terminalStrip()) if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount(); return -1; }; check(strip_of(lazy_strips) > 3 && strip_of(lazy_strips) == strip_of(eager), "a terminal strip keeps its terminals when the folios are not built"); } const QList folios = eager.folios(); check(folios.size() > 3, "several folios"); QList images; QList partners; int linked = 0; for (Diagram *folio : folios) { images << render(folio); partners << links(folio); for (Element *element : folio->elements()) linked += !element->linkedElements().isEmpty(); } check(linked > 0, "the project has linked symbols"); qputenv("QET_LAZY_FOLIOS", "1"); auto same = [&](QETProject &lazy, int i, const char *what) { Diagram *folio = lazy.folios().at(i); lazy.buildFolioToShow(folio); if (render(folio) != images.at(i) || links(folio) != partners.at(i)) { std::printf("folio %d, %s: differs\n", i + 1, what); throw std::runtime_error("a folio shown lazily draws or links as opened as usual"); } }; //Each folio first after opening for (int i = 0; i < folios.size(); ++i) { QETProject lazy(path); check(lazy.unloadedFolioCount() == folios.size() - strip_folios.size(), "opened building only the folios of the terminal strip's terminals"); same(lazy, i, "shown first"); check(lazy.unloadedFolioCount() > 0, "showing a folio does not build them all"); } //Every folio, one after the other, then the save QETProject lazy(path); for (int i = 0; i < folios.size(); ++i) same(lazy, i, "shown in turn"); check(lazy.toXml().toString() == eager.toXml().toString(), "saves as opened as usual"); check(lazy.undoStack()->isClean(), "showing folios changes nothing"); //Numbering asks every symbol of the project: it is answered for //the folios not built without building them, as when built QETProject numbered(path); check(numbering(numbered) == numbering(eager), "symbols' numbering as when built"); const QStringList titles = eager.elementAutoNum().keys(); check(!titles.isEmpty(), "the project has element numbering schemes"); for (const QString &title : titles) check(gaps(numbered, title) == gaps(eager, title), "a scheme's gaps as when built"); check(numbered.unloadedFolioCount() == folios.size() - strip_folios.size(), "numbering read without building"); { //Wire numbers exported from the database filled from the //document, as when built (D5) QETProject exported(path); check(ConductorNumExport(&exported).wiresNum() == ConductorNumExport(&eager).wiresNum(), "wire numbers of a .qet exported as when built"); check(exported.unloadedFolioCount() == folios.size() - strip_folios.size(), "exporting a .qet's wire numbers builds no folio"); } //A symbol placed on a folio, numbered by a scheme others follow QString location, title; int folio = -1; for (const auto &symbol : eager.symbolNumbering()) { if (symbol.numbered && symbol.element && !eager.elementAutoNumTitle(symbol.scheme).isEmpty()) { location = symbol.element->location().toString(); title = eager.elementAutoNumTitle(symbol.scheme); folio = eager.folios().indexOf(symbol.element->diagram()); break; } } check(folio >= 0, "a symbol following a scheme"); const QString placed = place(numbered, folio, location, title); std::printf("placed on folio %d with %s: %s\n", folio + 1, qPrintable(title), qPrintable(placed)); check(numbered.unloadedFolioCount() > 0, "numbering a placed symbol does not build every folio"); QETProject usual(path); check(placed == place(usual, folio, location, title), "a placed symbol numbered as when built"); //The candidates a link tab lists (ElementProvider): found as //when built, building only the folios which hold some const ElementProvider usual_search(&eager); for (const ElementData::Types kinds : {ElementData::Types(ElementData::Slave), ElementData::Types(ElementData::Master), ElementData::Types(ElementData::AllReport), ElementData::Types(ElementData::Terminal)}) { QETProject searched(path); const ElementProvider search(&searched); const QStringList lazy_free = found(search.freeElement(kinds)), usual_free = found(usual_search.freeElement(kinds)); if (lazy_free != usual_free) { for (const QString &u : usual_free) if (!lazy_free.contains(u)) std::printf(" only when built: %s\n", qPrintable(u)); for (const QString &u : lazy_free) if (!usual_free.contains(u)) std::printf(" only lazily: %s\n", qPrintable(u)); } check(lazy_free == usual_free, "free symbols found as when built"); std::printf("free symbols of kinds %d: %d folios built\n", int(kinds), int(folios.size() - searched.unloadedFolioCount())); check(found(search.find(kinds)) == found(usual_search.find(kinds)), "symbols found as when built"); } { QETProject searched(path); ElementProvider(&searched).freeElement(ElementData::Slave); check(searched.unloadedFolioCount() > 0, "a search for free contacts does not build every folio"); } //Selecting a symbol shows its properties, a link tab for a //coil, a contact or a report: the folio and few others built for (const Element::kind kind : {Element::Master, Element::Slave, Element::NextReport, Element::PreviousReport}) { QETProject selected(path); Element *element = nullptr; for (int i = 0 ; !element && i < folios.size() ; ++i) { for (Element *candidate : eager.folios().at(i)->elements()) { if (candidate->linkType() == kind) { selected.buildFolioToShow(selected.folios().at(i)); for (Element *e : selected.folios().at(i)->elements()) if (e->uuid() == candidate->uuid()) element = e; break; } } } check(element, "a symbol of each kind with a link tab"); const int before = selected.unloadedFolioCount(); delete new ElementPropertiesWidget(element); std::printf("properties of a symbol of kind %d: %d more folios built, %d not built\n", int(kind), before - selected.unloadedFolioCount(), selected.unloadedFolioCount()); check(selected.unloadedFolioCount() > 0, "selecting a symbol does not build every folio"); } //Everyday edits on a folio linked to others: the others stay //not built, and the project saves as when they are int busiest = 0, most = -1; for (int i = 0 ; i < folios.size() ; ++i) { int partners = 0; for (Element *element : folios.at(i)->elements()) for (Element *partner : element->linkedElements()) partners += partner->diagram() != folios.at(i); if (partners > most) { most = partners; busiest = i; } } { QETProject edited(path), usual(path); qunsetenv("QET_LAZY_FOLIOS"); QETProject usual_edited(path); qputenv("QET_LAZY_FOLIOS", "1"); edit(edited, busiest); std::printf("edits on folio %d (%d links to other folios): %d folios not built\n", busiest + 1, most, edited.unloadedFolioCount()); check(edited.unloadedFolioCount() > 0, "editing a folio does not build every folio"); edit(usual_edited, busiest); //Made by the edits, a new wire's uuid is random: any uuid the //file had not is "new" before comparing QFile saved(path); saved.open(QIODevice::ReadOnly); const QString before = QString::fromUtf8(saved.readAll()); static const QRegularExpression uuid_re(QStringLiteral("uuid=\"(\\{[0-9a-f-]{36}\\})\"")); auto known = [&before](QString xml) { QString out; qsizetype at = 0; for (auto it = uuid_re.globalMatch(xml) ; it.hasNext() ; ) { const auto match = it.next(); out += xml.mid(at, match.capturedStart() - at); out += before.contains(match.captured(1)) ? match.captured(0) : QStringLiteral("uuid=\"new\""); at = match.capturedEnd(); } return out + xml.mid(at); }; const QStringList lazy_rows = tables(edited), usual_rows = tables(usual_edited); static const QRegularExpression bare_uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}")); auto known_row = [&before](QString row) { for (auto it = bare_uuid_re.globalMatch(QString(row)) ; it.hasNext() ; ) { const QString uuid = it.next().captured(0); if (!before.contains(uuid)) row.replace(uuid, QStringLiteral("new")); } return row; }; check(edited.unloadedFolioCount() > 0, "reading the database does not build every folio"); QStringList lazy_known, usual_known; for (const QString &row : lazy_rows) lazy_known << known_row(row); for (const QString &row : usual_rows) usual_known << known_row(row); lazy_known.sort(); usual_known.sort(); int differing = 0; for (int i = 0 ; i < std::max(lazy_known.size(), usual_known.size()) ; ++i) if (lazy_known.value(i) != usual_known.value(i) && differing++ < 5) std::printf(" row %d: lazy %s\n usual %s\n", i, qPrintable(lazy_known.value(i).left(160)), qPrintable(usual_known.value(i).left(160))); std::printf("database after the edits: %d rows, %d differ\n", int(usual_rows.size()), differing); check(!differing, "the database after the edits as when every folio is built"); //A pasted copy has a random uuid, and a folio's symbols are //saved in uuid order: the lines are compared, not their order auto lines = [&known](const QString &xml) { QStringList list = known(xml).split(QLatin1Char('\n')); list.sort(); return list.join(QLatin1Char('\n')); }; const QString a = lines(edited.toXml().toString()), b = lines(usual_edited.toXml().toString()); if (a != b) { QFile fa(output + "/edited-lazy.xml"); fa.open(QIODevice::WriteOnly); fa.write(a.toUtf8()); QFile fb(output + "/edited-usual.xml"); fb.open(QIODevice::WriteOnly); fb.write(b.toUtf8()); } check(a == b, "edits save as when every folio is built"); } //Saving writes the folios not built as they were read, the same //bytes as building them would write const QString usual_text = saved(eager); { QETProject untouched(path); const int unbuilt = untouched.unloadedFolioCount(); const QString untouched_text = saved(untouched); if (untouched_text != usual_text) { QFile fa(output + "/untouched-lazy.xml"); fa.open(QIODevice::WriteOnly); fa.write(untouched_text.toUtf8()); QFile fb(output + "/untouched-usual.xml"); fb.open(QIODevice::WriteOnly); fb.write(usual_text.toUtf8()); } check(untouched_text == usual_text, "a project saved without building, as when built"); //Only the partners of the folios the terminal strip built std::printf("saved untouched: %d of %d folios not built built to save\n", unbuilt - untouched.unloadedFolioCount(), unbuilt); check(untouched.unloadedFolioCount() > 0, "saving does not build every folio"); } //After everyday edits on a folio: as when built, others not built { QETProject edited(path), usual(path); qunsetenv("QET_LAZY_FOLIOS"); QETProject usual_edited(path); qputenv("QET_LAZY_FOLIOS", "1"); edit(edited, busiest); edit(usual_edited, busiest); QFile file(path); file.open(QIODevice::ReadOnly); const QString before = QString::fromUtf8(file.readAll()); static const QRegularExpression uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}")); auto lines = [&before](QString text) { for (auto it = uuid_re.globalMatch(QString(text)) ; it.hasNext() ; ) { const QString uuid = it.next().captured(0); if (!before.contains(uuid)) text.replace(uuid, QStringLiteral("new")); } QStringList list = text.split(QLatin1Char('\n')); list.sort(); return list; }; const QStringList la = lines(saved(edited)), lb = lines(saved(usual_edited)); if (la != lb) { int shown = 0; for (const QString &l : lb) if (!la.contains(l) && shown++ < 6) std::printf(" only usual: %s\n", qPrintable(l.trimmed().left(150))); shown = 0; for (const QString &l : la) if (!lb.contains(l) && shown++ < 6) std::printf(" only lazy: %s\n", qPrintable(l.trimmed().left(150))); } check(la == lb, "edits saved without building, as when built"); std::printf("saved after the edits: %d folios not built\n", edited.unloadedFolioCount()); check(edited.unloadedFolioCount() > 0, "saving after edits does not build every folio"); } //A folio not built given a new title: it and the folios its //links reach are built to be saved, the others not { QETProject renamed(path); qunsetenv("QET_LAZY_FOLIOS"); QETProject usual_renamed(path); qputenv("QET_LAZY_FOLIOS", "1"); for (QETProject *project : {&renamed, &usual_renamed}) { Diagram *folio = project->folios().at(busiest); TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock(); block.title += QStringLiteral(" (renamed)"); folio->border_and_titleblock.importTitleBlock(block); } check(saved(renamed) == saved(usual_renamed), "a renamed folio saved as when built"); std::printf("saved after a folio not built was renamed: %d folios not built\n", renamed.unloadedFolioCount()); check(renamed.unloadedFolioCount() > 0, "renaming a folio does not build every folio to save"); } //Project properties, which any folio's texts can use: every //folio is built to be saved { QETProject changed(path); qunsetenv("QET_LAZY_FOLIOS"); QETProject usual_changed(path); qputenv("QET_LAZY_FOLIOS", "1"); for (QETProject *project : {&changed, &usual_changed}) { DiagramContext properties = project->projectProperties(); properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1")); project->setProjectProperties(properties); } check(saved(changed) == saved(usual_changed), "changed project properties saved as when built"); check(changed.unloadedFolioCount() == 0, "changed project properties build every folio to save"); } //A potential through folio reports is followed as far as when //every folio is built { Conductor *usual_wire = nullptr; for (Diagram *folio : eager.folios()) for (Element *element : folio->elements()) if ((element->linkType() & Element::AllReport) && !element->conductors().isEmpty() && !usual_wire) usual_wire = element->conductors().first(); check(usual_wire, "a wire on a folio report"); auto potential = [](Conductor *wire) { QStringList list; for (Conductor *c : wire->relatedPotentialConductors(true)) list << c->uuid().toString(); list.sort(); return list; }; QETProject followed(path); Diagram *folio = followed.folios().at(eager.folioIndex(usual_wire->diagram())); followed.buildFolioToShow(folio); Conductor *wire = nullptr; for (Conductor *c : folio->conductors()) if (c->uuid() == usual_wire->uuid()) wire = c; check(wire, "the same wire"); check(potential(wire) == potential(usual_wire), "a potential followed as when built"); std::printf("potential through reports: %d wires, %d folios not built\n", int(potential(wire).size()), followed.unloadedFolioCount()); } //Two symbols sharing a uuid: the database is filled from the //folios, which builds every one of them, so the project opens //with every folio built rather than building them all later { QFile file(path); check(file.open(QIODevice::ReadOnly), "fixture read"); QDomDocument document; check(bool(document.setContent(&file)), "fixture parsed"); file.close(); check(projectDataBase::documentRefusal(document).isEmpty(), "the fixture fills the database itself"); QDomNodeList elements = document.elementsByTagName(QStringLiteral("element")); QList placed; for (int i = 0; i < elements.size(); ++i) { QDomElement e = elements.at(i).toElement(); if (Element::valideXml(e)) placed << e; } check(placed.size() > 1, "two placed symbols"); placed.last().setAttribute(QStringLiteral("uuid"), placed.first().attribute(QStringLiteral("uuid"))); check(!projectDataBase::documentRefusal(document).isEmpty(), "a shared uuid is seen before the folios are read"); const QString shared = output + "/lazy_folios_shared_uuid.qet"; QFile out(shared); check(out.open(QIODevice::WriteOnly), "copy written"); out.write(document.toByteArray(4)); out.close(); qputenv("QET_LAZY_FOLIOS", "1"); qputenv("QET_LAZY_FOLIOS_TRACE", "1"); static int built_late; built_late = 0; const QtMessageHandler previous = qInstallMessageHandler( [](QtMsgType, const QMessageLogContext &, const QString &message) { if (message.contains(QStringLiteral("built for a caller of diagrams()"))) ++built_late; }); QETProject opened(shared); qInstallMessageHandler(previous); qunsetenv("QET_LAZY_FOLIOS_TRACE"); check(opened.state() == QETProject::Ok, "a project with a shared uuid opens"); check(opened.unloadedFolioCount() == 0, "a project with a shared uuid opens with every folio built"); check(built_late == 0, "a project with a shared uuid builds its folios as it reads them, not later"); } //A .qetz opened from its index (stage D2): no //folio read, each read from its part when shown, drawn and //linked as when built, saved as the .qet is { const QString zipped = output + "/lazy_folios.qetz"; QString error; check(eager.writeContainerFile(zipped, &error), "saved as a .qetz"); QETProject indexed(zipped); check(indexed.state() == QETProject::Ok, "the .qetz opens"); check(indexed.openedFromIndex(), "the .qetz opens from its index"); std::printf(".qetz opened from its index: %d of %d folios not built (strip folios %d)\n", indexed.unloadedFolioCount(), int(folios.size()), int(strip_folios.size())); check(indexed.unloadedFolioCount() == folios.size() - strip_folios.size(), "the .qetz opens building only the folios of the terminal strip's terminals"); check(numbering(indexed) == numbering(eager), "symbols' numbering from the index as when built"); for (int i = 0; i < folios.size(); ++i) same(indexed, i, "shown from the .qetz"); check(indexed.toXml().toString() == eager.toXml().toString(), "the .qetz saves as when built"); //A folio whose table continues one on another folio, shown //first: the index, not the folios' parts, says which folios //its table is built with (D5) int continued = 0; for (int i = 0; i < folios.size(); ++i) { bool continues = false; for (QGraphicsItem *item : folios.at(i)->items()) if (item->type() == QetGraphicsTableItem::Type && !static_cast(item)->previousTableUuid().isNull()) continues = true; if (!continues) continue; QETProject fresh(zipped); same(fresh, i, "a continued table shown first from the .qetz"); ++continued; } check(continued > 0, "a folio whose table continues another"); { //The wiring list export writes the whole project out //(toXml()), building every folio: as when built QETProject exported(zipped); check(WiringListExport(&exported).toCsvString() == WiringListExport(&eager).toCsvString(), "wiring list exported as when built"); } //Numbering, the searches for symbols and the unused symbols //are answered from the database: no folio file is read //but those of the folios built (D5) { QETProject asked(zipped); check(numbering(asked) == numbering(eager), "numbering of a .qetz as when built"); for (const QString &title : eager.elementAutoNum().keys()) check(gaps(asked, title) == gaps(eager, title), "a .qetz scheme's gaps as when built"); auto locations = [](const QList &list) { QStringList strings; for (const ElementsLocation &location : list) strings << location.toString(); strings.sort(); return strings; }; check(locations(asked.unusedElements()) == locations(eager.unusedElements()), "a .qetz's unused symbols as when built"); const ElementProvider usual_search(&eager), search(&asked); for (const ElementData::Types kinds : {ElementData::Types(ElementData::Slave), ElementData::Types(ElementData::Master), ElementData::Types(ElementData::AllReport)}) check(found(search.freeElement(kinds)) == found(usual_search.freeElement(kinds)), "free symbols of a .qetz found as when built"); const int built = int(folios.size()) - asked.unloadedFolioCount(); std::printf(".qetz asked numbering, searches, unused symbols: %d folio files read, %d folios built\n", asked.folioPartsRead(), built); check(asked.unloadedFolioCount() > 0, "asking a .qetz does not build every folio"); check(asked.folioPartsRead() == built, "asking a .qetz reads no file of a folio not built"); } //A folio added, moved and removed: no other folio built, the //folios' rows as when every folio is built (D5) { auto shuffle = [](QETProject &project) { Diagram *added = project.addNewDiagram(); project.diagramOrderChanged(1, 0); project.removeDiagram(added); }; auto folioRows = [](QETProject &project) { QStringList rows; for (const QString &row : tables(project)) if (row.startsWith("diagram")) rows << row; return rows; }; QETProject shuffled(zipped); const int unbuilt = shuffled.unloadedFolioCount(); shuffle(shuffled); std::printf(".qetz folio added, moved, removed: %d folios built\n", unbuilt - shuffled.unloadedFolioCount()); check(shuffled.unloadedFolioCount() == unbuilt, "adding, moving and removing a folio builds no other"); qunsetenv("QET_LAZY_FOLIOS"); QETProject usual_shuffled(zipped); qputenv("QET_LAZY_FOLIOS", "1"); shuffle(usual_shuffled); check(folioRows(shuffled) == folioRows(usual_shuffled), "folios' rows after a move as when built"); } //The tables a table can continue (its properties editor) and //the wiring list's count of wires left out: only the folios //holding a table built (D5) { QETProject listed(zipped); const int unbuilt = listed.unloadedFolioCount(); auto tableUuids = [](const QVector &list) { QStringList uuids; for (QetGraphicsTableItem *table : list) uuids << table->uuid().toString(); uuids.sort(); return uuids; }; const QStringList lazy_tables = tableUuids(ElementProvider(&listed).table(nullptr, nullptr)); check(!lazy_tables.isEmpty(), "the project has tables"); check(lazy_tables == tableUuids(ElementProvider(&eager).table(nullptr, nullptr)), "the project's tables listed as when built"); check(listed.dataBase()->excludedConductorCount() == eager.dataBase()->excludedConductorCount(), "wires left out of the wiring list counted as when built"); int with_tables = 0; for (Diagram *folio : folios) { for (QGraphicsItem *item : folio->items()) if (item->type() == QetGraphicsTableItem::Type) { ++with_tables; break; } } std::printf(".qetz tables listed: %d folios built, %d hold a table\n", unbuilt - listed.unloadedFolioCount(), with_tables); check(listed.unloadedFolioCount() > 0, "listing the tables does not build every folio"); } //The wire numbers export and the terminal //numbering dialog: as when built, building only the folios //holding a terminal (D5) { QETProject exported(zipped); const int unbuilt = exported.unloadedFolioCount(); check(ConductorNumExport(&exported).wiresNum() == ConductorNumExport(&eager).wiresNum(), "wire numbers exported as when built"); check(exported.unloadedFolioCount() == unbuilt, "exporting wire numbers builds no folio"); { TerminalNumberingDialog dialog(nullptr, &exported); } int with_terminals = 0; for (Diagram *folio : folios) for (Element *element : folio->elements()) if (element->elementData().m_type == ElementData::Terminal) { ++with_terminals; break; } std::printf(".qetz terminal numbering: %d folios built, %d hold a terminal\n", unbuilt - exported.unloadedFolioCount(), with_terminals); check(exported.folios().size() - exported.unloadedFolioCount() <= with_terminals + (folios.size() - unbuilt), "terminal numbering builds only the folios holding a terminal"); } //Saved as a .qetz without building the folios not touched //(D4): their parts and rows copied as they were QETProject untouched(zipped); const int unbuilt = untouched.unloadedFolioCount(); const QString kept_path = output + "/kept.qetz"; check(untouched.writeContainerFile(kept_path, &error), "saved as a .qetz without building"); std::printf(".qetz saved untouched: %d of %d folios not built before, %d after\n", unbuilt, int(folios.size()), untouched.unloadedFolioCount()); check(untouched.unloadedFolioCount() > 0, "saving an untouched .qetz does not build every folio"); qunsetenv("QET_LAZY_FOLIOS"); QETProject kept_back(kept_path); qputenv("QET_LAZY_FOLIOS", "1"); check(kept_back.toXml().toString() == eager.toXml().toString(), "a .qetz saved without building holds the project"); //After everyday edits on a folio QETProject edited_z(zipped); edit(edited_z, busiest); const QString edited_path = output + "/edited.qetz"; check(edited_z.writeContainerFile(edited_path, &error), "edited .qetz saved"); std::printf(".qetz saved after the edits: %d folios not built\n", edited_z.unloadedFolioCount()); check(edited_z.unloadedFolioCount() > 0, "saving an edited .qetz does not build every folio"); //Against the same edits made with every folio built, saved and //read back too: what a save and a reading do to a project is //not what is checked here const QString usual_path = output + "/edited-usual.qetz"; qunsetenv("QET_LAZY_FOLIOS"); { QETProject edited_usual(path); edit(edited_usual, busiest); check(edited_usual.writeContainerFile(usual_path, &error), "edits saved with every folio built"); } QETProject edited_back(edited_path), usual_back(usual_path); qputenv("QET_LAZY_FOLIOS", "1"); check(sortedLines(edited_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path), "an edited .qetz saved without building holds what the edits made"); //Each symbol's links as saved, partners on folios not built //included, in their order (D5a) check(savedLinks(edited_path, path) == savedLinks(usual_path, path), "an edited .qetz saved without building keeps every symbol's links"); } std::printf("PASS: %d folios\n", int(folios.size())); } catch (const std::exception &error) { std::printf("FAIL: %s\n", error.what()); std::fflush(stdout); return 1; } std::fflush(stdout); return 0; }