Mostrando entradas con la etiqueta testing. Mostrar todas las entradas
Mostrando entradas con la etiqueta testing. Mostrar todas las entradas

2015/07/11

RTFN

Para mi hay cuatro maneras principales, útiles y necesarias de documentar. Ninguna es descubrimiento mío ni tampoco es novedosa esta agrupación.


Nombres significativos en el código

Esto viene para contrarrestar la práctica iniciada con la falta de memoria, las tarjetas perforadas y la programación matemática y científica de usar letras en lugar de nombres. Esta ok usar x,y y z cuando estás hablando de coordenadas, pero no cuando deberían ser yaOrdenado, cantidadPajaros o nombre.

Esto lo he sacado del dolor de ver código ajeno o propio un tiempo despues.

xDoc (javaDoc, phpDoc, etc [1])

La vida es corta, por favor leé [2], no confundir con xDoc[3]


Intenciones y motivos

¿Por qué elegí sequential search en lugar de binary search? Pues por que se invoca dos veces en toda la ejecución y son cien elementos, no vale la pena ordenar.
El código puede ser muy claro, los nombres muy significativos, pero el propósito y los motivos es algo externo al programa o a su código fuente.

Esto lo he aprendido en un excelente curso de arquitectura de sofware en la ECI y el libro del docente que lo dictó [4]

Tests


Los test son de las mejores documentaciones, pues a diferencia de las anteriores, no se desincroniza con el código. Es común encontrar ejemplos que no se pueden ejecutar por algún motivo, pero los tests, si están funcionando, sí se pueden ejecutar, valga la redundancia.
Los tests son autodocumentación, aunque no tengan ningún comentario. Muestran exactamente como se utilizan los artefactos, que reciben y que deben devolver.


Quizás esto no cumpla con las normativas, pero si con el pragmatismo de "código funcionando antes que documentación exhaustiva" [5]


Voy a suponer que aunque no del todo de acuerdo, al menos me has comprendido.


¿Quá hay de malo en:

/**
 * Load configuration
 */
void loadConfig() {....}
?

¿Ya viste? ¿No? ¿Y ahora?

/**
 * Set date
 */
void setDate() {....}

/**
 * Get date
 */
Date getDate() {....}

/**
 * XXXX YYYY
 */
void xxxxYyyyyyy() {....}


Es esto casos es difícil defender xDoc y me frustra el haber usado "nombres significativos". ¿Para qué voy a ponerle nombre significativo y despues ponerle lo mismo en la documentación? Me recuerda esta atrocidad:

//increment cycle counter
countC++;



Propongo entonces la incorporación de un nuevo tag xDoc que sea RTFN (Read The Fucking Name)

/**
 * @RTFN
 */
void setDate() {....}

Como humanos no tenemos ninguna dificultad en entenderlo y si hiciera falta, se podría enseñar a xDoc o Doxygen a interpretarlo, cuando lo hallen que pongan en la descripción o el mismo nombre de la función o método o reemplacen según un mapa o una heurística.

El mapa puede tener nombres comunes como
[
   "setDate":"Set the date",
   "drawButton":"Draw the button"
]

Y la heurística se aprovecharía de camelCase:

setDate -> Set Date

Parece poco, pero la verdad es que poner uno esa documentación es kafkiano.

No tengo el ímpetu ni el tiempo para hacer los patches necesarios a las herramientas para poder ofrecerlas a sus desarrolladores (no es lo mismo "te propongo una idea" que "te propongo una idea y acá tenés la implementación"), pero estoy dispuesto a secundar a quien lo haga.

Gracias por las ilustraciones a Greta[6]



[4] Software Architecture: Foundations, Theory and Practice - Nenad Medvidović y otros http://www.wiley.com/WileyCDA/WileyTitle/productCd-EHEP000180.html

2014/07/06

Ejemplo de TDD, uso de tags para reportes de ocurrencias de eventos




En esta ocasión relataré un ejemplo de refactorización de clases, acompañado por la práctica de TDD en una aplicación de seguridad informática. No proveeré código y las entidades y atributos estarán parcialmente ofuscados, en parte por discreción y en parte como recurso didáctico de simplificar el modelo presentado.

El objetivo de la transformación tiene que ver con la generación de reportes estadísticos sobre la Ocurrencia de Eventos.

El modelo desarrollado y usado durante varios meses es bastante simple y razonable:

              Área
                |
Ocurrencia - Evento - Tag - Grupo de Tags 
por ejemplo Plataformas(windows,linux,mac
                |
           Provincia


El tag es... eso, un tag. Lo que ocurre es que sabemos que Áreas van a haber y que Provincias hay, pero más importante es que sabemos que nos interesan las Áreas y las Provincias. Otras categorías como Plataforma (windows, linux, mac) van apareciendo, pero no son tan interesantes como para convertirse en entidades. Para no estar cambiando el modelo correteando tras los caprichitos de las necesidades del negocio, existen los Grupos de Tags (Plataforma) con sus tags (windows, linux, mac). Tambien podría ser grupo BBDD con mysql, postqresql, mssql, oracle.

Hasta ahi vamos bárbaro, la operatoria como seda, hasta que vamos a generar reportes.

Quiero estadísticas de Ocurrencias de Eventos por Área, por Provincia, por Área y Provincia y para el otro lado. Ah, y las quiero por Plataformas, Color y Sabor.

Lo primero que uno hace es entonces cada combinación y el sistema nunca está terminado.

No me da la cabeza ni conozco tanto de reporting y encima no puedo estar incorporando módulos extra.

Tuve entonces una serie de ideas reveladoras:

Aunque es más sencillo hacer el reporte según una dimensión tipo entidad como Área o  Empresa que la de Grupo de Tags, está última luego es más reutilizable

El anidamiento es más fácil con los Grupos que con las entidades.

Si a esto le sumamos que la interfaz de selección de dimensiones es generable automáticamente a partir de los Grupos, todo indicaría que en términos de reporte estadístico fue un error no poner toda la información en Tags y Grupos. De todos modos, las entidades son más fáciles de manejar en términos de nulidad y cardinalidad. Si no me entendiste, pensá en como implementar un ABM de Provincias donde estas estan representadas mediante Tags y Grupos de Tags en comparación con app/console.php doctrine:generate:crud (en symfony o equivalente en tu framework habitual).

Habiendo decidido mantener esta suerte de modelo dual, procedí a implementar el reporte, primero con una dimensión, luego dos y finalmente tres.

El algoritmo consiste en iterar sobre todas las Ocurrencias de una fecha e ir acumulando en una estructura tipo árbol, que expresa a recursividad del problema.

Aquí se puede apreciar parte del código de testeo en php:

private function buildExpectedAreas($t1,$t2,$t3,$t4) {
    return array(
        'total'=>$t1,
        'agrupacion'=>array(
            'areas' => array(
                'entidades'=>array(
                    'contable'=> array(
                        'total'=>$t2,
                        'agrupacion'=>array()
                    ),
                    'rrhh'=> array(
                        'total'=>$t3,
                        'agrupacion'=>array()
                    ),
                    'logistica'=>array(
                        'total'=>$t4,
                        'agrupacion'=>array()
                    ),
                )
            )
        )
    );
}


Para una consulta de dos dimensiones, por ejemplo Areas y Plataformas, reemplazá cada una de las tres lineas que dice 'agrupacion'=>array() por

'agrupacion'=>array(
    'plataformas' => array(
        'entidades'=>array(
            'windows'=> array(
                'total'=>$$$,
                'agrupacion'=>array()
            ),
            'unix'=> array(
                'total'=>$$$,
                'agrupacion'=>array()
            ),
            'mainframe'=>array(
                'total'=>$$$,
                'agrupacion'=>array()
            ),
        )
    )
);


Si quisieras una tercera dimensión hay que repetir con la nueva agrupación nueve veces.

Primero implementé el reporte para una y dos dimensiones sobre Grupos.

Ahora bien, ¿qué hacer con Área y Provincia, que ya existen como entidades?

Mi primera idea fue transformarlas en Grupos con sus Tags en el modelo, pero por algo las queríamos como entidades, va a pegar fuerte en la interfaz y en las validaciones, como por ejemplo que Provincia sólo puede haber una y Áreas varias asociadas a un mismo Evento.

La segunda fue, en el momento de generar el reporte, inicio una transacción y genero los Grupos y Tags a partir de Área y Provincias. Tras el reporte va un rollback y no pasó nada.

Al final opté por una variación de la segunda: a cada Evento que el ORM me ha traido, le agrego los Tags necesarios tras haber creado los Grupos necesarios, sin persistir luego.

Problemas de performance no hay, ya que la generación de reportes es de baja frecuencia y concurrencia.

TDD acompañó todo el proceso, aunque no soy muy ortodoxo, sobre todo con el Timely de FIRST[1], Por lo general codeo primero y cuando alcanza una masa crítica hago los test y recién ahí entro en el ciclo test->fail->code->pass->refactor.

Asi que escribí unas pocas lineas y enganché con TDD. Con el siguiente awk se puede obtener las lineas que tenían en cada commit los archivos de código y test. Los AJUSTES se deben a que estas clases ya existían. CORTE es el primer commit. git lola es git log --graph --decorate --pretty=oneline --abbrev-commit --all

#!/bin/bash
CORTE=42ed122
ARCHIVO1=Lib/Codigo.php
ARCHIVO2=Tests/Lib/CodigoTest.php

git lola | cut -b 3-9 | awk -e '
    BEGIN {
       ARCHIVO1="'$ARCHIVO1'"
       ARCHIVO2="'$ARCHIVO2'"

       AJUSTE_CODIGO=346
       AJUSTE_TEST=368
       set +o posix
     }
    /.*/ {
            cmd1 ="git show "$0":"ARCHIVO1" | wc -l "
            cmd2 ="git show "$0":"ARCHIVO2" | wc -l "

            command cmd1  | getline LINEAS_CODIGO
            command cmd2  | getline LINEAS_TEST
         
            LINEAS_CODIGO -= AJUSTE_CODIGO
            LINEAS_TEST -= AJUSTE_TEST
            print LINEAS_CODIGO "\t" LINEAS_TEST          

    }
    /.*'$CORTE'.*/ {
            exit 0;
    }
'


La salida de esto va a tu hoja de cálculo favorita y se vé así:



Finalmente, apareció un requisito nuevo que me produjo un momento de iluminación y me llevó a la solución definitiva.

El requisito tiene que ver con algo que no había mencionado antes, que es el factor multiplicativo. Si un Evento tiene múltiples Empresas, Provincias o Plataformas, las Ocurrencias deben multiplicarse, por ejemplo:

Evento: detección de ataque xss
Áreas: contabilidad
Provicia: Tucumán
Plataformas: mac, windows, linux


En este caso cada Ocurrencia del Evento vale por tres. Si no te cierra bien por qué contabilidad tiene xss en Tucumán no te preocupes, tiene que ver con el enmascaramiento.

El requisito consiste en que si AHORA hago la estadística de Enero, me tiene que dar lo mismo que me dió en ENERO. Si mientras han cambiado las relaciones de "detección de ataque xss", como que en Área se agregue RRHH, la cuenta me daría 6 en Enero. Hay entonces que desnormalizar y en el momento en que se crea la Ocurrencia calcular los factores y guardarlos dentro de la Ocurrencia.

Cuando lo implemente, probablemente tire buena parte del código, pero los test me quedan aprovechables al cien por ciento. Sólo hay que agregar unos pocos que creen unos datos y calculen la estadística para ese momento. Luego, avacen en el tiempo, modifiquen algunas relaciones y vuelvan a calcular para ese momento y los resultados deberán ser los mismos.


Mi conclusión de esta experiencia es que de no haber contado con los tests no habría podido implementar nada, pues la verdad es que la complejidad del código resultante es un poco más grande que lo que mi mente puede abarcar a la vez. Además, ante ese nuevo requisito, no pierdo tanto trabajo pues la inversión está tanto en el código que pueda perder como en los tests que conservaré.



[1] FIRST:
F fast
I independient
R repeatable
S self
T timely

2013/07/27

Mutation Testing Framework


Presento aquí el resultado de mi trabajo colectivo de mutación de código para testing. Digo "colectivo" por las horas que invertí en análisis y diseño viajando en colectivo[1]. Seguramente estoy reinventando la rueda[2], pero no me importa, el camino avasalla en importancia al destino.

If you need an english version, ask me and I will add it to the top of my backlog

He utilizado bastante TDD pues de no ser así hubiese incurrido en pecado mortal. Hay que predicar con el ejemplo. Es más, el proceso fue reflexivo, se aplicó contra su mismo codigo fuente. Con respecto a este tema, me interesa aclarar que para mi TDD es tan sólo una técnica más, un recurso en nuestra valijita de herramientas. No significa que uno se sienta con la mente en blanco y tira un test y el código y así avanza. Me parece correcto hacerlo así en un taller de TDD, pero si nos detenemos ahí, es como quedarse con la idea de que la danza clásica son las posiciones de los pies y agarrarse de una barra frente a un espejo, que tocar el piano son unas escalas, boxear es saltar la soga y perseguir gallinas o programar es implementar sort() y search().

Mi idea original fue utilizar php que tiene un tokenizador[3] y es lo que más conozco, para mi desgracia. Cuando en los primeros viajes comencé a percibir que podía haber mucha recursividad pensé en erlang, el cual necesito practicar para recupera mi autoestima. Inmediatamente rechacé la idea, KISS[4]. Dos viajes después, tras haber visto que python tambien tiene un sencillo tokenizador[5] (y quizas ruby[6] y cobol[7]) y percibiendo el potencial de utilizar multicores en múltiples máquinas, había reincorporado erlang y en lugar de un mutador de código para php tenía en mente un framework para usar con cualquier lenguaje que alguien se tomara la molestia en preparar. Bueno, ese era el plan.

Testing por mutación


El testing por mutación de código se basa en la idea de que si los test están bien hechos, deberían detectar cualquier cambio en el código fuente. Entonces, hay que modificar arbitrariamente el código respetando las reglas del lenguaje y ejecutar los tests, alguno tiene que fallar. Si no es así, falta cobertura o faltan casos.

Las mutaciones sobre las que decidí trabajar son las más sencillas: cambiar operadores aritméticos y de comparación, no mucho más que eso. Para hacer cambios más complejos hay dos opciones: usar un AST[8] y ver como manipularlo o generar más mutaciones que serán eliminadas por el compilador (o interprete en la primera etapa).

El proceso consiste en tomar un archivo fuente, obtener los tokens, generar un archivo fuente variando un token, luego otro y así, siempre ejecutando los tests y esperando que no falle, en cuyo caso, se corta y avisa al humano para que mejore los tests.

En [9] cuento superficialmente mi experiencia académica de uso con Jumble, un mutador de código para java y otras técnicas de automatización de test. Este proyecto es resultado de haber tomado el curso "Generación Automática de Tests Unitarios" (ECI 2012 N2) [10] a cargo de Nazareno Aguirre [11], Universidad Nacional de Río Cuarto, Argentina

Alcance

Mis metas fueron haciéndose cada vez más ambiciosas:
  1. mutador de codigo para php
  2. framework para varios lenguajes
  3. usando erlang
  4. con procesamiento paralelo y distribuido
  5. comprender el trasfondo teórico
  6. para poder usar otros lenguajes que no tienen el tokenizador tan accesible y hacer mutaciones más complejas
He decido que hay tres etapas y sólo transitar la primera, quizás la segunda. Eventualmente, si surge la necesidad, la tercera.

Loops


Un problemas interesante que intuí es que si tocás la condición de un loop, podés entrar en infinito. Esto produce que haya que darle un tiempo máximo de ejecución a cada test. Lo que debe hacer al comienzo el framework es una ejecución normal de referencia en cada nodo para tomar el tiempo y usarlo como base para el timeout de las ejecuciones sobre el código mutado. Si hay timeout, considero que el test falló, o sea que esta ok.

Código original:

for ($i = 0; $i < 5; $i++)

Mutaciones:

for ($i = 0; $i <= 5; $i++) -> debería fallar el test

for ($i = 0; $i <= 5; $i--) -> loop infinito

Aunque luego exploro la posibilidad de controlar de modo selectivo algunas mutaciones, lo que implementé fue ejecutar un test de referencia para tomar el tiempo y luego usar timeout para cortar en caso de anomalía.

 

Combinatoria


Otro problema es la combinatoria. Si para cada token que es mutable se hace toda la combinatoria con los otros, el número puede llegar a ser muy, muy alto. No sé por que esa fué mi primera idea, pero pronto caí en cuenta de mi error y sólo se hacen las combinatorias para cada token independiente de los otros.

Dado

if a + b == c

tenemos

if a (+,-,*,/,%,^) b (<,>,<=,>=,!=,==) c

Si combinara todo tendría  6 x 6 = 36 ejecuciones, pero sólo hago 6 + 6 = 12. Con dos tokens mutables parece poca diferencia, pero si fueran cien tokens tendriamos 6100 = 6100 en lugar de 6 x 100 = 600. Unos pocos segundos contra algunos millones de años.

Falsos positivos


El siguiente código tiene una mutación que es inocua ya que produce el mismo comportamiento y sin embargo parece un error:

<?php 
$i = 0;
while ($i != 5) {
   print "$i\n";
   $i++;
}
<?php 
$i = 0;
while ($i < 5) {
   print "$i\n";
   $i++;
}

Queda un criterio humano final para decidir.

Concurrencia


Otro problema más, relacionado a la ejecución simultánea de los tests es la concurrencia sobre las distintas versiones del código. Una manera es replicar el ambiente para cada instancia. Dije "instancia", no "nodo", o sea que en una misma máquina con N cores hay N instancias. La otra es... tuve un esbozo pero lo olvidé, ya volverá cuando haga falta.


Arquitectura



La comunicación entre el componente del lenguaje y el core es mediante un mensaje json[12]. El componente del lenguaje debe identificar cada token con su valor y su clase. Había pensado que los tokens no mutables se simplifiquen en uno solo, de modo tal que:

if a + b == c {
  printf("hola");
}

se tokeniza:

if
a
+
b
==
c
{
printf
(
"hola"
)
;
}

y se simplica:


"if a" 
+ operadorAritmético
b
+ operadorComparación
c { printf("hola"); }

pero esto es optimización prematura, y le da a cada componente de lenguaje la responsabilidad duplicada de simplificar, así que decidí que el core se encargue de simplificar, aun así es responsabilidad del componente del lenguaje identificar la clase:

if       -> inmutable
a        -> inmutable
+        -> operadorAritmético
b        -> inmutable
==       -> operadorComparación
c        -> inmutable
{        -> inmutable
printf   -> inmutable
(        -> inmutable
"hola"   -> inmutable
)        -> inmutable
;        -> inmutable
}        -> inmutable

y definirla:

operadorAritmético extiende simétrica
   mutaciones: +,-,/,%,*

habiendo dos tipos de clases, las simétricas y las asimétricas, esto es porque hay elementos completamente intercambiables y otros que no:

operadorControl extiende asimétrica
   elemento: break
     mutaciones: continue, exit, blanco
   elemento: exit
     mutaciones: blanco
   elemento: continue
     mutaciones: break, exit, blanco

Por ejemplo, clone se puede reemplazar por =, pero no al revés.

El mensaje en json se parece a esto:

{
"header": [
     {"version":"1.0"},
     {"language":"php"}
   ],
"classes": [
     { "name":"assignment",
       "type":"symmetric",
       "pool":[ "+=","-=","*=","/=",".=","="]
     },
     { "name":"clone",
       "type":"asymmetric",
       "genes": [ {"gene":"clone",
                  {"pool":["="]
                ]
     },
     ...
  ],
"tokens: [ {
     "class":"inmutable",
     "value": "<?php"
   },
   {

     "class":"inmutable",
     "value": "$a"
   },
   {

     "class":"assignment",
     "value": "="
   },
   .... 
 ]
}

Aunque firmemente empeñado en no hacer el componente de python y mantener el alcance del proyecto reducido, recordé que hay un framework de unit testing de python llamado doctests [13] que tiene el código embebido en el mismo archivo fuente, como comentarios. Era lo que se usaba en w3af[14], pero me ha dicho Andrés, el papaito de w3af, que no escala.

Si el mutador mutara el código de testeo estariamos en problemas, ¿no? Hay que inventar algún mecanismo para lidiar con esto. Me imaginé, no probé, que el tokenizador lo consideraría comentario y no habría inconvenientes. Pero un segundo antes, ya había hallado la solución, que por casualidad sirve para lidiar con los falsos positivos y los loops.

La idea es usar una nueva clase "signal", que indique al core si debe mutar o no. Por ejemplo

<?php

se convertiría en:

{
 "class":"signal",
 "value":"<?php",
 "action":"start"
}


o en dos tokens, el normal de "<?php" y el signal, al que le podríamos quitar "value". Me gusta más la segunda opción, así tenemos dos canales, el de datos y el de control.

El componente python debería insertar como primer elemento:

{
 "class":"signal",
 "action":"start"
}


Luego, en el código, habría que insertar comentarios con una etiqueta arbitraria, a gusto de quién implemente. En php será #code_mutator_start|stop.

while... /*#code_mutator_stop*/ ) {
  ...
#code_mutator_start
  ...
}


Esto es feo, pero simple.

Uno puede sentir la tentación de usar tambien "action":"pause", como para que el core tenga un comportamiento distinto según sea "pause" (sigue aplicando la lógica) o "stop" (concatena todo el resto), pero sería optimización prematura.

La precaución que hay que tener es como procesar en el componente el manejo de estas etiquetas, ya que el código del componente debe ser mutable.

Por ejemplo,

if ($value == "#code_mutator_stop" )

provocaría que el core deje de mutar no siendo esa nuestra intención.

Se soluciona asi:

if ($value == "#code_mutator"."_stop" )

Esta técnica para falsos positivos abre la puerta a solucionar los loops infinitos, usando una nueva action:
{
 "class":"signal",
 "action":"restrict",
 "genes":["-=","/="]
}


Esto le avisa al core que no puede usar esos genes en los próximos pools, hasta que venga una nueva restricción, que puede ser vacía. Por ahora, dejemos esta puerta cerrada.

Otra puerta que tampoco quiero abrir es una clase para poder mutar valores enteros.

Por fortuna, justo me puse a leer Domain Specific Languajes[15] de Fowler, lo cual me ayudó a reflexionar mucho acerca de este mensaje que circula desde los componentes hasta el core. No califica de DSL, pero sí aplican muchos conceptos.

En algún momento, había resuelto simplificar la estructura de los token:

"tokens: [ {
     "signal": "start"
   },
   {
     "inmutable": "<?php"
   },
   {
     "inmutable": "$a"
   },
   ....   ]


pero me encontré con un inconveniente. ¿Qué pasa si quiero pasar metadata, como por ejemplo la linea y columna del token en el archivo fuente? Esto es algo indispensable para python:


"tokens: [ {
     "signal": "start"
   },
   {     "inmutable": "<?php"
   },
   {
     "metadata": [{ "row":"1"},{"col":"0"}]
   },
   {
     "inmutable": "$a"
   },
   {
     "metadata": [{ "row":"1"},{"col":"6"}]
   },
   ....   ]
No me gusta, me complica el procesamiento, asi que mejor conservo la estructura original que permite procesar de modo mucho más sencillo elementos adicionales y le agrego un campo "info".

"tokens: [ {
      "class":"signal",
      "action":"start"
   }

   {
     "class":"inmutable",
     "value": "<?php",
     "info": [{ "row":"1"},{"col":"6"}]
   },
   {
     "class":"inmutable",
     "value": "$a",
     "info": [{ "row":"1"},{"col":"6"}] 
   },
   {
     "class":"assignment",
     "value": "=",
     "info": [{ "row":"1"},{"col":"6"}] 
   },
   .... 
 ]

Este ejemplo es una ilusión en php, que sólo provee información de lineas y lo hace mal. Python da más información.


Seguridad

Más por disciplina que por una necesidad real:

Los puntos de ataque son mensajes json mal formados y el resultado de la ejecución.

Resultado de la ejecución: el proceso se agota cuando se hacen todas las mutaciones o no falla alguna, asi que no hay denegación de servicio por ahí.

json: no hay loops, no hay denegación de servicio por ahi. Quizás si hay muchos tokens o tienen valores muy largos haya que tomar alguna precaución.

Lo que si podría ocurrir es que el componente inyecte código malicioso para que sea ejecutado luego por xUnit. Pero esto no afecta al core, no me importa.
Como Tom Lehrer le hizo decir a von Braun: "'Once the rockets are up, who cares where they come down? That's not my department', says Wernher von Braun."[16]

Ahora en serio, el componente podría por si mismo ejecutar el código malicioso. Sólo tendría valor en caso de que este sistema progresara a la arquitectura multinodo, para propagarse a otros nodos.



Alcance de la primera etapa

Me contento con que funcione una sola instancia que entienda php y python. Esto es para respetar el espíritu agile: sólo implemento lo que produce valor tangible. Yo no necesito ahora los componentes para otros lenguajes ni necesito analizar tanto código que justifique múltiples instancias. Si bien me vendría bien ver como funcionan otros tokenizadores, estaría invirtiendo trabajo en algo que quizás nunca desarrolle.


Esta visión aparentemente de corto alcance no se contradice con mirar al horizonte y hacer una apuesta, es por eso que el framework de ejecución tiene su core en erlang:

  • Es más sencillo pasar luego a una versión concurrente y distribuida
  • Me sirve para practicar. Aunque no al proyecto, me da valor a mi.
No es que me haya lanzado a codear con la mente en blanco, tuve casi dos semanas de análisis "colectivo" hasta que pude ponerme frente a una máquina

Como mencioné antes, los componentes han sido mutados, no así el core en erlang y bash, al que sólo apliqué eunit[17] y sshunit2[18].



El código completo está disponible en https://github.com/cpantel/codeMutator, hay un Makefile que corre diversos tests, pero antes hay que instalar unas dependencias, lee REAME.md

Me faltan varias refactorizaciones como poner la definición de las clases (las de los tokens) en modo composición para poder modificarlas sin tener que rehacer los tests. Tambien que los .sh sean más generales.


[1] http://es.wikipedia.org/wiki/Colectivos_de_Buenos_Aires
[2] http://stackoverflow.com/questions/246495/what-mutation-testing-frameworks-exist
[3] http://php.net/manual/en/book.tokenizer.php
[4] http://en.wikipedia.org/wiki/KISS_principle 
[5] http://docs.python.org/2/library/tokenize.html
[6] http://pic.dhe.ibm.com/infocenter/pdthelp/v1r1/index.jsp?topic=%2Fcom.ibm.debugtool.doc_11.1%2Fvrmu1mst109.htm
[7] http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ripper/rdoc/Ripper.html
[8] http://en.wikipedia.org/wiki/Abstract_syntax_tree
[9] http://seguridad-agile.blogspot.com.ar/2012/09/automatizacion-de-test-eci-2012.html
[10] http://www.dc.uba.ar/events/eci/2012/cursos/aguirre
[11]  http://dc.exa.unrc.edu.ar/staff/naguirre/Pagina_personal_de_Nazareno_Aguirre/Principal.html

[12] http://www.json.org/
[13] http://docs.python.org/library/doctest.html
[14] http://w3af.org
[15] http://martinfowler.com/books/dsl.html
[16] http://en.wikipedia.org/wiki/Tom_Lehrer 
[17] http://www.erlang.org/doc/apps/eunit/chapter.html
[18] http://code.google.com/p/shunit2/

2013/03/01

Experiencia de refactorización aplicación de red c/c++ con shunit

El otro día me vi en la necesidad de modificar una aplicación de stress test de un protocolo de red y tuve una excelente experiencia de refactorización que paso a relatar.

Ten en cuenta que el objetivo de este relato es mostrar el espíritu del proceso de refactorización, no el programa en cuestión, que tratándose de una herramienta de stress test bien prodría utilizarse para denegación de servicio. Aunque el protocolo es marginal y carece de importancia, ¿quién sabe que puede ocurrir mañana?

Aplicación original


El código original consiste en un archivo con cinco funciones minúsculas y un main gigante.


Tiene una sección para procesar las opciones, una para crear el payload, una para armar el paquete ip y un loop donde modifica el paquete sin necesidad de rehacerlo y lo envía, tantas veces como se le pida.

La elección de que poner en funciones y que no, parece ser muy laxa. No es conceptual, ya que un par de operaciones inversas se han implementado con una función y con código suelto.

La característica más interesante es que para que los paquetes sean distintos y no sea trivial detectarlos como provenientes de la herramienta, hay unos punteritos al payload que permiten modificarlo y reenviar sin hacer la system call para construir el paquete, con una importante ventaja en la performance, como luego se apreciará.

Este es el pseudo código del programa original:

procesar opciones

crear payload


crear ip packet


tantas veces como se pida

   actualizar payload sin modificar el tamaño del packet
   enviar

limpieza



Pros y contras de la implementación original

+ Muy veloz

+ Simple si estás familiarizado con el protocolo (no era mi caso al comenzar)

- Código difícil de modificar

- Fácilmente detectable

Objetivos

Poder generar tráfico más creible.

Mantener la performance en la medida de lo posible.

Mejorar el código para modificaciones posteriores

Decisiones 


Pude haber optado por intentar comprender bien el protocolo y rescribir en algún lenguaje más sencillo que C o C++. En ese momento no sabía cual era el cuello de botella de la aplicación, asi que decidí no incorporar un elemento extra contra la performance.

Decidí convertir el código a C++ para poder utilizar los contenedores con los que ya estoy familiarizado, que al final no usé. En última instancia la elección fue muy influida por no invertir trabajo previo en comprender y el deseo de usar C++.

Decidí utilizar shunit2 y testear funcionalmente en lugar de utilizar testeo unitario, ya que pese ha haber decidido usar c++, la verdad es que no sabía si iba a terminar haciéndolo en perl. De hecho, de continuar el proyecto, será embebiendo un intérprete de algún lenguaje a determinar.

Primer paso: el test


Lo primero que hice fue un test con shunit2:

#! /bin/bash

testFullDump() {

    ./prg param1 param2 param3 | \

     sed -e "s/\(some headers=\)......../\1xxxxxxxx/" \
        -e "s/\(other header=\)......../\1xxxxxxxx/" \
        > /tmp/shunit.txt
    cmp output/fullDump.txt /tmp/shunit.txt
    result=$?
    assertEquals 0 $result || return
    rm /tmp/shunit.txt
}

. shunit2


O sea:

./prg param1 param2 param3

Ejecutá el programa con tales parámetros

     sed -e "s/\(some headers=\)......../\1xxxxxxxx/" \
         -e "s/\(other header=\)......../\1xxxxxxxx/" \


Que sed reemplace lo modificado por la actualización del payload por "xxxxxxxx" de modo tal que sea igual a mi archivo de referencia, output/fullDump.txt


> /tmp/shunit.txt

Poné en /tmp/shunit.txt la salida del programa

cmp output/fullDump.txt /tmp/shunit.txt

Compará utilizando cmp mi archivo de referencia con la salida del programa

result=$?

Costumbre mía, no confiar en que $? tenga el exit code más alla de la ejecución inmediata.

assertEquals 0 $result

Comprobá que la salida de cmp sea 0, o sea que sean iguales

|| return

Si no eran iguales, interrumpí el test

rm /tmp/shunit.txt

Limpiá

testFullDump() {
   ...
}

. shunit2


Este es el modo de hacer un test con shunit2, primero definir funciones de la forma testXXX y al final ejecutar en este mismo proceso shunit2.

Segundo paso: refactorización


Teniendo esta red de protección procedí a mover algunas secciones de código a funciones, cambiar de gcc a g++, creé clases y finalmente tuve la misma funcionalidad con la que había comenzado, pero en código modularizado.

También incorporé el uso de valgrind, para evitar perder memoria, ya que este programa bien podría ser ejecutado durante mucho tiempo.

Medí la performance antes y después y me dió parecido, así que por ese lado, no había problema.

Tercer paso: crear ip packet dentro del loop


Ahora, la llamada al sistema, que supongo debe ser cara, se hace dentro del loop:

procesar opciones


crear payload


tantas veces como se pida

   actualizar payload
   crear ip packet
   enviar

limpieza


Esto no agrega nada, es sólo un paso intermedio, desde el punto de vista del resultado sigue siendo refactorización. Estoy respetando "baby steps".

La performance se vió seriamente afectada, pero dentro de un rango aceptable.

Cuarto paso: crear payload packet dentro del loop


Ahora el payload es modificado dentro del loop, esto implica un payload de tamaño variable, cosa que en la implementación original no podiamos hacer, ya que hay que recrear el packet:


procesar opciones


tantas veces como se pida

   crear payload

   crear ip packet
   enviar

limpieza


Para crear payload usé unos pocos casos creados a mano, por ahora sólo me interesa que funcione y poder medir la performance. En teoría ya no puede empeorar.

Fin



Todo esto sólo fue el preparativo para poder cumplir el objetivo: crear payloads distintos pero coherentes.

Como me sigo resistiendo a terminar de entender el protocolo, en lugar de hacer generadores, me gusta la idea de generar las payloads a partir de la captura de tráfico legítimo, reduciendo la comprensión a que no se filtre información rastreable al tráfico original y que respeten el protocolo.

De un modo u otro, jamás lo haría en c++. Usaría una arquitectura de plugins con un intérprete de lua, python, perl, lo que sea. Si eso afectara la performance, prototiparía interpretando y al final volvería a c++.

Pero esas ya son otras historias...

Aprendizaje


Con un solo test, avanzando paso a paso y versionando con git, sin necesidad de una inversión inicial de investigación, pude tomar un programa funcionalmente correcto pero apestoso y convertirlo un programa limpio y manejable sin pérdida de performance.

Cuando empecé no sabía nada del protocolo, ni siquiera que existía; cuando terminé con esta etapa me vi obligado a leer algunas RFCs y no me costó nada pues ya estaba bastante familiarizado por haberlo usado.

Algunas métricas

Para medir las lineas usé:

for version in 0 1 2; do
   git checkout PerformanceV${version}
   echo "Version ${version}"
   for type in c h cpp hpp; do
       echo "  Type ${type}"
       echo -n "    Files "
       ls -1 *.$type 2>/dev/null | wc -l
       echo -n "    Lines "
       grep *.$type -ve "^ *$" -ve "^ */" 2>/dev/null | wc-l

   done
done


siendo $EXT hpp, cpp, h o c según el caso. Con el grep quité las lineas en blanco y casi todos los comentarios.


Para medir la performance utilicé un script para tirar ejecuciones simultáneas. Me da un poquito de vergüenza, no he sido muy estricto, ya sé que esta no es la manera de hacer profiling, es sólo una medida aproximada pero al menos tomé la precaución de dejar la velocidad del micro fija.

PROCESSES=$1
REQUESTS=$2

while [ $PROCESSES != 0 ] ; do
    ./run  $REQUESTS &
    PROCESSES=$(( $PROCESSES - 1 ))
done
wait



El  script se invoca:

time ./stresstest.sh $PROCESSES $REQUESTS

Cambiando el branch, recompiliando y con REQUESTS = 1000000

Original
c
Refactorizado
c++
Rediseñado
libnet_build_* en loop
Archivos headers 1 5 7
Archivos código 1 5 7
Lineas headers 71 98 117
Lineas código 349 473 473
1 proceso 1.2 1.4 7.5
2 procesos 2.3 2.5 8.5
3 procesos 1.8 1.9 8.1
4 procesos 3.9 3.7 8.8
5 procesos 4.9 4.9 12.5

Muy llamativa la anomalía con tres procesos, ¿sistema operativo? ¿hardware? ¡quién sabe! Mirá que lo probé varias veces y el comportamiento fué siempre el mismo.

Glosario


Dado que he utilizado algunos términos de modo un tanto "libre", paso a definirlos:

packet: me refiero al datagrama, lo que va a ser enviado a la red, se le ajustan las direcciones y puertos de origen y destino, el protocolo de red y los datos y se hace una llamada al sistema para que lo entienda y pueda enviarlo.

payload: es la carga específica del protocolo en cuestión, lo único en que afecta al packet es en su tamaño.

2013/02/14

TDD mental en ambientes hostiles

La vida me ha llevado a ambientes extremadamente hostiles, donde no se puede instalar nada, ni sacar luego nada. El que no pueda instalar o desarrollar un framework de Unit Testing en una máquina no significa que no pueda tenerlo en la mente, desplegarlo en el momento y descartarlo al final.
El asunto a resolver en esta ocasión es tomar una lista de usuarios y detectar los potenciales repetidos. Lo normal en unix es sort | uniq –d y en mainframe un SORT … OUTPUT PROCEDURE … en COBOL.
El problemas es que pueden haber nombres distintos que corresponden a una misma persona debido a la evolución histórica del sistema, por ejemplo “Lopez, Juan” y “Lopez_Juan” son buenos candidatos a ser la misma persona.
Antes de ordenar y buscar repeticiones, necesito una función de normalización de nombres, que lleve todos los nombres a la forma “Apellido(s), Nombres(s)”.
Para nuestro primer test, empezamos como siempre por el caso positivo.






Y una implementación sencilla que debería pasar el test:







Acá podemos ver como se ejecuta con ISPF:



Y su resultado:



El siguiente paso es un caso que falle:



Volvemos a ejecutar y vemos el error en el output



Y el valor de retorno RC=1, que era RC=0 en el caso correcto.



Esta es la nueva implementación para que funcione:



Ahí me asaltó una duda, pues space() dice que “remove all superfluous white space, including white space between words. “, así que agregué un test para más espacios y puse doble nombre, en dos pasos separados, obvio.



Nos encontramos con que el if expected… end se torna monótono, llegó el momento de refactorizar el test



Ya se parece más a lo que estamos acostumbrados


Para variar, he hecho trampa, pues ya había implementado tanto el código como los test, pero en COBOL. Sin embargo no te he engañado, pues seguí unos pasos similares y para la implementación en REXX recién en este punto pasé a copiar los casos que ya había utilizado en la versión en COBOL.

Este es el test:



Este es el código a testear



Tras un rato de idas y vueltas, ya tenemos un set aceptable




Y la implementación que pasa



Como se puede apreciar comparando los test en uno y otro lenguaje, en el caso de COBOL he optado por hacer algo parecido a un data provider pero no encapsulé la comparación como con el ASSERT de REXX. No vale la pena el data provider en REXX ya que es la misma cantidad de código y un poco más de complejidad. No vale la pena el ASSERT en COBOL, pues como es un lenguaje tipeado habría que hacer una versión para cada tipo de datos y como no hay overloading, tendría que usar nombres distintos y todo ese trabajo que asociamos con hacer un framework, que no era la idea.

De más está decir que no soy System Application Programmer, acepto críticas y sugerencias, pero no reproches.

Notas para entender un poco mejor el código y los conceptos presentados


ISPF


Es un programa que entre otras cosas permite editar datasets (archivos de mainframe) y en el caso de programas REXX ejecutar con “EX”.

REXX


Lenguaje interpretado con origen en mainframe pero extendido a múltiples arquitecturas y sistemas operativos.

Hay funciones y procedimientos, las funciones se llaman por su nombre y los procedimientos con CALL

Los argumentos tienen un sabor a perl, hay que sacarlos con PARSE, que tiene un sabor a erlang, ya que puede hacer pattern matching



En este caso, pone en ID todo lo que encuentre antes de “:”, luego en EXPECTED lo que haya entre “:” y “->” y por último en GOT el resto.

COBOL


Venerable lenguaje compilado con origen en mainframe y tambien extendido a múltiples arquitecturas y sistemas operativos.

¿Qué puedo decir en pocas lineas?

TDD


En una disciplina de programación/testing en la cual se identifica primero un requerimiento o error, se hace un test que lo expresa y que por supuesto falla, ya que aun no has implementado o corregido nada que lo haga pasar. Luego se implementa o corrige y se empieza otra vez.

La diferencia fundamental con el testeo natural que uno hace al programar es que los test se guardan y se siguen aplicando de modo acumulativo, lo cual da una cierta seguridad de que cualquier cambio que uno haga que rompa algo será detectado.

La diferencia fundamental con el testeo que se hace luego de la implementación, es que al testear al final uno se encuentra con desagradables sorpresas que pueden llevar a reestructuraciones drásticas. TDD da feedback inmediato y ayuda a comprender el problema. Además provee una suerte de documentación, ya que se muestra como se usa el código testeado.

Data Provider: cuando se hace el mismo test con distintos pares datos->expected, en lugar de hacer un test para cada dato, se hace uno solo y se lo alimenta con el conjunto de pares.

Refactorización: es una técnica que consiste en modificar la implementación sin modificar el comportamiento. Se suele usar para mejorar el código y va estrechamente ligado al testing, pues es éste quien avisa si ha cambiado el comportamiento.