diff options
author | Jarek Kobus <jaroslaw.kobus@theqtcompany.com> | 2016-05-04 16:02:06 +0200 |
---|---|---|
committer | Jarek Kobus <jaroslaw.kobus@qt.io> | 2016-05-20 11:09:03 +0000 |
commit | 3277abece0c1c258cd2fcbdb3044b9c64b0ccfef (patch) | |
tree | d57c11c39fc10e735fc3d731940b8fc5dafd7ff3 | |
parent | 6b1884cac1e54b6aa6fd19daf354ee07eeb6379b (diff) |
Refactor parser, parse recursively now
Splits the parsering function into several
smaller functions per element kind.
Fixes parsing of 11 scion tests.
Fixes parsing of non-terminated comment inside <script>
element.
Change-Id: I1d82c3164415a71e69e90bc6d3b96bda67dabb6c
Reviewed-by: Erik Verbruggen <erik.verbruggen@qt.io>
-rw-r--r-- | src/scxml/qscxmlparser.cpp | 1555 | ||||
-rw-r--r-- | src/scxml/qscxmlparser.h | 8 | ||||
-rw-r--r-- | src/scxml/qscxmlparser_p.h | 69 | ||||
-rw-r--r-- | tests/auto/qscxmlc/data/commentInScript.scxml | 19 | ||||
-rw-r--r-- | tests/auto/qscxmlc/tst_qscxmlc.qrc | 1 | ||||
-rw-r--r-- | tests/auto/scion/tst_scion.cpp | 61 |
6 files changed, 1078 insertions, 635 deletions
diff --git a/src/scxml/qscxmlparser.cpp b/src/scxml/qscxmlparser.cpp index 04c7828..b7e8924 100644 --- a/src/scxml/qscxmlparser.cpp +++ b/src/scxml/qscxmlparser.cpp @@ -1488,7 +1488,7 @@ void QScxmlParser::setLoader(QScxmlParser::Loader *newLoader) */ void QScxmlParser::parse() { - d->parse(); + d->readDocument(); d->verifyDocument(); } @@ -1548,14 +1548,6 @@ void QScxmlParser::instantiateDataModel(QScxmlStateMachine *stateMachine) const } /*! - * Returns the current parser state. - */ -QScxmlParser::State QScxmlParser::state() const -{ - return d->state(); -} - -/*! * Returns the list of parse errors. */ QVector<QScxmlError> QScxmlParser::errors() const @@ -1681,12 +1673,12 @@ bool QScxmlParserPrivate::ParserState::validChild(ParserState::Kind parent, Pars case ParserState::OnExit: return isExecutableContent(child); case ParserState::History: - return (child == ParserState::Transition); + return child == ParserState::Transition; case ParserState::Raise: return false; case ParserState::If: - return (child == ParserState::ElseIf || child == ParserState::Else - || isExecutableContent(child)); + return child == ParserState::ElseIf || child == ParserState::Else + || isExecutableContent(child); case ParserState::ElseIf: case ParserState::Else: return false; @@ -1701,11 +1693,10 @@ bool QScxmlParserPrivate::ParserState::validChild(ParserState::Kind parent, Pars case ParserState::Assign: return false; case ParserState::DoneData: - return (child == ParserState::Content || child == ParserState::Param); case ParserState::Send: - return (child == ParserState::Param || child == ParserState::Content); + return child == ParserState::Content || child == ParserState::Param; case ParserState::Content: - return isExecutableContent(child); + return child == ParserState::Scxml || isExecutableContent(child); case ParserState::Param: case ParserState::Cancel: return false; @@ -2065,7 +2056,6 @@ QScxmlParserPrivate::QScxmlParserPrivate(QScxmlParser *parser, QXmlStreamReader , m_defaultLoader(parser) , m_loader(&m_defaultLoader) , m_reader(reader) - , m_state(QScxmlParser::StartingParsing) , m_qtMode(QScxmlParser::QtModeFromInputFile) {} @@ -2113,12 +2103,23 @@ void QScxmlParserPrivate::parseSubDocument(DocumentModel::Invoke *parentInvoke, { QScxmlParser p(reader); p.setFileName(fileName); - p.d->parse(); + p.d->readDocument(); + parentInvoke->content.reset(p.d->m_doc.take()); + m_doc->allSubDocuments.append(parentInvoke->content.data()); + m_errors.append(p.errors()); +} + +bool QScxmlParserPrivate::parseSubElement(DocumentModel::Invoke *parentInvoke, QXmlStreamReader *reader, const QString &fileName) +{ + QScxmlParser p(reader); + p.setFileName(fileName); + p.d->resetDocument(); + bool ok = p.d->readElement(); parentInvoke->content.reset(p.d->m_doc.take()); m_doc->allSubDocuments.append(parentInvoke->content.data()); m_errors.append(p.errors()); - if (p.state() == QScxmlParser::ParsingError) - m_state = QScxmlParser::ParsingError; + parentInvoke->content->qtMode = m_doc->qtMode; + return ok; } static bool isWordEnd(const QStringRef &str, int start) @@ -2131,612 +2132,954 @@ static bool isWordEnd(const QStringRef &str, int start) return ch.isSpace(); } -void QScxmlParserPrivate::parse() +bool QScxmlParserPrivate::preReadElementScxml() +{ + if (m_doc->root) { + addError(QLatin1String("Doc root already allocated")); + return false; + } + m_doc->root = new DocumentModel::Scxml(xmlLocation()); + + auto scxml = m_doc->root; + const QXmlStreamAttributes attributes = m_reader->attributes(); + if (attributes.hasAttribute(QStringLiteral("initial"))) { + const QString initial = attributes.value(QStringLiteral("initial")).toString(); + scxml->initial += initial.split(QChar::Space, QString::SkipEmptyParts); + } + + const QStringRef datamodel = attributes.value(QLatin1String("datamodel")); + if (datamodel.isEmpty() || datamodel == QLatin1String("null")) { + scxml->dataModel = DocumentModel::Scxml::NullDataModel; + } else if (datamodel == QLatin1String("ecmascript")) { + scxml->dataModel = DocumentModel::Scxml::JSDataModel; + } else if (datamodel.startsWith(QLatin1String("cplusplus"))) { + scxml->dataModel = DocumentModel::Scxml::CppDataModel; + int firstColon = datamodel.indexOf(QLatin1Char(':')); + if (firstColon == -1) { + scxml->cppDataModelClassName = attributes.value(QStringLiteral("name")).toString() + QStringLiteral("DataModel"); + scxml->cppDataModelHeaderName = scxml->cppDataModelClassName + QStringLiteral(".h"); + } else { + int lastColon = datamodel.lastIndexOf(QLatin1Char(':')); + if (lastColon == -1) { + lastColon = datamodel.length(); + } else { + scxml->cppDataModelHeaderName = datamodel.mid(lastColon + 1).toString(); + } + scxml->cppDataModelClassName = datamodel.mid(firstColon + 1, lastColon - firstColon - 1).toString(); + } + } else { + addError(QStringLiteral("Unsupported data model '%1' in scxml") + .arg(datamodel.toString())); + } + const QStringRef binding = attributes.value(QLatin1String("binding")); + if (binding.isEmpty() || binding == QLatin1String("early")) { + scxml->binding = DocumentModel::Scxml::EarlyBinding; + } else if (binding == QLatin1String("late")) { + scxml->binding = DocumentModel::Scxml::LateBinding; + } else { + addError(QStringLiteral("Unsupperted binding type '%1'") + .arg(binding.toString())); + return false; + } + const QStringRef name = attributes.value(QLatin1String("name")); + if (!name.isEmpty()) { + scxml->name = name.toString(); + } + m_currentState = m_doc->root; + current().instructionContainer = &m_doc->root->initialSetup; + return true; +} + + +bool QScxmlParserPrivate::preReadElementState() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Normal, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + + if (attributes.hasAttribute(QStringLiteral("initial"))) { + const QString initial = attributes.value(QStringLiteral("initial")).toString(); + newState->initial += initial.split(QChar::Space, QString::SkipEmptyParts); + } + m_currentState = newState; + return true; +} + +bool QScxmlParserPrivate::preReadElementParallel() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Parallel, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + + m_currentState = newState; + return true; +} + +bool QScxmlParserPrivate::preReadElementInitial() +{ + DocumentModel::AbstractState *parent = currentParent(); + if (!parent) { + addError(QStringLiteral("<initial> found outside a state")); + return false; + } + + DocumentModel::State *parentState = parent->asState(); + if (!parentState) { + addError(QStringLiteral("<initial> found outside a state")); + return false; + } + + if (parentState->type == DocumentModel::State::Parallel) { + addError(QStringLiteral("Explicit initial state for parallel states not supported (only implicitly through the initial states of its substates)")); + return false; + } + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Initial, xmlLocation()); + m_currentState = newState; + return true; +} + +bool QScxmlParserPrivate::preReadElementTransition() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto transition = m_doc->newTransition(m_currentState, xmlLocation()); + transition->events = attributes.value(QLatin1String("event")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + transition->targets = attributes.value(QLatin1String("target")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + if (attributes.hasAttribute(QStringLiteral("cond"))) + transition->condition.reset(new QString(attributes.value(QLatin1String("cond")).toString())); + QStringRef type = attributes.value(QLatin1String("type")); + if (type.isEmpty() || type == QLatin1String("external")) { + transition->type = DocumentModel::Transition::External; + } else if (type == QLatin1String("internal")) { + transition->type = DocumentModel::Transition::Internal; + } else { + addError(QStringLiteral("invalid transition type '%1', valid values are 'external' and 'internal'").arg(type.toString())); + return true; // TODO: verify me + } + current().instructionContainer = &transition->instructionsOnTransition; + return true; +} + +bool QScxmlParserPrivate::preReadElementFinal() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto newState = m_doc->newState(m_currentState, DocumentModel::State::Final, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + m_currentState = newState; + return true; +} + +bool QScxmlParserPrivate::preReadElementHistory() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + + DocumentModel::AbstractState *parent = currentParent(); + if (!parent) { + addError(QStringLiteral("<history> found outside a state")); + return false; + } + auto newState = m_doc->newHistoryState(parent, xmlLocation()); + if (!maybeId(attributes, &newState->id)) + return false; + + const QStringRef type = attributes.value(QLatin1String("type")); + if (type.isEmpty() || type == QLatin1String("shallow")) { + newState->type = DocumentModel::HistoryState::Shallow; + } else if (type == QLatin1String("deep")) { + newState->type = DocumentModel::HistoryState::Deep; + } else { + addError(QStringLiteral("invalid history type %1, valid values are 'shallow' and 'deep'").arg(type.toString())); + return false; + } + m_currentState = newState; + return true; +} + +bool QScxmlParserPrivate::preReadElementOnEntry() +{ + const ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::Final: + case ParserState::State: + case ParserState::Parallel: + if (DocumentModel::State *s = m_currentState->asState()) { + current().instructionContainer = m_doc->newSequence(&s->onEntry); + break; + } + // intentional fall-through + default: + addError(QStringLiteral("unexpected container state for onentry")); + break; + } + return true; +} + +bool QScxmlParserPrivate::preReadElementOnExit() +{ + ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::Final: + case ParserState::State: + case ParserState::Parallel: + if (DocumentModel::State *s = m_currentState->asState()) { + current().instructionContainer = m_doc->newSequence(&s->onExit); + break; + } + // intentional fall-through + default: + addError(QStringLiteral("unexpected container state for onexit")); + break; + } + return true; +} + +bool QScxmlParserPrivate::preReadElementRaise() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto raise = m_doc->newNode<DocumentModel::Raise>(xmlLocation()); + raise->event = attributes.value(QLatin1String("event")).toString(); + current().instruction = raise; + return true; +} + +bool QScxmlParserPrivate::preReadElementIf() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *ifI = m_doc->newNode<DocumentModel::If>(xmlLocation()); + current().instruction = ifI; + ifI->conditions.append(attributes.value(QLatin1String("cond")).toString()); + current().instructionContainer = m_doc->newSequence(&ifI->blocks); + return true; +} + +bool QScxmlParserPrivate::preReadElementElseIf() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + + DocumentModel::If *ifI = lastIf(); + if (!ifI) + return false; + + ifI->conditions.append(attributes.value(QLatin1String("cond")).toString()); + previous().instructionContainer = m_doc->newSequence(&ifI->blocks); + return true; +} + +bool QScxmlParserPrivate::preReadElementElse() +{ + DocumentModel::If *ifI = lastIf(); + if (!ifI) + return false; + + previous().instructionContainer = m_doc->newSequence(&ifI->blocks); + return true; +} + +bool QScxmlParserPrivate::preReadElementForeach() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto foreachI = m_doc->newNode<DocumentModel::Foreach>(xmlLocation()); + foreachI->array = attributes.value(QLatin1String("array")).toString(); + foreachI->item = attributes.value(QLatin1String("item")).toString(); + foreachI->index = attributes.value(QLatin1String("index")).toString(); + current().instruction = foreachI; + current().instructionContainer = &foreachI->block; + return true; +} + +bool QScxmlParserPrivate::preReadElementLog() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto logI = m_doc->newNode<DocumentModel::Log>(xmlLocation()); + logI->label = attributes.value(QLatin1String("label")).toString(); + logI->expr = attributes.value(QLatin1String("expr")).toString(); + current().instruction = logI; + return true; +} + +bool QScxmlParserPrivate::preReadElementDataModel() +{ + return true; +} + +bool QScxmlParserPrivate::preReadElementData() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto data = m_doc->newNode<DocumentModel::DataElement>(xmlLocation()); + data->id = attributes.value(QLatin1String("id")).toString(); + data->src = attributes.value(QLatin1String("src")).toString(); + data->expr = attributes.value(QLatin1String("expr")).toString(); + if (DocumentModel::Scxml *scxml = m_currentState->asScxml()) { + scxml->dataElements.append(data); + } else if (DocumentModel::State *state = m_currentState->asState()) { + state->dataElements.append(data); + } else { + Q_UNREACHABLE(); + } + return true; +} + +bool QScxmlParserPrivate::preReadElementAssign() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto assign = m_doc->newNode<DocumentModel::Assign>(xmlLocation()); + assign->location = attributes.value(QLatin1String("location")).toString(); + assign->expr = attributes.value(QLatin1String("expr")).toString(); + current().instruction = assign; + return true; +} + +bool QScxmlParserPrivate::preReadElementDoneData() +{ + DocumentModel::State *s = m_currentState->asState(); + if (s && s->type == DocumentModel::State::Final) { + if (s->doneData) { + addError(QLatin1String("state can only have one donedata")); + } else { + s->doneData = m_doc->newNode<DocumentModel::DoneData>(xmlLocation()); + } + } else { + addError(QStringLiteral("donedata can only occur in a final state")); + } + return true; +} + +bool QScxmlParserPrivate::preReadElementContent() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::DoneData: { + DocumentModel::State *s = m_currentState->asState(); + Q_ASSERT(s); + s->doneData->expr = attributes.value(QLatin1String("expr")).toString(); + } break; + case ParserState::Send: { + DocumentModel::Send *s = previous().instruction->asSend(); + Q_ASSERT(s); + s->content = attributes.value(QLatin1String("expr")).toString(); + } break; + case ParserState::Invoke: { + DocumentModel::Invoke *i = previous().instruction->asInvoke(); + Q_ASSERT(i); + Q_UNUSED(i); + if (attributes.hasAttribute(QStringLiteral("expr"))) { + addError(QStringLiteral("expr attribute in content of invoke is not supported")); + break; + } + } break; + default: + addError(QStringLiteral("unexpected parent of content %1").arg(previous().kind)); + } + return true; +} + +bool QScxmlParserPrivate::preReadElementParam() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto param = m_doc->newNode<DocumentModel::Param>(xmlLocation()); + param->name = attributes.value(QLatin1String("name")).toString(); + param->expr = attributes.value(QLatin1String("expr")).toString(); + param->location = attributes.value(QLatin1String("location")).toString(); + + ParserState::Kind previousKind = previous().kind; + switch (previousKind) { + case ParserState::DoneData: { + DocumentModel::State *s = m_currentState->asState(); + Q_ASSERT(s); + Q_ASSERT(s->doneData); + s->doneData->params.append(param); + } break; + case ParserState::Send: { + DocumentModel::Send *s = previous().instruction->asSend(); + Q_ASSERT(s); + s->params.append(param); + } break; + case ParserState::Invoke: { + DocumentModel::Invoke *i = previous().instruction->asInvoke(); + Q_ASSERT(i); + i->params.append(param); + } break; + default: + addError(QStringLiteral("unexpected parent of param %1").arg(previous().kind)); + } + return true; +} + +bool QScxmlParserPrivate::preReadElementScript() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *script = m_doc->newNode<DocumentModel::Script>(xmlLocation()); + script->src = attributes.value(QLatin1String("src")).toString(); + current().instruction = script; + return true; +} + +bool QScxmlParserPrivate::preReadElementSend() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *send = m_doc->newNode<DocumentModel::Send>(xmlLocation()); + send->event = attributes.value(QLatin1String("event")).toString(); + send->eventexpr = attributes.value(QLatin1String("eventexpr")).toString(); + send->delay = attributes.value(QLatin1String("delay")).toString(); + send->delayexpr = attributes.value(QLatin1String("delayexpr")).toString(); + send->id = attributes.value(QLatin1String("id")).toString(); + send->idLocation = attributes.value(QLatin1String("idlocation")).toString(); + send->type = attributes.value(QLatin1String("type")).toString(); + send->typeexpr = attributes.value(QLatin1String("typeexpr")).toString(); + send->target = attributes.value(QLatin1String("target")).toString(); + send->targetexpr = attributes.value(QLatin1String("targetexpr")).toString(); + if (attributes.hasAttribute(QLatin1String("namelist"))) + send->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + current().instruction = send; + return true; +} + +bool QScxmlParserPrivate::preReadElementCancel() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + auto *cancel = m_doc->newNode<DocumentModel::Cancel>(xmlLocation()); + cancel->sendid = attributes.value(QLatin1String("sendid")).toString(); + cancel->sendidexpr = attributes.value(QLatin1String("sendidexpr")).toString(); + current().instruction = cancel; + return true; +} + +bool QScxmlParserPrivate::preReadElementInvoke() +{ + const QXmlStreamAttributes attributes = m_reader->attributes(); + DocumentModel::State *parentState = m_currentState->asState(); + if (!parentState || + (parentState->type != DocumentModel::State::Normal && parentState->type != DocumentModel::State::Parallel)) { + addError(QStringLiteral("invoke can only occur in <state> or <parallel>")); + return true; // TODO: verify me + } + auto *invoke = m_doc->newNode<DocumentModel::Invoke>(xmlLocation()); + parentState->invokes.append(invoke); + invoke->src = attributes.value(QLatin1String("src")).toString(); + invoke->srcexpr = attributes.value(QLatin1String("srcexpr")).toString(); + invoke->id = attributes.value(QLatin1String("id")).toString(); + invoke->idLocation = attributes.value(QLatin1String("idlocation")).toString(); + invoke->type = attributes.value(QLatin1String("type")).toString(); + invoke->typeexpr = attributes.value(QLatin1String("typeexpr")).toString(); + QStringRef autoforwardS = attributes.value(QLatin1String("autoforward")); + if (QStringRef::compare(autoforwardS, QLatin1String("true"), Qt::CaseInsensitive) == 0 + || QStringRef::compare(autoforwardS, QLatin1String("yes"), Qt::CaseInsensitive) == 0 + || QStringRef::compare(autoforwardS, QLatin1String("t"), Qt::CaseInsensitive) == 0 + || QStringRef::compare(autoforwardS, QLatin1String("y"), Qt::CaseInsensitive) == 0 + || autoforwardS == QLatin1String("1")) + invoke->autoforward = true; + else + invoke->autoforward = false; + invoke->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); + current().instruction = invoke; + return true; +} + +bool QScxmlParserPrivate::preReadElementFinalize() +{ + auto instr = previous().instruction; + if (!instr) { + addError(QStringLiteral("no previous instruction found for <finalize>")); + return false; + } + auto invoke = instr->asInvoke(); + if (!invoke) { + addError(QStringLiteral("instruction before <finalize> is not <invoke>")); + return false; + } + current().instructionContainer = &invoke->finalize; + return true; +} + +bool QScxmlParserPrivate::postReadElementScxml() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementState() +{ + currentStateUp(); + return true; +} + +bool QScxmlParserPrivate::postReadElementParallel() +{ + currentStateUp(); + return true; +} + +bool QScxmlParserPrivate::postReadElementInitial() +{ + currentStateUp(); + return true; +} + +bool QScxmlParserPrivate::postReadElementTransition() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementFinal() +{ + currentStateUp(); + return true; +} + +bool QScxmlParserPrivate::postReadElementHistory() +{ + currentStateUp(); + return true; +} + +bool QScxmlParserPrivate::postReadElementOnEntry() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementOnExit() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementRaise() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementIf() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementElseIf() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementElse() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementForeach() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementLog() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementDataModel() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementData() +{ + const ParserState parserState = current(); + DocumentModel::DataElement *data = Q_NULLPTR; + if (auto state = m_currentState->asState()) { + data = state->dataElements.last(); + } else if (auto scxml = m_currentState->asNode()->asScxml()) { + data = scxml->dataElements.last(); + } else { + Q_UNREACHABLE(); + } + if (!data->src.isEmpty() && !data->expr.isEmpty()) { + addError(QStringLiteral("data element with both 'src' and 'expr' attributes")); + return false; + } + if (!parserState.chars.trimmed().isEmpty()) { + if (!data->src.isEmpty()) { + addError(QStringLiteral("data element with both 'src' attribute and CDATA")); + return false; + } else if (!data->expr.isEmpty()) { + addError(QStringLiteral("data element with both 'expr' attribute and CDATA")); + return false; + } else { + // w3c-ecma/test558 - "if a child element of <data> is not a XML, + // treat it as a string with whitespace normalization" + // We've modified the test, so that a string is enclosed with quotes. + data->expr = parserState.chars; + } + } else if (!data->src.isEmpty()) { + if (!m_loader) { + addError(QStringLiteral("cannot parse a document with external dependencies without a loader")); + } else { + bool ok; + const QByteArray ba = load(data->src, &ok); + if (!ok) { + addError(QStringLiteral("failed to load external dependency")); + } else { + // w3c-ecma/test558 - "if XML is loaded via "src" attribute, + // treat it as a string with whitespace normalization" + // We've enclosed the text in file with quotes. + data->expr = QString::fromUtf8(ba); + } + } + } + return true; +} + +bool QScxmlParserPrivate::postReadElementAssign() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementDoneData() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementContent() +{ + const ParserState parserState = current(); + if (!parserState.chars.trimmed().isEmpty()) { + + switch (previous().kind) { + case ParserState::DoneData: // see test529 + m_currentState->asState()->doneData->contents = parserState.chars.simplified(); + break; + case ParserState::Send: // see test179 + previous().instruction->asSend()->content = parserState.chars.simplified(); + break; + default: + break; + } + } + return true; +} + +bool QScxmlParserPrivate::postReadElementParam() +{ + return true; +} + +bool QScxmlParserPrivate::postReadElementScript() +{ + const ParserState parserState = current(); + DocumentModel::Script *scriptI = parserState.instruction->asScript(); + if (!parserState.chars.trimmed().isEmpty()) { + scriptI->content = parserState.chars.trimmed(); + if (!scriptI->src.isEmpty()) + addError(QStringLiteral("both src and source content given to script, will ignore external content")); + } else if (!scriptI->src.isEmpty()) { + if (!m_loader) { + addError(QStringLiteral("cannot parse a document with external dependencies without a loader")); + } else { + bool ok; + const QByteArray data = load(scriptI->src, &ok); + if (!ok) { + addError(QStringLiteral("failed to load external dependency")); + } else { + scriptI->content = QString::fromUtf8(data); + } + } + } + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementSend() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementCancel() +{ + return flushInstruction(); +} + +bool QScxmlParserPrivate::postReadElementInvoke() +{ + DocumentModel::Invoke *i = current().instruction->asInvoke(); + const QString fileName = i->src; + if (!i->content.data()) { + if (!fileName.isEmpty()) { + bool ok = true; + const QByteArray data = load(fileName, &ok); + if (!ok) { + addError(QStringLiteral("failed to load external dependency")); + } else { + QXmlStreamReader reader(data); + parseSubDocument(i, &reader, fileName); + } + } else { // invoke always expects sub document + DocumentModel::ScxmlDocument *doc = new DocumentModel::ScxmlDocument(m_fileName); + doc->root = new DocumentModel::Scxml(DocumentModel::XmlLocation(0, 0)); + i->content.reset(doc); + m_doc->allSubDocuments.append(i->content.data()); + } + } else if (!fileName.isEmpty()) { + addError(QStringLiteral("both src and content given to invoke")); + } + + return true; +} + +bool QScxmlParserPrivate::postReadElementFinalize() +{ + return true; +} + +void QScxmlParserPrivate::resetDocument() { m_doc.reset(new DocumentModel::ScxmlDocument(fileName())); +} + +bool QScxmlParserPrivate::readDocument() +{ + resetDocument(); if (m_qtMode == QScxmlParser::QtModeEnabled) m_doc->qtMode = true; else if (m_qtMode == QScxmlParser::QtModeDisabled) m_doc->qtMode = false; m_currentState = m_doc->root; - while (!m_reader->atEnd()) { - QXmlStreamReader::TokenType tt = m_reader->readNext(); - switch (tt) { - case QXmlStreamReader::Comment: { - static const QString qtModeSwitch = QStringLiteral("enable-qt-mode:"); - QStringRef commentText = m_reader->text(); - int qtModeIdx = commentText.indexOf(qtModeSwitch); - if (qtModeIdx != -1) { - qtModeIdx += qtModeSwitch.size(); - while (qtModeIdx < commentText.size()) { - if (commentText.at(qtModeIdx).isSpace()) { - ++qtModeIdx; - } else { - break; - } - } - QStringRef value = commentText.mid(qtModeIdx); - if (value.startsWith(QStringLiteral("yes")) && isWordEnd(value, 3)) { - if (m_qtMode == QScxmlParser::QtModeFromInputFile) - m_doc->qtMode = true; - } else if (value.startsWith(QStringLiteral("no")) && isWordEnd(value, 2)) { - if (m_qtMode == QScxmlParser::QtModeFromInputFile) - m_doc->qtMode = false; - } else { - addError(QStringLiteral("expected 'yes' or 'no' after enable-qt-mode in comment")); - } + for (bool finished = false; !finished && !m_reader->hasError();) { + switch (m_reader->readNext()) { + case QXmlStreamReader::StartElement : { + const QStringRef newTag = m_reader->name(); + const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(newTag); + + auto ns = m_reader->namespaceUri(); + + if (ns != scxmlNamespace) { + m_reader->skipCurrentElement(); + } else if (newElementKind == ParserState::None) { + addError(QStringLiteral("Unknown element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); + } else if (newElementKind == ParserState::Scxml) { + if (readElement() == false) + return false; + } else { + addError(QStringLiteral("Unexpected element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); } - } break; - case QXmlStreamReader::NoToken: - // The reader has not yet read anything. - continue; - case QXmlStreamReader::Invalid: - // An error has occurred, reported in error() and errorString(). + } break; - case QXmlStreamReader::StartDocument: - // The reader reports the XML version number in documentVersion(), and the encoding - // as specified in the XML document in documentEncoding(). If the document is declared - // standalone, isStandaloneDocument() returns true; otherwise it returns false. + case QXmlStreamReader::EndElement : + finished = true; break; - case QXmlStreamReader::EndDocument: - // The reader reports the end of the document. - if (!m_stack.isEmpty() || m_state != QScxmlParser::FinishedParsing) { - addError(QStringLiteral("document finished without a proper scxml item")); - } + case QXmlStreamReader::Comment: + parseComment(); break; - case QXmlStreamReader::StartElement: - // The reader reports the start of an element with namespaceUri() and name(). Empty - // elements are also reported as StartElement, followed directly by EndElement. - // The convenience function readElementText() can be called to concatenate all content - // until the corresponding EndElement. Attributes are reported in attributes(), - // namespace declarations in namespaceDeclarations(). - { + default : + break; + } + } + if (!m_doc->root) { + addError(QStringLiteral("Missing root element")); + return false; + } + + if (m_reader->hasError() && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError) { + addError(QStringLiteral("Error parsing SCXML file: %1").arg(m_reader->errorString())); + return false; + } + + return true; +} + +bool QScxmlParserPrivate::readElement() +{ + const QStringRef currentTag = m_reader->name(); + const QXmlStreamAttributes attributes = m_reader->attributes(); + + const ParserState::Kind elementKind = ParserState::nameToParserStateKind(currentTag); + + if (!checkAttributes(attributes, elementKind)) + return false; + + if (elementKind == ParserState::Scxml && m_doc->root) { + if (!hasPrevious()) { + addError(QStringLiteral("misplaced scxml")); + return false; + } + + DocumentModel::Invoke *i = previous().instruction->asInvoke(); + if (!i) { + addError(QStringLiteral("misplaced scxml")); + return false; + } + + return parseSubElement(i, m_reader, m_fileName); + } + + if (elementKind != ParserState::Scxml && !m_stack.count()) { + addError(QStringLiteral("misplaced %1").arg(currentTag.toString())); + return false; + } + + ParserState pNew = ParserState(elementKind); + + m_stack.append(pNew); + + switch (elementKind) { + case ParserState::Scxml: if (!preReadElementScxml()) return false; break; + case ParserState::State: if (!preReadElementState()) return false; break; + case ParserState::Parallel: if (!preReadElementParallel()) return false; break; + case ParserState::Initial: if (!preReadElementInitial()) return false; break; + case ParserState::Transition: if (!preReadElementTransition()) return false; break; + case ParserState::Final: if (!preReadElementFinal()) return false; break; + case ParserState::History: if (!preReadElementHistory()) return false; break; + case ParserState::OnEntry: if (!preReadElementOnEntry()) return false; break; + case ParserState::OnExit: if (!preReadElementOnExit()) return false; break; + case ParserState::Raise: if (!preReadElementRaise()) return false; break; + case ParserState::If: if (!preReadElementIf()) return false; break; + case ParserState::ElseIf: if (!preReadElementElseIf()) return false; break; + case ParserState::Else: if (!preReadElementElse()) return false; break; + case ParserState::Foreach: if (!preReadElementForeach()) return false; break; + case ParserState::Log: if (!preReadElementLog()) return false; break; + case ParserState::DataModel: if (!preReadElementDataModel()) return false; break; + case ParserState::Data: if (!preReadElementData()) return false; break; + case ParserState::Assign: if (!preReadElementAssign()) return false; break; + case ParserState::DoneData: if (!preReadElementDoneData()) return false; break; + case ParserState::Content: if (!preReadElementContent()) return false; break; + case ParserState::Param: if (!preReadElementParam()) return false; break; + case ParserState::Script: if (!preReadElementScript()) return false; break; + case ParserState::Send: if (!preReadElementSend()) return false; break; + case ParserState::Cancel: if (!preReadElementCancel()) return false; break; + case ParserState::Invoke: if (!preReadElementInvoke()) return false; break; + case ParserState::Finalize: if (!preReadElementFinalize()) return false; break; + default: addError(QStringLiteral("Unknown element %1").arg(currentTag.toString())); return false; + } + + for (bool finished = false; !finished && !m_reader->hasError();) { + switch (m_reader->readNext()) { + case QXmlStreamReader::StartElement : { + const QStringRef newTag = m_reader->name(); + const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(newTag); + auto ns = m_reader->namespaceUri(); + if (ns != scxmlNamespace) { m_reader->skipCurrentElement(); - continue; - } - - const QStringRef elName = m_reader->name(); - const QXmlStreamAttributes attributes = m_reader->attributes(); - if (!m_stack.isEmpty() && m_stack.last().kind == ParserState::Data) { - break; - } - const ParserState::Kind elKind = ParserState::nameToParserStateKind(elName); - if (elKind == ParserState::None) { - addError(QStringLiteral("unexpected element %1").arg(elName.toString())); - break; - } - if (!m_doc->root && elKind != ParserState::Scxml) { - addError(QStringLiteral("found %1 instead of <scxml> as root element") - .arg(elName.toString())); - return; - } - ParserState pNew = ParserState(elKind); - if (!checkAttributes(attributes, elKind)) - return; - - if (elKind == ParserState::Scxml) { - if (m_state != QScxmlParser::StartingParsing || !m_stack.isEmpty()) { - addError(xmlLocation(), QStringLiteral("found scxml tag mid stream")); - return; - } else { - m_state = QScxmlParser::ParsingScxml; - } - m_doc->root = new DocumentModel::Scxml(xmlLocation()); - auto scxml = m_doc->root; - if (attributes.hasAttribute(QStringLiteral("initial"))) { - QString initial = attributes.value(QStringLiteral("initial")).toString(); - scxml->initial += initial.split(QChar::Space, QString::SkipEmptyParts); - } - QStringRef datamodel = attributes.value(QLatin1String("datamodel")); - if (datamodel.isEmpty() || datamodel == QLatin1String("null")) { - scxml->dataModel = DocumentModel::Scxml::NullDataModel; - } else if (datamodel == QLatin1String("ecmascript")) { - scxml->dataModel = DocumentModel::Scxml::JSDataModel; - } else if (datamodel.startsWith(QLatin1String("cplusplus"))) { - scxml->dataModel = DocumentModel::Scxml::CppDataModel; - int firstColon = datamodel.indexOf(QLatin1Char(':')); - if (firstColon == -1) { - scxml->cppDataModelClassName = attributes.value(QStringLiteral("name")).toString() + QStringLiteral("DataModel"); - scxml->cppDataModelHeaderName = scxml->cppDataModelClassName + QStringLiteral(".h"); - } else { - int lastColon = datamodel.lastIndexOf(QLatin1Char(':')); - if (lastColon == -1) { - lastColon = datamodel.length(); - } else { - scxml->cppDataModelHeaderName = datamodel.mid(lastColon + 1).toString(); - } - scxml->cppDataModelClassName = datamodel.mid(firstColon + 1, lastColon - firstColon - 1).toString(); - } - } else { - addError(QStringLiteral("Unsupported data model '%1' in scxml") - .arg(datamodel.toString())); - } - QStringRef binding = attributes.value(QLatin1String("binding")); - if (binding.isEmpty() || binding == QLatin1String("early")) { - scxml->binding = DocumentModel::Scxml::EarlyBinding; - } else if (binding == QLatin1String("late")) { - scxml->binding = DocumentModel::Scxml::LateBinding; - } else { - addError(QStringLiteral("Unsupperted binding type '%1'") - .arg(binding.toString())); - return; - } - QStringRef name = attributes.value(QLatin1String("name")); - if (!name.isEmpty()) { - scxml->name = name.toString(); - } - m_currentState = m_doc->root; - pNew.instructionContainer = &m_doc->root->initialSetup; - } else if (elKind == ParserState::State) { - auto newState = m_doc->newState(m_currentState, DocumentModel::State::Normal, xmlLocation()); - if (!maybeId(attributes, &newState->id)) return; - if (attributes.hasAttribute(QStringLiteral("initial"))) { - QString initial = attributes.value(QStringLiteral("initial")).toString(); - newState->initial += initial.split(QChar::Space, QString::SkipEmptyParts); - } - m_currentState = newState; - } else if (elKind == ParserState::Parallel) { - auto newState = m_doc->newState(m_currentState, DocumentModel::State::Parallel, xmlLocation()); - if (!maybeId(attributes, &newState->id)) return; - m_currentState = newState; - } else if (elKind == ParserState::Initial) { - DocumentModel::AbstractState *parent = currentParent(); - if (!parent) { - addError(QStringLiteral("<initial> found outside a state")); - return; - } else if (parent->asState()->type == DocumentModel::State::Parallel) { - addError(QStringLiteral("Explicit initial state for parallel states not supported (only implicitly through the initial states of its substates)")); - return; - } - auto newState = m_doc->newState(m_currentState, DocumentModel::State::Initial, xmlLocation()); - m_currentState = newState; - } else if (elKind == ParserState::Transition) { - auto transition = m_doc->newTransition(m_currentState, xmlLocation()); - transition->events = attributes.value(QLatin1String("event")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); - transition->targets = attributes.value(QLatin1String("target")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); - if (attributes.hasAttribute(QStringLiteral("cond"))) - transition->condition.reset(new QString(attributes.value(QLatin1String("cond")).toString())); - QStringRef type = attributes.value(QLatin1String("type")); - if (type.isEmpty() || type == QLatin1String("external")) { - transition->type = DocumentModel::Transition::External; - } else if (type == QLatin1String("internal")) { - transition->type = DocumentModel::Transition::Internal; - } else { - addError(QStringLiteral("invalid transition type '%1', valid values are 'external' and 'internal'").arg(type.toString())); - break; - } - pNew.instructionContainer = &transition->instructionsOnTransition; - } else if (elKind == ParserState::Final) { - auto newState = m_doc->newState(m_currentState, DocumentModel::State::Final, xmlLocation()); - if (!maybeId(attributes, &newState->id)) return; - m_currentState = newState; - } else if (elKind == ParserState::History) { - DocumentModel::AbstractState *parent = currentParent(); - if (!parent) { - addError(QStringLiteral("<history> found outside a state")); - return; - } - auto newState = m_doc->newHistoryState(parent, xmlLocation()); - if (!maybeId(attributes, &newState->id)) return; - QStringRef type = attributes.value(QLatin1String("type")); - if (type.isEmpty() || type == QLatin1String("shallow")) { - newState->type = DocumentModel::HistoryState::Shallow; - } else if (type == QLatin1String("deep")) { - newState->type = DocumentModel::HistoryState::Deep; - } else { - addError(QStringLiteral("invalid history type %1, valid values are 'shallow' and 'deep'").arg(type.toString())); - return; - } - m_currentState = newState; - } else if (elKind == ParserState::OnEntry) { - switch (m_stack.last().kind) { - case ParserState::Final: - case ParserState::State: - case ParserState::Parallel: - if (DocumentModel::State *s = m_currentState->asState()) { - pNew.instructionContainer = m_doc->newSequence(&s->onEntry); - break; - } - // intentional fall-through - default: - addError(QStringLiteral("unexpected container state for onentry")); - break; - } - } else if (elKind == ParserState::OnExit) { - switch (m_stack.last().kind) { - case ParserState::Final: - case ParserState::State: - case ParserState::Parallel: - if (DocumentModel::State *s = m_currentState->asState()) { - pNew.instructionContainer = m_doc->newSequence(&s->onExit); - break; - } - // intentional fall-through - default: - addError(QStringLiteral("unexpected container state for onexit")); - break; - } - } else if (elKind == ParserState::Raise) { - auto raise = m_doc->newNode<DocumentModel::Raise>(xmlLocation()); - raise->event = attributes.value(QLatin1String("event")).toString(); - pNew.instruction = raise; - } else if (elKind == ParserState::If) { - auto *ifI = m_doc->newNode<DocumentModel::If>(xmlLocation()); - pNew.instruction = ifI; - ifI->conditions.append(attributes.value(QLatin1String("cond")).toString()); - pNew.instructionContainer = m_doc->newSequence(&ifI->blocks); - } else if (elKind == ParserState::ElseIf) { - DocumentModel::If *ifI = lastIf(); - if (!ifI) return; - ifI->conditions.append(attributes.value(QLatin1String("cond")).toString()); - m_stack.last().instructionContainer = m_doc->newSequence(&ifI->blocks); - } else if (elKind == ParserState::Else) { - DocumentModel::If *ifI = lastIf(); - if (!ifI) return; - m_stack.last().instructionContainer = m_doc->newSequence(&ifI->blocks); - } else if (elKind == ParserState::Foreach) { - auto foreachI = m_doc->newNode<DocumentModel::Foreach>(xmlLocation()); - foreachI->array = attributes.value(QLatin1String("array")).toString(); - foreachI->item = attributes.value(QLatin1String("item")).toString(); - foreachI->index = attributes.value(QLatin1String("index")).toString(); - pNew.instruction = foreachI; - pNew.instructionContainer = &foreachI->block; - } else if (elKind == ParserState::Log) { - auto logI = m_doc->newNode<DocumentModel::Log>(xmlLocation()); - logI->label = attributes.value(QLatin1String("label")).toString(); - logI->expr = attributes.value(QLatin1String("expr")).toString(); - pNew.instruction = logI; - } else if (elKind == ParserState::DataModel) { - } else if (elKind == ParserState::Data) { - auto data = m_doc->newNode<DocumentModel::DataElement>(xmlLocation()); - data->id = attributes.value(QLatin1String("id")).toString(); - data->src = attributes.value(QLatin1String("src")).toString(); - data->expr = attributes.value(QLatin1String("expr")).toString(); - if (DocumentModel::Scxml *scxml = m_currentState->asScxml()) { - scxml->dataElements.append(data); - } else if (DocumentModel::State *state = m_currentState->asState()) { - state->dataElements.append(data); - } else { - Q_UNREACHABLE(); - } - } else if (elKind == ParserState::Assign) { - auto assign = m_doc->newNode<DocumentModel::Assign>(xmlLocation()); - assign->location = attributes.value(QLatin1String("location")).toString(); - assign->expr = attributes.value(QLatin1String("expr")).toString(); - pNew.instruction = assign; - } else if (elKind == ParserState::DoneData) { - bool handled = false; - if (DocumentModel::State *s = m_currentState->asState()) { - if (s->type == DocumentModel::State::Final) { - handled = true; - if (s->doneData) { - addError(QLatin1String("state can only have one donedata")); - } else { - s->doneData = m_doc->newNode<DocumentModel::DoneData>(xmlLocation()); - } - } - } - if (!handled) { - addError(QStringLiteral("donedata can only occur in a final state")); - } - } else if (elKind == ParserState::Content) { - switch (m_stack.last().kind) { - case ParserState::DoneData: { - DocumentModel::State *s = m_currentState->asState(); - Q_ASSERT(s); - s->doneData->expr = attributes.value(QLatin1String("expr")).toString(); - } break; - case ParserState::Send: { - DocumentModel::Send *s = m_stack.last().instruction->asSend(); - Q_ASSERT(s); - s->content = attributes.value(QLatin1String("expr")).toString(); - } break; - case ParserState::Invoke: { - DocumentModel::Invoke *i = m_stack.last().instruction->asInvoke(); - Q_ASSERT(i); - if (attributes.hasAttribute(QStringLiteral("expr"))) { - addError(QStringLiteral("expr attribute in content of invoke is not supported")); - break; - } - parseSubDocument(i, m_reader, m_fileName); - i->content->qtMode = m_doc->qtMode; - } break; - default: - addError(QStringLiteral("unexpected parent of content %1").arg(m_stack.last().kind)); - } - } else if (elKind == ParserState::Param) { - auto param = m_doc->newNode<DocumentModel::Param>(xmlLocation()); - param->name = attributes.value(QLatin1String("name")).toString(); - param->expr = attributes.value(QLatin1String("expr")).toString(); - param->location = attributes.value(QLatin1String("location")).toString(); - switch (m_stack.last().kind) { - case ParserState::DoneData: { - DocumentModel::State *s = m_currentState->asState(); - Q_ASSERT(s); - Q_ASSERT(s->doneData); - s->doneData->params.append(param); - } break; - case ParserState::Send: { - DocumentModel::Send *s = m_stack.last().instruction->asSend(); - Q_ASSERT(s); - s->params.append(param); - } break; - case ParserState::Invoke: { - DocumentModel::Invoke *i = m_stack.last().instruction->asInvoke(); - Q_ASSERT(i); - i->params.append(param); - } break; - default: - addError(QStringLiteral("unexpected parent of param %1").arg(m_stack.last().kind)); - } - } else if (elKind == ParserState::Script) { - auto *script = m_doc->newNode<DocumentModel::Script>(xmlLocation()); - script->src = attributes.value(QLatin1String("src")).toString(); - pNew.instruction = script; - } else if (elKind == ParserState::Send) { - auto *send = m_doc->newNode<DocumentModel::Send>(xmlLocation()); - send->event = attributes.value(QLatin1String("event")).toString(); - send->eventexpr = attributes.value(QLatin1String("eventexpr")).toString(); - send->delay = attributes.value(QLatin1String("delay")).toString(); - send->delayexpr = attributes.value(QLatin1String("delayexpr")).toString(); - send->id = attributes.value(QLatin1String("id")).toString(); - send->idLocation = attributes.value(QLatin1String("idlocation")).toString(); - send->type = attributes.value(QLatin1String("type")).toString(); - send->typeexpr = attributes.value(QLatin1String("typeexpr")).toString(); - send->target = attributes.value(QLatin1String("target")).toString(); - send->targetexpr = attributes.value(QLatin1String("targetexpr")).toString(); - if (attributes.hasAttribute(QLatin1String("namelist"))) - send->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); - pNew.instruction = send; - } else if (elKind == ParserState::Cancel) { - auto *cancel = m_doc->newNode<DocumentModel::Cancel>(xmlLocation()); - cancel->sendid = attributes.value(QLatin1String("sendid")).toString(); - cancel->sendidexpr = attributes.value(QLatin1String("sendidexpr")).toString(); - pNew.instruction = cancel; - } else if (elKind == ParserState::Invoke) { - auto *invoke = m_doc->newNode<DocumentModel::Invoke>(xmlLocation()); - DocumentModel::State *parentState = m_currentState->asState(); - if (!parentState || - (parentState->type != DocumentModel::State::Normal && parentState->type != DocumentModel::State::Parallel)) { - addError(QStringLiteral("invoke can only occur in <state> or <parallel>")); - break; - } - parentState->invokes.append(invoke); - invoke->src = attributes.value(QLatin1String("src")).toString(); - invoke->srcexpr = attributes.value(QLatin1String("srcexpr")).toString(); - invoke->id = attributes.value(QLatin1String("id")).toString(); - invoke->idLocation = attributes.value(QLatin1String("idlocation")).toString(); - invoke->type = attributes.value(QLatin1String("type")).toString(); - invoke->typeexpr = attributes.value(QLatin1String("typeexpr")).toString(); - QStringRef autoforwardS = attributes.value(QLatin1String("autoforward")); - if (QStringRef::compare(autoforwardS, QLatin1String("true"), Qt::CaseInsensitive) == 0 - || QStringRef::compare(autoforwardS, QLatin1String("yes"), Qt::CaseInsensitive) == 0 - || QStringRef::compare(autoforwardS, QLatin1String("t"), Qt::CaseInsensitive) == 0 - || QStringRef::compare(autoforwardS, QLatin1String("y"), Qt::CaseInsensitive) == 0 - || autoforwardS == QLatin1String("1")) - invoke->autoforward = true; - else - invoke->autoforward = false; - invoke->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), QString::SkipEmptyParts); - pNew.instruction = invoke; - } else if (elKind == ParserState::Finalize) { - auto instr = m_stack.last().instruction; - if (!instr) { - addError(QStringLiteral("no previous instruction found for <finalize>")); - return; - } - auto invoke = instr->asInvoke(); - if (!invoke) { - addError(QStringLiteral("instruction before <finalize> is not <invoke>")); - return; - } - pNew.instructionContainer = &invoke->finalize; - } - m_stack.append(pNew); - if (m_stack.size()>1 && !m_stack.at(m_stack.size()-2).validChild(m_stack.last().kind)) { - addError(QStringLiteral("invalid child")); + } else if (newElementKind == ParserState::None) { + addError(QStringLiteral("Unknown element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); + } else if (pNew.validChild(newElementKind)) { + if (readElement() == false) + return false; + } else { + addError(QStringLiteral("Unexpected element %1").arg(newTag.toString())); + m_reader->skipCurrentElement(); } - break; } - case QXmlStreamReader::EndElement: - // The reader reports the end of an element with namespaceUri() and name(). - { - const QStringRef elName = m_reader->name(); - if (m_stack.isEmpty()) { - addError(QStringLiteral("unbalanced XML tag %1").arg(elName.toString())); - break; - } - ParserState p = m_stack.last(); - m_stack.removeLast(); - switch (p.kind) { - case ParserState::Scxml: - if (m_state == QScxmlParser::ParsingScxml) { - m_state = QScxmlParser::FinishedParsing; - } else { - m_state = QScxmlParser::ParsingError; - } - return; - case ParserState::State: - case ParserState::Parallel: - case ParserState::Initial: - case ParserState::Final: - case ParserState::History: - Q_ASSERT(m_currentState->parent); - m_currentState = m_currentState->parent; - break; - case ParserState::Invoke: - { - DocumentModel::Invoke *i = p.instruction->asInvoke(); - const QString fileName = i->src; - if (!i->content.data()) { - if (!fileName.isEmpty()) { - bool ok = true; - QByteArray data = load(fileName, &ok); - if (!ok) { - addError(QStringLiteral("failed to load external dependency")); - } else { - QXmlStreamReader reader(data); - parseSubDocument(i, &reader, fileName); - } - } else { // invoke always expects sub document - DocumentModel::ScxmlDocument *doc = new DocumentModel::ScxmlDocument(m_fileName); - doc->root = new DocumentModel::Scxml(DocumentModel::XmlLocation(0, 0)); - i->content.reset(doc); - m_doc->allSubDocuments.append(i->content.data()); - } - } else if (!fileName.isEmpty()) { - addError(QStringLiteral("both src and content given to invoke")); - } - } - break; - case ParserState::Transition: - case ParserState::OnEntry: - case ParserState::OnExit: - case ParserState::ElseIf: - case ParserState::Else: - break; - case ParserState::Script: - { - DocumentModel::Script *scriptI = p.instruction->asScript(); - if (!p.chars.trimmed().isEmpty()) { - scriptI->content = p.chars.trimmed(); - if (!scriptI->src.isEmpty()) - addError(QStringLiteral("both src and source content given to script, will ignore external content")); - } else if (!scriptI->src.isEmpty()) { - if (!m_loader) { - addError(QStringLiteral("cannot parse a document with external dependencies without a loader")); - } else { - bool ok; - QByteArray data = load(scriptI->src, &ok); - if (!ok) { - addError(QStringLiteral("failed to load external dependency")); - } else { - scriptI->content = QString::fromUtf8(data); - } - } - } - } // very intentional fallthrough! - case ParserState::Raise: - case ParserState::If: - case ParserState::Foreach: - case ParserState::Log: - case ParserState::Assign: - case ParserState::Send: - case ParserState::Cancel: { - DocumentModel::InstructionSequence *instructions = m_stack.last().instructionContainer; - if (!instructions) { - addError(QStringLiteral("got executable content within an element that did not set instructionContainer")); - return; - } - instructions->append(p.instruction); - p.instruction = 0; - break; - } - case ParserState::Finalize: - case ParserState::DataModel: - case ParserState::DoneData: - case ParserState::Param: - case ParserState::None: - break; - case ParserState::Content: - if (!p.chars.trimmed().isEmpty()) { - Q_ASSERT(!m_stack.isEmpty()); - switch (m_stack.last().kind) { - case ParserState::DoneData: // see test529 - m_currentState->asState()->doneData->contents = p.chars.simplified(); - break; - case ParserState::Send: // see test179 - m_stack.last().instruction->asSend()->content = p.chars.simplified(); - break; - default: - break; - } - } - break; - case ParserState::Data: { - DocumentModel::DataElement *data = Q_NULLPTR; - if (auto state = m_currentState->asState()) { - data = state->dataElements.last(); - } else if (auto scxml = m_currentState->asNode()->asScxml()) { - data = scxml->dataElements.last(); - } else { - Q_UNREACHABLE(); - } - if (!data->src.isEmpty() && !data->expr.isEmpty()) { - addError(QStringLiteral("data element with both 'src' and 'expr' attributes")); - return; - } - if (!p.chars.trimmed().isEmpty()) { - if (!data->src.isEmpty()) { - addError(QStringLiteral("data element with both 'src' attribute and CDATA")); - return; - } else if (!data->expr.isEmpty()) { - addError(QStringLiteral("data element with both 'expr' attribute and CDATA")); - return; - } else { - // w3c-ecma/test558 - "if a child element of <data> is not a XML, - // treat it as a string with whitespace normalization" - // We've modified the test, so that a string is enclosed with quotes. - data->expr = p.chars; - } - } else if (!data->src.isEmpty()) { - if (!m_loader) { - addError(QStringLiteral("cannot parse a document with external dependencies without a loader")); - } else { - bool ok; - QByteArray ba = load(data->src, &ok); - if (!ok) { - addError(QStringLiteral("failed to load external dependency")); - } else { - // w3c-ecma/test558 - "if XML is loaded via "src" attribute, - // treat it as a string with whitespace normalization" - // We've enclosed the text in file with quotes. - data->expr = QString::fromUtf8(ba); - } - } - } - } break; - } // parser state - } // QXmlStreamReader::EndElement - case QXmlStreamReader::Characters: - // The reader reports characters in text(). If the characters are all white-space, - // isWhitespace() returns true. If the characters stem from a CDATA section, - // isCDATA() returns true. + break; + case QXmlStreamReader::EndElement : + finished = true; + break; + case QXmlStreamReader::Characters : if (m_stack.isEmpty()) break; - if (m_stack.last().collectChars()) - m_stack.last().chars.append(m_reader->text()); + if (current().collectChars()) + current().chars.append(m_reader->text()); break; - case QXmlStreamReader::DTD: - // The reader reports a DTD in text(), notation declarations in notationDeclarations(), - // and entity declarations in entityDeclarations(). Details of the DTD declaration are - // reported in in dtdName(), dtdPublicId(), and dtdSystemId(). + case QXmlStreamReader::Comment: + parseComment(); break; - case QXmlStreamReader::EntityReference: - // The reader reports an entity reference that could not be resolved. The name of - // the reference is reported in name(), the replacement text in text(). - break; - case QXmlStreamReader::ProcessingInstruction: + default : break; } } - if (m_reader->hasError() && (m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError - || !m_doc->root)) { + + switch (elementKind) { + case ParserState::Scxml: if (!postReadElementScxml()) return false; break; + case ParserState::State: if (!postReadElementState()) return false; break; + case ParserState::Parallel: if (!postReadElementParallel()) return false; break; + case ParserState::Initial: if (!postReadElementInitial()) return false; break; + case ParserState::Transition: if (!postReadElementTransition()) return false; break; + case ParserState::Final: if (!postReadElementFinal()) return false; break; + case ParserState::History: if (!postReadElementHistory()) return false; break; + case ParserState::OnEntry: if (!postReadElementOnEntry()) return false; break; + case ParserState::OnExit: if (!postReadElementOnExit()) return false; break; + case ParserState::Raise: if (!postReadElementRaise()) return false; break; + case ParserState::If: if (!postReadElementIf()) return false; break; + case ParserState::ElseIf: if (!postReadElementElseIf()) return false; break; + case ParserState::Else: if (!postReadElementElse()) return false; break; + case ParserState::Foreach: if (!postReadElementForeach()) return false; break; + case ParserState::Log: if (!postReadElementLog()) return false; break; + case ParserState::DataModel: if (!postReadElementDataModel()) return false; break; + case ParserState::Data: if (!postReadElementData()) return false; break; + case ParserState::Assign: if (!postReadElementAssign()) return false; break; + case ParserState::DoneData: if (!postReadElementDoneData()) return false; break; + case ParserState::Content: if (!postReadElementContent()) return false; break; + case ParserState::Param: if (!postReadElementParam()) return false; break; + case ParserState::Script: if (!postReadElementScript()) return false; break; + case ParserState::Send: if (!postReadElementSend()) return false; break; + case ParserState::Cancel: if (!postReadElementCancel()) return false; break; + case ParserState::Invoke: if (!postReadElementInvoke()) return false; break; + case ParserState::Finalize: if (!postReadElementFinalize()) return false; break; + default: break; + } + + m_stack.removeLast(); + + if (m_reader->hasError()/* && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError*/) { addError(QStringLiteral("Error parsing SCXML file: %1").arg(m_reader->errorString())); + return false; } + + return true; } -QByteArray QScxmlParserPrivate::load(const QString &name, bool *ok) const +void QScxmlParserPrivate::parseComment() { - return m_loader->load(name, m_fileName.isEmpty() ? - QString() : QFileInfo(m_fileName).path(), ok); + static const QString qtModeSwitch = QStringLiteral("enable-qt-mode:"); + const QStringRef commentText = m_reader->text(); + int qtModeIdx = commentText.indexOf(qtModeSwitch); + if (qtModeIdx != -1) { + qtModeIdx += qtModeSwitch.size(); + while (qtModeIdx < commentText.size()) { + if (commentText.at(qtModeIdx).isSpace()) { + ++qtModeIdx; + } else { + break; + } + } + const QStringRef value = commentText.mid(qtModeIdx); + if (value.startsWith(QStringLiteral("yes")) && isWordEnd(value, 3)) { + if (m_qtMode == QScxmlParser::QtModeFromInputFile) + m_doc->qtMode = true; + } else if (value.startsWith(QStringLiteral("no")) && isWordEnd(value, 2)) { + if (m_qtMode == QScxmlParser::QtModeFromInputFile) + m_doc->qtMode = false; + } else { + addError(QStringLiteral("expected 'yes' or 'no' after enable-qt-mode in comment")); + } + } +} + +void QScxmlParserPrivate::currentStateUp() +{ + Q_ASSERT(m_currentState->parent); + m_currentState = m_currentState->parent; } -QScxmlParser::State QScxmlParserPrivate::state() const +bool QScxmlParserPrivate::flushInstruction() +{ + if (!hasPrevious()) { + addError(QStringLiteral("missing instructionContainer")); + return false; + } + DocumentModel::InstructionSequence *instructions = previous().instructionContainer; + if (!instructions) { + addError(QStringLiteral("got executable content within an element that did not set instructionContainer")); + return false; + } + instructions->append(current().instruction); + return true; +} + + +QByteArray QScxmlParserPrivate::load(const QString &name, bool *ok) const { - return m_state; + return m_loader->load(name, m_fileName.isEmpty() ? + QString() : QFileInfo(m_fileName).path(), ok); } QVector<QScxmlError> QScxmlParserPrivate::errors() const @@ -2747,13 +3090,11 @@ QVector<QScxmlError> QScxmlParserPrivate::errors() const void QScxmlParserPrivate::addError(const QString &msg) { m_errors.append(QScxmlError(m_fileName, m_reader->lineNumber(), m_reader->columnNumber(), msg)); - m_state = QScxmlParser::ParsingError; } void QScxmlParserPrivate::addError(const DocumentModel::XmlLocation &location, const QString &msg) { m_errors.append(QScxmlError(m_fileName, location.line, location.column, msg)); - m_state = QScxmlParser::ParsingError; } QScxmlParser::QtMode QScxmlParserPrivate::qtMode() const @@ -2793,7 +3134,12 @@ bool QScxmlParserPrivate::maybeId(const QXmlStreamAttributes &attributes, QStrin DocumentModel::If *QScxmlParserPrivate::lastIf() { - DocumentModel::Instruction *lastI = m_stack.last().instruction; + if (!hasPrevious()) { + addError(QStringLiteral("No previous instruction found for else block")); + return Q_NULLPTR; + } + + DocumentModel::Instruction *lastI = previous().instruction; if (!lastI) { addError(QStringLiteral("No previous instruction found for else block")); return Q_NULLPTR; @@ -2806,6 +3152,21 @@ DocumentModel::If *QScxmlParserPrivate::lastIf() return ifI; } +QScxmlParserPrivate::ParserState &QScxmlParserPrivate::current() +{ + return m_stack.last(); +} + +QScxmlParserPrivate::ParserState &QScxmlParserPrivate::previous() +{ + return m_stack[m_stack.count() - 2]; +} + +bool QScxmlParserPrivate::hasPrevious() const +{ + return m_stack.count() > 1; +} + bool QScxmlParserPrivate::checkAttributes(const QXmlStreamAttributes &attributes, QScxmlParserPrivate::ParserState::Kind kind) { diff --git a/src/scxml/qscxmlparser.h b/src/scxml/qscxmlparser.h index fcd3197..3ce749f 100644 --- a/src/scxml/qscxmlparser.h +++ b/src/scxml/qscxmlparser.h @@ -67,13 +67,6 @@ public: QScxmlParser *m_parser; }; - enum State { - StartingParsing, - ParsingScxml, - ParsingError, - FinishedParsing, - }; - enum QtMode { QtModeDisabled, QtModeEnabled, @@ -94,7 +87,6 @@ public: QScxmlStateMachine *instantiateStateMachine() const; void instantiateDataModel(QScxmlStateMachine *stateMachine) const; - State state() const; QVector<QScxmlError> errors() const; void addError(const QString &msg); diff --git a/src/scxml/qscxmlparser_p.h b/src/scxml/qscxmlparser_p.h index 162b611..3103605 100644 --- a/src/scxml/qscxmlparser_p.h +++ b/src/scxml/qscxmlparser_p.h @@ -566,11 +566,11 @@ public: QScxmlParser::Loader *loader() const; void setLoader(QScxmlParser::Loader *loader); - void parse(); + bool readDocument(); void parseSubDocument(DocumentModel::Invoke *parentInvoke, QXmlStreamReader *reader, const QString &fileName); + bool parseSubElement(DocumentModel::Invoke *parentInvoke, QXmlStreamReader *reader, const QString &fileName); QByteArray load(const QString &name, bool *ok) const; - QScxmlParser::State state() const; QVector<QScxmlError> errors() const; void addError(const QString &msg); @@ -588,6 +588,67 @@ private: const QStringList &requiredNames, const QStringList &optionalNames); + bool preReadElementScxml(); + bool preReadElementState(); + bool preReadElementParallel(); + bool preReadElementInitial(); + bool preReadElementTransition(); + bool preReadElementFinal(); + bool preReadElementHistory(); + bool preReadElementOnEntry(); + bool preReadElementOnExit(); + bool preReadElementRaise(); + bool preReadElementIf(); + bool preReadElementElseIf(); + bool preReadElementElse(); + bool preReadElementForeach(); + bool preReadElementLog(); + bool preReadElementDataModel(); + bool preReadElementData(); + bool preReadElementAssign(); + bool preReadElementDoneData(); + bool preReadElementContent(); + bool preReadElementParam(); + bool preReadElementScript(); + bool preReadElementSend(); + bool preReadElementCancel(); + bool preReadElementInvoke(); + bool preReadElementFinalize(); + + bool postReadElementScxml(); + bool postReadElementState(); + bool postReadElementParallel(); + bool postReadElementInitial(); + bool postReadElementTransition(); + bool postReadElementFinal(); + bool postReadElementHistory(); + bool postReadElementOnEntry(); + bool postReadElementOnExit(); + bool postReadElementRaise(); + bool postReadElementIf(); + bool postReadElementElseIf(); + bool postReadElementElse(); + bool postReadElementForeach(); + bool postReadElementLog(); + bool postReadElementDataModel(); + bool postReadElementData(); + bool postReadElementAssign(); + bool postReadElementDoneData(); + bool postReadElementContent(); + bool postReadElementParam(); + bool postReadElementScript(); + bool postReadElementSend(); + bool postReadElementCancel(); + bool postReadElementInvoke(); + bool postReadElementFinalize(); + + bool readElement(); + + void resetDocument(); + void parseComment(); + void currentStateUp(); + bool flushInstruction(); + private: struct ParserState { enum Kind { @@ -645,6 +706,9 @@ private: }; bool checkAttributes(const QXmlStreamAttributes &attributes, QScxmlParserPrivate::ParserState::Kind kind); + ParserState ¤t(); + ParserState &previous(); + bool hasPrevious() const; private: QString m_fileName; @@ -657,7 +721,6 @@ private: QXmlStreamReader *m_reader; QVector<ParserState> m_stack; - QScxmlParser::State m_state; QVector<QScxmlError> m_errors; QScxmlParser::QtMode m_qtMode; }; diff --git a/tests/auto/qscxmlc/data/commentInScript.scxml b/tests/auto/qscxmlc/data/commentInScript.scxml new file mode 100644 index 0000000..2af8286 --- /dev/null +++ b/tests/auto/qscxmlc/data/commentInScript.scxml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- enable-qt-mode: yes --> +<scxml + xmlns="http://www.w3.org/2005/07/scxml" + version="1.0" + name="CommentInScript" + datamodel="ecmascript" +> + <datamodel> + <data id="media"/> + </datamodel> + + <script><!-- + function isValidMedia() { + var m = _event.data.media + return (m + "").length > 0 + } + <initial> +</scxml> diff --git a/tests/auto/qscxmlc/tst_qscxmlc.qrc b/tests/auto/qscxmlc/tst_qscxmlc.qrc index d6b8a6a..a86c786 100644 --- a/tests/auto/qscxmlc/tst_qscxmlc.qrc +++ b/tests/auto/qscxmlc/tst_qscxmlc.qrc @@ -1,5 +1,6 @@ <RCC> <qresource prefix="/tst_qscxmlc"> + <file>data/commentInScript.scxml</file> <file>data/empty.scxml</file> <file>data/misplacedinvoke.scxml</file> <file>data/invalidRoot.scxml</file> diff --git a/tests/auto/scion/tst_scion.cpp b/tests/auto/scion/tst_scion.cpp index 92dc291..0e017c7 100644 --- a/tests/auto/scion/tst_scion.cpp +++ b/tests/auto/scion/tst_scion.cpp @@ -43,7 +43,17 @@ Q_DECLARE_METATYPE(std::function<QScxmlStateMachine *()>); enum { SpyWaitTime = 12000 }; -static QSet<QString> weFailOnThese = QSet<QString>() +static QSet<QString> testFailOnParse = QSet<QString>() + // Currently we do not support loading data as XML content inside the <data> tag. + << QLatin1String("w3c-ecma/test557.txml") + // The following test uses the undocumented "exmode" attribute. + << QLatin1String("w3c-ecma/test441a.txml") + // The following test needs manual inspection of the result. However, note that we do not support the undocumented "exmode" attribute. + << QLatin1String("w3c-ecma/test441b.txml") + // The following test needs manual inspection of the result. + ; + +static QSet<QString> testFailOnRun = QSet<QString>() // The following test needs manual inspection of the result. However, note that we do not support multiple identical keys for event data. << QLatin1String("delayedSend/send1") // same as above << QLatin1String("delayedSend/send2") // same as above @@ -54,20 +64,12 @@ static QSet<QString> weFailOnThese = QSet<QString>() << QLatin1String("w3c-ecma/test201.txml") << QLatin1String("w3c-ecma/test364.txml") // initial attribute on <state> << QLatin1String("w3c-ecma/test388.txml") // Qt refuses to set an initial state to a "deep" state - - // Currently we do not support loading data as XML content inside the <data> tag. - << QLatin1String("w3c-ecma/test557.txml") - // The following test uses the undocumented "exmode" attribute. - << QLatin1String("w3c-ecma/test441a.txml") - // The following test needs manual inspection of the result. However, note that we do not support the undocumented "exmode" attribute. - << QLatin1String("w3c-ecma/test441b.txml") - // The following test needs manual inspection of the result. << QLatin1String("w3c-ecma/test230.txml") << QLatin1String("w3c-ecma/test250.txml") << QLatin1String("w3c-ecma/test307.txml") ; -static QSet<QString> differentSemantics = QSet<QString>() +static QSet<QString> testUseDifferentSemantics = QSet<QString>() // FIXME: looks like a bug in internal event ordering when writing to read-only variables. << QLatin1String("w3c-ecma/test329.txml") // Qt does not support forcing initial states that are not marked as such. @@ -165,7 +167,8 @@ void TestScion::initTestCase() enum TestStatus { TestIsOk, - TestFails, + TestFailsOnParse, + TestFailsOnRun, TestUsesDifferentSemantics }; Q_DECLARE_METATYPE(TestStatus) @@ -181,10 +184,12 @@ void TestScion::generateData() for (int i = 0; i < nrOfTests; ++i) { TestStatus testStatus; QString base = QString::fromUtf8(testBases[i]); - if (differentSemantics.contains(base)) + if (testUseDifferentSemantics.contains(base)) testStatus = TestUsesDifferentSemantics; - else if (weFailOnThese.contains(base)) - testStatus = TestFails; + else if (testFailOnParse.contains(base)) + testStatus = TestFailsOnParse; + else if (testFailOnRun.contains(base)) + testStatus = TestFailsOnRun; else testStatus = TestIsOk; QTest::newRow(testBases[i]) << base + QLatin1String(".scxml") @@ -226,22 +231,21 @@ void TestScion::dynamic() DynamicLoader loader(&parser); parser.setLoader(&loader); parser.parse(); - if (testStatus == TestFails && parser.state() != QScxmlParser::FinishedParsing) - QEXPECT_FAIL("", "This is expected to fail", Abort); - QCOMPARE(parser.state(), QScxmlParser::FinishedParsing); + if (testStatus == TestFailsOnParse) + QEXPECT_FAIL("", "This is expected to fail on parse", Abort); QVERIFY(parser.errors().isEmpty()); scxmlFile.close(); QScopedPointer<QScxmlStateMachine> stateMachine(parser.instantiateStateMachine()); - if (stateMachine == Q_NULLPTR && testStatus == TestFails) { - QEXPECT_FAIL("", "This is expected to fail", Abort); - } QVERIFY(stateMachine != Q_NULLPTR); + parser.instantiateDataModel(stateMachine.data()); - if (testStatus == TestFails) - QEXPECT_FAIL("", "This is expected to fail", Abort); - QVERIFY(runTest(stateMachine.data(), testDescription.object())); + const bool runResult = runTest(stateMachine.data(), testDescription.object()); + if (runResult == false && testStatus == TestFailsOnRun) + QEXPECT_FAIL("", "This is expected to fail on run", Abort); + + QVERIFY(runResult); QCoreApplication::processEvents(); // flush any pending events } @@ -279,14 +283,17 @@ void TestScion::compiled() jsonFile.close(); QScopedPointer<QScxmlStateMachine> stateMachine(creator()); - if (stateMachine == Q_NULLPTR && testStatus == TestFails) { + if (stateMachine == Q_NULLPTR && testStatus == TestFailsOnRun) { QEXPECT_FAIL("", "This is expected to fail", Abort); } QVERIFY(stateMachine != Q_NULLPTR); - if (testStatus == TestFails) - QEXPECT_FAIL("", "This is expected to fail", Abort); - QVERIFY(runTest(stateMachine.data(), testDescription.object())); + const bool runResult = runTest(stateMachine.data(), testDescription.object()); + if (runResult == false && testStatus == TestFailsOnRun) + QEXPECT_FAIL("", "This is expected to fail on run", Abort); + + QVERIFY(runResult); + QCoreApplication::processEvents(); // flush any pending events } |