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 ~ Essayez de développer quelque chose de proche de celui intégré avec TDD ~ traitement d'initialisation / de terminaison de libevdev ~ Développer quelque chose de proche de celui intégré avec la détection d'entrée TDD-Key-
Cet article peut ne pas être utile. Le prochain article sera intéressant.
Comme je l'ai annoncé la dernière fois, j'ai fait le contrôle LED en coulisses. Comme il n'y avait rien de spécial à mentionner, seuls les résultats seront affichés. Bien sûr, je l'ai implémenté avec TDD. J'ai sauté un peu et je n'ai pas géré l'erreur d'écriture par LED. N'imitez pas un bon garçon! Là encore, la mise en œuvre est telle que la structure interne n'est pas visible par l'utilisateur de ce module de commande à LED. Il prend également en charge plusieurs instances. Je pense qu'il existe de nombreux cas où plusieurs LED sont contrôlées, donc si vous le faites comme ça, il évoluera plus tard.
Code produit
typedef struct LedDriverStruct {
int fd;
LedStatus status;
} LedDriverStruct;
LedDriver CreateLedDriver() {
LedDriver led = calloc(1, sizeof(LedDriverStruct));
led->fd = -1;
led->status = LED_UNKNOWN;
return led;
}
int InitLedDriver(LedDriver self, const char* device_file) {
self->fd = IO_OPEN(device_file, O_WRONLY|O_NONBLOCK);
self->status = LED_TURN_OFF;
if (self->fd < 0) {
return LED_DRIVER_INIT_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void TurnOnLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_ON;
IO_WRITE(self->fd, "1\n", 2);
}
void TurnOffLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_OFF;
IO_WRITE(self->fd, "0\n", 2);
}
void ToggleLed(LedDriver self) {
if (self == NULL || self->status == LED_UNKNOWN) return;
if (self->status == LED_TURN_OFF) {
TurnOnLed(self);
} else {
TurnOffLed(self);
}
}
int CleanupLedDriver(LedDriver self) {
if (self == NULL) return LED_DRIVER_CLEANUP_ERROR;
int rc = IO_CLOSE(self->fd);
if (rc < 0) {
return LED_DRIVER_CLEANUP_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void DestroyLedDriver(LedDriver self) {
if (self == NULL) return;
free(self);
self = NULL;
}
Maintenant, que pensez-vous de ce code après une longue période?
main.c
#define KEYBOARD_DEVICE "/dev/input/event2"
#define LED_DEVICE "/sys/class/leds/input2::capslock/brightness"
#define KEY_RELEASED 0
#define KEY_PRESSED 1
static void mainloop() {
struct libevdev *dev = NULL;
int key_fd = open(KEYBOARD_DEVICE, O_RDONLY|O_NONBLOCK);
int rc = libevdev_new_from_fd(key_fd, &dev);
if (rc < 0) {
fprintf(stderr, "Failed to init libevdev (%s)\n", strerror(-rc));
exit(1);
}
int led_fd = open(LED_DEVICE, O_WRONLY|O_NONBLOCK);
if (led_fd < 0) {
fprintf(stderr, "Failed to init LED device.\n");
exit(1);
}
bool led_on = false;
do {
struct input_event ev;
rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == 0) {
if (ev.type == EV_KEY && ev.code == KEY_A && ev.value == KEY_PRESSED) {
led_on = !led_on;
char buf[2];
snprintf(buf, 2, "%d", led_on ? 1 : 0);
write(led_fd, buf, 2);
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
libevdev_free(dev);
close(key_fd);
close(led_fd);
}
int main() {
mainloop();
return 0;
}
C'est un code de cette taille, mais comment testez-vous ce code? Il semble que vous deviez tester manuellement l'ensemble du programme. (Si vous faites de votre mieux, vous pouvez écrire un test unitaire ...)
Comment pouvons-nous garantir que le dégraissage ne se produit pas lorsque nous ajoutons de nouvelles fonctionnalités? En l'absence de tests, vous ne savez même pas si vous avancez ou reculez.
Lorsque vous souhaitez refactoriser, pouvez-vous franchir le pas? Le courage de refactoriser est fourni par les tests. Le code non testé ne peut pas être remanié et pourrira progressivement.
Que faire si la bibliothèque que vous utilisez est instable? Cette fois, j'utilise une bibliothèque stable appelée libevdev. Si la bibliothèque utilisée est instable, on peut dire que la plage d'influence du changement de bibliothèque est l'ensemble du programme (pour être exact, dans la fonction mainloop).
Par exemple, que se passe-t-il si vous connectez un autre clavier et que vous souhaitez maintenant contrôler la LED NumLock avec la touche "B"? Quand il ressemble à celui ci-dessous, il y a déjà beaucoup d'odeur pourrie.
static void mainloop() {
struct libevdev *dev1 = NULL;
struct libevdev *dev2 = NULL;
int key_fd1 = open(KEYBOARD_DEVICE1, O_RDONLY|O_NONBLOCK);
int key_fd2 = open(KEYBOARD_DEVICE2, O_RDONLY|O_NONBLOCK);
int rc = libevdev_new_from_fd(key_fd1, &dev1);
int rc2 = libevdev_new_from_fd(key_fd2, &dev2);
//Je ne supporte plus d'écrire
}
Si vous êtes un peu plus rationnel, vous pouvez utiliser des tableaux. (Le code est approprié. Je n'ai pas essayé de compiler)
typedef struct LedStruct {
struct libevdev *dev;
int fd;
bool led_on;
}
static void mainloop() {
LedStruct leds[2];
leds[0].fd = open(KEYBOARD_DEVICE1, O_RDONLY|O_NONBLOCK);
leds[1].fd = open(KEYBOARD_DEVICE2, O_RDONLY|O_NONBLOCK);
...
do {
for (int i = 0; i < 2; i++) {
struct input_event ev;
rc = libevdev_next_event(leds[i].dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);
if (rc == 0) {
if (ev.type == target_keys[i] && ev.code == target_codes[i] && ev.value == target_values[i]) {
leds[i].led_on = !leds[i].led_on;
char buf[2];
snprintf(buf, 2, "%d", leds[i].led_on ? 1 : 0);
write(leds[i].fd, buf, 2);
}
}
}
} while (rc == 1 || rc == 0 || rc == -EAGAIN);
}
Ça va être beaucoup plus facile. Dans l'expression conditionnelle while, vous ne vérifiez le code de retour que lorsque i vaut 1 (avez-vous remarqué?).
Le fait que le béton (fonctionnement de l'appareil) soit solidement écrit et entrave l'abstraction est également un obstacle majeur à l'extension du logiciel.
Nous avons fait bon usage des tests unitaires avec TDD pour guider de bonnes conceptions. Jetons un coup d'œil au code que nous avons créé jusqu'à présent.
key_input_event.c
typedef struct KeyInputDeviceStruct {
int fd;
struct libevdev *evdev;
struct input_event target_event;
} KeyInputDeviceStruct;
KeyInputDevice CreateKeyInputDevice() {
KeyInputDevice dev = calloc(1, sizeof(KeyInputDeviceStruct));
dev->fd = -1;
dev->evdev = NULL;
return dev;
}
int InitKeyInputDevice(KeyInputDevice dev, const char *device_file) {
if(dev == NULL) return INPUT_DEV_INVALID_DEV;
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 INPUT_DEV_INIT_ERROR;
}
int rc = libevdev_new_from_fd(dev->fd, &dev->evdev);
if (rc < 0) return INPUT_DEV_INIT_ERROR;
return INPUT_DEV_SUCCESS;
}
int SetKeyInputDetectCondition(KeyInputDevice dev, const struct input_event *ev) {
if (dev == NULL) return INPUT_DEV_INVALID_DEV;
// Should I validate ev, here?
memcpy(&dev->target_event, ev, sizeof(struct input_event));
return INPUT_DEV_SUCCESS;
}
static bool HasPendingEvent(struct libevdev *evdev, struct input_event *ev) {
return libevdev_next_event(evdev, LIBEVDEV_READ_FLAG_NORMAL, ev)
== LIBEVDEV_READ_STATUS_SUCCESS;
}
static bool IsTargetEvent(const struct input_event *target,
const struct input_event *ev) {
return (target->type == ev->type
&& target->code == ev->code
&& target->value == ev->value);
}
int CheckKeyInput(KeyInputDevice dev) {
if (dev == NULL || dev->evdev == NULL) return INPUT_DEV_INVALID_DEV;
struct input_event ev = {};
if (HasPendingEvent(dev->evdev, &ev) && IsTargetEvent(&dev->target_event, &ev)) {
return INPUT_DEV_EVENT_DETECTED;
}
return INPUT_DEV_NO_EVENT;
}
int CleanupKeyInputDevice(KeyInputDevice dev) {
if(dev == NULL) return INPUT_DEV_INVALID_DEV;
libevdev_free(dev->evdev);
dev->evdev = NULL;
int rc = IO_CLOSE(dev->fd);
if (rc < 0) return INPUT_DEV_CLEANUP_ERROR;
return INPUT_DEV_SUCCESS;
}
void DestroyKeyInputDevice(KeyInputDevice dev) {
if(dev == NULL) return;
free(dev);
dev = NULL;
}
led_driver.c
typedef struct LedDriverStruct {
int fd;
LedStatus status;
} LedDriverStruct;
LedDriver CreateLedDriver() {
LedDriver led = calloc(1, sizeof(LedDriverStruct));
led->fd = -1;
led->status = LED_UNKNOWN;
return led;
}
int InitLedDriver(LedDriver self, const char* device_file) {
self->fd = IO_OPEN(device_file, O_WRONLY|O_NONBLOCK);
if (self->fd < 0) {
// TODO: Look into possible errors.
return LED_DRIVER_INIT_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void TurnOnLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_ON;
IO_WRITE(self->fd, "1\n", 2);
}
void TurnOffLed(LedDriver self) {
if (self == NULL) return;
self->status = LED_TURN_OFF;
IO_WRITE(self->fd, "0\n", 2);
}
void ToggleLed(LedDriver self) {
if (self == NULL || self->status == LED_UNKNOWN) return;
if (self->status == LED_TURN_OFF) {
TurnOnLed(self);
} else {
TurnOffLed(self);
}
}
int CleanupLedDriver(LedDriver self) {
if (self == NULL) return LED_DRIVER_CLEANUP_ERROR;
int rc = IO_CLOSE(self->fd);
if (rc < 0) {
return LED_DRIVER_CLEANUP_ERROR;
}
return LED_DRIVER_SUCCESS;
}
void DestroyLedDriver(LedDriver self) {
if (self == NULL) return;
free(self);
self = NULL;
}
main.c
#define KEYBOARD_DEVICE "/dev/input/event2"
#define LED_DEVICE "/sys/class/leds/input2::capslock/brightness"
int main(void) {
KeyInputDevice press_a = CreateKeyInputDevice();
InitKeyInputDevice(press_a, KEYBOARD_DEVICE);
struct timeval time = {};
const struct input_event kPressA = {time, EV_KEY, KEY_A, INPUT_KEY_PRESSED};
SetKeyInputDetectCondition(press_a, &kPressA);
LedDriver caps_led = CreateLedDriver();
InitLedDriver(caps_led, LED_DEVICE);
while(1) {
if(CheckKeyInput(press_a) == INPUT_DEV_EVENT_DETECTED)
ToggleLed(caps_led);
}
CleanupKeyInputDevice(press_a);
DestroyKeyInputDevice(press_a);
CleanupLedDriver(caps_led);
DestroyLedDriver(caps_led);
return 0;
}
Comme il inclut la gestion des erreurs, le support multi-instance, etc., les fonctions ne sont pas équivalentes, mais la quantité de code est plus que triplée. De plus, le code de test comporte plus de lignes que le code produit. C'est parce que nous travaillons sur le problème dans de petites unités, il est donc facile de voir "Que va-t-il se passer dans ce cas?", Et cette fois je pense que c'est le résultat de la mise en œuvre sérieuse d'une telle partie (dans une certaine mesure).
La quantité totale de code est importante, mais chaque fonction est courte et cohérente et ne fait qu'une seule chose (principe de responsabilité unique).
Comment garantir que le dégraissage ne se produit pas lorsque nous ajoutons de nouvelles fonctionnalités?
J'ai un test existant. Si le test existant réussit, nous avançons.
Lorsque vous souhaitez refactoriser, pouvez-vous franchir le pas?
Fiez-vous aux tests pour une refactorisation implacable (Je l'ai beaucoup refactorisé dans la minute suivante. C'est aussi grâce au test.)
Et si la bibliothèque utilisée est instable?
La logique d'entrée de clé est fermée dans * key_input_event.c *. De plus, main.c n'est ** pas conscient de la structure de données libevdev **. Même s'il y a un léger changement dans l'utilisation de la librairie dans key_input_event.c, le changement est fermé dans le module et ne se propage pas vers le module supérieur (fermé dans le principe d'ouverture / fermeture).
Que faire si vous souhaitez connecter un autre clavier et contrôler maintenant la LED NumLock avec la touche "B"?
L'initialisation doit être effectuée individuellement. Cependant, la partie de détection sera à peu près la suivante. (C'est un peu plus intelligent si vous apportez le tableau de longueur variable de la bibliothèque)
while(1) {
for (int i = 0; i < 2; i ++) {
if(CheckKeyInput(key_events[i]) == INPUT_DEV_EVENT_DETECTED)
ToggleLed(led_devices[i]);
}
}
En regardant l'API de key_input_event.c, si vous avez l'API suivante, il semble que vous puissiez vous unifier avec la même interface.
Je pense que si le principe de la responsabilité unique peut être réalisé au niveau de la fonction de cette manière, les parties communes peuvent être facilement trouvées et l'abstraction peut être promue. Surtout pour les choses immatures comme moi, pour la première fois après avoir fait une chose concrète, "Oh, je pensais que si je l'abstrais comme ça, ça serait bien organisé", ou inversement, "j'ai pensé que je pourrais l'abstraire comme ça, mais ce n'est pas bon. Ce n'était pas bon. " Dans Préparation, je voulais un mécanisme de rappel, mais je n'en avais pas besoin pour le moment (je crains de boucler à 100% du CPU). Si c'est le cas, il suffira de dormir pendant environ 10 ms).
Il est ouvert au public en tant que Release1.0.
Jusqu'à présent, nous avons atteint les spécifications suivantes.
Modifiez ceci pour les spécifications suivantes. Vous aurez besoin d'une minuterie comme élément.
Nous allons étendre et développer cela avec TDD.
Recommended Posts