saltar al contenido

Cómo probamos Raima Database Manager

Antes del lanzamiento de RDM, se desarrollan muchas pruebas y métodos para garantizar un producto robusto y utilizable. Esto se debe a que los requisitos de estabilidad y solidez de una base de datos son mucho más estrictos que muchos otros tipos de aplicaciones.

Por ejemplo, la durabilidad es de suma importancia. Un usuario nunca quiere perder sus datos solo porque su aplicación se bloqueó debido a un error de software o una falla de hardware. Raima hace todo lo que está a su alcance para proteger los datos.

Dentro de esta página habrá un vistazo de algunos de los procedimientos y métodos que sigue Raima para crear un producto robusto y bien probado.

Prueba de errores de Raima

Prueba invariante de base de datos

El marco de control de calidad C / C ++, desarrollado en Raima, tiene soporte integrado para pruebas invariantes. Una prueba invariante es un tipo especial de prueba en la que una condición decidida debe ser verdadera durante "algún tiempo". Esta condición se llama invariante. Lo que significa "algún tiempo" se explicará más adelante. Para una base de datos, el invariante es típicamente sobre los datos almacenados en la base de datos. El invariante podría ser bastante relajado o podría ser muy estricto. Un ejemplo de un invariante relajado es "la base de datos existe". Un ejemplo de un invariante más estricto es "calcular algún tipo de hash de todos los datos en la base de datos debería producir un valor dado".

Los marcos de control de calidad tienen un soporte especial para crear, ejecutar y destruir una base de datos invariante. Un caso de creación para una base de datos invariante creará la base de datos con un esquema dado y normalmente insertará algunos datos para establecer una base de datos invariante. Este caso especial siempre se ejecuta antes del caso o casos de ejecución normal. Los casos de ejecución normal se escriben para "mantener" el invariante. Lo que queremos decir con "mantener" se especifica más adelante. Una vez que se han ejecutado los casos normales, se ejecuta el caso de destrucción y el invariante ya no se mantiene. El tiempo desde que se devuelve el caso de creación y hasta que se llama al caso de destrucción es el "algún tiempo" que se mantiene el invariante.

Ejecución predeterminada

Una ejecución predeterminada de una prueba invariante de QA Framework hará el caso de destrucción, luego el caso de creación, seguido de los casos de ejecución normal y, por último, el caso de destrucción.

Múltiples hilos

Se puede indicar a QA Framework que ejecute los casos de ejecución normal desde varios subprocesos. QA Framework instanciará los subprocesos y ejecutará los casos normales en paralelo. Una prueba invariante escrita correctamente y un producto robusto deberían poder manejar esto.

Procesos

Otro escenario es ejecutar la prueba una vez, indicar al marco de control de calidad que no ejecute el caso de destrucción al final. Esto dejará intacta la invariante de la base de datos. Luego, podemos ejecutar la prueba en varios procesos en los que solo se ejecutan los casos de ejecución normal. Una prueba invariante escrita correctamente y un producto robusto también deberían poder manejar esto.

Independiente de la plataforma

Otro escenario es ejecutar la prueba una vez e indicarle al marco de control de calidad que omita el caso de destrucción al final, copie la imagen de la base de datos en otra arquitectura o sistema operativo y vuelva a ejecutar los casos normales. Una prueba invariante escrita correctamente, un producto robusto y un producto portátil también deberían poder manejar esta situación.

Compatibilidad de imágenes entre versiones de RDM

Otro escenario es ejecutar la prueba una vez, indicar al marco de control de calidad que omita el caso de destrucción al final y guardar la imagen de la base de datos para versiones posteriores. Antes de esas versiones posteriores, restauramos las imágenes de la base de datos y volvemos a ejecutar las pruebas solo con los casos normales. Una prueba invariante escrita correctamente, un producto robusto y un producto bien mantenido deberían poder manejar esta situación.

Prueba de choque

Se puede utilizar una prueba invariante de la base de datos para garantizar que la recuperación de la base de datos funcione correctamente.

No es práctico apagar y encender una máquina (aunque lo hemos hecho en el pasado). En su lugar, ahora simulamos un bloqueo del sistema conectando RDM y el sistema operativo. Esto se puede hacer precargándolo en Linux. Por tanto, podemos simular un fallo del sistema con escrituras perdidas. Volver a ejecutar una prueba invariante de base de datos con solo los casos de prueba normales debería tener éxito para una prueba escrita correctamente y un producto duradero.

Prueba de imágenes dañadas

También se puede utilizar una prueba invariante de la base de datos para verificar que la prueba y el RDM sean resistentes a la corrupción de la imagen de la base de datos.

Nuevamente, usar una prueba invariante de base de datos y realizar escrituras aleatorias en uno o más de los archivos de la base de datos al mismo tiempo puede resultar en una falla. Tal fracaso es aceptable. El requisito en este caso es que RDM o la prueba no deben fallar para una prueba escrita correctamente y un producto robusto.

TFS remoto vs TFS local

Una prueba que utiliza el marco de control de calidad C / C ++ puede conectarse al servidor de archivos transaccional (TFS) de muchas formas. Como ejemplo, el TFS puede estar integrado en la ejecución de una prueba invariante de base de datos y, al mismo tiempo, otra instancia de la misma prueba invariante de base de datos puede conectarse remotamente a la primera instancia. La segunda instancia puede ejecutarse localmente en la misma máquina que la primera instancia o de forma remota en una segunda máquina. La segunda máquina puede tener la misma arquitectura y / o sistema operativo que la primera, o puede ser diferente. Hay muchas combinaciones disponibles aquí.

Verificar pruebas

Tenemos algunas pruebas que no son estrictamente una prueba invariante de base de datos. Entre esos casos se encuentran las pruebas en las que el caso o los casos normales se pueden ejecutar solo una vez. Si volvemos a ejecutar los casos normales, se debe indicar al marco de control de calidad que solo haga una "verificación". Una "verificación" no cambiará la base de datos de ninguna manera.; simplemente verificará la invariante de la base de datos. Si se le indica a QA Framework que ejecute los casos normales en paralelo para una prueba de verificación de varios subprocesos, dicha instrucción se ignorará. Siempre lo ejecutará desde un solo hilo.

Pruebas de secuencia

El último tipo de pruebas invariantes de bases de datos son las pruebas de secuencia. Son como pruebas invariantes de bases de datos, excepto que los casos normales no se pueden ejecutar en paralelo. El invariante de la base de datos solo es válido después de una ejecución exitosa de todos los casos normales. No hay ninguna promesa de que el invariante de la base de datos sea válido mientras que los casos normales están siendo ejecutados.

Prueba difusa

Ejecutamos una serie de pruebas invariantes de base de datos como se describe anteriormente. Una de las pruebas invariantes de nuestra base de datos principal realiza muchas operaciones aleatorias. Cada vez que confirma la suma de algo, la suma se mantiene en 0. Esta prueba combina diferentes tipos de inserción, actualización y eliminación con partes de verificación. La prueba utiliza start-update y start-read anidados donde se revierte aleatoriamente a un estado anterior. Para la verificación, utiliza tanto start-read como star-snapshot. Esta prueba no está escrita para el rendimiento, sino para estresar nuestro motor de transacciones y asegurarnos de que estamos completamente ACID.

Prueba de datos de gran volumen

Se escribe otra prueba invariante de base de datos para un alto rendimiento. Su propósito principal es hacer algunas inserciones, verificar que los datos estén allí y luego eliminar los datos que insertó. Los datos se insertan basándose en números pseudoaleatorios. También se inserta la semilla utilizada para estos insertos. La invariante es que deberían existir los datos correspondientes a estas semillas. El objetivo principal de esta prueba es simplemente crear una gran cantidad de datos y estresar nuestro motor con la creación de nuevos archivos de base de datos. Ejecutar esta prueba de manera similar a nuestras otras pruebas garantiza que la transferencia a un nuevo archivo de base de datos funcione incluso en los casos que requieran recuperación.

Prueba de replicación

Las pruebas invariantes de la base de datos, incluidas las pruebas de verificación y las pruebas de secuencia, se pueden utilizar para probar la replicación unidireccional. Esto pronto se agregará a la matriz de pruebas que ejecutamos antes de un lanzamiento.

Conclusión

Las pruebas invariantes de la base de datos son fundamentales para las pruebas en Raima. Parte de la complejidad es manejada por el marco de control de calidad y una base de datos invariable.

Prueba de bloqueo

Este conjunto de pruebas se ha desarrollado específicamente para nuestro administrador de bloqueos. Algunas pruebas se han diseñado específicamente para cubrir casos generales y algunos especiales. Otros se han implementado como resultado de errores encontrados en nuestro motor SQL que pueden o no haber sido culpa del motor central.

Con nuestro modelo de bloqueo anidado, debemos asegurarnos de que las actualizaciones sean ACID. Con respecto al bloqueo, esto tiene ciertas implicaciones. Por ejemplo, con una actualización en combinación con una lectura, debemos asegurarnos de que el bloqueo de lectura se mantenga hasta que se confirme la actualización, aunque la aplicación haya liberado el bloqueo. Una prueba no puede observar esto a través de la API estándar sin usar una segunda conexión a la base de datos. Tenemos pruebas de un solo subproceso que verifican esto. Esto facilita la reproducción y la búsqueda de errores.

También realizamos pruebas de bloqueo difuso mediante llamadas aleatorias al administrador de bloqueo a través de la API pública. Esto genera una enorme cantidad de combinaciones que serían muy difíciles de cubrir de otra manera. El algoritmo utilizado en la prueba para verificar que el bloqueo se realiza correctamente se ha desarrollado independientemente del código del administrador de bloqueo en el producto. Esta prueba también tiene la capacidad de escribir código de prueba para la secuencia exacta de llamadas API utilizadas. Los errores encontrados con esta prueba se han agregado a nuestro conjunto de pruebas de regresión para facilitar la reproducibilidad y proteger el código de cambios futuros.

Prueba de Memcheck

Usamos Valgrind con la herramienta predeterminada memcheck en Linux, o Purify en Windows para encontrar problemas de memoria de software. Se ha demostrado que estas herramientas son valiosas para encontrar muchos tipos de errores.

Sin embargo, los asignadores estándar usados en RDM no usan los asignadores de la biblioteca C como malloc, calloc, realloc y free. En su lugar, usamos nuestros propios asignadores donde la memoria se recupera del sistema operativo en trozos más grandes o se proporciona a través de la API pública de RDM. Por lo tanto, estas herramientas no son muy útiles para encontrar problemas de memoria cuando se usan con nuestros asignadores de memoria estándar.

Para abordar esto, tenemos una implementación alternativa a nuestros asignadores de memoria estándar que utiliza las funciones de la biblioteca C mencionadas anteriormente. Esta implementación en combinación con Valgrind o Purify se utiliza para encontrar problemas de memoria de software. Una ejecución completa de nuestro conjunto predeterminado de pruebas para C y C ++ con este enfoque tarda varios días en completarse.

prueba de base de datos prueba raima software prueba todos los dispositivos encontrar errores y probador

Simulación de fallas de asignación de memoria

Como comentamos en la sección anterior, nuestros asignadores de memoria se pueden cambiar. Esto se puede hacer utilizando las definiciones del preprocesador en tiempo de compilación o mediante la precarga de la biblioteca en tiempo de ejecución en Linux. En el siguiente párrafo, describimos una tercera implementación de nuestros asignadores de memoria estándar.

Esta implementación también usa malloc y free, además de algunos datos de encabezado adicionales para cada asignación para ayudar a detectar algunos usos incorrectos de nuestra API de asignación de memoria estándar. Con alguna instrumentación del marco de control de calidad C / C ++, también puede simular fallas de asignación de memoria. QA Framework supervisa el número de asignaciones de memoria y puede ejecutarlo nuevamente cuando falle una asignación de memoria determinada. Se espera que estas ejecuciones fallen con eNOMEMORY. Si no falla como se esperaba, se informará la información necesaria para que el problema se pueda reproducir fácilmente.

Efence

Efence hace un pequeño subconjunto de lo que Memcheck puede hacer, pero es mucho más rápido. Ha sido algo útil para probar RDM para el desbordamiento del búfer en pruebas que duran más tiempo.

Pruebas de regresión

Cuando tenemos un error que se puede reproducir con una prueba, se agrega como prueba de regresión a una de nuestras suites de prueba. La gran mayoría de errores se pueden reproducir fácilmente. Una clase de errores que es difícil de reproducir son los que involucran múltiples subprocesos. Teniendo esto en cuenta, tenemos muchas pruebas que, por diseño, se pueden ejecutar en paralelo con varias instancias de sí mismas o en paralelo con otras pruebas dentro de una suite.

Cobertura de código

Compilamos nuestro código fuente usando gcc con opciones para producir cobertura de código. Luego usamos gcov, lcov y genhtml para producir algo que se pueda visualizar.

Prueba de Helgrind

Usamos Valgrind con la herramienta helgrind para probar RDM para la seguridad de los hilos. Las bibliotecas que por diseño no son reentrantes utilizan mutex para proteger las estructuras de datos compartidas. Helgrind puede encontrar lugares donde las estructuras de datos no están debidamente protegidas. Sin embargo, esto requiere pruebas que utilicen varios subprocesos contra algunas estructuras de datos compartidas.

Nuestras pruebas invariantes de base de datos discutidas anteriormente son buenas candidatas para este tipo de prueba, ya que QA Framework tiene la capacidad de ejecutar varias instancias de la prueba en paralelo. También se pueden ejecutar otros tipos de pruebas en paralelo; sin embargo, estas pruebas usarán bases de datos separadas, probando así las estructuras de datos compartidas entre bases de datos para asegurar la seguridad de los subprocesos.

Escribir programas para el paralelismo es difícil. No desea utilizar un mutex a menos que sea necesario para la corrección. El uso de mutex afecta el rendimiento. Para minimizar el impacto en el rendimiento de las exclusiones mutuas, hemos utilizado algoritmos que no requieren protección de semáforo para ciertas estructuras de datos compartidas en algunas áreas. Estos algoritmos se han diseñado cuidadosamente para garantizar la corrección y hemos utilizado una decoración especial en el código fuente de Helgrind para suprimir las advertencias. Esta decoración también es útil como documentación y cuando depuramos el código.

Prueba de reentrada

La mayoría de nuestras bibliotecas están diseñadas para ser reentradas. Aparte de usar la funcionalidad en otras bibliotecas, todo lo contenido dentro de la biblioteca es completamente reentrante. Significa que dos personas que llaman pueden usar la funcionalidad en la biblioteca sin ningún riesgo de condiciones de carrera y pérdida de información de una persona que llama a otra, siempre que el control del programa no vaya a otra biblioteca que no sea reentrante, ambos usen identificadores separados, y no hay desbordamientos de búfer. Este tipo de diseño hace que sea más fácil razonar sobre la corrección.

Con un diseño cuidadoso, esto se puede hacer cumplir fácilmente analizando las bibliotecas compiladas para verificar el cumplimiento. Para ello disponemos de pruebas automatizadas que analizan todas nuestras bibliotecas, incluso aquellas que no son reentrantes. La prueba se activará ante cualquier nueva incorporación de incumplimiento. Nuestra documentación incluye información sobre el cumplimiento de cada biblioteca.

Afirmar

Usamos afirma en todo nuestro código fuente. Una aserción es una declaración en un punto determinado del código que siempre debe ser verdadera. El uso de afirma correctamente requiere un diseño cuidadoso. Por ejemplo, las afirmaciones también deben mantenerse en el caso de una condición de error.

Las afirmaciones no están diseñadas para manejar errores de bits en la memoria caché de la CPU o la RAM. Sin embargo, para el acceso a archivos, se debe asumir que puede haber errores de bits. En realidad, cualquier tipo de daño en el disco puede ocurrir en el caso general y el motor debe ser lo suficientemente robusto para manejarlo. Cualquier decisión basada en el contenido leído debe validarse o el motor debe ser lo suficientemente robusto como para no bloquearse o ejecutarse en un bucle infinito. Si el error ocurrió en los datos del usuario, el motor de la base de datos puede devolver datos incompletos o incorrectos. Por otro lado, si ocurrió un error en los metadatos, el motor puede descubrirlo, en cuyo caso el error se informa al usuario.

Es conceptualmente útil suponer que no puede haber daños en el disco, pero cualquier afirmación que se base en tal suposición debe tratarse de manera especial. En un entorno de producción, se debe informar de un error al usuario. Durante las pruebas, es posible que queramos afirmar según el tipo de prueba que hagamos. Usamos un tipo separado de aserción para esto que se puede definir para que se comporte de una forma u otra. Esto nos permite ejecutar pruebas donde se simula la corrupción y pruebas donde se supone que la corrupción no ocurre.

Un caso ligeramente diferente es el manejo de una falla del motor de base de datos. Este es un escenario que es mucho más probable que suceda que una corrupción general del disco. Para ese caso se utiliza otro tipo de aserción.

Estas afirmaciones especializadas nos permiten ejecutar pruebas con diferentes supuestos. Esto, en combinación con la simulación de diferentes tipos de fallas, ha demostrado ser útil para encontrar y corregir errores.

Código portátil

Una parte importante del diseño de software es asegurarse de que el código sea portátil. Por lo tanto, evitamos ciertas funciones de C que no son portátiles. También nos aseguramos de que los formatos de archivo que utilizamos sean portátiles. Esto se garantiza ejecutando nuestras pruebas en una amplia variedad de plataformas, incluidas tanto plataformas de hardware reales como plataformas simuladas. Usamos plataformas con diferentes órdenes de bytes, diferentes alineaciones, donde char por defecto es sin firmar y firmado, por mencionar algunos.

Análisis estático

Usamos PC-Lint y FlexeLint para análisis estático. Estas herramientas han proporcionado información valiosa para encontrar algún tipo de error. Sin embargo, debemos tener cuidado ya que ciertos tipos de reescrituras del código fuente pueden ocultar o introducir errores fácilmente. Por lo tanto, utilizamos estas herramientas donde ciertas advertencias se ignoran globalmente y para otras advertencias decoramos el código fuente para suprimirlas en lugares específicos. Consulte la sección de pruebas de Memcheck.

Pruebas de usabilidad

Parte del proceso de control de calidad también es garantizar que nuestras interfaces estén cuerdas. Esto incluye asegurarse de que la interfaz y la implementación estén claramente separadas. También incluye convenciones de nomenclatura, orden de los argumentos, asegurarse de que esté completo, los casos especiales no son más difíciles de hacer que los casos simples y, en general, fáciles de usar.

Las API que hemos diseñado para RDM han pasado por un proceso riguroso para asegurarnos de que cumplimos con los estándares de la industria. Los archivos de encabezado públicos también incluyen documentación de DoxyGen. Descubrimos que es más fácil mantener la documentación actualizada si va junto con el código.

Proporcionamos un par de pruebas menores para compilar pruebas pequeñas que incluyen solo un archivo de encabezado público RDM. Esta compilación es necesaria para tener éxito sin errores ni advertencias. Esto se repite con la combinación de dos archivos de encabezado. Esto se hace tanto para nuestro C como para C ++ archivos de encabezado.

Prueba de interbloqueo

Por lo general, es un requisito que un sistema no se bloquee. Sin embargo, RDM ha sido diseñado con una API donde el usuario puede solicitar bloqueos explícitamente. Con una API de este tipo, es posible escribir una aplicación con garantía de bloqueo. Por lo tanto, tenemos pruebas que se bloquearán intencionalmente cuando se ejecuten. Estas pruebas no se ejecutan de forma predeterminada. Deben ejecutarse en un entorno especial donde podamos observar que, de hecho, se bloquean y fallarán si ese no es el caso.

Pruebas de rendimiento

El rendimiento es un aspecto importante de cualquier software de computadora, pero especialmente para una base de datos. Hay tres cifras principales que medimos. Carga de CPU, E / S de disco y uso de memoria.

Pruebas de alto rendimiento de Raima Database Manager

Prueba de carga de CPU

Para la carga de la CPU, tenemos una serie de pruebas de rendimiento. El marco de control de calidad de C / C ++ admite la creación de instancias de cronómetros que se pueden usar convenientemente para medir el rendimiento de llamadas API específicas. Utilizándolos en pruebas existentes, podemos obtener números para medir cómo lo estamos haciendo en comparación con versiones anteriores de nuestro producto.

También tenemos pruebas de rendimiento que no utilizan nuestro marco de control de calidad, desarrollado más específicamente para casos de clientes que pueden comparar RDM con otras bases de datos.

Estamos trabajando en mejoras en esta área.

Prueba de E / S de disco

Con RDM 14.1, hemos cambiado drásticamente la forma en que se escriben los datos en el disco. Al ejecutarse en un modo en el que se minimizan las escrituras en disco, RDM puede insertar, actualizar y eliminar con una E / S de disco mínima. Las compensaciones aquí son un mayor tiempo para abrir la base de datos y un mayor tiempo para la recuperación de fallos.

La prueba de la E / S del disco se realiza utilizando algunas de nuestras pruebas generales al precargar un código que interceptará ciertas llamadas al sistema operativo y recopilará algunas estadísticas. Esto es similar a las pruebas de choque descritas anteriormente.

Una ventaja de este enfoque es que la biblioteca que usamos se puede utilizar para cualquier aplicación sin tener que volver a compilar el código. Hay herramientas en Linux que tienen enfoques similares, pero hemos descubierto que este enfoque suele ser mejor. Nos permite ver ciertos patrones de escritura que estamos buscando y obtenemos una mejor comprensión de cómo los diferentes casos de uso afectan la E / S del disco. Por ejemplo, es importante que los archivos se sincronicen solo cuando se supone que deben sincronizarse y que no tengamos sincronizaciones o escrituras innecesarias.

Otro aspecto de la E / S de disco es el rendimiento de la caché del sistema de archivos. Si es posible, los datos a los que es probable que se pueda acceder nuevamente deben agruparse, y los datos a los que no se puede acceder también deben agruparse. De esta manera, es menos probable que la caché del sistema de archivos almacene en caché el contenido al que es poco probable que se acceda nuevamente. Para los archivos que sabemos que el motor no necesitará, excepto en el caso de ciertas fallas catastróficas, usamos posix_fadvise para aconsejar al kernel que suelte la página o la elimine del caché del sistema de archivos. Que esto se haga correctamente también se puede verificar interceptando esta llamada al sistema operativo.

Prueba de uso de memoria

Tenemos pruebas que llaman a la API pública de ciertas formas que no deberían afectar el uso de la memoria. Por ejemplo, llamar a una determinada función con los mismos argumentos repetidamente no debería aumentar el uso de memoria. Estas pruebas fallarán si aumenta el uso de la memoria. Cualquier prueba que utilice C / C ++ QA Framework informa el uso de memoria en el producto justo antes de la terminación.

También hay herramientas y otros mecanismos integrados en RDM que monitorean el uso de la memoria. Esos mecanismos pueden proporcionar una mejor comprensión de cómo se comporta cada subsistema. Valgrind tiene una herramienta llamada Cachegrind que puede analizar el rendimiento de la caché y la predicción de ramas de su código.

Marcos de control de calidad

Tenemos cuatro marcos de control de calidad diferentes desarrollados internamente. El escrito para C / C ++ es el más completo. Además, hay algunos escritos en Perl, Bash y Java. Todos comparten algunas similitudes, lo que facilita que nuestro equipo cambie entre ellos.

Marco de control de calidad de Perl

Esta es una adición posterior a nuestro conjunto de marcos de control de calidad. Su objetivo principal es probar las herramientas de línea de comandos RDM. Tiene soporte para alimentar entradas a las herramientas y puede comparar la salida con los resultados esperados o grep de la salida para ciertos patrones. Funciona tanto en Unix como en Windows. Muchas pruebas que anteriormente formaban parte de Bash QA Framework se han reescrito para utilizar Perl QA Framework. De esa forma podemos ejecutar esas pruebas tanto en Unix como en Windows. Se prueba una gran cantidad de SQL-PL utilizando este marco.

Marco de control de calidad de C / C ++

La mayoría de nuestras pruebas de C / C ++ están escritas utilizando este marco. Tiene la mayoria exhaustivo fisonomía. Para obtener más información, consulte la sección anterior sobre pruebas invariantes.

Integración continua

Nuestro sistema de compilación es capaz de generar archivos de creación y archivos de proyecto para varias plataformas de destino. También genera scripts de shell y archivos por lotes que se pueden usar para ejecutar todas o un subconjunto de nuestras pruebas en diferentes configuraciones. Esto se usa mucho en nuestra integración continua. También facilita a los desarrolladores ejecutar las mismas pruebas manualmente.