diff --git a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in index c1bbb8a14b2..09d71dabc96 100644 --- a/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/PyQt6/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -67,6 +67,20 @@ Returns the outputs generated by the child algorithm. Sets the ``outputs`` used for the child algorithm. .. seealso:: :py:func:`outputs` +%End + + QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`setHtmlLog` +%End + + void setHtmlLog( const QString &log ); +%Docstring +Sets the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`htmlLog` %End bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index 3d5c310e1eb..45847f9052c 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -67,6 +67,20 @@ Returns the outputs generated by the child algorithm. Sets the ``outputs`` used for the child algorithm. .. seealso:: :py:func:`outputs` +%End + + QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`setHtmlLog` +%End + + void setHtmlLog( const QString &log ); +%Docstring +Sets the HTML formatted contents of logged messages which occurred while running the child. + +.. seealso:: :py:func:`htmlLog` %End bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 41a7920e931..a9efe94b4ca 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -326,6 +326,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QVariantMap finalResults; QSet< QString > executed; bool executedAlg = true; + int previousHtmlLogLength = 0; while ( executedAlg && executed.count() < toExecute.count() ) { executedAlg = false; @@ -503,124 +504,142 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa childResults.insert( childId, results ); childResult.setOutputs( results ); - context.modelChildResults().insert( childId, childResult ); + if ( runResult ) + { + if ( feedback && !skipGenericLogging ) + { + const QVariantMap displayOutputs = QgsProcessingUtils::removePointerValuesFromMap( results ); + QStringList formattedOutputs; + for ( auto displayOutputIt = displayOutputs.constBegin(); displayOutputIt != displayOutputs.constEnd(); ++displayOutputIt ) + { + formattedOutputs << QStringLiteral( "%1: %2" ).arg( displayOutputIt.key(), + QgsProcessingUtils::variantToPythonLiteral( displayOutputIt.value() ) );; + } + feedback->pushInfo( QObject::tr( "Results:" ) ); + feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); + } + + // look through child alg's outputs to determine whether any of these should be copied + // to the final model outputs + const QMap outputs = child.modelOutputs(); + for ( auto outputIt = outputs.constBegin(); outputIt != outputs.constEnd(); ++outputIt ) + { + const int outputSortKey = mOutputOrder.indexOf( QStringLiteral( "%1:%2" ).arg( childId, outputIt->childOutputName() ) ); + switch ( mInternalVersion ) + { + case QgsProcessingModelAlgorithm::InternalVersion::Version1: + finalResults.insert( childId + ':' + outputIt->name(), results.value( outputIt->childOutputName() ) ); + break; + case QgsProcessingModelAlgorithm::InternalVersion::Version2: + if ( const QgsProcessingParameterDefinition *modelParam = modelParameterFromChildIdAndOutputName( child.childId(), outputIt.key() ) ) + { + finalResults.insert( modelParam->name(), results.value( outputIt->childOutputName() ) ); + } + break; + } + + if ( !results.value( outputIt->childOutputName() ).toString().isEmpty() ) + { + QgsProcessingContext::LayerDetails &details = context.layerToLoadOnCompletionDetails( results.value( outputIt->childOutputName() ).toString() ); + details.groupName = mOutputGroup; + if ( outputSortKey > 0 ) + details.layerSortKey = outputSortKey; + } + } + + executed.insert( childId ); + + std::function< void( const QString &, const QString & )> pruneAlgorithmBranchRecursive; + pruneAlgorithmBranchRecursive = [&]( const QString & id, const QString &branch = QString() ) + { + const QSet toPrune = dependentChildAlgorithms( id, branch ); + for ( const QString &targetId : toPrune ) + { + if ( executed.contains( targetId ) ) + continue; + + executed.insert( targetId ); + pruneAlgorithmBranchRecursive( targetId, branch ); + } + }; + + // prune remaining algorithms if they are dependent on a branch from this child which didn't eventuate + const QgsProcessingOutputDefinitions outputDefs = childAlg->outputDefinitions(); + for ( const QgsProcessingOutputDefinition *outputDef : outputDefs ) + { + if ( outputDef->type() == QgsProcessingOutputConditionalBranch::typeName() && !results.value( outputDef->name() ).toBool() ) + { + pruneAlgorithmBranchRecursive( childId, outputDef->name() ); + } + } + + if ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::PruneModelBranchesBasedOnAlgorithmResults ) + { + // check if any dependent algorithms should be canceled based on the outputs of this algorithm run + // first find all direct dependencies of this algorithm by looking through all remaining child algorithms + for ( const QString &candidateId : std::as_const( toExecute ) ) + { + if ( executed.contains( candidateId ) ) + continue; + + // a pending algorithm was found..., check it's parameter sources to see if it links to any of the current + // algorithm's outputs + const QgsProcessingModelChildAlgorithm &candidate = mChildAlgorithms[ candidateId ]; + const QMap candidateParams = candidate.parameterSources(); + QMap::const_iterator paramIt = candidateParams.constBegin(); + bool pruned = false; + for ( ; paramIt != candidateParams.constEnd(); ++paramIt ) + { + for ( const QgsProcessingModelChildParameterSource &source : paramIt.value() ) + { + if ( source.source() == Qgis::ProcessingModelChildParameterSource::ChildOutput && source.outputChildId() == childId ) + { + // ok, this one is dependent on the current alg. Did we get a value for it? + if ( !results.contains( source.outputName() ) ) + { + // oh no, nothing returned for this parameter. Gotta trim the branch back! + pruned = true; + // skip the dependent alg.. + executed.insert( candidateId ); + //... and everything which depends on it + pruneAlgorithmBranchRecursive( candidateId, QString() ); + break; + } + } + } + if ( pruned ) + break; + } + } + } + + childAlg.reset( nullptr ); + modelFeedback.setCurrentStep( executed.count() ); + if ( feedback && !skipGenericLogging ) + { + feedback->pushInfo( QObject::tr( "OK. Execution took %1 s (%n output(s)).", nullptr, results.count() ).arg( childTime.elapsed() / 1000.0 ) ); + } + } + + // trim out just the portion of the overall log which relates to this child + const QString thisAlgorithmHtmlLog = feedback->htmlLog().mid( previousHtmlLogLength ); + previousHtmlLogLength = feedback->htmlLog().length(); if ( !runResult ) { + const QString formattedException = QStringLiteral( "%1
" ).arg( error.toHtmlEscaped() ).replace( '\n', QLatin1String( "
" ) ); + const QString formattedRunTime = QStringLiteral( "%1
" ).arg( QObject::tr( "Failed after %1 s." ).arg( childTime.elapsed() / 1000.0 ).toHtmlEscaped() ).replace( '\n', QLatin1String( "
" ) ); + + childResult.setHtmlLog( thisAlgorithmHtmlLog + formattedException + formattedRunTime ); + context.modelChildResults().insert( childId, childResult ); + throw QgsProcessingException( error ); } - - if ( feedback && !skipGenericLogging ) + else { - const QVariantMap displayOutputs = QgsProcessingUtils::removePointerValuesFromMap( results ); - QStringList formattedOutputs; - for ( auto displayOutputIt = displayOutputs.constBegin(); displayOutputIt != displayOutputs.constEnd(); ++displayOutputIt ) - { - formattedOutputs << QStringLiteral( "%1: %2" ).arg( displayOutputIt.key(), - QgsProcessingUtils::variantToPythonLiteral( displayOutputIt.value() ) );; - } - feedback->pushInfo( QObject::tr( "Results:" ) ); - feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( formattedOutputs.join( QLatin1String( ", " ) ) ) ); + childResult.setHtmlLog( thisAlgorithmHtmlLog ); + context.modelChildResults().insert( childId, childResult ); } - - // look through child alg's outputs to determine whether any of these should be copied - // to the final model outputs - const QMap outputs = child.modelOutputs(); - for ( auto outputIt = outputs.constBegin(); outputIt != outputs.constEnd(); ++outputIt ) - { - const int outputSortKey = mOutputOrder.indexOf( QStringLiteral( "%1:%2" ).arg( childId, outputIt->childOutputName() ) ); - switch ( mInternalVersion ) - { - case QgsProcessingModelAlgorithm::InternalVersion::Version1: - finalResults.insert( childId + ':' + outputIt->name(), results.value( outputIt->childOutputName() ) ); - break; - case QgsProcessingModelAlgorithm::InternalVersion::Version2: - if ( const QgsProcessingParameterDefinition *modelParam = modelParameterFromChildIdAndOutputName( child.childId(), outputIt.key() ) ) - { - finalResults.insert( modelParam->name(), results.value( outputIt->childOutputName() ) ); - } - break; - } - - if ( !results.value( outputIt->childOutputName() ).toString().isEmpty() ) - { - QgsProcessingContext::LayerDetails &details = context.layerToLoadOnCompletionDetails( results.value( outputIt->childOutputName() ).toString() ); - details.groupName = mOutputGroup; - if ( outputSortKey > 0 ) - details.layerSortKey = outputSortKey; - } - } - - executed.insert( childId ); - - std::function< void( const QString &, const QString & )> pruneAlgorithmBranchRecursive; - pruneAlgorithmBranchRecursive = [&]( const QString & id, const QString &branch = QString() ) - { - const QSet toPrune = dependentChildAlgorithms( id, branch ); - for ( const QString &targetId : toPrune ) - { - if ( executed.contains( targetId ) ) - continue; - - executed.insert( targetId ); - pruneAlgorithmBranchRecursive( targetId, branch ); - } - }; - - // prune remaining algorithms if they are dependent on a branch from this child which didn't eventuate - const QgsProcessingOutputDefinitions outputDefs = childAlg->outputDefinitions(); - for ( const QgsProcessingOutputDefinition *outputDef : outputDefs ) - { - if ( outputDef->type() == QgsProcessingOutputConditionalBranch::typeName() && !results.value( outputDef->name() ).toBool() ) - { - pruneAlgorithmBranchRecursive( childId, outputDef->name() ); - } - } - - if ( childAlg->flags() & Qgis::ProcessingAlgorithmFlag::PruneModelBranchesBasedOnAlgorithmResults ) - { - // check if any dependent algorithms should be canceled based on the outputs of this algorithm run - // first find all direct dependencies of this algorithm by looking through all remaining child algorithms - for ( const QString &candidateId : std::as_const( toExecute ) ) - { - if ( executed.contains( candidateId ) ) - continue; - - // a pending algorithm was found..., check it's parameter sources to see if it links to any of the current - // algorithm's outputs - const QgsProcessingModelChildAlgorithm &candidate = mChildAlgorithms[ candidateId ]; - const QMap candidateParams = candidate.parameterSources(); - QMap::const_iterator paramIt = candidateParams.constBegin(); - bool pruned = false; - for ( ; paramIt != candidateParams.constEnd(); ++paramIt ) - { - for ( const QgsProcessingModelChildParameterSource &source : paramIt.value() ) - { - if ( source.source() == Qgis::ProcessingModelChildParameterSource::ChildOutput && source.outputChildId() == childId ) - { - // ok, this one is dependent on the current alg. Did we get a value for it? - if ( !results.contains( source.outputName() ) ) - { - // oh no, nothing returned for this parameter. Gotta trim the branch back! - pruned = true; - // skip the dependent alg.. - executed.insert( candidateId ); - //... and everything which depends on it - pruneAlgorithmBranchRecursive( candidateId, QString() ); - break; - } - } - } - if ( pruned ) - break; - } - } - } - - childAlg.reset( nullptr ); - modelFeedback.setCurrentStep( executed.count() ); - if ( feedback && !skipGenericLogging ) - feedback->pushInfo( QObject::tr( "OK. Execution took %1 s (%n output(s)).", nullptr, results.count() ).arg( childTime.elapsed() / 1000.0 ) ); } if ( feedback && feedback->isCanceled() ) diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index 0b197d3f5bc..378355d620a 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -86,9 +86,24 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult */ void setOutputs( const QVariantMap &outputs ) { mOutputs = outputs; } + /** + * Returns the HTML formatted contents of logged messages which occurred while running the child. + * + * \see setHtmlLog() + */ + QString htmlLog() const { return mHtmlLog; } + + /** + * Sets the HTML formatted contents of logged messages which occurred while running the child. + * + * \see htmlLog() + */ + void setHtmlLog( const QString &log ) { mHtmlLog = log; } + bool operator==( const QgsProcessingModelChildAlgorithmResult &other ) const { return mExecutionStatus == other.mExecutionStatus + && mHtmlLog == other.mHtmlLog && mInputs == other.mInputs && mOutputs == other.mOutputs; } @@ -102,6 +117,7 @@ class CORE_EXPORT QgsProcessingModelChildAlgorithmResult Qgis::ProcessingModelChildAlgorithmExecutionStatus mExecutionStatus = Qgis::ProcessingModelChildAlgorithmExecutionStatus::NotExecuted; QVariantMap mInputs; QVariantMap mOutputs; + QString mHtmlLog; }; diff --git a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp index 6a465ac5f27..df094d16e76 100644 --- a/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp +++ b/tests/src/analysis/testqgsprocessingmodelalgorithm.cpp @@ -69,7 +69,7 @@ class DummyRaiseExceptionAlgorithm : public QgsProcessingAlgorithm QString displayName() const override { return mName; } QVariantMap processAlgorithm( const QVariantMap &, QgsProcessingContext &, QgsProcessingFeedback * ) override { - throw QgsProcessingException( QString() ); + throw QgsProcessingException( QStringLiteral( "something bad happened" ) ); } static bool postProcessAlgorithmCalled; QVariantMap postProcessAlgorithm( QgsProcessingContext &, QgsProcessingFeedback * ) final @@ -2330,12 +2330,14 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QgsProcessingModelChildAlgorithm algWhichRaisesException; algWhichRaisesException.setChildId( QStringLiteral( "raise" ) ); + algWhichRaisesException.setDescription( QStringLiteral( "my second step" ) ); algWhichRaisesException.setAlgorithmId( "dummy4:raise" ); algWhichRaisesException.setDependencies( {QgsProcessingModelChildDependency( QStringLiteral( "buffer" ) )} ); m.addChildAlgorithm( algWhichRaisesException ); // run and check context details QgsProcessingContext context; + context.setLogLevel( Qgis::ProcessingLogLevel::ModelDebug ); QgsProcessingFeedback feedback; QVariantMap params; QgsVectorLayer *layer3111 = new QgsVectorLayer( "Point?crs=epsg:3111", "v1", "memory" ); @@ -2359,9 +2361,13 @@ void TestQgsProcessingModelAlgorithm::modelWithChildException() QCOMPARE( context.modelChildResults().value( "buffer" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Success ); QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "INPUT" ).toString(), QStringLiteral( "v1" ) ); QCOMPARE( context.modelChildResults().value( "buffer" ).inputs().value( "OUTPUT" ).toString(), QStringLiteral( "memory:Buffered" ) ); + QCOMPARE( context.modelChildResults().value( "buffer" ).htmlLog().left( 50 ), QStringLiteral( "Prepare algorithm: buffer" ) ); + QCOMPARE( context.modelChildResults().value( "buffer" ).htmlLog().right( 21 ), QStringLiteral( "s (1 output(s)).
" ) ); QVERIFY( context.temporaryLayerStore()->mapLayer( context.modelChildResults().value( "buffer" ).outputs().value( "OUTPUT" ).toString() ) ); QCOMPARE( context.modelChildResults().value( "raise" ).executionStatus(), Qgis::ProcessingModelChildAlgorithmExecutionStatus::Failed ); + QCOMPARE( context.modelChildResults().value( "raise" ).htmlLog().left( 49 ), QStringLiteral( "Prepare algorithm: raise" ) ); + QVERIFY( context.modelChildResults().value( "raise" ).htmlLog().contains( QStringLiteral( "Error encountered while running my second step: something bad happened" ) ) ); } void TestQgsProcessingModelAlgorithm::modelDependencies()