Files
qelectrotech-source-mirror/tests/qttest/tst_conductorselfretrace.cpp
T
ispyisail 2ed85f6fc7 Address review: qRound-consistent guards, regression test
Addresses scorpio810's review of PR #890 (bugtracker #734):

- The four cas "3"/"4" bridge-skip guards now compare qRound()ed
  coordinates, matching how the bridge coordinate itself is computed --
  an exact != would miss a pair already grid-equal after rounding but
  off by a sub-pixel remainder, and still route a degenerate bridge for
  it. Verified no behavior change on the shipped corpus: per-file
  self-retrace counts are identical before/after (every coordinate
  there already lands exactly on-grid).
- Added tests/qttest/tst_conductorselfretrace.cpp, fixture
  qet_bug_repro_resaved.qet (the report's own canonical reproduction):
  exports it via the built binary's --export-svg and asserts no
  conductor path is self-retracing. Confirmed it actually catches the
  regression, not just passes vacuously -- reverted conductor.cpp to
  master and reran: fails, 1 self-retracing path found.

Not changed in code: the cas "4" descending-branch dead-code note (kept
for symmetry, as already agreed) and the schema_unifilaire_voltaique2.qet
trade-off (flagged for the reviewer's own visual check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 05:08:14 +12:00

119 lines
4.0 KiB
C++

#include <QtTest>
#include <QDir>
#include <QProcess>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QTemporaryDir>
namespace {
struct Point {
double x = 0.0;
double y = 0.0;
bool operator==(const Point &other) const { return x == other.x && y == other.y; }
};
// Mirrors count_retraces.py (qelectrotech-docker), which this project's own
// regression checks for bugtracker #734 already use: restricted to the
// conductor path shape (4-6 points, every segment axis-aligned, not closed)
// so legitimate closed element-graphics shapes (rectangles, polygons) are
// never mistaken for a self-retracing conductor.
QList<Point> parsePoints(const QString &d)
{
QList<Point> points;
static const QRegularExpression re(QStringLiteral("[ML]\\s*(-?\\d+\\.?\\d*)[ ,](-?\\d+\\.?\\d*)"));
auto it = re.globalMatch(d);
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
points << Point{m.captured(1).toDouble(), m.captured(2).toDouble()};
}
return points;
}
bool isConductorShaped(const QList<Point> &points)
{
if (points.size() < 4 || points.size() > 6) return false;
if (points.first() == points.last()) return false; // closed shape, not a conductor
for (int i = 0; i + 1 < points.size(); ++i) {
if (points.at(i).x != points.at(i + 1).x && points.at(i).y != points.at(i + 1).y)
return false; // not axis-aligned
}
return true;
}
bool isSelfRetracing(const QList<Point> &points)
{
// (a) skip-one out-and-back: ... A B A ...
for (int i = 0; i + 2 < points.size(); ++i) {
if (points.at(i) == points.at(i + 2)) return true;
}
// (b) immediate duplicate vertex: ... A A ...
for (int i = 0; i + 1 < points.size(); ++i) {
if (points.at(i) == points.at(i + 1)) return true;
}
// (c) 6-point crossed-stub loop: depart0 == arrivee AND depart == arrivee0
if (points.size() == 6 && points.at(0) == points.at(4) && points.at(1) == points.at(5)) return true;
return false;
}
} // namespace
// Bugtracker #734: Conductor::generateConductorPath()'s cas "3"/"4" grid-
// snap bridge used to route an unnecessary out-and-back excursion, or a
// small looping detour, when depart and arrivee already shared the axis the
// bridge would run along. This fixture (the report's own canonical
// reproduction, "qet_bug_repro_resaved.qet") is known to trigger it.
class tst_conductorselfretrace : public QObject
{
Q_OBJECT
private slots:
void noSelfRetracingPaths()
{
const QString binary = QStringLiteral(QET_TEST_BINARY_PATH);
QVERIFY2(QFile::exists(binary), qPrintable(QStringLiteral("qelectrotech binary not found at '%1'").arg(binary)));
const QString fixture = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
QTemporaryDir out_dir;
QVERIFY(out_dir.isValid());
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(binary, {QStringLiteral("--export-svg"), fixture, out_dir.path()});
QVERIFY2(proc.waitForFinished(30000), "export --export-svg timed out or failed to start");
QCOMPARE(proc.exitCode(), 0);
QDir dir(out_dir.path());
const QStringList svgs = dir.entryList({QStringLiteral("*.svg")}, QDir::Files);
QVERIFY2(!svgs.isEmpty(), "no SVG exported");
static const QRegularExpression path_re(QStringLiteral("<path[^>]*\\bd=\"([^\"]+)\""));
int retraces = 0;
for (const QString &name : svgs) {
QFile file(dir.filePath(name));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(file.readAll());
auto it = path_re.globalMatch(content);
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
const QList<Point> points = parsePoints(m.captured(1));
if (isConductorShaped(points) && isSelfRetracing(points)) {
++retraces;
}
}
}
QCOMPARE(retraces, 0);
}
};
QTEST_APPLESS_MAIN(tst_conductorselfretrace)
#include "tst_conductorselfretrace.moc"