From 5641b031f9a9c83e96bb93806f323435cc9503a6 Mon Sep 17 00:00:00 2001 From: DaTweety <129504390+DaTweety@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:22:59 +0200 Subject: [PATCH] Jump between folio report ends with a pointing hand and a highlight A linked folio report now shows a pointing hand under the mouse, on its arrow as well as on the text that shows its label (which already turned blue), and a double click on the arrow goes to the other end, as a double click on the text did. Before, a double click on the arrow opened the report's properties, which stay reachable from the context menu and the properties panel. The end jumped to is highlighted for two seconds, with the same blue as the hover highlight but independent of it: the hover highlight is cleared as soon as the mouse leaves the end jumped from, so on the same folio the user saw nothing mark where the jump landed. The text of a linked slave that shows its master's label gets the same blue and hand, since a double click on it already jumped to the master. Co-Authored-By: Claude Opus 5.5 --- .../dynamicelementtextitem.cpp | 52 +-- .../qetgraphicsitem/dynamicelementtextitem.h | 1 + sources/qetgraphicsitem/element.cpp | 68 +++- sources/qetgraphicsitem/element.h | 6 + sources/qetgraphicsitem/qetgraphicsitem.cpp | 5 + tests/qttest/CMakeLists.txt | 10 + tests/qttest/fixtures/linked_reports.qet | 312 ++++++++++++++++++ tests/qttest/report_jump_probe.cpp | 143 ++++++++ tests/qttest/run_report_jump_probe.py | 60 ++++ 9 files changed, 631 insertions(+), 26 deletions(-) create mode 100644 tests/qttest/fixtures/linked_reports.qet create mode 100644 tests/qttest/report_jump_probe.cpp create mode 100755 tests/qttest/run_report_jump_probe.py diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 51a14ed0b..18410e455 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -758,14 +758,11 @@ void DynamicElementTextItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) if(isSelected()) return; - if ((parentElement()->linkType() & Element::AllReport) && m_other_report) + if (linkedElementShown()) { - if( (m_text_from == ElementInfo && m_info_name == "label") || - (m_text_from == CompositeText && m_composite_text.contains("%{label}")) ) - { - m_user_color = color(); - setDefaultTextColor(Qt::blue); - } + m_user_color = color(); + setDefaultTextColor(Qt::blue); + setCursor(Qt::PointingHandCursor); } } @@ -781,6 +778,7 @@ void DynamicElementTextItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { setDefaultTextColor(m_user_color); m_user_color = QColor(); //m_user_color is now invalid + unsetCursor(); } } @@ -1601,6 +1599,27 @@ QString DynamicElementTextItem::reportReplacedCompositeText() const return string; } +/** + @brief DynamicElementTextItem::linkedElementShown + @return the element this text leads to: the master of a linked slave + or the other end of a linked folio report, when the text shows its + label; nullptr otherwise. Such a text turns blue under the mouse, shows + a pointing hand and jumps there on a double click. +*/ +Element *DynamicElementTextItem::linkedElementShown() const +{ + if(!parentElement()) + return nullptr; + if (!((m_text_from == ElementInfo && m_info_name == "label") || + (m_text_from == CompositeText && m_composite_text.contains("%{label}")))) + return nullptr; + if(parentElement()->linkType() == Element::Slave && m_master_element) + return m_master_element.data(); + if((parentElement()->linkType() & Element::AllReport) && m_other_report) + return m_other_report.data(); + return nullptr; +} + /** @brief DynamicElementTextItem::zoomToLinkedElement If the parent element is a folio report or a slave element, @@ -1608,24 +1627,7 @@ QString DynamicElementTextItem::reportReplacedCompositeText() const */ void DynamicElementTextItem::zoomToLinkedElement() { - if(!parentElement()) - return; - - Element *zoomed_element = nullptr; - - if(parentElement()->linkType() == Element::Slave && m_master_element) - { - if ((m_text_from == ElementInfo && m_info_name == "label") || - (m_text_from == CompositeText && m_composite_text.contains("%{label}"))) - zoomed_element = m_master_element.data(); - } - if((parentElement()->linkType() & Element::AllReport) && m_other_report) - { - if((m_text_from == ElementInfo && m_info_name == "label") || - (m_text_from == CompositeText && m_composite_text.contains("%{label}"))) - zoomed_element = m_other_report.data(); - } - + Element *zoomed_element = linkedElementShown(); if(zoomed_element) { //Unselect and ungrab mouse to prevent unwanted diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index 524dd91ad..2a6044890 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -105,6 +105,7 @@ class DynamicElementTextItem : public DiagramTextItem static QString xmlTagName() {return QString("dynamic_elmt_text");} void setInfoName(const QString &info_name); QString infoName() const; + Element *linkedElementShown() const; void setCompositeText(const QString &text); QString compositeText() const; void setFrame(const bool frame); diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index c603a1e33..98ab82b48 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -49,6 +49,7 @@ #include #include #include +#include static const QString plcTerminalKeys[] = { QETInformation::ELMT_PLC_T1, @@ -253,6 +254,36 @@ void Element::setHighlighted(bool hl) update(); } +/** + @brief Element::showAsJumpTarget + Highlight this element for a moment, so the user sees where a jump + from a folio report (or a cross reference) landed. Independent of + setHighlighted(), which follows the mouse over a linked element and + would be cleared as soon as the mouse leaves the element jumped from. +*/ +void Element::showAsJumpTarget() +{ + ++m_jump_highlight; + update(); + QTimer::singleShot(2000, this, [this]() { + if (m_jump_highlight > 0) + --m_jump_highlight; + update(); + }); +} + +/** + @brief Element::jumpTarget + @return the element a double click on this one jumps to: the other end + of a linked folio report, or nullptr. +*/ +Element *Element::jumpTarget() const +{ + if ((linkType() & AllReport) && !connected_elements.isEmpty()) + return connected_elements.first(); + return nullptr; +} + /** @brief Element::displayHelpLine Display the help line of each terminal if b is true @@ -474,7 +505,7 @@ void Element::paint( const QStyleOptionGraphicsItem *options, QWidget *) { - if (m_must_highlight) { + if (m_must_highlight || m_jump_highlight > 0) { drawHighlight(painter, options); } @@ -2354,6 +2385,29 @@ void Element::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } } +/** + @brief Element::mouseDoubleClickEvent + A double click on a linked folio report goes to its other end, as a + double click on its text does; on any other element it opens the + properties. A report's properties stay reachable from the context menu + and the properties panel. + @param event +*/ +void Element::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + if (Element *target = jumpTarget()) + { + event->accept(); + //Unselect and ungrab mouse to prevent unwanted + //move when the target is in the same scene as this. + setSelected(false); + ungrabMouse(); + QetGraphicsItem::showItem(target); + return; + } + QetGraphicsItem::mouseDoubleClickEvent(event); +} + /** When mouse over element change m_mouse_over to true (used in paint() function ) @@ -2367,6 +2421,13 @@ void Element::hoverEnterEvent(QGraphicsSceneHoverEvent *e) foreach (Element *elmt, linkedElements()) elmt -> setHighlighted(true); + //A linked folio report jumps to its other end on a double + //click: the pointing hand says so + if (jumpTarget()) { + setCursor(Qt::PointingHandCursor); + m_jump_cursor = true; + } + m_mouse_over = true; setToolTip( name() ); update(); @@ -2385,6 +2446,11 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e) foreach (Element *elmt, linkedElements()) elmt -> setHighlighted(false); + if (m_jump_cursor) { + unsetCursor(); + m_jump_cursor = false; + } + m_mouse_over = false; update(); } diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index b5f6764f5..8a7afb311 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -176,6 +176,9 @@ class Element : public QetGraphicsItem }; ReloadPictureResult reloadPicture(); virtual void setHighlighted(bool); + void showAsJumpTarget(); + bool isShownAsJumpTarget() const { return m_jump_highlight > 0; } + Element *jumpTarget() const; void displayHelpLine(bool b = true); QSize size() const; QPixmap pixmap(); @@ -316,6 +319,7 @@ class Element : public QetGraphicsItem void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent( QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override; QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; @@ -359,6 +363,8 @@ class Element : public QetGraphicsItem void storeInformation(); projectDataBase *placedStore() const; bool m_must_highlight = false; + int m_jump_highlight = 0; ///< pending jump-target highlights, see showAsJumpTarget() + bool m_jump_cursor = false; QSize dimensions; QPoint hotspot_coord; bool m_mouse_over = false; diff --git a/sources/qetgraphicsitem/qetgraphicsitem.cpp b/sources/qetgraphicsitem/qetgraphicsitem.cpp index 631ee8d85..45a17fdd6 100644 --- a/sources/qetgraphicsitem/qetgraphicsitem.cpp +++ b/sources/qetgraphicsitem/qetgraphicsitem.cpp @@ -18,6 +18,7 @@ #include "qetgraphicsitem.h" #include "../diagram.h" +#include "element.h" /** @brief QetGraphicsItem::QetGraphicsItem @@ -38,6 +39,10 @@ void QetGraphicsItem::showItem(QetGraphicsItem *item) fit.adjust(-200, -200, 200, 200); view->fitInView(fit, Qt::KeepAspectRatioByExpanding); } + + //Mark where the jump landed + if (Element *element = dynamic_cast(item)) + element->showAsJumpTarget(); } } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 644cf4725..83249d7a1 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -112,6 +112,16 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet) set_tests_properties(tst_crosshairintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + # A linked folio report shows a pointing hand and jumps to its other + # end on a double click, on the arrow and on its label text; the end + # jumped to is highlighted for a moment. + add_test(NAME tst_reportjumpintegration + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_report_jump_probe.py + --build ${CMAKE_BINARY_DIR} --binary $ + --source ${CMAKE_CURRENT_SOURCE_DIR}/report_jump_probe.cpp + --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/linked_reports.qet) + set_tests_properties(tst_reportjumpintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") endif() if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") add_test(NAME tst_darkimageintegration diff --git a/tests/qttest/fixtures/linked_reports.qet b/tests/qttest/fixtures/linked_reports.qet new file mode 100644 index 000000000..57ab5bd11 --- /dev/null +++ b/tests/qttest/fixtures/linked_reports.qet @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2-C4 + label + + + + + + + + + + + + + label + + + + + + + + + + + + + + + + + + + + 1-C4 + label + + + + + + + + + + Elements importats + Zavedené prvky + Importerede elementer + Importierte Elemente + Εισηγμένα στοιχεία + Imported elements + Elementos importados + Éléments importés + Uvezeni elementi + Elementi importati + インバートされた要素 + 전기 심볼 요소 + 전기 심볼 요소 + Elementen geïmporteerd + Elementen geïmporteerd + Elementy importowane + Elementos importados + Elementos importados + Elemente importate + Импортированные элементы + Uvoženi elementi + Importerade element + İthal öğeler + Імпортовані елементи + 导入元件 + + + + كهربائي + Elèctric + Elektrotechnika + Elektrisk + Elektrik + Ηλεκτρικά + Electric + Eléctrica + Sähkö + Electrique + Električno + Elektromos + Elettrica + 電気 + 전기 + Цахилгаан + Elektrisk + Elektrotechniek + Elektrisch + Elektrotechnika + Elétrico + Elétrica + Electric + Электротехника + Elektrické + Elektrika + Електрика + El + Elektrik + Електрика + 10_电气 + 电气工程 + + + + متعدد الأقطاب + Multifilar + Vícežilový + Flere ledere + Allpolig + Πολυγραμμικό + All-pole + Multifilar + Yleisnapa + Multifilaire + Univerzalni + Többvonalas szimbólumok + Multifilare + 複線 + 다-극 + Бүх туйл + Alle poler + Veel polig + All-pole + Schematy wieloliniowe + Multipolar + Multifilar + Bipolar + Многополюсные + Všepólové + Vsi poli + Свe-полни + Allpolig + Çok kutuplu + Багатополюсні + 10_全极 + + + + إحالات + Referències als fulls + Odkazování listů + Ark henvisninger + Folienverweise + Αναφορές σελίδας + Folio referencing + Referencias + Lehtiviittaus + Reports de folio + Referenciranje folija + Tervlap hivatkozások + Riferimenti + フォリオ参照 + 도면 간 참조 + Хуудасны холбоос + Folio-referanse + Referenties + Folioverwijzing + Odsyłacze + Renvoi de folio + Referências + Renvoi de folio + Точки разрыва + Odkazy na listy + Sklici na list + Референцирање фолија + Bladhänvisning + Sayfa referansı + Посилання на аркуші + 图页引用 + + + + + + الصفحة التالية + Enllaç sortint + Další list + Går pil + Nächste Folie + Επόμενη σελίδα + Going arrow + Folio siguiente + Lähtevä nuoli + Folio suivant + Odlazna strelica + Elmenő hivatkozás + Foglio successivo + 出矢印 + 나가는 화살표 + Явж буй сум + Går pil + Referentie volgend + Gaande pijl + Wyjście adresowe + Seta de saída + Seta de saída (vai) + Săgeată de ieșire + Следующая страница + Odchádzajúca šípka + Odhodna puščica + Одлазна стрелица + Utgående pil + Giden ok + Стрілка виходу + 02引出箭头 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + label + + + + + + + + + + الصفحة السابقة + Enllaç entrant + Předchozí list + Kommer pil + Vorherige Folie + Προηγούμενη σελίδα + Coming arrow + folio anterior + Saapuva nuoli + Folio précédent + Dolazna strelica + Beérkező hivatkozás + Foglio precedente + 入り矢印 + 들어오는 화살표 + Ирж буй сум + Kommer pil + Referentie Vorig + Komende pijl + Wejście adresowe + Seta de entrada + Seta de entrada (vem) + Săgeată de intrare + Предыдущая страница + Prichádzajúca šípka + Prihajajoča puščica + Долазна стрелица + Inkommande pil + Gelen ok + Стрілка входу + 01引入箭头 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + label + + + + + + + + + + + diff --git a/tests/qttest/report_jump_probe.cpp b/tests/qttest/report_jump_probe.cpp new file mode 100644 index 000000000..f6aebe073 --- /dev/null +++ b/tests/qttest/report_jump_probe.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Jumping between the two ends of a folio report, linked against the +// application's objects. +// +// A linked folio report shows a pointing hand under the mouse, on its +// arrow and on the text that shows its label, and a double click on +// either goes to the other end. The end jumped to is highlighted for a +// moment, independently of the hover highlight, which the mouse leaving +// the end jumped from would clear at once. An unlinked report shows no +// hand and has nowhere to jump to. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../sources/diagram.h" +#include "../../sources/qetproject.h" +#include "../../sources/qetmessagebox.h" +#include "../../sources/qetgraphicsitem/element.h" +#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h" + +static void check(bool value, const char *message) +{ + if (!value) throw std::runtime_error(message); +} + +static void hover(Diagram *scene, QGraphicsItem *item, QEvent::Type type) +{ + QGraphicsSceneHoverEvent event(type); + event.setScenePos(item->sceneBoundingRect().center()); + event.setPos(item->boundingRect().center()); + scene->sendEvent(item, &event); +} + +static void doubleClick(Diagram *scene, QGraphicsItem *item) +{ + QGraphicsSceneMouseEvent event(QEvent::GraphicsSceneMouseDoubleClick); + event.setButton(Qt::LeftButton); + event.setButtons(Qt::LeftButton); + event.setScenePos(item->sceneBoundingRect().center()); + event.setPos(item->boundingRect().center()); + scene->sendEvent(item, &event); +} + +static void wait(int ms) +{ + QEventLoop loop; + QTimer::singleShot(ms, &loop, &QEventLoop::quit); + loop.exec(); +} + +static bool showsHand(const QGraphicsItem *item) +{ + return item->hasCursor() && item->cursor().shape() == Qt::PointingHandCursor; +} + +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("QETReportJumpRegression"); + QETProject::setBackupEnabled(false); + QET::QetMessageBox::setNonInteractive(true); + QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf"); + + try + { + QETProject project(QString::fromLocal8Bit(argv[1])); + check(project.state() == QETProject::Ok, "fixture opens"); + check(project.diagrams().size() == 2, "fixture has two folios"); + project.buildFolios(project.diagrams()); + Diagram *one = project.diagrams().at(0); + Diagram *two = project.diagrams().at(1); + + // Folio one: a going report linked to folio two, and one alone. + Element *going = nullptr, *alone = nullptr; + for (Element *element : one->elements()) { + if (!(element->linkType() & Element::AllReport)) continue; + (element->linkedElements().isEmpty() ? alone : going) = element; + } + check(going && alone, "a linked and an unlinked report on folio one"); + Element *coming = going->linkedElements().first(); + check(coming->diagram() == two, "the linked end is on folio two"); + + check(going->jumpTarget() == coming, "a linked report jumps to its other end"); + check(alone->jumpTarget() == nullptr, "an unlinked report has nowhere to jump to"); + + // The arrow: a pointing hand while the mouse is over it, only + // when linked. + hover(one, going, QEvent::GraphicsSceneHoverEnter); + check(showsHand(going), "pointing hand over a linked report"); + hover(one, going, QEvent::GraphicsSceneHoverLeave); + check(!going->hasCursor(), "hand gone when the mouse leaves"); + hover(one, alone, QEvent::GraphicsSceneHoverEnter); + check(!showsHand(alone), "no hand over an unlinked report"); + hover(one, alone, QEvent::GraphicsSceneHoverLeave); + + // Double click on the arrow: the other end is selected and + // highlighted, and stays highlighted when the mouse leaves the + // end jumped from. + hover(one, going, QEvent::GraphicsSceneHoverEnter); + doubleClick(one, going); + check(coming->isSelected(), "the end jumped to is selected"); + check(coming->isShownAsJumpTarget(), "the end jumped to is highlighted"); + hover(one, going, QEvent::GraphicsSceneHoverLeave); + check(coming->isShownAsJumpTarget(), "leaving the report keeps the jump highlight"); + wait(2300); + check(!coming->isShownAsJumpTarget(), "the jump highlight goes after a moment"); + + // The text showing the label: a pointing hand, and a double click + // jumps back. + DynamicElementTextItem *label = nullptr; + for (DynamicElementTextItem *text : coming->dynamicTextItems()) + if (text->linkedElementShown()) label = text; + check(label, "the coming report has a text that leads to its other end"); + check(label->linkedElementShown() == going, "the text leads to the going report"); + hover(two, label, QEvent::GraphicsSceneHoverEnter); + check(showsHand(label), "pointing hand over the label text"); + hover(two, label, QEvent::GraphicsSceneHoverLeave); + check(!label->hasCursor(), "hand gone when the mouse leaves the text"); + doubleClick(two, label); + check(going->isSelected(), "a double click on the text jumps back"); + check(going->isShownAsJumpTarget(), "the end jumped back to is highlighted"); + + std::puts("PASS: linked folio reports show a hand and jump to their highlighted other end"); + } + catch (const std::exception &error) + { + std::printf("FAIL: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/tests/qttest/run_report_jump_probe.py b/tests/qttest/run_report_jump_probe.py new file mode 100755 index 000000000..1da8b0528 --- /dev/null +++ b/tests/qttest/run_report_jump_probe.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Link the report jump integration probe using an existing Ninja app build. + +Reuses app objects without duplicating the app or introducing a core-library +refactor. No application source or existing executable is replaced. The +generated manifest, probe binary, reports and export fixtures stay in build/. +""" +import argparse +import pathlib +import re +import shutil +import subprocess +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('--build', required=True) +parser.add_argument('--binary', required=True) +parser.add_argument('--source', required=True) +parser.add_argument('--fixture', required=True) +args = parser.parse_args() +build = pathlib.Path(args.build).resolve() +binary = pathlib.Path(args.binary).resolve() +source = pathlib.Path(args.source).resolve() +fixture = pathlib.Path(args.fixture).resolve() +manifest = (build / 'build.ninja').read_text(encoding='utf-8') +match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE) +if not match: + sys.exit('Cannot find the application main object in the Ninja build.') +main_object = match.group(1) +probe_object = main_object.replace('main.cpp.', 'report_jump_probe.cpp.') +probe_binary = binary.with_name('report_jump_probe' + binary.suffix) +def escape(path): + return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ') +main_source = source.parents[2] / 'sources' / 'main.cpp' +manifest = manifest.replace(main_object, probe_object) +manifest = manifest.replace(escape(main_source), escape(source)) +if binary.suffix: + manifest = manifest.replace(binary.name, probe_binary.name) +else: + old_target = escape(binary.name if binary.parent == build else binary) + new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary) + manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':') + old_file = binary.name if binary.parent == build else binary.as_posix() + new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix() + manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$', + ' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE) +probe_manifest = build / 'report-jump-probe.ninja' +probe_manifest.write_text(manifest, encoding='utf-8') +ninja = shutil.which('ninja') +if not ninja: + sys.exit('Ninja is required for the integration probe.') +target = probe_binary.name if probe_binary.parent == build else str(probe_binary) +subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True) +output = build / 'report-jump' +output.mkdir(exist_ok=True) +report = output / 'results.txt' +result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90) +if report.exists(): + print(report.read_text(encoding='utf-8', errors='replace')) +sys.exit(result.returncode)