[GO] Développons quelque chose de proche de l'implémentation avec TDD ~ traitement d'initialisation / de terminaison de libevdev ~

introduction

En utilisant googletest / googlemock, je développe un logiciel qui fonctionne sur Linux embarqué avec TDD. Puisque la procédure réellement développée avec TDD est écrite telle quelle en temps réel, il y a une partie aléatoire. J'espère que vous apprécierez également le processus. Si vous avez des questions ou des erreurs, veuillez commenter. Ce sera encourageant.

Si vous souhaitez en savoir plus sur l'histoire jusqu'à présent, veuillez consulter les articles précédents. Essayez de développer quelque chose de proche de intégré avec TDD ~ Préparation ~ Essayez de développer quelque chose de proche de celui intégré avec TDD ~ Résolution de problèmes ~ Essayez de développer quelque chose de proche de celui intégré avec TDD ~ file open ~

initialisation de libevdev

Test de réussite de l'initialisation

Considérons maintenant un test pour initialiser libevdev. Ce serait bien si la fonction d'initialisation de libevdev était appelée en plus de open () créé la dernière fois.

TEST_F(KeyInputEventTest, CanInitEvdev) {
  const int kFd = 3;
  //Mock is IO_OPEN()Descripteur de fichier dans"3"rends le
  EXPECT_CALL(*mock_io, IO_OPEN(_, _)).WillOnce(Return(kFd));
  //Valeurs attendues pour la maquette libevdev nouvellement ajoutée
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(kFd, _))
    .WillOnece(Return(0));

  EXPECT_TRUE(InitKeyInputDevice("./test_event"));
}

Dans ce EXPECT_CALL (), nous nous attendons à ce que le descripteur de fichier ouvert soit passé au premier argument de libevdev_new_from_fd () ("_" signifie que l'argument est indifférent). Le simulacre teste si la fonction a été appelée avec les arguments spécifiés.

Comme promis par TDD, ce test ne se compile même pas. Tout d'abord, faisons une maquette pour qu'elle puisse être compilée.

L'en-tête comprend le vrai libevdev.h pour éviter des erreurs mineures telles que des fautes d'orthographe.

#include <gmock/gmock.h>
//Cet en-tête est réel
#include <libevdev/libevdev.h>

class MOCK_LIBEVDEV {
 public:
    MOCK_METHOD2(libevdev_new_from_fd, int(int, libevdev**));
};

extern MOCK_LIBEVDEV *mock_libevdev;

extern "C" {
  //Le code produit est libevdev_new_from_fd()Cette fonction est appelée lorsque vous appelez
  //Mock y est appelé.
  int libevdev_new_from_fd(int fd, libevdev **dev)
  {
    return mock_libevdev->libevdev_new_from_fd(fd, dev);
  }
}

Si vous implémentez le simulacre, il se compilera. Dans le test, veillez à ne pas lier la vraie bibliothèque, mais à lier la maquette. Même si la compilation réussit, le test échoue car il ne répond pas à la valeur attendue de l'appel de libevdev_new_from_fd () dans Init ().

[ RUN      ] KeyInputEventTest.CanInitEvdev
led_controller/test/key_input_event_test.cpp:76: Failure
Actual function call count doesn't match EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(kFd, _))...
         Expected: to be called once
           Actual: never called - unsatisfied and active
[  FAILED  ] KeyInputEventTest.CanInitEvdev (0 ms)

Ajoutez les deux lignes suivantes à Init () et le test réussira.

  struct libevdev *evdev = NULL;
  libevdev_new_from_fd(fd, &evdev);

Erreur d'initialisation

Considérons maintenant un test lorsqu'une erreur d'initialisation de libevdev se produit. En regardant la documentation de libevdev, libevdev_new_from_fd () semble retourner 0 en cas de succès et un code d'erreur négatif sinon. (Dans EXPECT_CALL () que j'ai ajouté plus tôt, la valeur de retour de libevdev_new_from_fd () était définie sur 0 car je connaissais cette spécification.) Le code d'erreur éventuel ne peut pas être lu à partir du document.

libevdev libevdev_new_from_fd()

Jetons un coup d'œil au test unitaire fourni avec le code source de libevdev pour voir quel code d'erreur est renvoyé.

[libevdev github mirror] (https://github.com/whot/libevdev/blob/master/test/test-libevdev-init.c)

Il y avait un test que je cherchais.

START_TEST(test_init_from_invalid_fd)
{
	int rc;
	struct libevdev *dev = NULL;

	rc = libevdev_new_from_fd(-1, &dev);

	ck_assert(dev == NULL);
	ck_assert_int_eq(rc, -EBADF);

	rc = libevdev_new_from_fd(STDIN_FILENO, &dev);
	ck_assert(dev == NULL);
	ck_assert_int_eq(rc, -ENOTTY);
}
END_TEST

Il semble que EBADF soit retourné lorsque fd donné à libevdev vaut -1, et ENOTTY est retourné lorsque fd est l'entrée standard. Aucun autre cas de test d'échec ne peut être trouvé. C'est intuitif, mais il est étrange qu'aucune autre erreur ne soit renvoyée. Allez voir le code source. ENOMEM en raison d'un échec d'allocation de mémoire, ENOENT lorsqu'il n'y a pas de périphérique, EBADF lorsque la structure libevdev passée pour l'initialisation a déjà été initialisée, et d'autres situations où errno est retourné semble renvoyer cet errno. Si vous êtes intéressé, veuillez consulter le code ci-dessous.

github libevdev.c

Jusqu'à présent, il ne semble y avoir aucune erreur nécessitant une attention particulière. Si libevdev_new_from_fd () renvoie un nombre négatif, laissez Init () retourner false. Le test était le suivant. Je ne suis pas intéressé par l'argument de libevdev_new_from_fd (), alors ne vous inquiétez pas.

TEST_F(KeyInputEventTest, InitEvdevFailed) {
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(_, _))
    .WillOnce(Return(-EBADF));  //EBADF en tant que représentant. Les valeurs limites peuvent être testées si la rigueur est un problème.

  EXPECT_FALSE(InitKeyInputDevice("./test_event"));
}

Pour obtenir un code produit répondant à ce test, ajoutez les deux lignes suivantes:

  int rc = libevdev_new_from_fd(fd, &evdev);
  if (rc < 0) return false;

Si vous écrivez le code comme celui-ci, googlemock affichera l'avertissement suivant. Il s'agit d'un fonctionnement normal de googlemock, et il n'y a aucun problème avec le code de test et le code produit.

[ RUN      ] KeyInputEventTest.InitEvdevFailed

GMOCK WARNING:
Uninteresting mock function call - returning default value.
    Function call: IO_OPEN(0x4e6015 pointing to "./test_event", 2048)
          Returns: 0
NOTE: You can safely ignore the above warning unless this call should not happen.  Do not suppress it by blindly adding an EXPECT_CALL() if you don't mean to enforce the call.  See https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md#knowing-when-to-expect for details.
[       OK ] KeyInputEventTest.InitEvdevFailed (0 ms)

La cause du texte d'avertissement est que lorsque Init () est appelé, IO_OPEN () est appelé, mais la valeur attendue n'est pas écrite. Comme vous pouvez le voir dans l'avertissement, ce test n'est pas intéressé par l'appel de IO_OPEN () et peut être ignoré en toute sécurité. Si vous souhaitez déboguer, vous pouvez limiter la sortie de cet avertissement en spécifiant les options suivantes lors de l'exécution du test. --gmock_verbose=error Cependant, l'avertissement doit être lu correctement pour vous assurer que vous n'effectuez pas un appel involontaire.

Erreur gérée par libevdev_new_from_fd () (peut être ignorée)

libevdev_new_from_fd () ne semble pas être en mesure de comprendre pourquoi l'ouverture du fichier a échoué. Que l'échec d'ouverture du fichier soit Autorisation refusée ou Aucun fichier ou répertoire de ce type, il est traité comme un descripteur de fichier incorrect.

En guise de test, j'ai écrit le test suivant et l'ai exécuté sans privilèges root. Tenter d'ouvrir un périphérique événementiel sans privilèges root entraîne une erreur Permission refusée, donc la valeur attendue renvoyée par libevdev_new_from_fd () a été définie sur -EACCES.

TEST_F(EvdevSampleOpenTest, TestEvdevError) {
  struct libevdev *dev {nullptr};
  int fd = open("/dev/input/event2", O_RDONLY|O_NONBLOCK);
  int rc = libevdev_new_from_fd(fd, &dev);

  EXPECT_EQ(-EACCES, rc);
}

Le résultat échoue comme suit.

[ RUN      ] EvdevSampleOpenTest.TestEvdevError
/home/tomoyuki/work/02.TDD/TDDforEmbeddedSystem/evdev_test/test/evdev_sample_test.cpp:46: Failure
Expected equality of these values:
  -13
  rc
    Which is: -9
[  FAILED  ] EvdevSampleOpenTest.TestEvdevError (0 ms)

Je voulais que ce soit -EACCES (-13), mais c'était en fait une erreur de descripteur de fichier Bad, à savoir -EBADF (-9).

processus de terminaison de libevdev

Il y a des moments où le test de traitement final ne fonctionne pas bien. Écrivons-le à la fois sur le lapin et sur le coin.

TEST_F(KeyInputEventTest, CleanupKeyInputDevice) {
  EXPECT_TRUE(CleanupKeyInputDevice());
}

Que renvoie Cleanup () pour vrai? Peut-il retourner faux? Ne vous précipitez pas, vérifiez les spécifications de libevdev et fermez.

libevdev libevdev_free()

void libevdev_free(struct libevdev *dev)	

Surtout, il est peu probable qu'une erreur soit renvoyée. Cependant, j'ai trouvé des informations qui pourraient être utiles à l'avenir.

After completion, the struct libevdev is invalid and must not be used.

Autrement dit, un appel de fonction qui utilise libevdev après avoir appelé Cleanup () devrait échouer. Ajoutez-le à la liste ToDo.

Puis Man page of close.

close () renvoie 0 en cas de succès. Si une erreur se produit, renvoyez -1 et définissez errno de manière appropriée. EBADF fd n'est pas un descripteur ouvert valide. L'appel EINTR close () a été interrompu par un signal. Voir signal (7). Une erreur d'E / S EIO s'est produite.

EBADF est susceptible de se produire. C'est un modèle qu'un descripteur qui n'est pas ouvert passe pour se fermer. Un test qui quitte libevdev avec succès ressemble à ceci:

TEST_F(KeyInputEventTest, CanCleanupKeyInputDevice) {
  InitKeyInputDevice(kFilePath);
  EXPECT_TRUE(CleanupKeyInputDevice());
}

Dans le code produit actuel, fd et libevdev sont définis comme des variables locales dans Init (). En l'état, la cible à traiter par Cleanup () est inconnue, alors partagez fd et libevdev avec Init (). Collectez les données nécessaires dans une structure et définissez-la dans la portée du fichier. Initialisons-le avec un descripteur de fichier explicitement invalide.

typedef struct {
  int fd;
  struct libevdev *evdev;
} KeyInputDeviceStruct;

enum {INVALID_FD = -1};
static KeyInputDeviceStruct dev = {INVALID_FD, NULL};

// Init()Initialise les variables membres de dev.
bool InitKeyInputDevice(const char *device_file) {
  dev.fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (dev.fd < 0) {
  ...
}

et ça? Il ne semble pas y avoir de problème particulier. Implémentons Cleanup (). Le code est omis, mais la simulation nécessaire est également implémentée.

bool CleanupKeyInputDevice() {
  libevdev_free(dev.evdev);  //Appelez le simulacre dans le test
  int rc = IO_CLOSE(dev.fd);  //Appelez le simulacre dans le test
  if (rc < 0) return false;  //Vous pouvez mettre en œuvre sans cette ligne dans un premier temps

  return true;
}

Le test réel pour tester le code ci-dessus ressemble à ceci: (En fait, j'ai écrit le test en premier)

//J'ai préparé un assistant pour Init parce que je pense que je vais l'utiliser à l'avenir.
static void InitHelper(const char *path, int fd, int res_evdev_new) {
  EXPECT_CALL(*mock_io, IO_OPEN(path, _)).WillOnce(Return(fd));
  EXPECT_CALL(*mock_libevdev, libevdev_new_from_fd(fd, _))
    .WillOnce(Return(res_evdev_new));

  InitKeyInputDevice(path);
}

TEST_F(KeyInputEventTest, CanCleanupKeyInputDevice) {
  constexpr int kFd = 3;  //Descripteur de fichier"3"
  InitHelper(kFilePath, kFd, 0);

  // libevdev_free()Assurez-vous juste que c'est appelé
  EXPECT_CALL(*mock_libevdev, libevdev_free(_)).Times(1);
  //Descripteur de fichier"3"Devrait fermer
  EXPECT_CALL(*mock_io, IO_CLOSE(kFd)).WillOnce(Return(0));

  EXPECT_TRUE(CleanupKeyInputDevice());
}

Le test passe avec succès!

Ensuite, c'est le suivant. L'appel de Cleanup avant Init devrait échouer.

TEST_F(KeyInputEventTest, CleanupKeyInputDeviceFileNotOpenYet) {
  EXPECT_FALSE(CleanupKeyInputDevice());
}

Maintenant, la structure dev est initialisée comme suit. Ainsi, la simulation de IO_CLOSE () est susceptible de réussir le test si elle renvoie -1 lorsque l'argument est INVALID_FD.

enum {INVALID_FD = -1};
static KeyInputDeviceStruct dev = {INVALID_FD, NULL};

bool CleanupKeyInputDevice() {
  libevdev_free(dev.evdev);
  int rc = IO_CLOSE(dev.fd);  //In non initialisé, dev.fd est INVALIDE_FD(-1)
  if (rc < 0) return false;

  return true;
}

En d'autres termes, c'est un test.

TEST_F(KeyInputEventTest, CleanupKeyInputDevice) {
  EXPECT_CALL(*mock_io, IO_CLOSE(-1)).WillOnce(Return(-1));

  EXPECT_FALSE(CleanupKeyInputDevice());
}

Eh bien, la construction et le test passent ... non!

[ RUN      ] KeyInputEventTest.CleanupKeyInputDevice
Unexpected mock function call - returning default value.
    Function call: IO_CLOSE(3)
          Returns: 0
Google Mock tried the following 1 expectation, but it didn't match:

led_controller/test/key_input_event_test.cpp:111: EXPECT_CALL(*mock_io, IO_CLOSE(-1))...
  Expected arg #0: is equal to -1
           Actual: 3
         Expected: to be called any number of times
           Actual: never called - satisfied and active
[  FAILED  ] KeyInputEventTest.CleanupKeyInputDevice (0 ms)

L'argument de IO_CLOSE () est "3". Ce résultat est naturel. Car, avant que ce test ne soit exécuté, il y a un test en cours qui attribue "3" à dev.fd! Puisque dev est une structure statique, sa durée de vie est aussi longue que le programme est en cours d'exécution. En d'autres termes, le résultat de ce test dépend d'autres exécutions de test.

Les modules à instance unique (ou les classes qui appliquent le modèle à une tonne) ont tendance à être difficiles à tester. En effet, un test indépendant ne peut être écrit que si tous les états modifiés lors du test précédent sont restaurés. Sauf pour les modules qui doivent être à une seule instance, je pense qu'il est préférable de supposer autant que possible la multi-instance.

Maintenant, modifions le code pour qu'il puisse être multi-instancié. Ce correctif force les modifications apportées aux API existantes. Cependant, vous pouvez continuer tout en confirmant que les tests que vous avez créés jusqu'à présent réussissent même avec la nouvelle API.

Ajoutez / modifiez l'API comme suit pour la rendre multi-instance.

struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

KeyInputDevice CreateKeyInputDevice();
bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file);
bool CleanupKeyInputDevice(KeyInputDevice dev);
void DestroyKeyInputDevice(KeyInputDevice dev);

KeyInputDeviceStruct a des pointeurs vers des descripteurs de fichiers et des structures libevdev comme suit:

typedef struct KeyInputDeviceStruct {
  int fd;
  struct libevdev *evdev;
} KeyInputDeviceStruct;

Create () alloue de la mémoire, définit les valeurs initiales et renvoie un pointeur vers elle comme suit.

KeyInputDevice CreateKeyInputDevice() {
  KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
  dev->fd = -1;
  dev->evdev = NULL;

  return dev;
}

Destroy () libère le pointeur passé.

void DestroyKeyInputDevice(KeyInputDevice dev) {
  if(!dev) {
    free(dev);
    dev = NULL;
  }
}

Init () et Cleanup () ne manipulent que le pointeur donné en argument au lieu de la structure précédemment définie comme variable statique.

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  //  dev.fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  dev->fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
...

En modifiant l'API de cette manière, le test Cleanup () suivant qui a échoué plus tôt réussira.

TEST_F(KeyInputEventTest, CleanupKeyInputDevice) {
  EXPECT_CALL(*mock_io, IO_CLOSE(-1)).WillRepeatedly(Return(-1));

  EXPECT_FALSE(CleanupKeyInputDevice(dev_));
}

À propos, dans tous les tests, Create () et Destroy () sont utilisés pour créer et détruire KeyInputDevice. Pour éliminer la duplication du code de test, écrivez le traitement commun dans SetUp () / TearDown () du dispositif de test. Gardez votre code de test SEC (Ne vous répétez pas).

    virtual void SetUp()
    {
      dev_ = CreateKeyInputDevice();
    }

    virtual void TearDown()
    {
      DestroyKeyInputDevice(dev_);
    }

Désormais, toutes les API doivent être préparées pour les pointeurs nuls. Par exemple, si vous écrivez un test comme celui-ci, le test mourra avec un défaut de segmentation.

TEST_F(KeyInputEventTest, AllApiHaveNullPointerGuard) {
  const KeyInputDevice kNullPointer = NULL;
  EXPECT_FALSE(InitKeyInputDevice(kNullPointer, kFilePath));
  EXPECT_FALSE(CleanupKeyInputDevice(kNullPointer));
}

A l'entrée de la fonction, mettons une garde contre le pointeur nul.

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  if(dev == NULL) return false;
  // if(!dev)Ne pas faire est un passe-temps

Encapsuler la structure interne

Le module que nous créons a maintenant un descripteur de fichier de périphérique d'événement et libevdev comme données. ** Cette structure interne est quelque chose que les utilisateurs de ce module n'ont pas besoin de connaître. ** ** L'utilisateur a seulement besoin de savoir comment créer une instance de KeyInputDevice avec Create () et transmettre l'instance créée à chaque fonction. Si vous exposez mal la structure interne à l'utilisateur, vous ne pouvez pas vous plaindre même si l'utilisateur la met en œuvre en fonction de la structure interne. En conséquence, ** l'effet de la modification de la structure interne s'étend au code de l'utilisateur, et dans le pire des cas, la structure interne ne peut pas être modifiée. ** **

Cette fois, j'ai divisé l'en-tête en key_input_event.h et key_input_event_private.h. Dans le premier, seules les informations ** dont l'utilisateur a besoin pour utiliser ce module ** sont écrites.

【key_input_event.h】

//Définir la déclaration avant de la structure et son type de pointeur comme KeyInputDevice
struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

KeyInputDevice CreateKeyInputDevice();
bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file);
bool CleanupKeyInputDevice(KeyInputDevice dev);
void DestroyKeyInputDevice(KeyInputDevice dev);

Les détails d'implémentation sont définis dans des en-têtes privés. 【key_input_event_private.h】

typedef struct KeyInputDeviceStruct {
  int fd;
  struct libevdev *evdev;
} KeyInputDeviceStruct;

Ce faisant, la structure interne peut être modifiée sans se propager à l'utilisateur. (Sauf si l'utilisateur est dans un état approximatif!)

[Une addition] Personne d'autre ne voit le KeyInputDeviceStruct à ce stade. Par conséquent, il a été modifié pour écrire la définition de la structure dans key_input_event.c.

Organiser l'initialisation / le traitement de l'arrêt de libevdev

Code de test

Ajout de 5 nouveaux tests. Le code est omis car la coloration syntaxique ne fonctionne pas tellement et le code est difficile à voir. github est un peu mieux, donc si vous voulez voir tout le code de test, veuillez aller à ↓. github:key_input_event_test.cpp

Code produit

En pensant au code testable, il a naturellement évolué vers un module multi-instance. L'API est la suivante.

struct KeyInputDeviceStruct;
typedef struct KeyInputDeviceStruct *KeyInputDevice;

KeyInputDevice CreateKeyInputDevice();
bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file);
bool CleanupKeyInputDevice(KeyInputDevice dev);
void DestroyKeyInputDevice(KeyInputDevice dev);

La mise en œuvre est la suivante.

KeyInputDevice CreateKeyInputDevice() {
  KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
  dev->fd = -1;
  dev->evdev = NULL;

  return dev;
}

bool InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
  if(dev == NULL) return false;

  dev->fd = IO_OPEN(device_file, O_RDONLY|O_NONBLOCK);
  if (dev->fd < 0) {
    if (errno == EACCES)
      DEBUG_LOG("Fail to open file. You may need root permission.");
    return false;
  }

  int rc = libevdev_new_from_fd(dev->fd, &dev->evdev);
  if (rc < 0) return false;

  return true;
}

bool CleanupKeyInputDevice(KeyInputDevice dev) {
  if(dev == NULL) return false;

  libevdev_free(dev->evdev);
  int rc = IO_CLOSE(dev->fd);
  if (rc < 0) return false;

  return true;
}

void DestroyKeyInputDevice(KeyInputDevice dev) {
  if(dev == NULL) return;

  free(dev);
  dev = NULL;
}

À ce stade, il n'y a rien à faire, mais le côté code produit peut également être construit et exécuté. Le main.c du code produit est le suivant.

int main(void) {
  KeyInputDevice dev = CreateKeyInputDevice();
  InitKeyInputDevice(dev, "/dev/input/event2");
  CleanupKeyInputDevice(dev);
  DestroyKeyInputDevice(dev);

  return 0;
}

Liste de choses à faire

L'état de la liste des tâches concernant l'entrée des touches est le suivant. Les éléments énumérés au début ont pris fin.

Aperçu de la prochaine fois

Détectez la pression sur la touche «A» et terminez l'implémentation des modules liés à libevdev.

Postscript

Je pense qu'il est difficile de comprendre que les valeurs de retour de Init () et Cleanup () sont booléennes. Ajout de l'énumération suivante et modification de l'implémentation pour renvoyer le code. En modifiant le test puis le code produit, vous pouvez modifier le code en toute confiance et audace.

enum {
  INPUT_DEV_SUCCESS = 0,
  INPUT_DEV_INIT_ERROR = -1,
  INPUT_DEV_CLEANUP_ERROR = -2,
};

Recommended Posts

Développons quelque chose de proche de l'implémentation avec TDD ~ traitement d'initialisation / de terminaison de libevdev ~
Développons quelque chose qui se rapproche du TDD
Développons quelque chose de proche de intégré avec TDD ~ Revue intermédiaire ~
Essayez de développer quelque chose de proche de l'intégration avec TDD ~ Problème ~
Développons quelque chose de proche de celui intégré avec TDD ~ Design pattern ~
Développons quelque chose de proche de celui intégré avec TDD ~ édition ouverte de fichier ~
Développons quelque chose de proche de celui intégré avec TDD ~ Version de détection d'entrée de clé ~
[Jouons avec Python] Traitement d'image en monochrome et points