LCOV - code coverage report
Current view: top level - src/cli - abstractcommand.cpp (source / functions) Coverage Total Hit
Project: Dokit Lines: 97.0 % 166 161
Version: Functions: 59.4 % 101 60

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2022-2026 Paul Colby <git@colby.id.au>
       2              : // SPDX-License-Identifier: LGPL-3.0-or-later
       3              : 
       4              : #include "abstractcommand.h"
       5              : #include "../stringliterals_p.h"
       6              : 
       7              : #include <qtpokit/pokitdevice.h>
       8              : #include <qtpokit/pokitdiscoveryagent.h>
       9              : 
      10              : #include <QLocale>
      11              : #include <QTimer>
      12              : #include <QtMath>
      13              : 
      14              : #include <cmath>
      15              : #include <ratio>
      16              : 
      17              : DOKIT_USE_STRINGLITERALS
      18              : 
      19              : /*!
      20              :  * \class AbstractCommand
      21              :  *
      22              :  * The AbstractCommand class provides a consistent base for the classes that implement CLI commands.
      23              :  */
      24              : 
      25              : /*!
      26              :  * Constructs a new command with \a parent.
      27              :  */
      28        62781 : AbstractCommand::AbstractCommand(QObject * const parent) : QObject(parent),
      29        62781 :     discoveryAgent(new PokitDiscoveryAgent(this))
      30        32451 : {
      31        66729 :     connect(discoveryAgent, &PokitDiscoveryAgent::pokitDeviceDiscovered,
      32        47211 :             this, &AbstractCommand::deviceDiscovered);
      33        66729 :     connect(discoveryAgent, &PokitDiscoveryAgent::finished,
      34        47211 :             this, &AbstractCommand::deviceDiscoveryFinished);
      35        66729 :     connect(discoveryAgent,
      36        12237 :         #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
      37        12237 :         QOverload<PokitDiscoveryAgent::Error>::of(&PokitDiscoveryAgent::error),
      38              :         #else
      39        20214 :         &PokitDiscoveryAgent::errorOccurred,
      40        20214 :         #endif
      41        32592 :         this, [](const PokitDiscoveryAgent::Error &error) {
      42          699 :         qCWarning(lc).noquote() << tr("Bluetooth discovery error:") << error;
      43          168 :         QTimer::singleShot(0, QCoreApplication::instance(), [](){
      44            0 :             QCoreApplication::exit(EXIT_FAILURE);
      45            0 :         });
      46          288 :     });
      47        66729 : }
      48              : 
      49              : /*!
      50              :  * Returns a list of CLI option names required by this command. The main console appication may
      51              :  * use this list to output an error (and exit) if any of the returned names are not found in the
      52              :  * parsed CLI options.
      53              :  *
      54              :  * The (already parsed) \a parser may be used adjust the returned required options depending on the
      55              :  * value of other options. For example, the `logger` command only requires the `--mode` option if
      56              :  * the `--command` option is `start`.
      57              :  *
      58              :  * This base implementation simply returns an empty list. Derived classes should override this
      59              :  * function to include any required options.
      60              :  */
      61        29680 : QStringList AbstractCommand::requiredOptions(const QCommandLineParser &parser) const
      62        32784 : {
      63        32784 :     Q_UNUSED(parser)
      64        62464 :     return QStringList();
      65        32784 : }
      66              : 
      67              : /*!
      68              :  * Returns a list of CLI option names supported by this command. The main console appication may
      69              :  * use this list to output a warning for any parsed CLI options not included in the returned list.
      70              :  *
      71              :  * The (already parsed) \a parser may be used adjust the returned supported options depending on the
      72              :  * value of other options. For example, the `logger` command only supported the `--timestamp` option
      73              :  * if the `--command` option is `start`.
      74              :  *
      75              :  * This base implementation simply returns requiredOptions(). Derived classes should override this
      76              :  * function to include optional options, such as:
      77              :  *
      78              :  * ```
      79              :  * QStringList Derived::supportedOptions(const QCommandLineParser &parser) const
      80              :  * {
      81              :  *     const QStringList list = AbstractCommand::supportedOptions(parser) + QStringList{ ... };
      82              :  *     list.sort();
      83              :  *     list.removeDuplicates(); // Optional, recommended.
      84              :  *     return list;
      85              :  * }
      86              :  * ```
      87              :  */
      88        14560 : QStringList AbstractCommand::supportedOptions(const QCommandLineParser &parser) const
      89        15948 : {
      90       125564 :     return requiredOptions(parser) + QStringList{
      91        15948 :         u"debug"_s,
      92        15948 :         u"device"_s, u"d"_s,
      93        15948 :         u"output"_s,
      94        15948 :         u"timeout"_s,
      95       110796 :     };
      96        15948 : }
      97              : 
      98              : /*!
      99              :  * Returns a human-readable version of \a value, with up-to \a precision decimal digits, and SI unit prefix when
     100              :  * appropiate.
     101              :  *
     102              :  * \note Only supports positive values. Tip: use appendSiPrefix(qAbs(...)) for now.
     103              :  */
     104         1991 : QString AbstractCommand::appendSiPrefix(const double value, int precision) {
     105              :     // Scale the number to an appropriate SI prefix.
     106         4257 :     QStringList siPrefixes{ u""_s, u"m"_s, u"μ"_s };
     107         1221 :     int index = 0;
     108         1991 :     if (value != 0.)
     109         4402 :         for (index=0; (index < siPrefixes.length()-1) && (qAbs(value) * qPow(1000.0, (double)index) < 1); ++index);
     110         1991 :     QString number = QString::number(value * qPow(1000.0, (double)index), 'f', precision);
     111              : 
     112              :     // Trim trailing zeros, and decimal indicator if no decimals remain.
     113         1991 :     if (const DOKIT_STRING_INDEX_TYPE decimalPos = number.indexOf(u'.'); decimalPos > 0) {
     114         1221 :         DOKIT_STRING_INDEX_TYPE nonZeroPos;
     115        13937 :         for (nonZeroPos = number.length()-1; (nonZeroPos > decimalPos) && (number.at(nonZeroPos) == u'0'); --nonZeroPos);
     116         1991 :         number.truncate((nonZeroPos == decimalPos) ? nonZeroPos : nonZeroPos+1);
     117         1221 :     }
     118         3267 :     return QString(u"%1%2"_s).arg(number, siPrefixes.at(index));
     119         1485 : }
     120              : 
     121              : /*!
     122              :  * Returns an RFC 4180 compliant version of \a field. That is, if \a field contains any of the
     123              :  * the below four characters, than any double quotes are escaped (by addition double-quotes), and
     124              :  * the string itself surrounded in double-quotes. Otherwise, \a field is returned verbatim.
     125              :  *
     126              :  * Some examples:
     127              :  * ```
     128              :  * QCOMPARE(escapeCsvField("abc"), "abc");           // Returned unchanged.
     129              :  * QCOMPARE(escapeCsvField("a,c"), R"("a,c")");      // Wrapped in double-quotes.
     130              :  * QCOMPARE(escapeCsvField(R"(a"c)"), R("("a""c")"); // Existing double-quotes doubled, then wrapped.
     131              :  * ```
     132              :  */
     133         8170 : QString AbstractCommand::escapeCsvField(const QString &field)
     134         5688 : {
     135        33888 :     if (field.contains(','_L1) || field.contains('\r'_L1) || field.contains('"'_L1) || field.contains('\n'_L1)) {
     136          392 :         return uR"("%1")"_s.arg(QString(field).replace('"'_L1, uR"("")"_s));
     137         5466 :     } else return field;
     138         5688 : }
     139              : 
     140              : /*!
     141              :  * \internal
     142              :  * A (run-time) class approximately equivalent to the compile-time std::ratio template.
     143              :  */
     144              : struct Ratio {
     145              :     std::intmax_t num { 0 }; ///< Numerator.
     146              :     std::intmax_t den { 0 }; ///< Denominator.
     147              :     //! Returns \a true if both #num and #den are non-zero.
     148        34744 :     bool isValid() const { return (num != 0) && (den != 0); }
     149              : };
     150              : 
     151              : /*!
     152              :  * \internal
     153              :  * Returns a (run-time) Ratio representation of (compile-time) ratio \a R.
     154              :  */
     155        33622 : template<typename R> constexpr Ratio makeRatio() { return Ratio{ R::num, R::den }; }
     156              : 
     157              : /*!
     158              :  * Returns \a value as an integer multiple of the ratio \a R, as number of type \a T. The string \a value
     159              :  * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
     160              :  * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
     161              :  * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
     162              :  * convenient use like:
     163              :  *
     164              :  * ```
     165              :  * const quint32 timeout = parseNumber<std::milli>(parser.value("window"), 's', (quint32)500'000);
     166              :  * ```
     167              :  *
     168              :  * So that an unqualified period like "300" will be assumed to be 300 milliseconds, and not 300
     169              :  * microseconds, while a period like "1000" will be assume to be 1 second.
     170              :  *
     171              :  * If conversion fails for any reason, quiet-NaN (if supported by \a T) or 0 is returned.
     172              :  */
     173              : template<typename R, typename T>
     174        23380 : T AbstractCommand::parseNumber(const QString &value, const QString &unit, const T sensibleMinimum)
     175        18714 : {
     176        42289 :     static const QMap<QChar, Ratio> unitPrefixScaleMap {
     177        18714 :         { 'E'_L1,        makeRatio<std::exa>()   },
     178        18714 :         { 'P'_L1,        makeRatio<std::peta>()  },
     179        18714 :         { 'T'_L1,        makeRatio<std::tera>()  },
     180        18714 :         { 'G'_L1,        makeRatio<std::giga>()  },
     181        18714 :         { 'M'_L1,        makeRatio<std::mega>()  },
     182        18714 :         { 'K'_L1,        makeRatio<std::kilo>()  }, // Not official SI unit prefix, but commonly used.
     183        18714 :         { 'k'_L1,        makeRatio<std::kilo>()  },
     184        18714 :         { 'h'_L1,        makeRatio<std::hecto>() },
     185        18714 :         { 'd'_L1,        makeRatio<std::deci>()  },
     186        18714 :         { 'c'_L1,        makeRatio<std::centi>() },
     187        18714 :         { 'm'_L1,        makeRatio<std::milli>() },
     188        18714 :         { 'u'_L1,        makeRatio<std::micro>() }, // Not official SI unit prefix, but commonly used.
     189        18714 :         { QChar(0x00B5), makeRatio<std::micro>() }, // Unicode micro symbol (µ).
     190        18714 :         { QChar(0x03BC), makeRatio<std::micro>() }, // Unicode lower mu (μ).
     191        18714 :         { 'n'_L1,        makeRatio<std::nano>()  },
     192        18714 :         { 'p'_L1,        makeRatio<std::pico>()  },
     193        18714 :         { 'f'_L1,        makeRatio<std::femto>() },
     194        18714 :         { 'a'_L1,        makeRatio<std::atto>()  },
     195        18714 :     };
     196              : 
     197              :     // Remove the optional (whole) unit suffix.
     198        18714 :     Ratio ratio;
     199        18714 :     QString number = value.trimmed();
     200        42094 :     if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
     201        24728 :         number.chop(unit.length());
     202        10168 :         ratio = makeRatio<std::ratio<1>>();
     203        10168 :     }
     204              : 
     205              :     // Parse, and remove, the optional SI unit prefix.
     206        42094 :     if (!number.isEmpty()) {
     207        17685 :         #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
     208        33003 :         const QChar siPrefix = number.back(); // QString::back() introduced in Qt 5.10.
     209              :         #else
     210         1584 :         const QChar siPrefix = number.at(number.size() - 1);
     211          918 :         #endif
     212        18603 :         const auto iter = unitPrefixScaleMap.constFind(siPrefix);
     213        41913 :         if (iter != unitPrefixScaleMap.constEnd()) {
     214        10584 :             Q_ASSERT(iter->isValid());
     215        26264 :             ratio = *iter;
     216        26264 :             number.chop(1);
     217        10584 :         }
     218        18603 :     }
     219              : 
     220              :     // Parse the number as an (unsigned) integer.
     221        42094 :     QLocale locale; bool ok;
     222        42094 :     if (const qulonglong integer = locale.toULongLong(number, &ok); (ok)) {
     223        32199 :         if (integer == 0) {
     224          111 :             return static_cast<T>(integer); // Otherwise the next for loop would be be infinite.
     225          111 :         }
     226        18068 :         #define DOKIT_RESULT(var) (static_cast<T>(var) * ratio.num * R::den / ratio.den / R::num)
     227        13258 :         if (!ratio.isValid()) {
     228        11810 :             for (ratio = makeRatio<R>(); DOKIT_RESULT(integer) < sensibleMinimum; ratio.num *= 1000);
     229         2934 :         }
     230        32018 :         return DOKIT_RESULT(integer);
     231        13369 :         #undef DOKIT_RESULT
     232        13369 :     }
     233              : 
     234              :     // Parse the number as a (double) floating point number, and check that it is positive.
     235         9895 :     if (const double dbl = locale.toDouble(number, &ok); (ok) && (dbl > 0.0)) {
     236         1958 :         if (dbl == 0.) {
     237            0 :             return static_cast<T>(dbl); // Otherwise the next for loop would be be infinite.
     238            0 :         }
     239         4100 :         #define DOKIT_RESULT(var) (var * ratio.num * R::den / ratio.den / R::num)
     240         1606 :         if (!ratio.isValid()) {
     241         1448 :             for (ratio = makeRatio<R>(); DOKIT_RESULT(dbl) < sensibleMinimum; ratio.num *= 1000);
     242          666 :         }
     243         2726 :         return (std::is_integral_v<T>) ? static_cast<T>(std::llround(DOKIT_RESULT(dbl))) : DOKIT_RESULT(dbl);
     244         1606 :         #undef DOKIT_RESULT
     245         1606 :     }
     246              : 
     247              :     // Failed to parse as either integer, or float.
     248         3739 :     return (std::numeric_limits<T>::has_quiet_NaN) ? std::numeric_limits<T>::quiet_NaN() : 0;
     249        28725 : }
     250              : 
     251              : #define DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(Ratio, Type) template \
     252              : Type AbstractCommand::parseNumber<Ratio>(const QString &value, const QString &unit, const Type sensibleMinimum)
     253              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::exa,      quint32);
     254              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::peta,     quint32);
     255              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::tera,     quint32);
     256              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::giga,     quint32);
     257              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::mega,     quint32);
     258              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::kilo,     quint32);
     259              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::hecto,    quint32);
     260              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::deca,     quint32);
     261              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::ratio<1>, quint32);
     262              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::ratio<1>, float);
     263              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::deci,     quint32);
     264              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::centi,    quint32);
     265              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::milli,    quint32);
     266              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::micro,    quint32);
     267              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::nano,     quint32);
     268              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::pico,     quint32);
     269              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::femto,    quint32);
     270              : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::atto,     quint32);
     271              : #undef DOKIT_INSTANTIATE_TEMPLATE_FUNCTION
     272              : 
     273              : /*!
     274              :  * Processes the relevant options from the command line \a parser.
     275              :  *
     276              :  * On success, returns an empty QStringList, otherwise returns a list of CLI errors that the caller
     277              :  * should report appropriately before exiting.
     278              :  *
     279              :  * This base implementations performs some common checks, such as ensuring that required options are
     280              :  * present. Derived classes should override this function to perform further processing, typically
     281              :  * invoking this base implementation as a first step, such as:
     282              :  *
     283              :  * ```
     284              :  * QStringList CustomCommand::processOptions(const QCommandLineParser &parser)
     285              :  * {
     286              :  *     QStringList errors = AbstractCommand::processOptions(parser);
     287              :  *     if (!errors.isEmpty()) {
     288              :  *         return errors;
     289              :  *     }
     290              :  *
     291              :  *     // Do further procession of options.
     292              :  *
     293              :  *     return errors;
     294              :  * }
     295              :  * ```
     296              :  */
     297        13090 : QStringList AbstractCommand::processOptions(const QCommandLineParser &parser)
     298        13617 : {
     299              :     // Report any supplied options that are not supported by this command.
     300        26707 :     const QStringList suppliedOptionNames = parser.optionNames();
     301        26707 :     const QStringList supportedOptionNames = supportedOptions(parser);
     302        74388 :     for (const QString &option: suppliedOptionNames) {
     303        61298 :         if (!supportedOptionNames.contains(option)) {
     304          331 :             qCInfo(lc).noquote() << tr("Ignoring option: %1").arg(option);
     305          111 :         }
     306        27138 :     }
     307        22219 :     QStringList errors;
     308              : 
     309              :     // Parse the device (name/addr/uuid) option.
     310        35309 :     if (parser.isSet(u"device"_s)) {
     311          454 :         deviceToScanFor = parser.value(u"device"_s);
     312          222 :     }
     313              : 
     314              :     // Parse the output format options (if supported, and supplied).
     315        43911 :     if ((supportedOptionNames.contains(u"output"_s)) && // Derived classes may have removed.
     316        39254 :         (parser.isSet(u"output"_s)))
     317         1443 :     {
     318         3263 :         const QString output = parser.value(u"output"_s).toLower();
     319         2353 :         if (output == u"csv"_s) {
     320          543 :             format = OutputFormat::Csv;
     321         1810 :         } else if (output == u"json"_s) {
     322          543 :             format = OutputFormat::Json;
     323         1267 :         } else if (output == u"text"_s) {
     324          543 :             format = OutputFormat::Text;
     325          444 :         } else {
     326          908 :             errors.append(tr("Unknown output format: %1").arg(output));
     327          444 :         }
     328         1755 :     }
     329              : 
     330              :     // Parse the device scan timeout option.
     331        35309 :     if (parser.isSet(u"timeout"_s)) {
     332         3081 :         const quint32 timeout = parseNumber<std::milli>(parser.value(u"timeout"_s), u"s"_s, 500);
     333         2353 :         if (timeout == 0) {
     334         1614 :             errors.append(tr("Invalid timeout: %1").arg(parser.value(u"timeout"_s)));
     335         1267 :         } else if (discoveryAgent->lowEnergyDiscoveryTimeout() == -1) {
     336          833 :             qCWarning(lc).noquote() << tr("Platform does not support Bluetooth scan timeout");
     337          777 :         } else {
     338          924 :             discoveryAgent->setLowEnergyDiscoveryTimeout(timeout);
     339         1064 :             qCDebug(lc).noquote() << tr("Set scan timeout to %1").arg(
     340            0 :                 discoveryAgent->lowEnergyDiscoveryTimeout());
     341          714 :         }
     342         1443 :     }
     343              : 
     344              :     // Return errors for any required options that are absent.
     345        26707 :     const QStringList requiredOptionNames = this->requiredOptions(parser);
     346        47477 :     for (const QString &option: requiredOptionNames) {
     347        34047 :         if (!parser.isSet(option)) {
     348         2725 :             errors.append(tr("Missing required option: %1").arg(option));
     349          985 :         }
     350        15357 :     }
     351        26707 :     return errors;
     352        13617 : }
     353              : 
     354              : /*!
     355              :  * \fn virtual bool AbstractCommand::start()
     356              :  *
     357              :  * Begins the functionality of this command, and returns `true` if begun successfully, `false`
     358              :  * otherwise.
     359              :  */
     360              : 
     361              : /*!
     362              :  * \fn virtual void AbstractCommand::deviceDiscovered(const QBluetoothDeviceInfo &info) = 0
     363              :  *
     364              :  * Handles PokitDiscoveryAgent::pokitDeviceDiscovered signal. Derived classes must
     365              :  * implement this slot to begin whatever actions are relevant when a Pokit device has been
     366              :  * discovered. For example, the 'scan' command would simply output the \a info details, whereas
     367              :  * most other commands would begin connecting if \a info is the device they're after.
     368              :  */
     369              : 
     370              : /*!
     371              :  * \fn virtual void AbstractCommand::deviceDiscoveryFinished() = 0
     372              :  *
     373              :  * Handles PokitDiscoveryAgent::deviceDiscoveryFinished signal. Derived classes must
     374              :  * implement this slot to perform whatever actions are appropriate when discovery is finished.
     375              :  * For example, the 'scan' command would simply exit, whereas most other commands would verify that
     376              :  * an appropriate device was found.
     377              :  */
        

Generated by: LCOV version 2.4-0