Archives de l’auteur : Fylhan

Créer et exécuter des tests cases avec QTestLib

Publié dans C / C++ | Un commentaire

Après avoir soliloqué sur l’architecture de tests Qt / C++ que j’aimerai obtenir, commençons par le commencement : créer un test avec QTestLib et l’exécuter. A vrai dire, cet article traine dans mes brouillons depuis au moins 2014, mais je me suis décidé à le publier. Les liens datent un peu, mais tout reste très fonctionnel.

Un mini rappel sur les tests unitaires :

  • Un test case par classe : ce test case testera les différentes fonctions de cette classe
    • dans des cas nominaux,
    • des cas d’erreurs,
    • voire des cas limites.
  • Tester le plus petit bloc possible : si la classe à tester utilise d’autres composants, on dit qu’elle a des « dépendances », il faut alors « mocker » ces dépendances (si possible) ou en tout cas les maîtriser au maximum. Ce sont les integration tests qui testeront plusieurs blocs ensemble.

Créer un test case

Avec QTestLib, un test case est un QObject classique. La classe de test contient plusieurs slots privés (i.e. « private slots », un slot public ne sera pas exécuté lors du passage des tests) qui seront exécutés les uns à la suite des autres. La documentation de Qt fournit aussi un exemple de test case, mais nous en verrons d’autres plus loin.

Dans chaque test, on va utiliser des assertions, permettant de déterminer si un test est en Success ou Failure. On place dans l’assertion ce que l’on considère comme le bon comportement pour un bout de code selon le contexte donné. En enchaînant plusieurs assertions, on est censé tester un morceau code dans tous les contextes possibles.

  • QVERIFY(bool assessmentExpectedTrue) : « assessmentExpectedTrue » doit être vrai pour que le test soit en Success
  • QVERIFY2(bool assessmentExpectedTrue, char *descriptionIfItFailed) : Idem mais on peut afficher un message de debug si l’assertion est faux
  • QCOMPARE(actual, expected) : Idem, sauf que cette fois-ci la valeur obtenue (actual) et attendue (expected) doivent être égales. L’avantage, c’est que ces valeurs apparaissent dans les logs si le test échoue (ce qui n’est pas le cas avec QVERIFY) et c’est bien pratique pour le debug ! A noter : il faut que l’opérateur « == » et l’opérateur QDebug operator<<(QDebug dbg, T val) soit définie pour les types complexes.

En plus des slots privés qui représentent chacun un test, on peut définir au besoin 4 méthodes particulières dans le test case :

  • initTestCase (static) : exécuté au tout début du test case.
  • cleanupTestCase (static) : exécuté à la toute fin du test case.
  • init : exécuté avant chaque méthode de test. On parle parfois de « set up » ou « préambule ».
  • cleanup : exécuté après chaque méthode de test. On parle parfois de « tear down » ou « postambule »

Au niveau des bonnes pratiques, pour ma part je reprends les notations d’autres moteurs de tests (PHPUnit et JUnit), et donc j’aime bien :

  • préfixer le nom des test cases par « Test ». Pour une classe MyClass → MyClassTest.
  • préfixer le nom des tests (les private slots) par « test ». Ex : « testAdd » et « testAddWithErrors » pour tester la méthode MyClass::add.

Voici un exemple de test case, le header et sa source :

[cpp title="MyClassTest.h"]
#ifndef MYCLASSTEST_H
#define MYCLASSTEST_H

#include <QObject>

class MyClassTest : public QObject
{
  Q_OBJECT
private slots:
  void initTestCase();
  void cleanupTestCase();
  void init();
  void cleanup();

  void testHelloThere();
};

#endif // MYCLASSTEST_H
[/cpp]
[cpp title="MyClassTest.cpp"]
#include "MyClassTest.h"

#include <QDebug>
#include <QTest>

#include "MyClass.h"

void MyClassTest::initTestCase()
{
  qDebug()<<"Test is starting";
}

void MyClassTest::cleanupTestCase()
{
  qDebug()<<"Test is finished";
}

void MyClassTest::init()
{
  qDebug()<<"init";
}

void MyClassTest::cleanup()
{
  qDebug()<<"cleanup";
}

void MyClassTest::testHelloThere()
{
  MyClass myClass;
  QVERIFY("Hello!" == myClass.helloThere());
  QVERIFY2("Hello Martin!" == myClass.helloThere("Martin"), "Martin should have been saluted...");
  QCOMPARE(myClass.helloThere("Riggs"), QStringLiteral("Hello Riggs!"));
}
[/cpp]

Voilà qui est bien beau, mais voyons voir comment exécuter ce bout de test.

Exécuter un test case

Lancer un test case se fait relativement simple via la méthode qExec. Un petit exemple qui se contente d’exécuter le test case « MyClassTest » et d’afficher le résultat :

[cpp]
#include <QDebug>
#include <QTest>

#include "MyClassTest.h"

int main(int argc, char **argv)
{
  MyClassTest testCase;
  int testCaseResult = QTest::qExec(&testCase, argc, argv);
  qDebug()<<"MyClassTest: "<<(0 == testCaseResult ? "Success" : "Failure");
  return testCaseResult;
}
[/cpp]

Voici qui devrait afficher :

********* Start testing of MyClassTest *********

Config: Using QtTest library 5.3.2, Qt 5.3.2

QDEBUG : MyClassTest::initTestCase() Test is starting

PASS : MyClassTest::initTestCase()

QDEBUG : MyClassTest::testHelloThere() init

QDEBUG : MyClassTest::testHelloThere() cleanup

PASS : MyClassTest::testHelloThere()

QDEBUG : MyClassTest::cleanupTestCase() Test is finished

PASS : MyClassTest::cleanupTestCase()

Totals: 3 passed, 0 failed, 0 skipped

********* Finished testing of MyClassTest *********

MyClassTest: Success

Lancer une série de test cases

Allons plus loin en se fabriquant un petit outil, un test suite runner, qui va lancer plusieurs test cases d’un coup. Pour se faire nous allons créer une classe TestRunner. Chaque test case aura un log séparé, et le TesterRunner doit décrire globalement le statut de chaque test case (passed / failure), ainsi que le statut du test suite (passed / failure) c’est-à-dire un « ET » logique sur le résultat de tous les test cases.

[code file="TestRunner.h" language="cpp"]
#ifndef TESTRUNNER_H
#define TESTRUNNER_H

#include <QHash>
#include <QTimer>
#include <QDateTime>
#include <QCoreApplication>
#include <QtTest>
#include "Logger.h"

class TestRunner: public QObject
{
    Q_OBJECT

public:
    Logger logTestsResult;
    TestRunner()  : overallResult(0)
    {
        logTestsResult = Logger("TestRunner", "testsresult_", Logger::DontAddToGeneralLog);
        logTestsResult.setFolder("result");
        logTestsResult.cleanAll();
    }

    void addTest(QString testCaseName, QObject * testCase) {
        testCase-&amp;gt;setParent(this);
        testCaseList.insert(testCaseName, testCase);
    }

    bool runTests() {
        int argc =0;
        char * argv[] = {0};
        QCoreApplication app(argc, argv);
        QTimer::singleShot(0, this, SLOT(run()));
        app.exec();
        return overallResult == 0;
    }

private slots:
    void run() {
        // Launch
        doRunTests();
        // Log overall result
        logTestsResult.i(0 == overallResult ? "Passed" : "Failure");
        // Quit
        QApplication::instance()-&amp;gt;quit();
    }

private:
    void doRunTests() {
        foreach (QString testCaseName, testCaseList.keys()) {
            QStringList outputLogfileCmd;
            outputLogfileCmd<<" "<<"-o"<<logTestsResult.getFolder()+testCaseName+".log";
            int testCaseResult = QTest::qExec(testCaseList.value(testCaseName), outputLogfileCmd);
            logTestsResult.i(testCaseName+": "+(0 == testCaseResult ? "Passed" : "Faillure"));
            overallResult |= testCaseResult;
        }
    }

    QHash<QString, QObject *> testCaseList;
    int overallResult;
};

#endif // TESTRUNNER_H

[/code]
[code file="TestRunner.cpp" language="cpp"]
#include "TestRunner.h"
#include "tst_ExampleTest.h"

#include <QString>

int main() {
 TestRunner testRunner;
 testRunner.addTest("ExampleTest", new ExampleTest());
 testRunner.runTests();
 return 0;
}
[/code]
[code file="tst_ExampleTest.h" language="cpp"]
#ifndef TST_EXAMPLETEST_H
#define TST_EXAMPLETEST_H

#include <QtCore/QString>
#include <QtTest/QtTest>

class ExampleTest: public QObject
{
    Q_OBJECT

public:
    ExampleTest(bool debugMode=false);

private Q_SLOTS:
    void initTestCase();
    void cleanupTestCase();

    void testEx1();

};

#endif // TST_EXAMPLETEST_H
[/code]
[code file="tst_ExampleTest.cpp" language="cpp"]
#include "tst_ExampleTest.h"

ExampleTest::ExampleTest()
{
}

void ExampleTest::initTestCase()
{
}

void ExampleTest::cleanupTestCase()
{
}

void ExampleTest::testEx1()
{
    QVERIFY2(true, "Should be true");
}
[/code]

L’environnement de tests de mes rêves

Voici ma liste personnelle du test suite runner de mes rêves en C++/Qt :

  • Pas de TestRunner à créer, juste des classes de tests (héritant d’un type spécifique, ou au pire s’inscrivant quelque part)
  • Lancement des tests unitaires au choix lors de la compilation (grâce à une config ou un define)
  • Résultat des tests (par test case et résultat global) dans des fichiers de logs, et/ou au choix dans stdout (coloré dans ce cas). En utilisant exclusivement qDebug bien sûr, ou stdout, pour ne pas inclure de bibliothèque supplémentaire.
  • Fonction de setup/teardown par test case, et avant chaque test
  • Supporter les fixtures (les quoi ? cf l’excellent article Increase your QTest productivity)
  • Tester les signaux / slots Qt avec QSignalSpy (outil indispensable pour les tests unitaires Qt !)
  • Ajouter une librairie pour gérer facilement les stub / mock

La bonne nouvelle c’est que j’ai partiellement atteint ce rêve (ohoh) avec un jeu de classes TestRunner et IUnitTest, empaqueté dans un package Qompoter appelé autotest pour l’utiliser facilement. C’est ce que l’on va voir au prochain épisode 🙂

Plus d’informations

Logo Git

Tips to dig into git log

Publié dans Développement | Laisser un commentaire

I am a regular user of git dag <sourcefilepath> to check all commits applying on a single source file. You can also do it with gitk <sourcefile> but I always find git dag more convenient.

git dag interface

Recently, I had to check the changes made on a specific line of a source file. I started digging into 7 years of commits… and obviously it was time to search for a better option 🙂

Fortunately, git log is really powerful.

First you can list commits with an impact on a specific function using the -L option. The diff are display immediately which is pretty handy. For example: git log -L:sendTcpRequest:src/evcc/Evcc.cpp. The -L option be used for numerous other stuff, please check git log --help.

This was still listing dozen of diff. The -G option allows to directly search on all the diffs and list related commit. For example git log -G "flush" Evcc.cpp was listing me 3 commits with a diff containing the word « flush ».

Plus de folie avec les QThread

Publié dans C / C++ | Laisser un commentaire

La programmation avec des threads est complexe et ne doit jamais être prise à la légère. Tête froide, mutex et variables atomiques sont de rigueur si on veut s’éviter les affres de l’horreur.

Qt peut nous simplifier la vie. Il peut nous y aider en tout cas. Mais. Oui il y a un mais. En regardant la documentation disponible sur Internet, c’est le bazar car : il y a bien longtemps, entre Qt 4.4 et Qt 4.7, on utilisait les mêmes noms de méthodes (run) pour quelque chose de différent ! Du coup, même aujourd’hui on trouve de tout sur le Web.

Voici quelques conseils en vrac avant de pointer vers de la documentation intéressante.

Conseils en vrac

  • Si cela ne vous parle pas, allez regarder les définitions de thread-safe et re-entrant. En fait, renseignez-vous sur les threads avant d’utiliser les QThreads, vous gagnerez du temps.
  • Un QObject appartient au thread dans lequel il est créé, ou dans lequel il est déplacé (mais dans ce cas, il ne peut avoir de parents)
    • Corolaire : les enfants d’un QObject vivent dans le même thread que leur parent. Mais attention, les variables membres ne deviennent pas automatiquement enfant de leur QObject ! (bigre)
  • Si la notion de QEventLoop Qt ne vous parle pas, renseignez-vous !
  • De manière assez logique, on peut appeler tranquillement un QObject via plusieurs thread, donc la même fonction d’une même classe peut être appelée par différents threads. En général on évite. Pour faire de la communication inter-thread, le plus safe est d’utiliser les signaux / slots en mode QueueConnection (mode par défaut dans ce cas). Si le thread A émet un signal variableChanged(int), ce signal peut être catché par une classe dans le thread principal via un slot handleVariableChanged(int). Voir QueuedConnection
  • En fait, si on ne fait pas gaffe, notamment si on n’utilise pas les signaux / slots, ou si on utilise le mode DirectConnection, on peut sans se rendre compte avoir plusieurs threads qui modifient en même temps la même donnée. Le mécanisme de protection de base, c’est les mutex (QMutexLocker et QMutex sont vos amis). Mais on peut aussi jouer avec les variables atomiques. Plus difficile, pas toujours possible, mais indispensable si on veut éviter de manière fiable les interlocks.
  • Pour utiliser les signaux / slots entre thread, il faut au préalable s’assurer que les paramètres des signaux soient bien enregistrés avec Q_DECLARE_METATYPE(Type) et qRegisterMetaType<Type>(). Sans cela, en QueuedConnection le slot ne serait juste pas appelé (dommage, try again).
  • D’une manière générale, je préconise d’utiliser QConcurrent et QFutureWatcher qui encapsule le comportement des QThreads. C’est mieux, plus simple et presque toujours possible. Je ne suis pas le seul à le préconiser, c’est la doc Qt qui le dit.
  • Si on doit utiliser directement QThread, alors la lecture de la doc Qt et des articles de Maya et FabienPN est fortement encourager.

Continuer la lecture

Logo KDE

X11, SDDM, Kwin et Plasma desktop

Publié dans Geekeries | Laisser un commentaire

Cinq mois après mes mésaventures suites à une mise à jour Debian/KDE testing, rebelote, j’ai à nouveau un problème suite à une mise à jour classique. Ce qui n’est pas si étonnant quand on utilise testing, surtout que testing a switché assez récemment sur bookworm.

Les symptômes très classiques : la page de login s’affiche, je m’authentifie, la page de chargement de Plasma KDE s’affiche, le loader tourne puis s’arrête au bout de quelques secondes et… voilà ! J’ai la souris, mais pour le reste c’est figé sur l’écran de chargement.

J’ai fini par trouver quel était le problème (incompatibilité temporaire entre kwin et kdecorator), mais voici les étapes par lesquels j’ai dû passer, pour finalement trouver la réponse sur un rapport de bug dans une mailing list Debian. J’en sais désormais d’avantage sur SDDM, X11, Kwin et le montage du bureau.

Au delà de l’analyse de sa machine, le bon réflexe à avoir c’est de regarder la liste des bugs en cours sur Debian : le portail des bugs et son moteur de recherche.

Continuer la lecture

Copier / coller un tableau HTML

Publié dans HTML et CSS | Laisser un commentaire

Il existe une astuce pour copier / coller un tableau HTML d’un navigateur vers un fichier Excel (par exemple).

De base, si je sélectionne un tableau avec ma souris, la sélection s’effectue ligne par ligne.

Pratique pour copier un tableau entier. Mais embêtant lorsque l’on ne souhaite copier que quelques colonnes comme c’est mon cas ici.

Dans ce cas, pendant la sélection, il faut appuyer sur la touche Ctrl. On obtient alors une sélection par colonne.

Continuer la lecture
Logo KDE

Akonadi pour 15 go

Publié dans Geekeries | Laisser un commentaire

Sur ma Debian KDE bulleyes, j’ai récemment remarqué qu’Akonadi utilisait plus de 15 go d’espace disque ! Akonadi, c’est ce mécanisme de base de données partagées que peuvent utiliser les applications Linux pour faciliter l’accès aux données de l’utilisateur : email, agenda, rappels, etc. C’est principalement utilisé par de plus en plus d’applications KDE comme KMail, KOrganizer, etc.

Pourtant, il ne me semble pas utiliser particulièrement ces applications… J’utilise Thunderbird et je m’en porte très bien ! Akonadi est normalement coupé par défaut, et activé dès qu’une application le nécessite. Il semblerait que l’horloge KDE puisse l’activer (eh oui, elle peut afficher un agenda), mais à part ça, mystère.

Mon hypothèse c’est que j’ai dû tester un jour KMail, puis le désinstaller, et Akonadi s’est ainsi retrouvée avec 15 go d’emails dans sa base de données.

J’ai cherché, et je n’ai pas trouvé comment savoir si ces données étaient vraiment utiliser, ni comment purger. Alors j’ai suivi les conseils de ce thread KDE : couper Akonadi, tout supprimer, relancer Akonadi. Oui.

Continuer la lecture
Logo KDE

Astuces en cas de pépin avec KDE Plasma

Publié dans Geekeries | Un commentaire

Plasma crashe au boot à cause d’EventCalendar

Pour ceux qui sont sous Debian Testing Bulleyes (so 2020), si jamais vous avez le même problème que moi (j’ai juste ouvert mon ordi pour rien d’important, et pof écran noir, juste la souris, même après le reboot, bref galère) :

Je ne sais pas qui est la cause de quoi, mais il s’avère que le plugin qui affiche l’heure EventCalendar s’est aussi mis à crasher et le résultat c’est que Plasma crash au démarrage et affiche un écran noir. Le plus simple c’est de sauvegarder puis supprimer `.config/plasma-org.kde.plasma.desktop-appletsrc` puis de reboot (Donc `Alt+F2` pour afficher un terminal, ou bien `Ctrl+Alt+F2`). ça recréera une config par défaut. J’ai pu comparer les 2, faire des tests, détecter le pb d’affichage de l’heure par hasard, et du coup remettre ma config d’origine corrigée.
Plasma s’affichait bien, mais pas le décorateur de fenêtre (pas possible de déplacer, fermer des fenêtres, alt+tab marchait pas).

EventCalendar est toujours planté aujourd’hui, donc je ne l’utilise plus. Lorsque j’ai eu ce problème, j’avais probablement un double bug EventCalendar + kwin. C’est peut-être dû à la dernière mise à jour que j’ai faite, soit ça a cassé mon kwin (le « window decorator »), soit ça a introduit un bug dans kwin. Bref, dans la soirée une nouvelle version de kwin est apparu et cela a réglé le souci. J’ai cherché à savoir si c’était ma config ou un bug, mais je n’ai pas trouvé.

Au moins, je sais maintenant où trouver la config de Plasma, et je sais ce qu’est un window decorator 😀

Redémarrage live de Plasma

La commande pour redémarrer plasma c’est : `killall plasmasheel` (pour tuer l’ancien) puis tout simplement `plasmashell &`.

Picotech

libpicoipp installation error

Publié dans Geekeries | Un commentaire

Sur mon Debian bulleyes (encore « testing » pour l’instant, mais plus pour longtemps), je me suis récemment retrouvé avec une erreur étrange lors de la mise à jour de mon système :

$ sudo apt upgrade                                                                                                                  
Lecture des listes de paquets... Fait                                                                                               
Construction de l'arbre des dépendances                                                                                             
Lecture des informations d'état... Fait                                                                                             
Vous pouvez lancer « apt --fix-broken install » pour corriger ces problèmes.                                                        
Les paquets suivants contiennent des dépendances non satisfaites :                                                                  
 libps2000a : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                       
 libps3000a : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                       
 libps4000 : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                        
 libps4000a : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                       
 libps5000a : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                       
 libps6000 : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                        
 libps6000a : Dépend: libpicoipp (>= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                       
 picoscope : Dépend: libpicoipp (= 1.3.0-4r78) mais 1.3.0-4r29 est installé                                                         
E: Dépendances non satisfaites. Essayez « apt --fix-broken install » sans paquet                                                    
   (ou indiquez une solution).
Continuer la lecture

Monter le Storex AivX avec Samba

Publié dans Geekeries | Laisser un commentaire

J’ai depuis plusieurs année un disque dur Wifi / media center Storex AivX – 372 HD. A part que c’est un peu vieux, c’est assez cool : 1to et il permet de lire facilement des films sur un écran (très pratique, je n’ai que faire d’un télé).

Voilà longtemps que je voulais pouvoir y accéder à distance via mes autres ordinateurs. C’est censé être possible puisqu’il est connecté en Wifi à ma box Internet. Via Windows c’est un peu la croix et la bannière, mais ça marche. Sous Linux… je n’avais jamais réussi ! L’autre jour, je me suis un peu acharné, j’ai trouvé et relu la doc, et j’ai fini par trouver la solution (et les raisons) juste avant de lâcher prise. Ouf !

Continuer la lecture
syncthing

Installer syncthing sur recalbox

Publié dans Geekeries | Laisser un commentaire

J’ai récemment installé l’OS Recalbox, l’émulateur de consoles de jeux, sur mon RaspberryPi3. Une petite manette USB, et… ah oui il me faudra une manette USB ou Bluetooth supplémentaire, et prendre le temps de retrouver des jeux sympas… et cela devrait animer les soirées d’hiver !

Mon objectif est d’avoir un RaspberryPi près du bureau pour :

  • Faire console de jeux (checked)
  • Ah tiens Recalbox est fourni avec le media center Kodi (à creuser)
  • Rendre mon imprimante utilisable via le réseau (peut-être compliqué avec Recalbox, c’est mon prochain défi)
  • Rajouter un noeud Syncthing à la maison (checked)

Mais comment installer synchting sur Recalbox ? Puisque comme le disent certains sur les forums : il n’y a pas de gestionnaire de paquet sur Recalbox.

Continuer la lecture