mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-16 02:34:14 +02:00
Merge pull request #1472 from ispyisail/feat/qetz-db-truth-v2
Save as .qetz by default, with the project database as the source of truth
This commit is contained in:
@@ -33,7 +33,13 @@
|
||||
# 2. let it run for a moment, then interrupt it -- the main thread is now
|
||||
# inside the dialog's nested loop, which is the only place the bug lives
|
||||
# (a deleteLater() posted before exec() started is not processed by it);
|
||||
# 3. call QETApp::quitQET(), which closes every editor;
|
||||
# 3. let it run on to the start of the loop's next QEventLoop::processEvents()
|
||||
# on the main thread, and call QETApp::quitQET() there, which closes
|
||||
# every editor. Not where the interrupt lands: that can be inside
|
||||
# malloc(), whose lock a call into the program then waits on for ever
|
||||
# (1 run in 20 timed out that way, on master too). Between two
|
||||
# events the loop holds nothing, and QElectroTech's event loop
|
||||
# watchdog wakes it every 50 ms, so the stop comes at once;
|
||||
# 4. let it run again: the unfixed build aborts within a second, the fixed
|
||||
# one keeps running until the test interrupts it.
|
||||
#
|
||||
@@ -104,14 +110,32 @@ if command -v nm >/dev/null; then
|
||||
fi
|
||||
|
||||
SANDBOX="$(mktemp -d /tmp/qet-modal-quit.XXXXXX)"
|
||||
cleanup() { [ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX"; }
|
||||
|
||||
# SingleApplication keeps a System V shared memory block, keyed from a file in
|
||||
# the temporary folder: 16 bits of the key vary. This run ends QElectroTech
|
||||
# with gdb's kill, which leaves its block behind; blocks left so pile up, and
|
||||
# once a new run's key falls on one, SingleApplication cannot start ("Unable to
|
||||
# attach to shared memory block") -- 1 run in 40 with 976 of them. So the
|
||||
# blocks this run leaves, attached to no process, are removed at the end, and
|
||||
# a run that meets someone else's is tried again (below).
|
||||
orphan_blocks() {
|
||||
command -v ipcs >/dev/null || return 0
|
||||
ipcs -m 2>/dev/null | awk -v me="$(id -un)" 'NR > 3 && $3 == me && $6 == 0 { print $2 }'
|
||||
}
|
||||
BLOCKS_BEFORE=" $(orphan_blocks | tr '\n' ' ') "
|
||||
cleanup() {
|
||||
if command -v ipcrm >/dev/null; then
|
||||
for id in $(orphan_blocks); do
|
||||
case "$BLOCKS_BEFORE" in *" $id "*) ;; *) ipcrm -m "$id" 2>/dev/null ;; esac
|
||||
done
|
||||
fi
|
||||
[ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# A unique binary path gives this run its own SingleApplication socket, so it
|
||||
# can neither be captured by nor capture a QElectroTech already running. A
|
||||
# symlink would not do: applicationFilePath() resolves it back to the real path.
|
||||
TEST_BINARY="$SANDBOX/qelectrotech-modalquit"
|
||||
cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; }
|
||||
|
||||
# Keep the original file name. QElectroTech decides what to open from the
|
||||
# extension, so copying a .elmt to "project.qet" would quietly turn an
|
||||
@@ -141,9 +165,16 @@ end
|
||||
break QDialog::exec
|
||||
run
|
||||
delete
|
||||
if \$_isvoid(\$_exitcode)
|
||||
printf "QET_TEST: dialog exec() entered\n"
|
||||
else
|
||||
printf "QET_TEST: exited before any dialog, code %d\n", \$_exitcode
|
||||
end
|
||||
python qet_interrupt_later($SETTLE)
|
||||
continue
|
||||
tbreak QEventLoop::processEvents thread 1
|
||||
continue
|
||||
printf "QET_TEST: at the dialog's event loop\n"
|
||||
thread 1
|
||||
set \$app = ((void* (*)(void))'QETApp::instance()')()
|
||||
call ((void (*)(void*))'QETApp::quitQET()')(\$app)
|
||||
@@ -162,14 +193,29 @@ EOF
|
||||
# the full watchdog period no matter how fast gdb finished: ten seconds of
|
||||
# work reported as two minutes, a stone's throw from the CTest timeout.
|
||||
GDB_TIMEOUT=120
|
||||
if command -v timeout >/dev/null; then
|
||||
timeout --signal=KILL "$GDB_TIMEOUT" \
|
||||
# Each try under a binary path of its own: a new SingleApplication key. Tried
|
||||
# again only when QElectroTech stopped before any dialog because
|
||||
# SingleApplication met a block it could not use -- the scenario never ran,
|
||||
# the code under test played no part.
|
||||
for attempt in 1 2 3; do
|
||||
TEST_BINARY="$SANDBOX/qelectrotech-modalquit-$attempt"
|
||||
cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; }
|
||||
if command -v timeout >/dev/null; then
|
||||
timeout --signal=KILL "$GDB_TIMEOUT" \
|
||||
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
|
||||
> "$SANDBOX/gdb.log" 2>&1
|
||||
else
|
||||
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
|
||||
> "$SANDBOX/gdb.log" 2>&1
|
||||
else
|
||||
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
|
||||
> "$SANDBOX/gdb.log" 2>&1
|
||||
fi
|
||||
> "$SANDBOX/gdb.log" 2>&1
|
||||
fi
|
||||
if grep -q "QET_TEST: exited before any dialog" "$SANDBOX/gdb.log" \
|
||||
&& grep -q "SingleApplication: Unable to" "$SANDBOX/gdb.log"; then
|
||||
echo "note: try $attempt: SingleApplication could not set up its shared memory (a block left by another run); trying again"
|
||||
cp "$SANDBOX/gdb.log" "$SANDBOX/gdb-try$attempt.log"
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
log="$SANDBOX/gdb.log"
|
||||
if ! grep -q "QET_TEST: dialog exec() entered" "$log"; then
|
||||
|
||||
@@ -92,7 +92,7 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/industrial.qet)
|
||||
set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 600 RUN_SERIAL TRUE
|
||||
set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 1800 RUN_SERIAL TRUE
|
||||
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
# Deleting a cable, undoing the delete twice and then editing anything
|
||||
# has both cable commands tearing down the same cable. Runs under the
|
||||
@@ -1073,10 +1073,13 @@ endif()
|
||||
|
||||
add_executable(
|
||||
tst_resaveunchanged
|
||||
tst_resaveunchanged.cpp)
|
||||
tst_resaveunchanged.cpp
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/miniz/miniz.c)
|
||||
add_test(NAME tst_resaveunchanged COMMAND tst_resaveunchanged)
|
||||
add_dependencies(tst_resaveunchanged qelectrotech)
|
||||
target_link_libraries(tst_resaveunchanged PRIVATE Qt::Test)
|
||||
target_include_directories(tst_resaveunchanged PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_resaveunchanged PRIVATE Qt::Test SQLite3::SQLite3)
|
||||
target_compile_definitions(tst_resaveunchanged PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Lazy folios (QET_LAZY_FOLIOS),
|
||||
// Lazy folios (on by default, QET_EAGER_FOLIOS=1 for every folio built),
|
||||
// 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
|
||||
@@ -23,14 +23,22 @@
|
||||
#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/conductornumexport.h"
|
||||
#include "../../sources/scripting/qetscriptapi.h"
|
||||
#include "../../sources/wiringlistexport.h"
|
||||
#include "../../sources/ui/terminalnumberingdialog.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"
|
||||
@@ -41,6 +49,73 @@
|
||||
#include <QDomDocument>
|
||||
|
||||
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
||||
static QStringList sortedLines(QString xml, const QString &path);
|
||||
|
||||
//A new revision on every folio (its title block's revision index and
|
||||
//author), saved lazily and with every folio built: what saving builds,
|
||||
//and whether the two saves hold the same. A change to folio
|
||||
//@p label_folio's label (what formulas read) when it is not -1
|
||||
static void revisionEveryFolio(const QString &source, const QString &output, const QString &name,
|
||||
const QString &path, int label_folio = -1)
|
||||
{
|
||||
QString error;
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
QETProject revised(source);
|
||||
const int unbuilt = revised.unloadedFolioCount();
|
||||
QetScriptApi api(&revised);
|
||||
for (int i = 0 ; i < api.folioCount() ; ++i) {
|
||||
check(api.setFolioProperty(i, "indexrev", "B"), "a folio's revision set");
|
||||
check(api.setFolioProperty(i, "author", "Revised"), "a folio's author set");
|
||||
}
|
||||
if (label_folio >= 0)
|
||||
check(api.setFolioProperty(label_folio, "folio", "X%id"), "a folio's label set");
|
||||
check(revised.unloadedFolioCount() == unbuilt, "setting the folios' title blocks builds none");
|
||||
int with_tables = 0;
|
||||
for (Diagram *folio : revised.folios())
|
||||
if (!folio->isLoaded() && revised.unbuiltFolioHasTables(folio)) ++with_tables;
|
||||
const bool qetz = QETProject::isContainerPath(source);
|
||||
const QString lazy_path = output + "/" + name + (qetz ? ".qetz" : ".qet");
|
||||
if (qetz) check(revised.writeContainerFile(lazy_path, &error), "revised project saved");
|
||||
else { revised.setFilePath(lazy_path); check(revised.write().isOk(), "revised project saved"); }
|
||||
const int built = unbuilt - revised.unloadedFolioCount();
|
||||
std::printf("%s: new revision on every folio, %d folios built by saving, %d hold a table%s\n",
|
||||
qPrintable(name), built, with_tables,
|
||||
label_folio >= 0 ? ", one label changed" : "");
|
||||
if (label_folio < 0)
|
||||
check(built <= with_tables, "saving a new revision builds only the folios holding a table");
|
||||
else
|
||||
check(revised.folios().at(label_folio)->isLoaded(), "a folio whose label changed is built to save");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
const QString usual_path = output + "/" + name + "-usual" + (qetz ? ".qetz" : ".qet");
|
||||
{
|
||||
QETProject usual(source);
|
||||
QetScriptApi usual_api(&usual);
|
||||
for (int i = 0 ; i < usual_api.folioCount() ; ++i) {
|
||||
usual_api.setFolioProperty(i, "indexrev", "B");
|
||||
usual_api.setFolioProperty(i, "author", "Revised");
|
||||
}
|
||||
if (label_folio >= 0) usual_api.setFolioProperty(label_folio, "folio", "X%id");
|
||||
if (qetz) check(usual.writeContainerFile(usual_path, &error), "revision saved when built");
|
||||
else { usual.setFilePath(usual_path); check(usual.write().isOk(), "revision saved when built"); }
|
||||
}
|
||||
QETProject lazy_back(lazy_path), usual_back(usual_path);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
//What a save says of itself (file name, path, time) left out
|
||||
auto lines = [&path](QETProject &project) {
|
||||
QStringList list = sortedLines(project.toXml().toString(), path);
|
||||
list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); });
|
||||
return list;
|
||||
};
|
||||
const QStringList lazy_lines = lines(lazy_back), usual_lines = lines(usual_back);
|
||||
if (lazy_lines != usual_lines) {
|
||||
int shown = 0;
|
||||
for (const QString &line : usual_lines)
|
||||
if (!lazy_lines.contains(line) && shown++ < 8) std::printf(" when built: %s\n", qPrintable(line.left(300)));
|
||||
for (const QString &line : lazy_lines)
|
||||
if (!usual_lines.contains(line) && shown++ < 16) std::printf(" lazily: %s\n", qPrintable(line.left(300)));
|
||||
}
|
||||
check(lazy_lines == usual_lines, "a new revision saved as when built");
|
||||
}
|
||||
|
||||
//Every symbol's numbering, in a stable order
|
||||
static QStringList numbering(const QETProject &project)
|
||||
@@ -176,6 +251,55 @@ static QString saved(QETProject &project)
|
||||
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_uuid, linked_uuid FROM link ORDER BY element_uuid, 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)
|
||||
@@ -240,7 +364,7 @@ int main(int argc, char **argv)
|
||||
qputenv("QET_LAZY_SAVE_KEPT", "1");
|
||||
try {
|
||||
const QString output = QString::fromLocal8Bit(argv[3]);
|
||||
qunsetenv("QET_LAZY_FOLIOS");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
//A file saved by this version: its folios carry their uuids,
|
||||
//which opening without building them needs
|
||||
const QString path = output + "/lazy_folios.qet";
|
||||
@@ -267,9 +391,9 @@ int main(int argc, char **argv)
|
||||
QETProject eager(path);
|
||||
check(eager.state() == QETProject::Ok, "project opens");
|
||||
{
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
QETProject lazy_strips(path);
|
||||
qunsetenv("QET_LAZY_FOLIOS");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
auto strip_of = [](const QETProject &project) {
|
||||
for (TerminalStrip *strip : project.terminalStrip())
|
||||
if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount();
|
||||
@@ -290,7 +414,7 @@ int main(int argc, char **argv)
|
||||
}
|
||||
check(linked > 0, "the project has linked symbols");
|
||||
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
auto same = [&](QETProject &lazy, int i, const char *what) {
|
||||
Diagram *folio = lazy.folios().at(i);
|
||||
lazy.buildFolioToShow(folio);
|
||||
@@ -323,6 +447,24 @@ int main(int argc, char **argv)
|
||||
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");
|
||||
revisionEveryFolio(path, output, "revised-qet", path);
|
||||
{
|
||||
//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");
|
||||
const QString wiring = WiringListExport(&exported).toCsvString(), usual_wiring = WiringListExport(&eager).toCsvString();
|
||||
if (wiring != usual_wiring) {
|
||||
const QStringList a = wiring.split('\n'), b = usual_wiring.split('\n');
|
||||
for (const QString &line : b) if (!a.contains(line)) std::printf(" wiring when built: %s\n", qPrintable(line));
|
||||
for (const QString &line : a) if (!b.contains(line)) std::printf(" wiring lazily: %s\n", qPrintable(line));
|
||||
std::printf(" %d lines lazily, %d when built\n", int(a.size()), int(b.size()));
|
||||
}
|
||||
check(wiring == usual_wiring, "wiring list of a .qet exported as when built");
|
||||
check(exported.unloadedFolioCount() == folios.size() - strip_folios.size(),
|
||||
"exporting a .qet's wire numbers and wiring list builds no folio");
|
||||
}
|
||||
//A symbol placed on a folio, numbered by a scheme others follow
|
||||
QString location, title;
|
||||
int folio = -1;
|
||||
@@ -400,9 +542,9 @@ int main(int argc, char **argv)
|
||||
}
|
||||
{
|
||||
QETProject edited(path), usual(path);
|
||||
qunsetenv("QET_LAZY_FOLIOS");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
QETProject usual_edited(path);
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
edit(edited, busiest);
|
||||
std::printf("edits on folio %d (%d links to other folios): %d folios not built\n",
|
||||
busiest + 1, most, edited.unloadedFolioCount());
|
||||
@@ -482,9 +624,9 @@ int main(int argc, char **argv)
|
||||
//After everyday edits on a folio: as when built, others not built
|
||||
{
|
||||
QETProject edited(path), usual(path);
|
||||
qunsetenv("QET_LAZY_FOLIOS");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
QETProject usual_edited(path);
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
edit(edited, busiest);
|
||||
edit(usual_edited, busiest);
|
||||
QFile file(path); file.open(QIODevice::ReadOnly);
|
||||
@@ -514,9 +656,9 @@ int main(int argc, char **argv)
|
||||
//links reach are built to be saved, the others not
|
||||
{
|
||||
QETProject renamed(path);
|
||||
qunsetenv("QET_LAZY_FOLIOS");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
QETProject usual_renamed(path);
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
for (QETProject *project : {&renamed, &usual_renamed}) {
|
||||
Diagram *folio = project->folios().at(busiest);
|
||||
TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock();
|
||||
@@ -531,9 +673,9 @@ int main(int argc, char **argv)
|
||||
//folio is built to be saved
|
||||
{
|
||||
QETProject changed(path);
|
||||
qunsetenv("QET_LAZY_FOLIOS");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
QETProject usual_changed(path);
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
for (QETProject *project : {&changed, &usual_changed}) {
|
||||
DiagramContext properties = project->projectProperties();
|
||||
properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1"));
|
||||
@@ -594,7 +736,7 @@ int main(int argc, char **argv)
|
||||
check(out.open(QIODevice::WriteOnly), "copy written");
|
||||
out.write(document.toByteArray(4));
|
||||
out.close();
|
||||
qputenv("QET_LAZY_FOLIOS", "1");
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
qputenv("QET_LAZY_FOLIOS_TRACE", "1");
|
||||
static int built_late; built_late = 0;
|
||||
const QtMessageHandler previous = qInstallMessageHandler(
|
||||
@@ -607,6 +749,408 @@ int main(int argc, char **argv)
|
||||
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 lazily: no folio built but those
|
||||
//the terminal strip needs, each built 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");
|
||||
std::printf(".qetz opened: %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 of a .qetz as when built");
|
||||
//The drawn items, through the view made of their tables: the
|
||||
//folios built for it, as for the tables (D6: none were)
|
||||
{
|
||||
QETProject drawn(zipped);
|
||||
auto count = [](QETProject &project) {
|
||||
QSqlQuery query = project.dataBase()->newQuery(QStringLiteral("SELECT COUNT(*) FROM drawing_item_view"));
|
||||
return query.exec() && query.next() ? query.value(0).toInt() : -1;
|
||||
};
|
||||
const int lazily = count(drawn), built = count(eager);
|
||||
std::printf(".qetz drawn items through their view: %d lazily, %d when built\n", lazily, built);
|
||||
check(built > 0, "the project has drawn items");
|
||||
check(lazily == built, "the drawn items' view as when built");
|
||||
}
|
||||
//Every wire's potential from the database as QElectroTech
|
||||
//follows it with every folio built
|
||||
{
|
||||
QETProject indexed_potentials(zipped);
|
||||
int same = 0, differ = 0, unknown = 0;
|
||||
for (Diagram *folio : eager.folios())
|
||||
for (Conductor *wire : folio->conductors()) {
|
||||
QSet<QUuid> live{wire->uuid()};
|
||||
for (Conductor *c : wire->relatedPotentialConductors(true)) live.insert(c->uuid());
|
||||
for (QETProject *project : {&eager, &indexed_potentials}) {
|
||||
const auto stored = project->dataBase()->potentialWires(wire->uuid());
|
||||
if (!stored) { ++unknown; continue; }
|
||||
if (*stored == live) { ++same; continue; }
|
||||
if (differ++ < 3)
|
||||
std::printf(" potential of %s: %d wires followed, %d from the database\n",
|
||||
qPrintable(wire->uuid().toString()), int(live.size()), int(stored->size()));
|
||||
}
|
||||
}
|
||||
std::printf("potentials from the database: %d same, %d differ, %d not answered\n", same, differ, unknown);
|
||||
check(differ == 0 && same > 0, "every wire's potential from the database as followed");
|
||||
check(indexed_potentials.unloadedFolioCount() > 0, "potentials read without building");
|
||||
}
|
||||
//The folios' rows as saved, not as reading their headers one
|
||||
//by one numbers them ("1/1" for the first of three, D7)
|
||||
{
|
||||
auto folioRows = [](QETProject &project) {
|
||||
QStringList rows;
|
||||
for (const QString &row : tables(project))
|
||||
if (row.startsWith("diagram")) rows << row;
|
||||
return rows;
|
||||
};
|
||||
check(folioRows(indexed) == folioRows(eager), "the folios' rows of a .qetz as when built");
|
||||
//Folio numbers that count the folios ("%id/%total")
|
||||
const QString totals = output + "/totals.qetz";
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
{
|
||||
QETProject numbered(zipped);
|
||||
QetScriptApi api(&numbered);
|
||||
for (int i = 0 ; i < api.folioCount() ; ++i)
|
||||
api.setFolioProperty(i, "folio", "%id/%total");
|
||||
check(numbered.writeContainerFile(totals, &error), "folio numbers with totals saved");
|
||||
}
|
||||
QETProject usual_totals(totals);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
QETProject lazy_totals(totals);
|
||||
check(lazy_totals.unloadedFolioCount() > 0, "opened lazily");
|
||||
const QStringList lazy_rows = folioRows(lazy_totals), usual_rows = folioRows(usual_totals);
|
||||
if (lazy_rows != usual_rows)
|
||||
for (int i = 0 ; i < lazy_rows.size() && i < 3 ; ++i)
|
||||
std::printf(" lazily %s\n built %s\n", qPrintable(lazy_rows.at(i).left(140)),
|
||||
qPrintable(usual_rows.value(i).left(140)));
|
||||
check(lazy_rows == usual_rows, "folio numbers with totals of a .qetz 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 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 (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 folios built\n", built);
|
||||
check(asked.unloadedFolioCount() > 0, "asking a .qetz does not build every folio");
|
||||
}
|
||||
|
||||
//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");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
QETProject usual_shuffled(zipped);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
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<QetGraphicsTableItem *> &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");
|
||||
}
|
||||
|
||||
//A script (and the assistant's live mode) asking about the
|
||||
//folios and one folio's items: that folio built, no other (D5)
|
||||
{
|
||||
QETProject scripted(zipped);
|
||||
const int unbuilt = scripted.unloadedFolioCount();
|
||||
QetScriptApi api(&scripted), usual_api(&eager);
|
||||
check(api.folioCount() == usual_api.folioCount(), "a script counts the folios as when built");
|
||||
for (int i = 0 ; i < folios.size() ; ++i) {
|
||||
check(api.folioTitle(i) == usual_api.folioTitle(i) && api.folioUuid(i) == usual_api.folioUuid(i)
|
||||
&& api.folioBorder(i, "columns") == usual_api.folioBorder(i, "columns"),
|
||||
"a script reads a folio's own data as when built");
|
||||
}
|
||||
check(scripted.unloadedFolioCount() == unbuilt, "reading the folios' own data builds none");
|
||||
//The folio with the most links to other folios
|
||||
const int target = busiest;
|
||||
check(!scripted.folios().at(target)->isLoaded(), "a folio not built");
|
||||
QStringList lazy_uuids = api.elementUuids(target), usual_uuids = usual_api.elementUuids(target);
|
||||
lazy_uuids.sort(); usual_uuids.sort();
|
||||
check(lazy_uuids == usual_uuids, "a script lists a folio's symbols as when built");
|
||||
QStringList lazy_wires = api.conductors(target), usual_wires = usual_api.conductors(target);
|
||||
lazy_wires.sort(); usual_wires.sort();
|
||||
check(lazy_wires == usual_wires, "a script lists a folio's wires as when built");
|
||||
check(scripted.folios().at(target)->isLoaded(), "the folio a script reads is built");
|
||||
std::printf(".qetz script on folio %d: %d folios built\n", target + 1,
|
||||
unbuilt - scripted.unloadedFolioCount());
|
||||
check(unbuilt - scripted.unloadedFolioCount() == 1, "a script reading one folio builds that folio only");
|
||||
}
|
||||
|
||||
//A script changes the information of a symbol on a folio not
|
||||
//built, which nothing else shows: through the project
|
||||
//database, no folio built; saved as when built; undone (D5)
|
||||
{
|
||||
QETProject changed(zipped);
|
||||
const int unbuilt = changed.unloadedFolioCount();
|
||||
int folio_index = -1;
|
||||
QString uuid;
|
||||
for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) {
|
||||
if (changed.folios().at(i)->isLoaded()) continue;
|
||||
for (Element *element : folios.at(i)->elements())
|
||||
if (changed.dataBase()->canEditUnbuilt(element->uuid())) {
|
||||
folio_index = i; uuid = element->uuid().toString(); break;
|
||||
}
|
||||
}
|
||||
check(!uuid.isEmpty(), "a symbol to change through the database");
|
||||
QetScriptApi api(&changed);
|
||||
const QString before = api.elementInfo(folio_index, uuid, "comment");
|
||||
const QString value = QStringLiteral("changed through the database");
|
||||
check(api.setElementInfo(folio_index, uuid, "comment", value), "a script changes a symbol not built");
|
||||
check(api.elementInfo(folio_index, uuid, "comment") == value, "the change read back");
|
||||
check(changed.unloadedFolioCount() == unbuilt, "changing a symbol not built builds no folio");
|
||||
const QString changed_path = output + "/changed.qetz";
|
||||
check(changed.writeContainerFile(changed_path, &error), "saved after the change");
|
||||
std::printf(".qetz symbol on folio %d changed by a script: %d folios built, %d by saving\n",
|
||||
folio_index + 1, 0, unbuilt - changed.unloadedFolioCount());
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
const QString usual_path = output + "/changed-usual.qetz";
|
||||
{
|
||||
QETProject usual_changed(zipped);
|
||||
QetScriptApi usual_api(&usual_changed);
|
||||
check(usual_api.setElementInfo(folio_index, uuid, "comment", value), "the same change when built");
|
||||
check(usual_changed.writeContainerFile(usual_path, &error), "saved after the change when built");
|
||||
}
|
||||
QETProject changed_back(changed_path), usual_back(usual_path);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
check(sortedLines(changed_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path),
|
||||
"a symbol changed through the database saved as when built");
|
||||
check(QetScriptApi(&changed_back).elementInfo(folio_index, uuid, "comment") == value, "the change saved");
|
||||
//Undone, then redone and the folio built: the symbol shows it
|
||||
changed.undoStack()->undo();
|
||||
check(api.elementInfo(folio_index, uuid, "comment") == before, "the change undone");
|
||||
changed.undoStack()->redo();
|
||||
changed.buildFolioToShow(changed.folios().at(folio_index));
|
||||
Element *built_symbol = nullptr;
|
||||
for (Element *element : changed.folios().at(folio_index)->elements())
|
||||
if (element->uuid().toString() == uuid) built_symbol = element;
|
||||
check(built_symbol && built_symbol->elementInformations().value("comment").toString() == value,
|
||||
"a symbol changed before its folio was built shows the change");
|
||||
}
|
||||
|
||||
//A new revision on every folio: saved from the folios as read,
|
||||
//not built; a label change builds its folio (D5)
|
||||
revisionEveryFolio(zipped, output, "revised", path);
|
||||
revisionEveryFolio(zipped, output, "relabelled", path, busiest);
|
||||
|
||||
//A script changes a wire's cable by its uuid, on folios not
|
||||
//built, the wire and its potential numbered by hand: through
|
||||
//the project database, no folio built; saved as when built;
|
||||
//undone; the folio built afterwards shows it (D5)
|
||||
{
|
||||
QETProject cabled(zipped);
|
||||
const int unbuilt = cabled.unloadedFolioCount();
|
||||
int folio_index = -1;
|
||||
QString uuid;
|
||||
QSet<QUuid> potential;
|
||||
for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) {
|
||||
if (cabled.folios().at(i)->isLoaded()) continue;
|
||||
for (Conductor *wire : folios.at(i)->conductors()) {
|
||||
QSet<Conductor *> wires = wire->relatedPotentialConductors(true);
|
||||
wires << wire;
|
||||
bool usable = true;
|
||||
for (Conductor *c : std::as_const(wires))
|
||||
if (!c->properties().m_formula.isEmpty()
|
||||
|| cabled.folios().at(eager.folioIndex(c->diagram()))->isLoaded())
|
||||
usable = false;
|
||||
if (!usable || wires.size() < 2) continue;
|
||||
folio_index = i;
|
||||
uuid = wire->uuid().toString();
|
||||
for (Conductor *c : std::as_const(wires)) potential.insert(c->uuid());
|
||||
break;
|
||||
}
|
||||
}
|
||||
check(!uuid.isEmpty(), "a wire numbered by hand, its potential on folios not built");
|
||||
QetScriptApi api(&cabled);
|
||||
const QString before = api.conductorPropertyByUuid(folio_index, uuid, "cable");
|
||||
const QString value = QStringLiteral("W-42");
|
||||
check(api.setConductorPropertyByUuid(folio_index, uuid, "cable", value), "a script changes a wire not built");
|
||||
check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == value, "the wire's change read back");
|
||||
for (const QUuid &wire : std::as_const(potential))
|
||||
check(cabled.dataBase()->conductorProperties(wire).m_cable == value, "every wire of the potential changed");
|
||||
check(cabled.unloadedFolioCount() == unbuilt, "changing wires not built builds no folio");
|
||||
const QString cabled_path = output + "/cabled.qetz";
|
||||
check(cabled.writeContainerFile(cabled_path, &error), "saved after the wire change");
|
||||
std::printf(".qetz wire on folio %d, potential of %d wires, cable changed by uuid: 0 folios built, %d by saving\n",
|
||||
folio_index + 1, int(potential.size()), unbuilt - cabled.unloadedFolioCount());
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
const QString usual_path = output + "/cabled-usual.qetz";
|
||||
{
|
||||
QETProject usual_cabled(zipped);
|
||||
check(QetScriptApi(&usual_cabled).setConductorPropertyByUuid(folio_index, uuid, "cable", value),
|
||||
"the same wire change when built");
|
||||
check(usual_cabled.writeContainerFile(usual_path, &error), "saved after the wire change when built");
|
||||
}
|
||||
QETProject cabled_back(cabled_path), usual_back(usual_path);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
auto lines = [&path](QETProject &project) {
|
||||
QStringList list = sortedLines(project.toXml().toString(), path);
|
||||
list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); });
|
||||
return list;
|
||||
};
|
||||
check(lines(cabled_back) == lines(usual_back), "a wire changed through the database saved as when built");
|
||||
cabled.undoStack()->undo();
|
||||
check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == before, "the wire change undone");
|
||||
cabled.undoStack()->redo();
|
||||
Conductor *built_wire = nullptr;
|
||||
for (Conductor *wire : cabled.folios().at(folio_index)->conductors())
|
||||
if (wire->uuid().toString() == uuid) built_wire = wire;
|
||||
check(!built_wire, "the folio not built yet");
|
||||
cabled.buildFolioToShow(cabled.folios().at(folio_index));
|
||||
for (Conductor *wire : cabled.folios().at(folio_index)->conductors())
|
||||
if (wire->uuid().toString() == uuid) built_wire = wire;
|
||||
check(built_wire && built_wire->properties().m_cable == value,
|
||||
"a wire changed before its folio was built shows the change");
|
||||
}
|
||||
|
||||
//The wire numbers and wiring list exports, 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");
|
||||
const QString wiring = WiringListExport(&exported).toCsvString(), usual_wiring = WiringListExport(&eager).toCsvString();
|
||||
if (wiring != usual_wiring) {
|
||||
const QStringList a = wiring.split('\n'), b = usual_wiring.split('\n');
|
||||
for (const QString &line : b) if (!a.contains(line)) std::printf(" wiring when built: %s\n", qPrintable(line));
|
||||
for (const QString &line : a) if (!b.contains(line)) std::printf(" wiring lazily: %s\n", qPrintable(line));
|
||||
}
|
||||
check(wiring == usual_wiring, "wiring list exported as when built");
|
||||
check(exported.unloadedFolioCount() == unbuilt, "exporting wire numbers and the wiring list 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");
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
QETProject kept_back(kept_path);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
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";
|
||||
qputenv("QET_EAGER_FOLIOS", "1");
|
||||
{
|
||||
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);
|
||||
qunsetenv("QET_EAGER_FOLIOS");
|
||||
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) {
|
||||
|
||||
@@ -54,7 +54,7 @@ subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], che
|
||||
output = build / 'lazy-folios'
|
||||
output.mkdir(exist_ok=True)
|
||||
report = output / 'results.txt'
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=500)
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=1500)
|
||||
if report.exists():
|
||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||
sys.exit(result.returncode)
|
||||
|
||||
@@ -69,6 +69,19 @@ class tst_qetcontainer : public QObject
|
||||
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
|
||||
}
|
||||
|
||||
//@p document without what QElectroTech computes, which a .qetz
|
||||
//does not keep (format 4); tst_resaveunchanged checks that
|
||||
//QElectroTech gives it back
|
||||
static QDomDocument withoutComputed(const QDomDocument &document)
|
||||
{
|
||||
QDomDocument copy = document.cloneNode(true).toDocument();
|
||||
const bool own_uuids = QetContainer::ownUuids(copy.documentElement());
|
||||
for (QDomElement diagram = copy.documentElement().firstChildElement(QStringLiteral("diagram"));
|
||||
!diagram.isNull() ; diagram = diagram.nextSiblingElement(QStringLiteral("diagram")))
|
||||
QetContainer::dropComputed(diagram, own_uuids);
|
||||
return copy;
|
||||
}
|
||||
|
||||
static QByteArray entry(const QList<QetZip::Entry> &entries, const QString &name)
|
||||
{
|
||||
for (const QetZip::Entry &e : entries)
|
||||
@@ -117,7 +130,7 @@ class tst_qetcontainer : public QObject
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
if (!QetContainer::join(entries, &back, &error)) return error;
|
||||
return difference(original.documentElement(), back.documentElement(), QString());
|
||||
return difference(withoutComputed(original).documentElement(), back.documentElement(), QString());
|
||||
}
|
||||
|
||||
private slots:
|
||||
@@ -151,7 +164,8 @@ private slots:
|
||||
QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error));
|
||||
QDomDocument back;
|
||||
QVERIFY2(QetContainer::join(unzipped, &back, &error), qPrintable(error));
|
||||
const QString d = difference(original.documentElement(), back.documentElement(), QString());
|
||||
const QString d = difference(withoutComputed(original).documentElement(),
|
||||
back.documentElement(), QString());
|
||||
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||
}
|
||||
|
||||
@@ -187,7 +201,9 @@ private slots:
|
||||
|
||||
// The engineering data is in project.sqlite and nowhere else: the folio
|
||||
// files hold no symbol information, links, wire numbers or title-block
|
||||
// fields, and the database holds them all (industrial.qet: 50 folios).
|
||||
// fields, nor what QElectroTech computes from them (a wire's
|
||||
// description of its ends, a symbol text showing its information), and
|
||||
// the database holds them all (industrial.qet: 50 folios).
|
||||
void databaseHoldsTheData()
|
||||
{
|
||||
QDomDocument original;
|
||||
@@ -232,6 +248,37 @@ private slots:
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
// Nor does a folio file hold what QElectroTech computes (format 3): a
|
||||
// wire's description of its ends, a symbol text showing its information.
|
||||
// A file holding both, so that the check can fail.
|
||||
void computedNotKept()
|
||||
{
|
||||
QDomDocument original;
|
||||
QString error;
|
||||
QVERIFY(QetContainer::parse(read(QStringLiteral(QET_EXAMPLES_DIR "/schema_unifilaire_voltaique2.qet")),
|
||||
&original, &error));
|
||||
const QDomNodeList texts = original.elementsByTagName(QStringLiteral("dynamic_elmt_text"));
|
||||
bool filled = false;
|
||||
for (int i = 0 ; i < texts.size() && !filled ; ++i) {
|
||||
QDomElement text = texts.at(i).toElement();
|
||||
if (text.attribute(QStringLiteral("text_from")) != QLatin1String("ElementInfo")) continue;
|
||||
QDomElement content = text.firstChildElement(QStringLiteral("text"));
|
||||
while (content.hasChildNodes()) content.removeChild(content.firstChild());
|
||||
content.appendChild(original.createTextNode(QStringLiteral("COMPUTED-LABEL")));
|
||||
filled = true;
|
||||
}
|
||||
QVERIFY(filled);
|
||||
QVERIFY(original.toString().contains(QStringLiteral("element1_label=")));
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||
for (const QetZip::Entry &e : entries) {
|
||||
if (!e.name.startsWith(QStringLiteral("folios/"))) continue;
|
||||
QVERIFY2(!e.data.contains("element1_label=") && !e.data.contains("terminalname2="),
|
||||
qPrintable(e.name));
|
||||
QVERIFY2(!e.data.contains("COMPUTED-LABEL"), qPrintable(e.name));
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal strips move into the database, and come back exactly:
|
||||
// industrial.qet with two strips on its own terminal symbols, parsed as
|
||||
// a save leaves it (no whitespace text).
|
||||
@@ -268,7 +315,8 @@ private slots:
|
||||
QVERIFY2(!e.data.contains("<terminal_strip>"), "the strips stayed in project.xml");
|
||||
QDomDocument back;
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
||||
const QString d = difference(original.documentElement(), back.documentElement(), QString());
|
||||
const QString d = difference(withoutComputed(original).documentElement(),
|
||||
back.documentElement(), QString());
|
||||
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||
}
|
||||
|
||||
@@ -307,7 +355,7 @@ private slots:
|
||||
QDomDocument back;
|
||||
QetContainer::Pictures pictures;
|
||||
QString error;
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error, &pictures), qPrintable(error));
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, &pictures), qPrintable(error));
|
||||
const QDomNodeList images = back.elementsByTagName(QStringLiteral("image"));
|
||||
const QDomNodeList original_images = original.elementsByTagName(QStringLiteral("image"));
|
||||
QCOMPARE(images.size(), 121);
|
||||
@@ -326,7 +374,7 @@ private slots:
|
||||
for (int i = 0 ; i < entries.size() ; ++i)
|
||||
if (entries.at(i).name.startsWith(QStringLiteral("images/"))) { entries.removeAt(i); break; }
|
||||
QDomDocument broken;
|
||||
QVERIFY(!QetContainer::join(entries, &broken, &error, &pictures));
|
||||
QVERIFY(!QetContainer::join(entries, &broken, &error, nullptr, &pictures));
|
||||
QVERIFY2(error.contains(QStringLiteral("missing")), qPrintable(error));
|
||||
}
|
||||
|
||||
@@ -348,9 +396,9 @@ private slots:
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
int format = 0;
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, &format), qPrintable(error));
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, nullptr, &format), qPrintable(error));
|
||||
QCOMPARE(format, later);
|
||||
const QString d = difference(original.documentElement(), back.documentElement(), QString());
|
||||
const QString d = difference(withoutComputed(original).documentElement(), back.documentElement(), QString());
|
||||
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||
}
|
||||
|
||||
@@ -389,7 +437,7 @@ private slots:
|
||||
}
|
||||
|
||||
// The schema in QETZ-FORMAT.md is the one written: same tables, same
|
||||
// columns in the same order, same types and keys
|
||||
// columns in the same order, same types; written without keys (format 4)
|
||||
void specMatchesSchema()
|
||||
{
|
||||
const QString spec = QString::fromUtf8(read(QStringLiteral(QET_FORMAT_SPEC)));
|
||||
@@ -402,15 +450,15 @@ private slots:
|
||||
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||
const QByteArray written = entry(entries, QStringLiteral("project.sqlite"));
|
||||
|
||||
auto describe = [](sqlite3 *db) {
|
||||
auto describe = [](sqlite3 *db, const char *columns = "m.name, p.name, p.type") {
|
||||
QStringList out;
|
||||
sqlite3_stmt *st = nullptr;
|
||||
sqlite3_prepare_v2(db, "SELECT m.name, p.name, p.type, p.pk, p.\"notnull\" FROM sqlite_master m, "
|
||||
sqlite3_prepare_v2(db, QByteArray("SELECT ") + columns + " FROM sqlite_master m, "
|
||||
"pragma_table_info(m.name) p WHERE m.type = 'table' ORDER BY m.name, p.cid",
|
||||
-1, &st, nullptr);
|
||||
while (st && sqlite3_step(st) == SQLITE_ROW) {
|
||||
QStringList row;
|
||||
for (int i = 0 ; i < 5 ; ++i)
|
||||
for (int i = 0 ; i < sqlite3_column_count(st) ; ++i)
|
||||
row << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(st, i)));
|
||||
out << row.join(QLatin1Char(' '));
|
||||
}
|
||||
@@ -426,6 +474,9 @@ private slots:
|
||||
memcpy(copy, written.constData(), size_t(written.size()));
|
||||
sqlite3_deserialize(from_file, "main", copy, written.size(), written.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
|
||||
const QStringList spec_tables = describe(from_spec), file_tables = describe(from_file);
|
||||
const QStringList keys = describe(from_file, "p.pk + p.\"notnull\"");
|
||||
QVERIFY(!keys.isEmpty());
|
||||
QCOMPARE(keys.count(QStringLiteral("0")), keys.size());
|
||||
sqlite3_close(from_spec);
|
||||
sqlite3_close(from_file);
|
||||
QVERIFY(!spec_tables.isEmpty());
|
||||
@@ -461,6 +512,25 @@ private slots:
|
||||
|
||||
// A crafted project.sqlite whose "element" is a view, not a table, is
|
||||
// refused: nothing in a file's schema is trusted.
|
||||
void garbageDatabaseRefused()
|
||||
{
|
||||
//Bytes that are no database: refused, not read (crashed reading
|
||||
//the first statement until stage D7)
|
||||
QDomDocument original;
|
||||
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original);
|
||||
bool replaced = false;
|
||||
for (QetZip::Entry &entry : entries)
|
||||
if (entry.name == QLatin1String("project.sqlite")) {
|
||||
entry.data = QByteArray("not a database").repeated(100);
|
||||
replaced = true;
|
||||
}
|
||||
QVERIFY(replaced);
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
QVERIFY(!QetContainer::join(entries, &back, &error));
|
||||
QVERIFY(!error.isEmpty());
|
||||
}
|
||||
|
||||
// project.sqlite changed by @p sql in @p entries
|
||||
static void alterDatabase(QList<QetZip::Entry> &entries, const char *sql)
|
||||
{
|
||||
@@ -492,6 +562,9 @@ private slots:
|
||||
<< QByteArray("INSERT INTO element SELECT 'x', folio_uuid, prefix, freezeLabel, info_attrs,"
|
||||
" links_attrs FROM element LIMIT 1")
|
||||
<< QStringLiteral("symbol rows");
|
||||
QTest::newRow("symbol row twice")
|
||||
<< QByteArray("INSERT INTO element SELECT * FROM element LIMIT 1")
|
||||
<< QStringLiteral("symbol rows");
|
||||
QTest::newRow("wire row of no wire")
|
||||
<< QByteArray("INSERT INTO conductor SELECT 'x', folio_uuid, element1, terminal1, element2, terminal2,"
|
||||
" num, formula, function, tension_protocol, conductor_color, conductor_section, cable,"
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include "container/qetzip.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
// Saving a project that was just saved must change nothing. Two things
|
||||
// made the second save differ from the first, both cleanup done on save
|
||||
// but not on load:
|
||||
@@ -306,6 +310,10 @@ private slots:
|
||||
QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed");
|
||||
QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip");
|
||||
QVERIFY2(read(zipped).size() < read(straight).size() / 2, "the .qetz is not smaller");
|
||||
QCOMPARE(databaseTables(zipped), QStringList({
|
||||
"conductor", "element", "element_info", "folio", "folio_property", "link", "meta",
|
||||
"project_property", "strip_bridge", "strip_bridge_terminal", "strip_terminal",
|
||||
"terminal_strip"}));
|
||||
const QString back = resave(zipped);
|
||||
QVERIFY2(!back.isEmpty(), "opening the .qetz failed");
|
||||
|
||||
@@ -317,6 +325,31 @@ private slots:
|
||||
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
|
||||
}
|
||||
|
||||
// The tables of the database in the .qetz @p path: the document's
|
||||
// engineering data only, each fact once -- not QElectroTech's own
|
||||
// project database, built from it when the project opens
|
||||
static QStringList databaseTables(const QString &path)
|
||||
{
|
||||
QList<QetZip::Entry> entries;
|
||||
if (!QetZip::fromBytes(read(path), &entries)) return {QStringLiteral("not a zip")};
|
||||
QByteArray database;
|
||||
for (const QetZip::Entry &entry : std::as_const(entries))
|
||||
if (entry.name == QLatin1String("project.sqlite")) database = entry.data;
|
||||
sqlite3 *db = nullptr;
|
||||
sqlite3_open(":memory:", &db);
|
||||
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
|
||||
memcpy(copy, database.constData(), size_t(database.size()));
|
||||
sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
|
||||
sqlite3_stmt *list = nullptr;
|
||||
QStringList tables;
|
||||
sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", -1, &list, nullptr);
|
||||
while (list && sqlite3_step(list) == SQLITE_ROW)
|
||||
tables << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(list, 0)));
|
||||
sqlite3_finalize(list);
|
||||
sqlite3_close(db);
|
||||
return tables;
|
||||
}
|
||||
|
||||
// QElectroTech run in a sandbox of its own with @p args, the store
|
||||
// check on (QET_CHECK_ELEMENT_INFO_STORE); its output
|
||||
QString runChecked(const QStringList &args, bool eager = false)
|
||||
@@ -328,7 +361,7 @@ private slots:
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1"));
|
||||
if (eager) {
|
||||
env.remove(QStringLiteral("QET_LAZY_FOLIOS")); //every folio built on opening
|
||||
env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1")); //every folio built on opening
|
||||
}
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
@@ -479,6 +512,40 @@ private slots:
|
||||
QCOMPARE(rowsOf(edited), rowsOf(reopened));
|
||||
}
|
||||
|
||||
// A project read from a .qet and saved where it is, is saved as a .qetz
|
||||
// beside it, and the .qet is left as it was (stage D1).
|
||||
// What the .qetz holds is what a plain save of the .qet holds.
|
||||
void qetSavedAsQetzBeside()
|
||||
{
|
||||
#ifndef QET_HAS_SCRIPTING
|
||||
QSKIP("needs --run: this QElectroTech is built without Qt Qml");
|
||||
#endif
|
||||
const QString qet = m_dir.filePath(QStringLiteral("beside.qet"));
|
||||
QFile::remove(qet);
|
||||
QVERIFY(QFile::copy(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), qet));
|
||||
const QByteArray before = read(qet);
|
||||
const QString script = m_dir.filePath(QStringLiteral("beside.js"));
|
||||
QFile js(script);
|
||||
QVERIFY(js.open(QIODevice::WriteOnly));
|
||||
js.write("qet.log('SAVED ' + qet.save(''));\n");
|
||||
js.close();
|
||||
const QString log = runChecked({QStringLiteral("--run"), script, qet});
|
||||
QVERIFY2(log.contains(QStringLiteral("SAVED true")), qPrintable(log.right(400)));
|
||||
|
||||
QCOMPARE(read(qet), before);
|
||||
const QString qetz = m_dir.filePath(QStringLiteral("beside.qetz"));
|
||||
QVERIFY2(read(qetz).startsWith("PK\x03\x04"), "no .qetz beside the .qet");
|
||||
|
||||
auto without_save_stamps = [](QByteArray xml) {
|
||||
static const QRegularExpression stamp(QStringLiteral(
|
||||
"\\s*<property [^>]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*</property>"));
|
||||
return QString::fromUtf8(xml).remove(stamp);
|
||||
};
|
||||
const QString straight = resave(qet);
|
||||
const QString back = resave(qetz);
|
||||
QVERIFY(!straight.isEmpty() && !back.isEmpty());
|
||||
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
|
||||
}
|
||||
|
||||
// A title-block value that is a single space is kept through two saves
|
||||
// (#973), and a value with accents comes back as it went in.
|
||||
|
||||
Reference in New Issue
Block a user