From 9a4c599e22928bdbc246804d8e5ef3032578d3fb Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 22 Jan 2021 14:49:20 +0100 Subject: [PATCH 1/7] [libcalamares] Tidy logging a little for Python errors --- src/libcalamares/PythonJob.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libcalamares/PythonJob.cpp b/src/libcalamares/PythonJob.cpp index 6944f38e5..98f284ecc 100644 --- a/src/libcalamares/PythonJob.cpp +++ b/src/libcalamares/PythonJob.cpp @@ -261,12 +261,12 @@ PythonJob::exec() { m_description.truncate( i_newline ); } - cDebug() << "Job description from __doc__" << prettyName() << '=' << m_description; + cDebug() << Logger::SubEntry << "Job description from __doc__" << prettyName() << '=' << m_description; } } else { - cDebug() << "Job description from pretty_name" << prettyName() << '=' << m_description; + cDebug() << Logger::SubEntry << "Job description from pretty_name" << prettyName() << '=' << m_description; } emit progress( 0 ); From f0fd47eeb31de923cc6bad6384d888c26f17e5ec Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 26 Jan 2021 00:13:10 +0100 Subject: [PATCH 2/7] [libcalamares] Simplify logging-manipulators Writing `Logger::NoQuote{}`` has annoyed me for a while, so switch it to a constant, like SubEntry, so it looks more like a regular manipulator object. --- src/libcalamares/utils/CalamaresUtilsSystem.cpp | 8 ++++---- src/libcalamares/utils/Logger.cpp | 2 ++ src/libcalamares/utils/Logger.h | 12 +++++++----- src/libcalamaresui/Branding.cpp | 2 +- src/libcalamaresui/ViewManager.cpp | 6 +++--- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/libcalamares/utils/CalamaresUtilsSystem.cpp b/src/libcalamares/utils/CalamaresUtilsSystem.cpp index 841d52969..29f743743 100644 --- a/src/libcalamares/utils/CalamaresUtilsSystem.cpp +++ b/src/libcalamares/utils/CalamaresUtilsSystem.cpp @@ -188,7 +188,7 @@ System::runCommand( System::RunLocation location, : -1 ) ) { cWarning() << "Process" << args.first() << "timed out after" << timeoutSec.count() << "s. Output so far:\n" - << Logger::NoQuote {} << process.readAllStandardOutput(); + << Logger::NoQuote << process.readAllStandardOutput(); return ProcessResult::Code::TimedOut; } @@ -196,7 +196,7 @@ System::runCommand( System::RunLocation location, if ( process.exitStatus() == QProcess::CrashExit ) { - cWarning() << "Process" << args.first() << "crashed. Output so far:\n" << Logger::NoQuote {} << output; + cWarning() << "Process" << args.first() << "crashed. Output so far:\n" << Logger::NoQuote << output; return ProcessResult::Code::Crashed; } @@ -206,7 +206,7 @@ System::runCommand( System::RunLocation location, { if ( showDebug && !output.isEmpty() ) { - cDebug() << Logger::SubEntry << "Finished. Exit code:" << r << "output:\n" << Logger::NoQuote {} << output; + cDebug() << Logger::SubEntry << "Finished. Exit code:" << r << "output:\n" << Logger::NoQuote << output; } else { @@ -218,7 +218,7 @@ System::runCommand( System::RunLocation location, if ( !output.isEmpty() ) { cDebug() << Logger::SubEntry << "Target cmd:" << RedactedList( args ) << "Exit code:" << r << "output:\n" - << Logger::NoQuote {} << output; + << Logger::NoQuote << output; } else { diff --git a/src/libcalamares/utils/Logger.cpp b/src/libcalamares/utils/Logger.cpp index 0a7dcefd0..262ff59e1 100644 --- a/src/libcalamares/utils/Logger.cpp +++ b/src/libcalamares/utils/Logger.cpp @@ -207,6 +207,8 @@ constexpr FuncSuppressor::FuncSuppressor( const char s[] ) const constexpr FuncSuppressor Continuation( s_Continuation ); const constexpr FuncSuppressor SubEntry( s_SubEntry ); +const constexpr NoQuote_t NoQuote {}; +const constexpr Quote_t Quote {}; QString toString( const QVariant& v ) diff --git a/src/libcalamares/utils/Logger.h b/src/libcalamares/utils/Logger.h index 58603c82d..a53ab7e19 100644 --- a/src/libcalamares/utils/Logger.h +++ b/src/libcalamares/utils/Logger.h @@ -25,15 +25,17 @@ struct FuncSuppressor const char* m_s; }; -struct NoQuote +struct NoQuote_t { }; -struct Quote +struct Quote_t { }; DLLEXPORT extern const FuncSuppressor Continuation; DLLEXPORT extern const FuncSuppressor SubEntry; +DLLEXPORT extern const NoQuote_t NoQuote; +DLLEXPORT extern const Quote_t Quote; enum { @@ -74,13 +76,13 @@ operator<<( QDebug& s, const FuncSuppressor& f ) } inline QDebug& -operator<<( QDebug& s, const NoQuote& ) +operator<<( QDebug& s, const NoQuote_t& ) { return s.noquote().nospace(); } inline QDebug& -operator<<( QDebug& s, const Quote& ) +operator<<( QDebug& s, const Quote_t& ) { return s.quote().space(); } @@ -254,7 +256,7 @@ operator<<( QDebug& s, const DebugMap& t ) inline QDebug& operator<<( QDebug& s, const Pointer& p ) { - s << NoQuote {} << '@' << p.ptr << Quote {}; + s << NoQuote << '@' << p.ptr << Quote; return s; } } // namespace Logger diff --git a/src/libcalamaresui/Branding.cpp b/src/libcalamaresui/Branding.cpp index 8145ad57c..a5038d7ee 100644 --- a/src/libcalamaresui/Branding.cpp +++ b/src/libcalamaresui/Branding.cpp @@ -35,7 +35,7 @@ [[noreturn]] static void bail( const QString& descriptorPath, const QString& message ) { - cError() << "FATAL in" << descriptorPath << Logger::Continuation << Logger::NoQuote {} << message; + cError() << "FATAL in" << descriptorPath << Logger::Continuation << Logger::NoQuote << message; ::exit( EXIT_FAILURE ); } diff --git a/src/libcalamaresui/ViewManager.cpp b/src/libcalamaresui/ViewManager.cpp index f43152209..0614c786f 100644 --- a/src/libcalamaresui/ViewManager.cpp +++ b/src/libcalamaresui/ViewManager.cpp @@ -142,9 +142,9 @@ ViewManager::onInstallationFailed( const QString& message, const QString& detail { bool shouldOfferWebPaste = false; // TODO: config var - cError() << "Installation failed:"; - cDebug() << "- message:" << message; - cDebug() << "- details:" << details; + cError() << "Installation failed:" << message; + cDebug() << Logger::SubEntry << "- message:" << message; + cDebug() << Logger::SubEntry << "- details:" << Logger::NoQuote << details; QString heading = Calamares::Settings::instance()->isSetupMode() ? tr( "Setup Failed" ) : tr( "Installation Failed" ); From 4f78afe67e6b71ec16ece4810826c04e6a75fd5d Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 26 Jan 2021 00:37:08 +0100 Subject: [PATCH 3/7] [libcalamaresui] Display a reduced amount of details Cut the error message from down to a maximum of 8 lines so that the messagebox does not hopelessly overflow. --- src/libcalamaresui/ViewManager.cpp | 36 +++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/libcalamaresui/ViewManager.cpp b/src/libcalamaresui/ViewManager.cpp index 0614c786f..a34c55741 100644 --- a/src/libcalamaresui/ViewManager.cpp +++ b/src/libcalamaresui/ViewManager.cpp @@ -136,6 +136,40 @@ ViewManager::insertViewStep( int before, ViewStep* step ) emit endInsertRows(); } +/** Makes a long details message suitable for display + * + * @returns a (possibly shortened) version of @p text that + * will fit sensibly in a messagebox. + */ +static QString +simplifyText( const QString& text ) +{ + constexpr const int maxLines = 8; + constexpr const int maxChars = 640; + QString shorter = text.simplified(); + if ( shorter.count( '\n' ) >= maxLines ) + { + int from = -1; + for ( int i = 0; i < maxLines; ++i ) + { + from = shorter.indexOf( '\n', from + 1 ); + if ( from < 0 ) + { + // That's odd, we counted at least maxLines newlines before + break; + } + } + if ( from > 0 ) + { + shorter.truncate( from ); + } + } + if ( shorter.length() > maxChars ) + { + shorter.truncate( maxChars ); + } + return shorter; +} void ViewManager::onInstallationFailed( const QString& message, const QString& details ) @@ -152,7 +186,7 @@ ViewManager::onInstallationFailed( const QString& message, const QString& detail QString text = "

" + message + "

"; if ( !details.isEmpty() ) { - text += "

" + details + "

"; + text += "

" + simplifyText( details ) + "

"; } if ( shouldOfferWebPaste ) { From 8cc114bf2c77566020c6cf78ca6aa1e1e70550ba Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 27 Jan 2021 23:51:03 +0100 Subject: [PATCH 4/7] [libcalamares] Move smart-string-truncation to library Expand the API a little to support first-lines, last-lines, and something of both. Use strong types to make the names clear for each. --- src/libcalamares/utils/String.cpp | 49 ++++++++++++++++++++++++++++++ src/libcalamares/utils/String.h | 30 ++++++++++++++++++ src/libcalamaresui/ViewManager.cpp | 38 ++--------------------- 3 files changed, 81 insertions(+), 36 deletions(-) diff --git a/src/libcalamares/utils/String.cpp b/src/libcalamares/utils/String.cpp index 34a7038e3..e2409d3c0 100644 --- a/src/libcalamares/utils/String.cpp +++ b/src/libcalamares/utils/String.cpp @@ -121,4 +121,53 @@ obscure( const QString& string ) return result; } + +QString +truncateMultiLine( const QString& string, CalamaresUtils::LinesStartEnd lines, CalamaresUtils::CharCount chars ) +{ + const int maxLines = lines.atStart + lines.atEnd; + if ( ( string.length() <= chars.total ) && ( string.count( '\n' ) <= maxLines ) ) + { + return string; + } + + QString shorter = string.simplified(); + QString front, back; + if ( shorter.count( '\n' ) >= maxLines ) + { + int from = -1; + for ( int i = 0; i < lines.atStart; ++i ) + { + from = shorter.indexOf( '\n', from + 1 ); + if ( from < 0 ) + { + // That's strange, we counted at least maxLines newlines before + break; + } + } + if ( from > 0 ) + { + front = shorter.left( from ); + } + + int lastNewLine = -1; + int lastCount = 0; + for ( auto i = shorter.rbegin(); i != shorter.rend() && lastCount < lines.atEnd; ++i ) + { + if ( *i == '\n' ) + { + ++lastCount; + lastNewLine = shorter.length() - int( i - shorter.rbegin() ); + } + } + if ( ( lastNewLine >= 0 ) && ( lastCount >= lines.atEnd ) ) + { + back = shorter.right( lastNewLine ); + } + } + + return front + back; +} + + } // namespace CalamaresUtils diff --git a/src/libcalamares/utils/String.h b/src/libcalamares/utils/String.h index 48bb17aac..405c6caad 100644 --- a/src/libcalamares/utils/String.h +++ b/src/libcalamares/utils/String.h @@ -61,6 +61,36 @@ DLLEXPORT QString removeDiacritics( const QString& string ); * @return the obfuscated string. */ DLLEXPORT QString obscure( const QString& string ); + +struct LinesStartEnd +{ + int atStart; + int atEnd; +}; + +struct CharCount +{ + int total; +}; + +/** @brief Truncate a string to some reasonable length for display + * + * Keep the first few, or last few (or both) lines of a possibly lengthy + * message @p string and reduce it to a displayable size (e.g. for + * pop-up windows that display the message). If the message is longer + * than @p chars, then characters are removed from the front (if + * @p lines.atStart is zero) or end (if @p lines.atEnd is zero) or in the middle + * (if both are nonzero). + * + * @param string the input string. + * @param lines number of lines to preserve. + * @param chars maximum number of characters in the returned string. + * @return a string built from parts of the input string. + */ +DLLEXPORT QString truncateMultiLine( const QString& string, + LinesStartEnd lines = LinesStartEnd { 3, 5 }, + CharCount chars = CharCount { 812 } ); + } // namespace CalamaresUtils #endif diff --git a/src/libcalamaresui/ViewManager.cpp b/src/libcalamaresui/ViewManager.cpp index a34c55741..aaa55059e 100644 --- a/src/libcalamaresui/ViewManager.cpp +++ b/src/libcalamaresui/ViewManager.cpp @@ -19,6 +19,7 @@ #include "utils/Logger.h" #include "utils/Paste.h" #include "utils/Retranslator.h" +#include "utils/String.h" #include "viewpages/BlankViewStep.h" #include "viewpages/ExecutionViewStep.h" #include "viewpages/ViewStep.h" @@ -136,41 +137,6 @@ ViewManager::insertViewStep( int before, ViewStep* step ) emit endInsertRows(); } -/** Makes a long details message suitable for display - * - * @returns a (possibly shortened) version of @p text that - * will fit sensibly in a messagebox. - */ -static QString -simplifyText( const QString& text ) -{ - constexpr const int maxLines = 8; - constexpr const int maxChars = 640; - QString shorter = text.simplified(); - if ( shorter.count( '\n' ) >= maxLines ) - { - int from = -1; - for ( int i = 0; i < maxLines; ++i ) - { - from = shorter.indexOf( '\n', from + 1 ); - if ( from < 0 ) - { - // That's odd, we counted at least maxLines newlines before - break; - } - } - if ( from > 0 ) - { - shorter.truncate( from ); - } - } - if ( shorter.length() > maxChars ) - { - shorter.truncate( maxChars ); - } - return shorter; -} - void ViewManager::onInstallationFailed( const QString& message, const QString& details ) { @@ -186,7 +152,7 @@ ViewManager::onInstallationFailed( const QString& message, const QString& detail QString text = "

" + message + "

"; if ( !details.isEmpty() ) { - text += "

" + simplifyText( details ) + "

"; + text += "

" + CalamaresUtils::truncateMultiLine( details, CalamaresUtils::LinesStartEnd{8, 0}) + "

"; } if ( shouldOfferWebPaste ) { From 3be360e433f24da4220cc6a8441e73ea453932d6 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 28 Jan 2021 00:23:13 +0100 Subject: [PATCH 5/7] [libcalamares] Add tests to string truncation - check that basic manipulations succeed - trailing-lines selection fails, though --- src/libcalamares/utils/String.cpp | 7 ++++ src/libcalamares/utils/String.h | 13 +++++-- src/libcalamares/utils/Tests.cpp | 64 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/libcalamares/utils/String.cpp b/src/libcalamares/utils/String.cpp index e2409d3c0..f032fa61b 100644 --- a/src/libcalamares/utils/String.cpp +++ b/src/libcalamares/utils/String.cpp @@ -126,6 +126,13 @@ QString truncateMultiLine( const QString& string, CalamaresUtils::LinesStartEnd lines, CalamaresUtils::CharCount chars ) { const int maxLines = lines.atStart + lines.atEnd; + if ( maxLines < 1 ) + { + QString shorter( string ); + shorter.truncate( chars.total ); + return shorter; + } + if ( ( string.length() <= chars.total ) && ( string.count( '\n' ) <= maxLines ) ) { return string; diff --git a/src/libcalamares/utils/String.h b/src/libcalamares/utils/String.h index 405c6caad..43e0474fa 100644 --- a/src/libcalamares/utils/String.h +++ b/src/libcalamares/utils/String.h @@ -62,15 +62,22 @@ DLLEXPORT QString removeDiacritics( const QString& string ); */ DLLEXPORT QString obscure( const QString& string ); +/** @brief Parameter for counting lines at beginning and end of string + * + * This is used by truncateMultiLine() to indicate how many lines from + * the beginning and how many from the end should be kept. + */ struct LinesStartEnd { - int atStart; - int atEnd; + int atStart = 0; + int atEnd = 0; }; +/** @brief Parameter for counting characters in truncateMultiLine() + */ struct CharCount { - int total; + int total = 0; }; /** @brief Truncate a string to some reasonable length for display diff --git a/src/libcalamares/utils/Tests.cpp b/src/libcalamares/utils/Tests.cpp index 6d4f5b265..ffeec1078 100644 --- a/src/libcalamares/utils/Tests.cpp +++ b/src/libcalamares/utils/Tests.cpp @@ -13,6 +13,7 @@ #include "Entropy.h" #include "Logger.h" #include "RAII.h" +#include "String.h" #include "Traits.h" #include "UMask.h" #include "Variant.h" @@ -63,6 +64,8 @@ private Q_SLOTS: void testVariantStringListYAMLDashed(); void testVariantStringListYAMLBracketed(); + /** @brief Test smart string truncation. */ + void testStringTruncation(); private: void recursiveCompareMap( const QVariantMap& a, const QVariantMap& b, int depth ); @@ -495,6 +498,67 @@ strings: [ aap, noot, mies ] QVERIFY( !getStringList( m, key ).contains( "lam" ) ); } +void +LibCalamaresTests::testStringTruncation() +{ + using namespace CalamaresUtils; + + const QString longString( R"(--- +--- src/libcalamares/utils/String.h ++++ src/libcalamares/utils/String.h +@@ -62,15 +62,22 @@ DLLEXPORT QString removeDiacritics( const QString& string ); + */ + DLLEXPORT QString obscure( const QString& string ); + ++/** @brief Parameter for counting lines at beginning and end of string ++ * ++ * This is used by truncateMultiLine() to indicate how many lines from ++ * the beginning and how many from the end should be kept. ++ */ + struct LinesStartEnd + { +- int atStart; +- int atEnd; ++ int atStart = 0; ++ int atEnd = 0; +)" ); + + const int sufficientLength = 812; + // There's 18 lines in all + QCOMPARE( longString.count( '\n' ), 18 ); + QVERIFY( longString.length() < sufficientLength ); + + // If we ask for more, we get everything back + QCOMPARE( longString, truncateMultiLine( longString, LinesStartEnd { 20, 0 }, CharCount { sufficientLength } ) ); + QCOMPARE( longString, truncateMultiLine( longString, LinesStartEnd { 0, 20 }, CharCount { sufficientLength } ) ); + + // If we ask for no lines, only characters, we get that + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 0 }, CharCount { 4 } ); + QCOMPARE( s.length(), 4 ); + QCOMPARE( s, QString( "---\n" ) ); + } + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 0 }, CharCount { sufficientLength } ); + QCOMPARE( s, longString ); + } + + // Lines at the start + { + auto s = truncateMultiLine( longString, LinesStartEnd { 4, 0 }, CharCount { sufficientLength } ); + QVERIFY( longString.startsWith( s ) ); + QCOMPARE( s.count( '\n' ), 4 ); + } + + // Lines at the end + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 4 }, CharCount { sufficientLength } ); + QVERIFY( longString.endsWith( s ) ); + QCOMPARE( s.count( '\n' ), 4 ); + } +} + + QTEST_GUILESS_MAIN( LibCalamaresTests ) #include "utils/moc-warnings.h" From b144d819797a50084a1e0403c59ff9d64d2442c3 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 28 Jan 2021 01:02:46 +0100 Subject: [PATCH 6/7] [libcalamares] Fix up smart-string-truncation - off-by-one when source ends with a newline - lastNewLine was being calculated as a left-index into the string, then used as a count-from-right --- src/libcalamares/utils/String.cpp | 9 +++++---- src/libcalamares/utils/Tests.cpp | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libcalamares/utils/String.cpp b/src/libcalamares/utils/String.cpp index f032fa61b..c2a9f7bf7 100644 --- a/src/libcalamares/utils/String.cpp +++ b/src/libcalamares/utils/String.cpp @@ -15,6 +15,7 @@ */ #include "String.h" +#include "Logger.h" #include @@ -138,7 +139,7 @@ truncateMultiLine( const QString& string, CalamaresUtils::LinesStartEnd lines, C return string; } - QString shorter = string.simplified(); + QString shorter = string; QString front, back; if ( shorter.count( '\n' ) >= maxLines ) { @@ -154,17 +155,17 @@ truncateMultiLine( const QString& string, CalamaresUtils::LinesStartEnd lines, C } if ( from > 0 ) { - front = shorter.left( from ); + front = shorter.left( from + 1 ); } int lastNewLine = -1; - int lastCount = 0; + int lastCount = shorter.endsWith( '\n' ) ? -1 : 0; for ( auto i = shorter.rbegin(); i != shorter.rend() && lastCount < lines.atEnd; ++i ) { if ( *i == '\n' ) { ++lastCount; - lastNewLine = shorter.length() - int( i - shorter.rbegin() ); + lastNewLine = int( i - shorter.rbegin() ); } } if ( ( lastNewLine >= 0 ) && ( lastCount >= lines.atEnd ) ) diff --git a/src/libcalamares/utils/Tests.cpp b/src/libcalamares/utils/Tests.cpp index ffeec1078..84449bdb9 100644 --- a/src/libcalamares/utils/Tests.cpp +++ b/src/libcalamares/utils/Tests.cpp @@ -501,6 +501,8 @@ strings: [ aap, noot, mies ] void LibCalamaresTests::testStringTruncation() { + Logger::setupLogLevel( Logger::LOGDEBUG ); + using namespace CalamaresUtils; const QString longString( R"(--- @@ -546,14 +548,18 @@ LibCalamaresTests::testStringTruncation() // Lines at the start { auto s = truncateMultiLine( longString, LinesStartEnd { 4, 0 }, CharCount { sufficientLength } ); + QVERIFY( s.length() > 1 ); QVERIFY( longString.startsWith( s ) ); + cDebug() << "Result-line" << Logger::Quote << s; QCOMPARE( s.count( '\n' ), 4 ); } // Lines at the end { auto s = truncateMultiLine( longString, LinesStartEnd { 0, 4 }, CharCount { sufficientLength } ); + QVERIFY( s.length() > 1 ); QVERIFY( longString.endsWith( s ) ); + cDebug() << "Result-line" << Logger::Quote << s; QCOMPARE( s.count( '\n' ), 4 ); } } From 319a720d1b78439082463358d2496eacf653999a Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 28 Jan 2021 01:06:09 +0100 Subject: [PATCH 7/7] [libcalamares Expand tests --- src/libcalamares/utils/Tests.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libcalamares/utils/Tests.cpp b/src/libcalamares/utils/Tests.cpp index 84449bdb9..3493f9579 100644 --- a/src/libcalamares/utils/Tests.cpp +++ b/src/libcalamares/utils/Tests.cpp @@ -562,6 +562,20 @@ LibCalamaresTests::testStringTruncation() cDebug() << "Result-line" << Logger::Quote << s; QCOMPARE( s.count( '\n' ), 4 ); } + + // Lines at both ends + { + auto s = truncateMultiLine( longString, LinesStartEnd { 2, 2 }, CharCount { sufficientLength } ); + QVERIFY( s.length() > 1 ); + cDebug() << "Result-line" << Logger::Quote << s; + QCOMPARE( s.count( '\n' ), 4 ); + + auto firsttwo = truncateMultiLine( s, LinesStartEnd { 2, 0 }, CharCount { sufficientLength } ); + auto lasttwo = truncateMultiLine( s, LinesStartEnd { 0, 2 }, CharCount { sufficientLength } ); + QCOMPARE( firsttwo + lasttwo, s ); + QVERIFY( longString.startsWith( firsttwo ) ); + QVERIFY( longString.endsWith( lasttwo ) ); + } }