mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-15 10:04:13 +02:00
8ae937007c
The project database's element table now holds each symbol's definition (the path its file names it by) and its sequential numbers, filled from the document and from built folios alike. Numbering, the searches for symbols of a kind (a link tab's candidates) and the list of unused symbols read them for the folios not built, instead of reading those folios' files. Saving looks up a folio's tables in the index it was opened from. A .qetz asked its numbering, searches and unused symbols reads only the files of the folios it builds (tst_lazyfolios, which fails when the unused symbols are read from the folio files: 50 read, 22 built). ctest 96/96 eager and lazy; 342 corpus projects save the same in all four arms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
778 lines
34 KiB
C++
778 lines
34 KiB
C++
// 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 <QApplication>
|
|
#include <QFile>
|
|
#include <QFontDatabase>
|
|
#include <QImage>
|
|
#include <QSqlRecord>
|
|
#include <QSqlQuery>
|
|
#include <QRegularExpression>
|
|
#include <QPainter>
|
|
#include <QSettings>
|
|
#include <QTemporaryDir>
|
|
#include <cstdio>
|
|
#include <stdexcept>
|
|
#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 <QSqlDatabase>
|
|
#include <QMap>
|
|
#include "../../sources/TerminalStrip/terminalstrip.h"
|
|
#include "../../sources/ui/elementpropertieswidget.h"
|
|
#include "../../sources/conductorautonumerotation.h"
|
|
#include "../../sources/diagramcontent.h"
|
|
#include "../../sources/diagramview.h"
|
|
#include "../../sources/diagramevent/diagrameventaddpaste.h"
|
|
#include <QKeyEvent>
|
|
#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 <QDomDocument>
|
|
|
|
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<QPointer<Element>> &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<Element *> 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<int, QByteArray> 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<QetZip::Entry> 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<QString, QStringList> 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<int> 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<Diagram *> folios = eager.folios();
|
|
check(folios.size() > 3, "several folios");
|
|
QList<QImage> images; QList<QStringList> 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");
|
|
//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<QDomElement> 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<QetGraphicsTableItem *>(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");
|
|
|
|
//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<ElementsLocation> &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");
|
|
}
|
|
|
|
//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;
|
|
}
|