LCOV - code coverage report
Current view: top level - src/cli - abstractcommand.cpp (source / functions) Hit Total Coverage
Project: Dokit Lines: 73 75 97.3 %
Version: Functions: 10 24 41.7 %

          Line data    Source code
       1             : // SPDX-FileCopyrightText: 2022-2023 Paul Colby <git@colby.id.au>
       2             : // SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : #include "abstractcommand.h"
       5             : 
       6             : #include <qtpokit/pokitdevice.h>
       7             : #include <qtpokit/pokitdiscoveryagent.h>
       8             : 
       9             : #include <QLocale>
      10             : 
      11             : #include <ratio>
      12             : 
      13             : /*!
      14             :  * \class AbstractCommand
      15             :  *
      16             :  * The AbstractCommand class provides a consistent base for the classes that implement CLI commands.
      17             :  */
      18             : 
      19             : /*!
      20             :  * Constructs a new command with \a parent.
      21             :  */
      22        8349 : AbstractCommand::AbstractCommand(QObject * const parent) : QObject(parent),
      23        8349 :     discoveryAgent(new PokitDiscoveryAgent(this))
      24             : {
      25        8349 :     connect(discoveryAgent, &PokitDiscoveryAgent::pokitDeviceDiscovered,
      26             :             this, &AbstractCommand::deviceDiscovered);
      27        8349 :     connect(discoveryAgent, &PokitDiscoveryAgent::finished,
      28             :             this, &AbstractCommand::deviceDiscoveryFinished);
      29        8349 :     connect(discoveryAgent,
      30             :         #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
      31             :         QOverload<PokitDiscoveryAgent::Error>::of(&PokitDiscoveryAgent::error),
      32             :         #else
      33             :         &PokitDiscoveryAgent::errorOccurred,
      34             :         #endif
      35          48 :     [](const PokitDiscoveryAgent::Error &error) {
      36          96 :         qCWarning(lc).noquote() << tr("Bluetooth controller error:") << error;
      37          48 :         QCoreApplication::exit(EXIT_FAILURE);
      38          48 :     });
      39        8349 : }
      40             : 
      41             : /*!
      42             :  * Returns a list of CLI option names required by this command. The main console appication may
      43             :  * use this list to output an eror (and exit) if any of the returned names are not found in the
      44             :  * parsed CLI options.
      45             :  *
      46             :  * The (already parsed) \a parser may be used adjust the returned required options depending on the
      47             :  * value of other options. For example, the `logger` command only requires the `--mode` option if
      48             :  * the `--command` option is `start`.
      49             :  *
      50             :  * This base implementation simply returns an empty list. Derived classes should override this
      51             :  * function to include any required options.
      52             :  */
      53        5408 : QStringList AbstractCommand::requiredOptions(const QCommandLineParser &parser) const
      54             : {
      55             :     Q_UNUSED(parser)
      56        5408 :     return QStringList();
      57             : }
      58             : 
      59             : /*!
      60             :  * Returns a list of CLI option names supported by this command. The main console appication may
      61             :  * use this list to output a warning for any parsed CLI options not included in the returned list.
      62             :  *
      63             :  * The (already parsed) \a parser may be used adjust the returned supported options depending on the
      64             :  * value of other options. For example, the `logger` command only supported the `--timestamp` option
      65             :  * if the `--command` option is `start`.
      66             :  *
      67             :  * This base implementation simply returns requiredOptions(). Derived classes should override this
      68             :  * function to include optional options, such as:
      69             :  *
      70             :  * ```
      71             :  * QStringList Derived::supportedOptions(const QCommandLineParser &parser) const
      72             :  * {
      73             :  *     const QStringList list = AbstractCommand::supportedOptions(parser) + QStringList{ ... };
      74             :  *     list.sort();
      75             :  *     list.removeDuplicates(); // Optional, recommended.
      76             :  *     return list;
      77             :  * }
      78             :  * ```
      79             :  */
      80        2632 : QStringList AbstractCommand::supportedOptions(const QCommandLineParser &parser) const
      81             : {
      82       19336 :     return requiredOptions(parser) + QStringList{
      83             :         QLatin1String("debug"),
      84             :         QLatin1String("device"), QLatin1String("d"),
      85             :         QLatin1String("output"),
      86             :         QLatin1String("timeout"),
      87       18272 :     };
      88           0 : }
      89             : 
      90             : /*!
      91             :  * Returns an RFC 4180 compliant version of \a field. That is, if \a field contains any of the
      92             :  * the below four characters, than any double quotes are escaped (by addition double-quotes), and
      93             :  * the string itself surrounded in double-quotes. Otherwise, \a field is returned verbatim.
      94             :  *
      95             :  * Some examples:
      96             :  * ```
      97             :  * QCOMPARE(escapeCsvField("abc"), "abc");           // Returned unchanged.
      98             :  * QCOMPARE(escapeCsvField("a,c"), R"("a,c")");      // Wrapped in double-quotes.
      99             :  * QCOMPARE(escapeCsvField(R"(a"c)"), R("("a""c")"); // Existing double-quotes doubled, then wrapped.
     100             :  * ```
     101             :  */
     102        1769 : QString AbstractCommand::escapeCsvField(const QString &field)
     103             : {
     104        1751 :     if (field.contains(QLatin1Char(','))||field.contains(QLatin1Char('\r'))||
     105        5253 :         field.contains(QLatin1Char('"'))||field.contains(QLatin1Char('\n')))
     106             :     {
     107          44 :         return QString::fromLatin1(R"("%1")").arg(
     108          36 :             QString(field).replace(QLatin1Char('"'), QLatin1String(R"("")")));
     109             :     } else return field;
     110             : }
     111             : 
     112             : /*!
     113             :  * \internal
     114             :  * A (run-time) class approximately equivalent to the compile-time std::ratio template.
     115             :  */
     116             : struct Ratio {
     117             :     std::intmax_t num { 0 }; ///< Numerator.
     118             :     std::intmax_t den { 0 }; ///< Denominator.
     119             :     //! Returns \a true if both #num and #den are non-zero.
     120        1690 :     bool isValid() const { return (num != 0) && (den != 0); }
     121             : };
     122             : 
     123             : /*!
     124             :  * \internal
     125             :  * Returns a (run-time) Ratio representation of (compile-time) ratio \a R.
     126             :  */
     127             : template<typename R> constexpr Ratio makeRatio() { return Ratio{ R::num, R::den }; }
     128             : 
     129             : /*!
     130             :  * Returns \a value as an integer multiple of the ratio \a R. The string \a value
     131             :  * may end with the optional \a unit, such as `V` or `s`, which may also be preceded with a SI unit
     132             :  * prefix such as `m` for `milli`. If \a value contains no SI unit prefix, then the result will be
     133             :  * multiplied by 1,000 enough times to be greater than \a sensibleMinimum. This allows for
     134             :  * convenient use like:
     135             :  *
     136             :  * ```
     137             :  * const quint32 timeout = parseNumber<std::milli>(parser.value("window"), 's', 500'000);
     138             :  * ```
     139             :  *
     140             :  * So that an unqalified period like "300" will be assumed to be 300 milliseconds, and not 300
     141             :  * microseconds, while a period like "1000" will be assume to be 1 second.
     142             :  *
     143             :  * If conversion fails for any reason, 0 is returned.
     144             :  */
     145             : template<typename R>
     146        2218 : quint32 AbstractCommand::parseNumber(const QString &value, const QString &unit, const quint32 sensibleMinimum)
     147             : {
     148        2278 :     static const QMap<QChar, Ratio> unitPrefixScaleMap {
     149             :         { QLatin1Char('E'), makeRatio<std::exa>()   },
     150             :         { QLatin1Char('P'), makeRatio<std::peta>()  },
     151             :         { QLatin1Char('T'), makeRatio<std::tera>()  },
     152             :         { QLatin1Char('G'), makeRatio<std::giga>()  },
     153             :         { QLatin1Char('M'), makeRatio<std::mega>()  },
     154             :         { QLatin1Char('K'), makeRatio<std::kilo>()  }, // Not official SI unit prefix, but commonly used.
     155             :         { QLatin1Char('k'), makeRatio<std::kilo>()  },
     156             :         { QLatin1Char('h'), makeRatio<std::hecto>() },
     157             :         { QLatin1Char('d'), makeRatio<std::deci>()  },
     158             :         { QLatin1Char('c'), makeRatio<std::centi>() },
     159             :         { QLatin1Char('m'), makeRatio<std::milli>() },
     160             :         { QLatin1Char('u'), makeRatio<std::micro>() }, // Not official SI unit prefix, but commonly used.
     161             :         { QChar   (0x00B5), makeRatio<std::micro>() }, // Unicode micro symbol (μ).
     162             :         { QLatin1Char('n'), makeRatio<std::nano>()  },
     163             :         { QLatin1Char('p'), makeRatio<std::pico>()  },
     164             :         { QLatin1Char('f'), makeRatio<std::femto>() },
     165             :         { QLatin1Char('a'), makeRatio<std::atto>()  },
     166             :     };
     167             : 
     168             :     // Remove the optional (whole) unit suffix.
     169             :     Ratio ratio;
     170             :     QString number = value.trimmed();
     171        2218 :     if ((!unit.isEmpty()) && (number.endsWith(unit, Qt::CaseInsensitive))) {
     172        1138 :         number.chop(unit.length());
     173             :         ratio = makeRatio<std::ratio<1>>();
     174             :     }
     175             : 
     176             :     // Parse, and remove, the optional SI unit prefix.
     177        2218 :     if (!number.isEmpty()) {
     178             :         #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
     179         512 :         const QChar siPrefix = number.back(); // QString::back() introduced in Qt 5.10.
     180             :         #else
     181         244 :         const QChar siPrefix = number.at(number.size() - 1);
     182             :         #endif
     183             :         const auto iter = unitPrefixScaleMap.constFind(siPrefix);
     184        2208 :         if (iter != unitPrefixScaleMap.constEnd()) {
     185             :             Q_ASSERT(iter->isValid());
     186        1128 :             ratio = *iter;
     187        1128 :             number.chop(1);
     188             :         }
     189             :     }
     190             : 
     191             :     #define DOKIT_RESULT(var) (var * ratio.num * R::den / ratio.den / R::num)
     192             :     // Parse the number as an (unsigned) integer.
     193        2218 :     QLocale locale; bool ok;
     194        1702 :     qulonglong integer = locale.toULongLong(number, &ok);
     195        2218 :     if (ok) {
     196        1502 :         if (integer == 0) {
     197             :             return 0;
     198             :         }
     199             :         if (!ratio.isValid()) {
     200         572 :             for (ratio = makeRatio<R>(); DOKIT_RESULT(integer) < sensibleMinimum; ratio.num *= 1000);
     201             :         }
     202        1492 :         return (integer == 0) ? 0u : (quint32)DOKIT_RESULT(integer);
     203             :     }
     204             : 
     205             :     // Parse the number as a (double) floating point number, and check that it is positive.
     206         548 :     const double dbl = locale.toDouble(number, &ok);
     207         716 :     if ((ok) && (dbl > 0.0)) {
     208             :         if (!ratio.isValid()) {
     209         126 :             for (ratio = makeRatio<R>(); DOKIT_RESULT(dbl) < sensibleMinimum; ratio.num *= 1000);
     210             :         }
     211         198 :         return (quint32)DOKIT_RESULT(dbl);
     212             :     }
     213             :     #undef DOKIT_RESULT
     214             :     return 0; // Failed to parse as either integer, or float.
     215        2218 : }
     216             : 
     217             : #define DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(type) template \
     218             : quint32 AbstractCommand::parseNumber<type>(const QString &value, const QString &unit, const quint32 sensibleMinimum)
     219             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::exa);
     220             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::peta);
     221             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::tera);
     222             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::giga);
     223             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::mega);
     224             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::kilo);
     225             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::hecto);
     226             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::deca);
     227             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::ratio<1>);
     228             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::deci);
     229             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::centi);
     230             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::milli);
     231             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::micro);
     232             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::nano);
     233             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::pico);
     234             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::femto);
     235             : DOKIT_INSTANTIATE_TEMPLATE_FUNCTION(std::atto);
     236             : #undef DOKIT_INSTANTIATE_TEMPLATE_FUNCTION
     237             : 
     238             : /*!
     239             :  * Processes the relevant options from the command line \a parser.
     240             :  *
     241             :  * On success, returns an empty QStringList, otherwise returns a list of CLI errors that the caller
     242             :  * should report appropriately before exiting.
     243             :  *
     244             :  * This base implementations performs some common checks, such as ensuring that required options are
     245             :  * present. Derived classes should override this function to perform further processing, typically
     246             :  * inovking this base implementation as a first step, such as:
     247             :  *
     248             :  * ```
     249             :  * QStringList CustomCommand::processOptions(const QCommandLineParser &parser)
     250             :  * {
     251             :  *     QStringList errors = AbstractCommand::processOptions(parser);
     252             :  *     if (!errors.isEmpty()) {
     253             :  *         return errors;
     254             :  *     }
     255             :  *
     256             :  *     // Do further procession of options.
     257             :  *
     258             :  *     return errors;
     259             :  * }
     260             :  * ```
     261             :  */
     262        2254 : QStringList AbstractCommand::processOptions(const QCommandLineParser &parser)
     263             : {
     264             :     // Report any supplied options that are not supported by this command.
     265        2254 :     const QStringList suppliedOptionNames = parser.optionNames();
     266        2254 :     const QStringList supportedOptionNames = supportedOptions(parser);
     267        6510 :     for (const QString &option: suppliedOptionNames) {
     268        4256 :         if (!supportedOptionNames.contains(option)) {
     269          40 :             qCInfo(lc).noquote() << tr("Ignoring option: %1").arg(option);
     270             :         }
     271             :     }
     272         524 :     QStringList errors;
     273             : 
     274             :     // Parse the device (name/addr/uuid) option.
     275        2778 :     if (parser.isSet(QLatin1String("device"))) {
     276          44 :         deviceToScanFor = parser.value(QLatin1String("device"));
     277             :     }
     278             : 
     279             :     // Parse the output format options (if supported, and supplied).
     280        3971 :     if ((supportedOptionNames.contains(QLatin1String("output"))) && // Derived classes may have removed.
     281        4194 :         (parser.isSet(QLatin1String("output"))))
     282             :     {
     283         468 :         const QString output = parser.value(QLatin1String("output")).toLower();
     284         234 :         if (output == QLatin1String("csv")) {
     285          54 :             format = OutputFormat::Csv;
     286         180 :         } else if (output == QLatin1String("json")) {
     287          54 :             format = OutputFormat::Json;
     288         126 :         } else if (output == QLatin1String("text")) {
     289          54 :             format = OutputFormat::Text;
     290             :         } else {
     291          88 :             errors.append(tr("Unknown output format: %1").arg(output));
     292             :         }
     293         182 :     }
     294             : 
     295             :     // Parse the device scan timeout option.
     296        2778 :     if (parser.isSet(QLatin1String("timeout"))) {
     297         182 :         const quint32 timeout = parseNumber<std::milli>(parser.value(QLatin1String("timeout")), QLatin1String("s"), 500);
     298         130 :         if (timeout == 0) {
     299         120 :             errors.append(tr("Invalid timeout: %1").arg(parser.value(QLatin1String("timeout"))));
     300             :         } else {
     301          70 :             discoveryAgent->setLowEnergyDiscoveryTimeout(timeout);
     302          70 :             qCDebug(lc).noquote() << tr("Set scan timeout to %1").arg(
     303           0 :                 discoveryAgent->lowEnergyDiscoveryTimeout());
     304             :         }
     305             :     }
     306             : 
     307             :     // Return errors for any required options that are absent.
     308        2254 :     const QStringList requiredOptionNames = this->requiredOptions(parser);
     309        4940 :     for (const QString &option: requiredOptionNames) {
     310        2686 :         if (!parser.isSet(option)) {
     311         242 :             errors.append(tr("Missing required option: %1").arg(option));
     312             :         }
     313             :     }
     314        2254 :     return errors;
     315             : }
     316             : 
     317             : /*!
     318             :  * \fn virtual bool AbstractCommand::start()
     319             :  *
     320             :  * Begins the functionality of this command, and returns `true` if begun successfully, `false`
     321             :  * otherwise.
     322             :  */
     323             : 
     324             : /*!
     325             :  * \fn virtual void AbstractCommand::deviceDiscovered(const QBluetoothDeviceInfo &info) = 0
     326             :  *
     327             :  * Handles PokitDiscoveryAgent::pokitDeviceDiscovered signal. Derived classes must
     328             :  * implement this slot to begin whatever actions are relevant when a Pokit device has been
     329             :  * discovered. For example, the 'scan' command would simply output the \a info details, whereas
     330             :  * most other commands would begin connecting if \a info is the device they're after.
     331             :  */
     332             : 
     333             : /*!
     334             :  * \fn virtual void AbstractCommand::deviceDiscoveryFinished() = 0
     335             :  *
     336             :  * Handles PokitDiscoveryAgent::deviceDiscoveryFinished signal. Derived classes must
     337             :  * implement this slot to perform whatever actions are appropraite when discovery is finished.
     338             :  * For example, the 'scan' command would simply exit, whereas most other commands would verify that
     339             :  * an appropriate device was found.
     340             :  */

Generated by: LCOV version 1.14