summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2023-05-04 13:52:15 +0200
committerShawn Rutledge <shawn.rutledge@qt.io>2023-05-04 21:25:05 +0000
commita5ad166f4362a4b8c9708d0fb3c20579b384ed99 (patch)
tree7979cbb530ae9df964a1a022747832f624ff0192
parent4d5efe889860f910d7d817a37df1ab08e97fa30a (diff)
Add support for PlantUML output
...and use that by default. QML can be obtained by setting the new MessageTrace.outputFormat property. Change-Id: I9a8a8caa8ed2c993c8681bfd50be74f1ec99738c Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
-rw-r--r--examples/hover.qml1
-rw-r--r--src/trace/qmlmessagetrace.cpp162
-rw-r--r--src/trace/qmlmessagetrace_p.h21
3 files changed, 166 insertions, 18 deletions
diff --git a/examples/hover.qml b/examples/hover.qml
index a807d50..55db541 100644
--- a/examples/hover.qml
+++ b/examples/hover.qml
@@ -48,6 +48,7 @@ Rectangle {
MessageTrace {
category: "qt.quick.hover.trace"
outputPrefix: "hoverSequence-"
+// outputFormat: MessageTrace.QML
}
MouseArea {
diff --git a/src/trace/qmlmessagetrace.cpp b/src/trace/qmlmessagetrace.cpp
index 98e5a50..616a40c 100644
--- a/src/trace/qmlmessagetrace.cpp
+++ b/src/trace/qmlmessagetrace.cpp
@@ -48,9 +48,10 @@
\class QmlMessageTrace
\inmodule UmlQuick
- \brief The QmlMessageTrace class provides a QML output stream for rendering
+ \brief The QmlMessageTrace class provides an output stream for rendering
a UML \l {https://en.wikipedia.org/wiki/Sequence_diagram}{Sequence diagram}
- (also known as a Message Trace diagram).
+ (also known as a Message Trace diagram). The outputFormat property specifies
+ what syntax to use for that.
\section1 Basic Use
@@ -63,9 +64,13 @@
emit signals) will be enough to reconstruct a fairly complete UML Message
Trace diagram.
- When the trace is completed, simply run the qml output file with the qml
- runtime tool. It will import components from Qt.labs.UmlQuick.Sequence, which
- should be installed already if you have built this module properly.
+ When the trace is completed, you have an output file. If outputFormat was
+ QML, simply run the qml output file with the qml runtime tool. It will
+ import components from Qt.labs.UmlQuick.Sequence, which should be installed
+ already if you have built this module properly.
+
+ If outputFormat was PlantUML, you need to use the plantuml tool to process
+ the puml file.
For example, to trace hover events in a QML application, create a file
traceHover.qml which looks like the following:
@@ -138,13 +143,15 @@
QT_BEGIN_NAMESPACE
-//#define MT_DEBUG_ENABLED
+#define MT_DEBUG_ENABLED
#ifdef MT_DEBUG_ENABLED
#define MT_DEBUG printf
#else
#define MT_DEBUG(format, args...) ((void)0)
#endif
+using namespace Qt::StringLiterals;
+
QHash<QByteArray, QList<QmlMessageTrace*> > QmlMessageTrace::m_categoryInstances;
QtMessageHandler QmlMessageTrace::m_parentMessageHandler(nullptr);
@@ -213,7 +220,7 @@ QmlMessageTrace::~QmlMessageTrace()
if (--m_refCount == 0)
qInstallMessageHandler(nullptr);
if (!m_messages.isEmpty())
- writeQml();
+ write();
}
void QmlMessageTrace::setCategory(QString cat)
@@ -257,10 +264,24 @@ void QmlMessageTrace::setEnabled(bool enabled)
m_enabled = enabled;
if (!enabled)
- writeQml();
+ write();
emit enabledChanged();
}
+QmlMessageTrace::OutputFormat QmlMessageTrace::outputFormat() const
+{
+ return m_outputFormat;
+}
+
+void QmlMessageTrace::setOutputFormat(OutputFormat fmt)
+{
+ if (m_outputFormat == fmt)
+ return;
+
+ m_outputFormat = fmt;
+ emit outputFormatChanged();
+}
+
void QmlMessageTrace::categoryFilter(QLoggingCategory *cat)
{
if (m_categoryInstances.contains(cat->categoryName())) {
@@ -321,7 +342,7 @@ void QmlMessageTrace::addObjectInstance(void *obj, const QString &objClass)
}
}
-void QmlMessageTrace::writeObjectInstance(QFile &f, QObject *o)
+void QmlMessageTrace::writeObjectInstanceQml(QFile &f, QObject *o)
{
if (!o)
return;
@@ -336,7 +357,33 @@ void QmlMessageTrace::writeObjectInstance(QFile &f, QObject *o)
if (oName.isEmpty())
oName = QStringLiteral("0x%1").arg(qulonglong(o), 0, 16);
f.write(QStringLiteral(" ObjectInstance { id: %1; objectName: \"%2\"; objectClass: \"%3\" }\n")
- .arg(pointerHash(o)).arg(oName).arg(className).toUtf8());
+ .arg(pointerHash(o)).arg(oName).arg(className).toUtf8());
+}
+
+void QmlMessageTrace::writeObjectInstancePuml(QFile &f, QObject *o)
+{
+ if (!o)
+ return;
+ QString className = QString::fromUtf8(o->metaObject()->className());
+ int qmlSuffixIdx = className.indexOf(QStringLiteral("_QML"));
+ if (qmlSuffixIdx > 0)
+ className = className.left(qmlSuffixIdx);
+ QObjectPrivate *opriv = QObjectPrivate::get(o);
+ QString oName;
+ if (!opriv->wasDeleted && !opriv->isDeletingChildren)
+ oName = o->objectName();
+ if (oName.isEmpty())
+ oName = QStringLiteral("0x%1").arg(qulonglong(o), 0, 16);
+ if (className.isEmpty()) {
+ qWarning() << "unknown class for" << o;
+ return;
+ }
+ if (oName.isEmpty()) {
+ qWarning() << "unknown object name for" << o;
+ return;
+ }
+ f.write(u"participant %1 as %2\n"_s
+ .arg(className).arg(oName).toUtf8());
}
void QmlMessageTrace::logBacktrace(QStringList trace)
@@ -401,10 +448,10 @@ void QmlMessageTrace::log(QtMsgType type, const QMessageLogContext &context, con
calleeInfo.remove(0, 1);
calleeInfo = calleeInfo.trimmed();
}
-// MT_DEBUG("callee %s::%s %p guts %s from %s\n", qPrintable(calleeClassName), qPrintable(m.calleeMethod), m.calleePointer, qPrintable(calleeInfo), qPrintable(m.params));
+ MT_DEBUG("callee %s::%s %p guts %s from %s\n", qPrintable(calleeClassName), qPrintable(m.calleeMethod), m.calleePointer, qPrintable(calleeInfo), qPrintable(m.params));
} else if (hasMatch(m_regexPointer, match, m.params) && match.capturedStart() == 0) {
// Got just a plain pointer to "this" which was supposed to be the first parameter to qCDebug
-// MT_DEBUG("captured pointer %s @%d from %s\n", qPrintable(match.captured(1)), match.capturedStart(), qPrintable(m.params));
+ MT_DEBUG("captured pointer %s @%lld from %s\n", qPrintable(match.captured(1)), match.capturedStart(), qPrintable(m.params));
callee0xPointerStr = match.captured(0);
m.calleePointer = stringToPointer(match.captured(1));
} else {
@@ -497,9 +544,23 @@ void QmlMessageTrace::log(QtMsgType type, const QMessageLogContext &context, con
m_messages.append(m);
}
-void QmlMessageTrace::writeQml()
+
+void QmlMessageTrace::write()
+{
+ QString filePath = m_outputPrefix + QDateTime::currentDateTime().toString(Qt::ISODate);
+ switch (m_outputFormat) {
+ case OutputFormat::QML:
+ writeQml(filePath);
+ break;
+ case OutputFormat::PlantUML:
+ writePuml(filePath);
+ break;
+ }
+}
+
+void QmlMessageTrace::writeQml(const QString &plainFilePath)
{
- QString filePath = QString::fromLocal8Bit("%1%2.qml").arg(m_outputPrefix).arg(QDateTime::currentDateTime().toString(Qt::ISODate));
+ QString filePath = plainFilePath + u".qml"_s;
MT_DEBUG("-> %s\n", qPrintable(filePath));
QFile f(filePath);
if (f.open(QFile::WriteOnly)) {
@@ -510,7 +571,7 @@ void QmlMessageTrace::writeQml()
QObject *callee = m_objects.value(m.calleePointer);
m.calleePointer = callee;
if (callee && !objectsWritten.contains(callee)) {
- writeObjectInstance(f, callee);
+ writeObjectInstanceQml(f, callee);
objectsWritten.insert(callee);
}
QObject *caller = m_objects.value(m.callerPointer);
@@ -521,7 +582,7 @@ void QmlMessageTrace::writeQml()
m.callerPointer = caller;
if (caller) {
if (!objectsWritten.contains(caller)) {
- writeObjectInstance(f, caller);
+ writeObjectInstanceQml(f, caller);
objectsWritten.insert(caller);
}
} else if (!ufosWritten.contains(m.callerClass)) {
@@ -538,6 +599,47 @@ void QmlMessageTrace::writeQml()
}
}
+void QmlMessageTrace::writePuml(const QString &plainFilePath)
+{
+ QString filePath = plainFilePath + u".puml"_s;
+ MT_DEBUG("-> %s\n", qPrintable(filePath));
+ QFile f(filePath);
+ if (f.open(QFile::WriteOnly)) {
+ f.write("@startuml\n");
+ QSet<void *> objectsWritten;
+ QSet<QString> ufosWritten;
+ for (Message &m : m_messages) {
+ QObject *callee = m_objects.value(m.calleePointer);
+// m.calleePointer = callee;
+ if (callee && !objectsWritten.contains(callee)) {
+ writeObjectInstancePuml(f, callee);
+ objectsWritten.insert(callee);
+ }
+ QObject *caller = m_objects.value(m.callerPointer);
+ if (!caller) {
+ void *p = m_recentObjectsByClass.value(m.callerClass);
+ caller = m_objects.value(p);
+ }
+ m.callerPointer = caller;
+ if (caller) {
+ if (!objectsWritten.contains(caller)) {
+ writeObjectInstancePuml(f, caller);
+ objectsWritten.insert(caller);
+ }
+ } else if (!m.callerClass.isEmpty() && !ufosWritten.contains(m.callerClass)) {
+ f.write(u"participant %1 as ufo_%2\n"_s
+ .arg(m.callerClass).arg(m.callerClass).toUtf8());
+ ufosWritten.insert(m.callerClass);
+ }
+ f.write(m.toPuml().toUtf8());
+ }
+ f.write("@enduml\n");
+ f.close();
+ } else {
+ printf("failed to write %s", qPrintable(filePath));
+ }
+}
+
QString QmlMessageTrace::Message::toQml() const
{
QString callerId = callerPointer ? pointerHash(callerPointer) : QStringLiteral("ufo_") + callerClass;
@@ -548,4 +650,32 @@ QString QmlMessageTrace::Message::toQml() const
.arg(callerMethod).arg(timestamp).arg(backtrace.join(QStringLiteral("\\n"))).arg(params).arg(inferredFromBacktrace ? "true" : "false");
}
+/*
+ Example:
+
+ @startuml
+ Alice -> Bob: Authentication Request
+ Bob --> Alice: Authentication Response
+
+ Alice -> Bob: Another authentication Request
+ Alice <-- Bob: Another authentication Response
+ @enduml
+*/
+QString QmlMessageTrace::Message::toPuml() const
+{
+ QString callerId = callerPointer ? pointerHash(callerPointer) : QStringLiteral("ufo_") + callerClass;
+
+MT_DEBUG(" callee %s %p\n", qPrintable(calleeSignature), calleePointer);
+
+ QString ret = u"%1 -> %2: %3(%4)\n"_s
+ .arg(callerId).arg(pointerHash(calleePointer)).arg(calleeMethod).arg(params);
+ if (!calleeSignature.isEmpty()) {
+ auto sigWords = calleeSignature.split(u' ');
+ if (sigWords.first() != u"void")
+ ret += u"%1 <-- %2: %3\n"_s
+ .arg(callerId).arg(pointerHash(calleePointer)).arg(sigWords.first());
+ }
+ return ret;
+}
+
QT_END_NAMESPACE
diff --git a/src/trace/qmlmessagetrace_p.h b/src/trace/qmlmessagetrace_p.h
index ba4625b..db27fa3 100644
--- a/src/trace/qmlmessagetrace_p.h
+++ b/src/trace/qmlmessagetrace_p.h
@@ -52,8 +52,15 @@ class QmlMessageTrace : public QObject
Q_PROPERTY(QString category READ category WRITE setCategory NOTIFY categoryChanged)
Q_PROPERTY(QString outputPrefix READ outputPrefix WRITE setOutputPrefix NOTIFY outputPrefixChanged)
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged)
+ Q_PROPERTY(OutputFormat outputFormat READ outputFormat WRITE setOutputFormat NOTIFY outputFormatChanged)
public:
+ enum class OutputFormat {
+ QML,
+ PlantUML
+ };
+ Q_ENUM(OutputFormat)
+
QmlMessageTrace();
~QmlMessageTrace();
@@ -66,11 +73,16 @@ public:
bool enabled() const { return m_enabled; }
void setEnabled(bool enabled);
+ OutputFormat outputFormat() const;
+ void setOutputFormat(OutputFormat fmt);
+
signals:
void categoryChanged();
void outputPrefixChanged();
void enabledChanged();
+ void outputFormatChanged();
+
private:
static void categoryFilter(QLoggingCategory *cat);
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &text);
@@ -78,8 +90,11 @@ private:
void logBacktrace(QStringList trace);
void parseClassAndMethod(const QString &classAndMethod, QString &className, QString &methodName);
void addObjectInstance(void *obj, const QString &objClass);
- void writeObjectInstance(QFile &f, QObject *o);
- void writeQml();
+ void writeObjectInstanceQml(QFile &f, QObject *o);
+ void writeObjectInstancePuml(QFile &f, QObject *o);
+ void write();
+ void writeQml(const QString &plainFilePath);
+ void writePuml(const QString &plainFilePath);
private:
struct Message {
@@ -95,6 +110,7 @@ private:
QStringList backtrace;
QString params;
QString toQml() const;
+ QString toPuml() const;
};
static QHash<QByteArray, QList<QmlMessageTrace*> > m_categoryInstances;
@@ -116,6 +132,7 @@ private:
bool m_enabled;
QByteArray m_category;
QString m_outputPrefix;
+ OutputFormat m_outputFormat = OutputFormat::PlantUML;
};
QT_END_NAMESPACE