scieee AI-readable full text Open interactive document viewer

Generadores ScalaCheck para property-based testing de programas Spark y Spark Streaming

Tello Ortiz, Max Arnulfo

Abstract

En los últimos años hemos sido testigos de la expansión del paradigma big data a una velocidad vertiginosa. Los cambios en este campo, nos permiten ampliar las áreas a tratar; lo que a su vez implica una mayor complejidad de los sistemas software asociados a estas tareas, como sucede en sistemas de monitorización o en el Internet de las Cosas (Internet of Things). Asimismo, la necesidad de implementar programas cada vez robustos y eficientes, es decir, que permitan el cómputo de datos a mayor velocidad y de los se obtengan información relevante, ahorrando costes y tiempo, ha propiciado la necesidad cada vez mayor de herramientas que permitan evaluar estos programas. En este contexto, el presente proyecto se centra en extender la herramienta sscheck. Sscheck permite la generación de casos de prueba basados en propiedades de programas escritos en Spark y Spark Streaming. Estos lenguajes forman parte de un mismo marco de código abierto para la computación distribuida en clúster. Dado que las pruebas basadas en propiedades generan datos aleatorios, es difícil reproducir los problemas encontrados en una cierta sesion; por ello, la extensión se centrará en cargar y guardar casos de test en disco mediante el muestreo de datos desde colecciones mayores.

Full text

Generadores ScalaCheck para property-based testing de programas Spark y Spark Streaming Trabajo fin de grado del Grado en Ingeniería Informática Universidad Complutense de Madrid Año 2015 - 2016 AUTOR Max Arnulfo Tello Ortiz DIRECTORES Adrián Riesco Rodriguez Juan Rodríguez Hortalá 1 2 3 DEDICATION To my parents, for the incessant support and their invaluable teachings. 4 Autorización de difusión y utilización Yo, Max Arnulfo Tello Ortiz, autorizo a la Universidad Complutense de Madrid a difundir y utilizar con fines académicos, no comerciales y mencionando expresamente a su autor, tanto la presente memoria, el código y/o los contenidos extras desarrollados durante la realización de este proyecto. Fdo. Max Arnulfo Tello Ortiz 5 6 Resumen En los últimos años hemos sido testigos de la expansión del paradigma big data a una velocidad vertiginosa. Los cambios en este campo, nos permiten ampliar las áreas a tratar; lo que a su vez implica una mayor complejidad de los sistemas software asociados a estas tareas, como sucede en sistemas de monitorización o en el Internet de las Cosas (Internet of Things). Asimismo, la necesidad de implementar programas cada vez robustos y eficientes, es decir, que permitan el cómputo de datos a mayor velocidad y de los se obtengan información relevante, ahorrando costes y tiempo, ha propiciado la necesidad cada vez mayor de herramientas que permitan evaluar estos programas. En este contexto, el presente proyecto se centra en extender la herramienta sscheck. Sscheck permite la generación de casos de prueba basados en propiedades de programas escritos en Spark y Spark Streaming. Estos lenguajes forman parte de un mismo marco de código abierto para la computación distribuida en clúster. Dado que las pruebas basadas en propiedades generan datos aleatorios, es difícil reproducir los problemas encontrados en una cierta sesion; por ello, la extensión se centrará en cargar y guardar casos de test en disco mediante el muestreo de datos desde colecciones mayores. Palabras clave Spark, testeo basado en propiedades, Scala, generadores, ScalaCheck, big data. 7 Abstract In recent years, we have been witness to the speedy expansion of big data. The changes in this field have led us to expand the treatable areas; which also implies more complex software systems associated to these tasks, as it happens in monitoring systems or in the Internet of Things. Likewise, the necessity of implementing reliable and efficient programs, that is, ones that allows us to compute data in a faster pace and to extract valuable information, saving money and time, has led to the increasing need of tools that permit us evaluate these programs. In this context, the present project centres its mission in extend the sscheck tool. Sscheck is able to generate test cases based in properties of programs wrote in Spark and Spark Streaming. These languages are part of the same open source cluster computing framework. Since the property-based tests generate random data, it’s complicated to reproduce the problems found in a given session; therefore, the extension will center in loading and saving test cases from disk through the sampling of data from bigger collections. Keywords Spark, property-based testing, Scala, generators, ScalaCheck, big data. 8 15 Para el primer valor elige un valor aleatorio entre 1 y 50, el segundo elemento depende directamente del primero: será un valor entre dicho elemento y su doble. Imagen 2.2. Generador ScalaCheck. En el siguiente ejemplo se intenta explicar el resultado de combinar propiedades y generadores ScalaCheck de forma que permita hacerse una idea del potencial de esta herramienta. En este ejemplo definimos una propiedad que, dados dos generadores que devuelven un valor entero positivo cada uno, para todo valor x e y, la suma de ambos será siempre mayor que el primer y que el segundo individualmente (ver imagen 2.3). Imagen 2.3. Propiedad con dos generadores. Especificada la propiedad pudiera parecer a primer vista que esta se cumple para cualquier caso, sin embargo al ejecutarla (ver imagen 2.4), Scalacheck encuentra un contraejemplo rápidamente, en este caso en el primer intento, aún más interesante, nos muestra el par de valores más sencillos para el cual la propiedad es refutada. Esto se debe a una implementación interna llamada Shrinking la cual una vez refutada la propiedad minimiza automáticamente el contraejemplo hasta encontrar una entrada más sencilla para el cual la propiedad sigue sin cumplirse. 16 Imagen 2.4. Ejecución de la propiedad ScalaCheck. Bajo esta premisa, los profesores Adrián Riesco y Juan Rodríguez al involucrarse con tecnologías big data se dieron cuenta que los programas que desarrollaban eran muchas veces bastante complicados de testear y el escribir test de unidades suponía un excesivo empleo de tiempo y recursos. Por dicha razón surge sscheck [7,8] una herramienta que extiende ScalaCheck y está escrita en Scala para el testeo de programas basado en Spark y Spark Streaming. Esta biblioteca permite usar una variante de la lógica lineal temporal para especificar propiedades que pueden ser evaluadas por ScalaCheck. En dichas propiedades se busca poner a prueba sistemas de procesamiento de flujo de datos y encontrar posibles brechas que vulneren la integridad de estos programas. Por ejemplo, para el caso descrito en la imagen 2.5 en el que se especifica una fórmula para Spark Streaming, para 20 lotes de datos, que recibe un flujo de tuplas [número, Booleano], donde los números son identificadores de usuario y el Booleano indica si se puede confiar en esa persona. Queremos que la función cuando encuentre a alguien en quien no se puede confiar, guarde a ese alguien en una lista, por si le vemos en el futuro no confiar en el. La fórmula indica que hasta que no se encuentra a nadie sospechoso nos fiamos de todos y, una vez desconfiamos de alguien, desconfiamos de él para siempre (para más información en fórmulas temporales ver referencia 9). Sin embargo, el hecho de generar casos de test aleatoriamente es tanto una ventaja como un problema en el TBP. Si nos topamos con un contraejemplo que refuta una propiedad nos enfrentamos a una situación que plantea dos problemas: el primero es que hemos llegado a un contraejemplo al azar, por lo que si volviésemos a testear la función, tal vez no daríamos con el mismo error y nuestro test tendría un resultado favorable, encontrándonos con un falso positivo; la segunda es que está asociada a la primera: un contraejemplo es un caso valioso de test (podría incluso considerarse su inclusión como test de unidad) y en este marco lo estamos perdiendo. 17 Imagen 2.5. Ejemplo de fórmula para flujos continuos en Spark Streaming. La motivación es, pues, encontrar una solución al problema de no poder reproducir las situaciones que ocasionan fallos, desde un acercamiento que nos permita guardar los contraejemplos que se encuentren. Si podemos almacenar estos contraejemplos podremos reutilizarlos en el futuro. De forma que nos permitan hacer cambios en el programa y constatar si una vez arreglados, siguen fallando. 2.2. Objetivos y justificación El objetivo de este proyecto es el de complementar la herramienta sscheck ampliando su funcionalidad, de tal forma que nos permita especificar generadores ScalaCheck a partir de schemas SparkSQL y schemas Apache Avro para un determinado flujo de datos, guardando y volviendo a usar los contraejemplos encontrados para posteriormente reutilizarlos y verificar si el programa se ha corregido adecuadamente. Para ellos planteamos una serie de hitos que, primero, permitan familiarizarnos con las tecnologías aquí utilizadas, ya que en muchos casos algunas tienen pocos años de vida, la documentación existente es relativamente escasa y no 18 se tocan en ninguna asignatura de la carrera. Lo que supondrá como veremos más adelante, un reto añadido al desarrollo de la herramienta. Segundo, una vez asentados los conocimientos, plantear una situación en la que podamos desarrollar generadores incisivos, los cuales nos permiten especificar una propiedad, un generador Scalacheck y un colección de datos. Ejecutarlos en un test evaluando la colección y ser capaces de guardar los contraejemplos encontrados en un fichero. La extensión de la herramienta supondrá que una vez finalizados los tests la información persista, podamos corregir posibles errores en los programas evaluados y nos permita simular nuevos tests con los contraejemplos encontrados. Si estos resultan satisfactorios supondrá que hemos podido corregir realmente dichos programas. 2.3. Plan de trabajo El proyecto se llevó a cabo entre los meses de octubre de 2015 y junio de 2016, bajo la tutela de los profesores Adrián Riesco y Juan Rodríguez Hortalá. Se mantuvo el contacto durante el curso regularmente mediante correo electrónico y reuniones en su despacho, al principio con mucha menor regularidad siendo solo necesarias unas pequeñas directrices para empezar, hasta las reuniones semanales entrado el segundo cuatrimestre para ir apuntalando dudas y cambios. Se discutió que además de los hitos de implementación necesarios para la realización del proyecto existía la necesidad de desarrollar una fase de de preparación previa que resultó tener una curva de aprendizaje bastante marcada, ya que el lenguaje de programación y el paradigma en el que se desenvuelve Scala (híbrido entre la programación orientada a objetos y la funcional) nunca habían sido dadas por el alumno. Esto sumado a la juventud (y por tanto escasamente documentadas) de las bibliotecas usadas sumaron un rato añadido al proyecto. Posteriormente a esto y bajo previa reunión con los tutores se establecieron tres hitos de rigurosa implementación para la realización del proyecto. En la siguiente sección paso a describirlos brevemente y con la mera intención de dar una idea general del trabajo hecho. Posteriormente explicaremos detalladamente el trabajo realizado desde un acercamiento más riguroso. 19 2.3.1. Fases del proyecto Preparación previa, comprendió los meses entre octubre y febrero, tiempo que el alumno usó para familiarizarse con el lenguaje Scala, valiéndose de libros de la facultad [7] o realizando cursos online, como el dado por Martin Odersky, creador del lenguaje, en Coursera 5 . Posteriormente a esto hubo que familiarizarse con el uso de las bibliotecas ScalaCheck 6 y specs2 7 necesarias para la especificación de los tests, labor bastante compleja debido a la poca documentación existente dado que las herramientas usadas son bastante recientes, lo que en muchos casos dificultó el aprendizaje y la adaptación por parte del alumno. Por último la configuración del entorno necesario para el desarrollo (sbt, ScalaIDE, gitHub y Travis CI, herramientas que describiremos en la sección 3 de esta memoria) requirió especial dedicación, debido primero a la complejidad a la hora de coordinar las versiones de cada biblioteca para que el proyecto compilara y segundo porque el alumno no había trabajado nunca con un software de integración continua sumado a la gestión del uso de una herramienta para la gestión y construcción de proyectos como es sbt. Hito 1, apuntalados los conocimientos necesarios se procedió a la implementación del primer hito en el cual implementamos un generador ScalaCheck a partir de la lectura de casos de test serializados en un fichero avro (se decidió por este tipo de ficheros como formato de serialización ya que ofrece la posibilidad de almacenar datos junto a su schema, es decir, la forma en la que están estructurados los datos). Dicho generador debía devolver los datos leídos desde el fichero hasta su final o en caso contrario, hasta leer la última entrada y seguir devolviendo este último dato hasta la finalización de la prueba. El principal reto de esta fase fue encontrar la forma en la que el generador tuviese estado, de manera que supiéramos cuándo todos los casos de test del fichero habían sido leídos y evitar que el test falle y termine abruptamente. Hito 2, los resultados del primer hito son usados para obtener generadores scalaCheck desde batches Spark con muestreo. La idea principal es la de obtener, a partir de un RDD (unidad básica de abstracción de Spark, explicado con mayor detalle en la sección 3) con un tipo genérico, una muestra de datos con un tamaño especificado por el 5 https://www.coursetalk.com/providers/coursera/courses/functional-programming-principles-in-scala 6 https://www.scalacheck.org/documentation.html 7 https://etorreborre.github.io/specs2/guide/SPECS2-3.8.3/org.specs2.guide.UserGuide.html 20 usuario y devolver para cada registro contenido un generador scalaCheck del mismo tipo. Indicando también si llegado al final de la muestra se devuelve el último registro leído o si por el contrario se devuelve un error (indicado por un generador fallido). Implementamos esta funcionalidad, primero leyendo desde un fichero avro y luego transformando lo leído a un RDD, para posteriormente muestrear en una lista que nos permita extraer registros y devolverlos dentro de un generador. Hito 3, concluidos los hito 1 y 2, la idea principal es la de implementar generadores incisivos. De forma que al implementar un test, cuando un caso de test no satisfaga una propiedad especificada por el usuario, no perdamos este contraejemplo. Los casos de test los obtenemos desde un generador que devuelve registros extraídos desde un RDD. Siendo el fin el ser capaces de almacenar este contraejemplo en un fichero de manera que este hallazgo persista y posteriormente, una vez el usuario arregle el motivo por lo que la propiedad falló, pueda ser cargado y probado de nuevo. Para ello nos valemos de propiedades de orden superior que nos permiten especificar un generador determinado, una propiedad y una ruta desde la cual cargar los datos y probar para todos la casos si la propiedad se mantiene. Analizando los hitos, se puede apreciar que el proyecto toca una parte del programa estudiado a lo largo de la carrera. Con el desarrollo de esta herramienta se ponen en práctica conocimientos de programación (FP, TP y EDA) y lógica matemática. Pero también puso a prueba la capacidad del alumno de aprendizaje y adaptabilidad, familiarizándose con la programación funcional, el tratamiento de ficheros serializados, la implementación de tests de unidad y el uso del framework Spark. Dado que el trabajo se desarrolló de manera individual el alumno no valoró la necesidad de elaborar un plan de proyecto propiamente dicho junto a una especificación de requisitos, al no ser una aplicación como tal, se decidió descartar este acercamiento. 21 3. Tecnologías empleadas Antes de explicar el desarrollo del proyecto, es relevante dar a conocer las herramientas (software) que se han utilizado para poder llevarlo a cabo. Este proyecto no ha necesitado de ningún soporte hardware especial. Se trabajó sobre un ordenador personal con las tecnologías descritas en este apartado. Valiéndose en muchos casos de bibliotecas experimentales y de corta existencia y que carecían de ningún tipo de soporte técnico al momento de realizarse este proyecto debido a la juventud de la tecnología en la que se basó esta herramienta (la primera versión estable de Spark data del 2014). 3.1. Lenguajes y bibliotecas A continuación presento cada una de las tecnologías usadas en este proyecto junto con un breve resumen de las mismas y su aportación. Scala (ver. 2.10.6) es un lenguaje de programación multi-paradigma diseñado para expresar patrones comunes de programación en forma concisa, elegante y con tipos seguros. Integra características de lenguajes funcionales y orientados a objetos. La implementación actual corre en la máquina virtual de Java y es compatible con las aplicaciones Java existentes. Como extra añadido Spark está escrito en Scala, por lo que era conveniente decantarnos por este lenguaje. ScalaCheck (ver 1.12.2) 8 es una herramienta para testeo de programas escritos en Scala y Java desarrollada por Rickard Nilsson, basado en una biblioteca de Haskell del que también coge su nombre llamada QuickCheck. Permitió definir las propiedades que describen el comportamiento de los flujos de datos y de corroborar que estas se cumpliesen. Todos los datos de prueba se generan de forma automática y de forma aleatoria. Por norma general al ejecutar un test ScalaCheck genera 100 casos distintos aleatorios y si son satisfactorios el test se da por pasado. Spark - Spark Streaming (ver. 1.6.1.) es un framework de código abierto para la computación en paralelo de clústeres. Originalmente desarrollado por la Universidad de California y luego donado a la fundación Apache. Provee una interfaz para la programación de clústeres con paralelismo implícito de datos y con tolerancia a fallos. Permitió cargar mediante RDDs (acrónimo para resilient 8 https://github.com/rickynils 22 distributed dataset, su estructura de datos central que representa una colección particionada de elementos inmutables que pueden ser operados en paralelo), batches desde local para su posterior tratamiento, análisis y testeo. También fueron usados DataFrames, otra tipo de colección distribuida que ofrece Spark dentro de su biblioteca SparkSQL con columnas nombradas. Specs2 9 (ver. 3.6.4) es una biblioteca que nos permite escribir especificaciones ejecutables basadas en Scala. Integra ScalaCheck, lo que nos permitió tener un pequeño ecosistema que integraba nuestras propiedades, generadores y un ámbito definido en el que se desarrollasen los tests, todo en una sola clase. Sscheck (ver. 0.2), herramienta escrita en Scala que permite el testeo con scalaCheck para programas Spark y Spark Streaming. Para la implementación de los tests de unidad usa la biblioteca specs2 que a su vez integra perfectamente ScalaCheck. Travis-CI 10 es un servicio alojado y distribuido de integración continua, que nos permite crear y testear proyectos creados en GitHub. Nos permitió previo acceso a su web llevar el control de los commits y push que se realizaban en el repositorio y si estas actualizaciones habían permitido que el proyecto se construyese de manera satisfactoria, ejecutando y mostrando los resultados de los tests creados (ver imagenes 3.1 y 3.2). SBT 11 (ver 0.13), es una herramienta de código abierto para construir proyectos Scala y Java similares a Maven, del cual hablamos brevemente en la sección 4.1, pero dejando de lado su estructura XML. Permitió mediante un plugin para Eclipse crear un archivo especificando las bibliotecas usadas en el proyecto lo que facilitó la portabilidad de este, ya que compilando con sbt desde consola en cualquier ordenador permitía descargarse todas las bibliotecas necesarias para la correcta ejecución del proyecto. 9 https://etorreborre.github.io/specs2/ 10 https://travis-ci.org/ 11 http://www.scala-sbt.org/ 23 Imagen 3.1. Interfaz web de Travis-CI avisando de una construcción fallida. Imagen 3.2. Log con el resultado de los tests ejecutados mostrado por Travis-CI. 24 Avrohugger 12 (ver 0.10.1) es una herramienta que permite la creación de case-classes de Scala a partir de una fichero avro que especifica un schema determinado. Fue usado en el proyecto como acercamiento a la implementación del hito 1 para la creación de clases dado un fichero avro que contenía datos y su respectivo schema embebido. Scalavro 13 (ver. 0.6.2) es una biblioteca que permite, en tiempo de ejecución y basándose en la reflexión, la des/serialización de datos desde Scala hacia un archivo avro. Fue usado en el primer hito (ver sección 4.2), como herramienta para la creación de un fichero avro con los datos obtenidos en los tests. Se descontinuó su uso ya que, aunque permitía la serialización de datos, no incluía en el fichero el schema, por lo que en el siguiente hito se hizo uso de la biblioteca Sparkavro. Sparkavro 14 (ver 2.0.1) es una biblioteca para la lectura y escritura de datos avro desde SparkSQL. Usado en el hito 2 para la creación de dataframes que nos permitieron a partir de un fichero avro tratar los datos en Spark previa conversión a RDDs y así poder realizar muestreos. Apache Avro 15 (ver 1.7.7) es un framework desarrollado dentro del proyecto Apache Hadoop. Usa JSON para definir tipos de datos y permite serializar información persistente en binario. Incluye dentro del fichero avro que crea la manera en la que se estructuran los datos. 3.2. Entorno de desarrollo La herramienta sscheck fue desarrollada usando el IDE para Scala de Eclipse por lo que era natural seguir utilizando esta opción. El proyecto está configurado en un ecosistema en el que en el que Scala-IDE era usado para la codificación del proyecto y su ejecución. A su vez este estaba integrado a un repositorio en github. En lo que se refiere a construir el proyecto esta labor quedó relegada al sbt por lo que el alumno tuvo que familiarizarse con esta herramienta y su uso desde consola. Hubo que aprender a especificar las bibliotecas usadas en el archivo 12 https://github.com/julianpeeters/avrohugger 13 https://github.com/GenslerAppsPod/scalavro 14 https://github.com/databricks/spark-avro 15 https://avro.apache.org/ 31 Imagen 4.7. Test superado exitosamente. Para defaultToLast igual a false y todos los demás parámetros conservando el mismo valor los resultados se pueden apreciar en las imágenes 4.8 y 4.9 donde después de devolver el quinto registro y no habiendo más que leer, se devuelve un Gen.Fail, el test se interrumpe y se da por fallido. Imagen 4.8. Al intentar probar el sexto caso el test falla. Imagen 4.9. Resultado del test. 32 4.3. Hito 2 Como segundo hito, y valiéndonos de los resultados obtenidos en el primero, nos centramos en implementar un generador de orden superior ScalaCheck que trabaje con lotes de Spark, es decir que dada una función que recibe un RDD[A], esta devuelve un Gen[A], siendo A un tipo genérico. Los RDD pueden alcanzar tamaños intratables, especialmente si estamos trabajando en un entorno local, por lo que un generador de casos de test que extrajese datos directamente sería un acercamiento erróneo. Se decidió implementar un primer planteamiento con una clase FromRDDGen que se basa en la estructura implementada en el primer hito, pero que además recibe un parámetro entero bufferSize. La idea es, dado un valor entero, utilizar la función RDD.takeSample() que implementan los RDDs y que nos permite realizar un sampleo de ese tamaño y guardar esos casos de test en una lista concurrente. Un RDD en ningún caso se modifica ya que es un objeto inmutable, es decir, una vez creado, los elementos que almacena no pueden ser alterados. Una vez hemos obtenido la muestra, implementamos una función que dada una lista de casos de test, los va extrayendo uno por uno y devolviendo envuelto en un Gen[A]. Análogamente a lo realizado en el primer hito implementamos también una variable defaultToLast que determina si una vez llegado al último caso de test es extraído y devuelto o simplemente devuelto n veces hasta satisfacer el test. Probamos este acercamiento definiendo un test con specs2. Primero, para poder utilizar Spark hace falta tener un contexto configurado, la herramienta sscheck define ya uno, este contexto define el punto de entrada para todas las funcionalidades de Spark, con el cual podemos especificar la conexión a un clúster y poder crear RDDs. En el test especificamos la ruta que almacena los casos de tests, creamos el RDD y definimos todos los parámetros necesarios para la clase FromRDDGen (ver imagen 4.10). Dado que tratamos con ficheros avro, es necesario leerlos de manera especial. Para poder leerlos Spark nos ofrece una biblioteca llamada SparkSQL, que nos facilita la lectura desde este tipo de ficheros a dataFrames conservando su schema. Un dataFrame organiza sus datos en columnas y nos permite la conversión a RDD de manera inmediata. Para defaultToLast igual a true, un muestreo de 3 casos de test y un mínimo de test pasados igual a 5 el comportamiento esperado es el indicado en las imágenes 4.11 y 4.12 donde, en la primera imagen especificamos los parámetros necesarios para la carga de los casos de test y procedemos a la creación del RDD a partir del fichero avro que contiene 10 entradas. En la 33 segunda imagen se puede observar cómo devuelto el 3 registro se procede a devolver este mismo otras dos veces, se da por bueno el test y el contexto de Spark se detiene automáticamente. Imagen 4.10. Parámetros de la clase FromRDDGen. Imagen 4.11. Resultado de la ejecución del test para FromRDDGen. Aunque este planteamiento pueda parecer adecuado y hayamos logrado implementar un generador arbitrario, es decir, un generador que en lugar de devolver datos aleatorios devuelve datos a partir de unos casos de test extraídos de un RDD y que conocemos. Surgen algunos problemas a tener en cuenta: si buscamos evaluar estos generadores con futuras propiedades scalaCheck nos veremos en la obligación de realizar muchos muestreos, incluso tal vez del total de los elementos del RDD. La función RDD.takeSample(), usada para muestrear los casos nos permite, modificando un parámetro Booleano que recibe llamado withReplacement, especificar si queremos que las muestras no se repitan. Pero nos vemos en la misma situación ya que aunque la muestra no contenga casos repetidos el RDD sigue siendo el mismo ya que es inmutable por lo que nos podemos topar con un test que al evaluar una propiedad nos dé un falso positivo. Para solventar el problema de tener siempre la misma fuente de muestreo realizamos algunos cambios a la clase anterior y creamos otra llamada FromRDDReplacementGen, que recibe los mismos parámetros pero en este caso el RDD consiste en una tupla [caso de test, id único]. Esta clase nos permite, en sucesivas muestras crear, un RDD nuevo solo con casos de test no seleccionados. Este RDD es asignado a una variable rdd y no a un valor ya que 34 así podemos reasignar la variable una vez hayamos muestreados los casos de test a un nuevo RDD filtrado. Hacemos esto, primero, porque los valores [val] en Scala son lo que, a una variable final supone en Java, una vez asignados no se pueden cambiar y segundo porque un RDD es inmutable y no se puede modificar por lo que debemos crear uno nuevo (ver imagen 4.12). Esta operación tiene un coste constante ya que un RDD en sí no contiene los datos propiamente dichos sino que almacena los metadatos necesarios para poder tratarlos en memoria y a menos que realizamos un RDD.collect() la herramienta no se ve en la necesidad de re-computar todos los datos cada vez que se haga un muestreo lo que supondría que la operación tuviese un coste lineal. Imagen 4.12. Filtrado del RDD de la clase FromRDDReplacementGen. Una vez modificada esta clase, el test que se implementa con specs2 lee el fichero avro con los casos de tests desde la ruta especificada y crea el rdd con las tuplas. Para esto se vale de la función RDD.zipWithUniqueId(). Si tenemos un fichero con 10 casos de test , especificamos como 10 también la cantidad de tests pasados y un muestreo de 5, el resultado esperado es que todos los casos de tests serán evaluados. Como se muestra en la imagen 4.11 donde después de devolver los primero 5 registros se avisa que el buffer está vacío y se procede a un nuevo muestreo, extrayendo los registros restantes del RDD sin que ninguno se repita. Una vez finalizado el test se detiene el contexto Spark y se da el test por pasado. 35 Imagen 4.13. Muestreo con filtrado. 4.4. Hito 3 Alcanzados los objetivos establecidos en el primer y segundo hito, en este último nos centramos en implementar una propiedad de orden superior, es decir, que reciba otra propiedad y un casos de test como parámetros y que sea capaz de almacenar el caso de test que resulte ser un contraejemplo. Para ello, creamos una clase IncisiveProp y nos centramos en crear una función forAll[A] (ver imagen 4.14) que recibe un generador de tipo genérico Gen[A] y una ruta con casos de test. La idea es que dada la ruta podamos cargar los casos de test usando los resultados del anterior hito a una variable mixedGen. Primero utilizamos los datos que proporciona el Gen[A] y una vez que se acaben pasamos a usar los casos de test cargados desde fichero. El objetivo es poder alternar entre un generador que tengamos con casos de test y otros que tengamos guardados en disco. Imagen 4.14. Función forAll de orden superior. Una vez hayamos resuelto la fuente de casos de test, creamos un valor de Scala de tipo propiedad llamado prop y dado un mixedGen, evaluamos el caso de test con una propiedad x. Esta propiedad es especificada en el test de prueba mediante el uso de la función Prop de ScalaCheck. El resultado, obtenido como una función parcial es resuelta por f, que finalmente lo devuelve como un tipo Property (ver imagen 4.15). 36 Imagen 4.15. Scalacheck Prop de orden superior que evalúa el caso de test. Para poder guardar el contraejemplo en caso de que algún caso de test incumpla la propiedad, redefinimos la función Prop de ScalaCheck de forma que, además de evaluar esta propiedad, pueda guardar el contraejemplo que no la satisface. Esto lo logramos valiéndonos del estado de Result: si la propiedad no se cumple tomará el valor Prop.False, la implementación se muestra como indica la figura 4.16, en la que dado un if que distingue casos según su valor, si este resulta ser Prop.False llamamos a una función writeToFile que guarda el contraejemplo en un fichero. Imagen 4.16. Extracto de la redefinición de Prop, para el guardado del contraejemplo. Posteriormente se implementaron también sobrecargas de la función forAll de manera que pueda aceptar de 2 a 4 generadores y de 2 a 4 rutas de ficheros respectivamente. En cada una de estas situaciones tendríamos tuplas de generadores de casos de test y un contraejemplo se consideraría como tal si una propiedad x no se mantiene para una tupla y. Para la imagen 4.17 con 2 generadores y 2 rutas de fichero, se resuelve recursivamente primero la función forAll para 1 generador y 1 ruta. El resultado obtenido se evalúa mediante una función parcial con el segundo generador y la segunda ruta. Imagen 4.17. Función forAll para dos generadores y dos rutas. 37 Finalizada la implementación del generador incisivo, procedemos a crear una clase IncisivePropTest. En este test probamos mediante un generador sencillo Gen.oneOf(), que nos permite especificar manualmente unos valores como parámetros eligiendo uno aleatoriamente. Definimos dos casos para dos funciones forAll() la primera p2 con 2 generadores y otra p1 con 1. Como se puede observar en la imagen 4.18 para dos valores enteros que pueden ser positivos o negativos, elegidos por los generadores, especificamos una propiedad que dado un valor v, el producto de x e y debe ser mayor que v para que se satisfaga. Análogamente para p3, una propiedad que compara dos strings. Especificamos un generador sencillo entre dos palabras. La propiedad se evalúa a true o false si las palabras coinciden. El valor generado se devuelve almacenado en sentence y si es igual a “adios” se satisface la propiedad. 4.18. Implementación de los dos test de prueba. El resultado de la ejecución se observa en la imagen 4.19. Para p2, definimos dos ejecuciones, una con v igual a -7 y otra con v igual a -3. Como especificamos un mínimo de 1 test pasado, para v igual a -7 la propiedad se cumple con la tupla [-5, -3] y la propiedad se evalúa a true. Sin embargo, para v igual a -3 y la tupla [2,-10] la propiedad no se cumple, por lo que el resultado es false. Esta tupla nos sirve de contraejemplo por lo que la guardamos en el fichero “counterExamples.txt” (ver imagen 4.20). En ambos test para simplicidad del ejemplo establecemos que no hay casos de test que cargar desde disco. 38 4.19. Resultado de la ejecución de los tests. 4.20. Fichero donde se guardan los contraejemplos. 4.5. Ejemplo En esta sección mostramos con un sencillo ejemplo lo alcanzado con la implementación del generador incisivo. Para ello, seguimos la línea del ejemplo usado en la sección 2.1 para describir sscheck y su funcionamiento. Tenemos un fichero avro con 50 entradas y su respectivo schema. Cada una de las entradas alberga una tupla [userID, trustable] que determina si un usuario es de fiar o no, en este caso vamos a definir una propiedad que evalúe la consistencia de los nombres usados por cada usuario. Definimos una propiedad p1 que dada un generador y una ruta a un fichero avro, evalúa los casos de test cargados a un RDD como se muestra en la imagen 4.21. La propiedad dicta que dado un userID si transformamos todos sus caracteres a mayúscula y luego a minúscula el resultado debería ser igual si directamente cambiamos todos a minúscula. 39 Imagen 4.21. Implementación de la propiedad p1. Para un mínimo de 30 test pasados y realizando muestreos del RDD de 5 casos de test por vez, el resultado es el que se muestra en la imagen 4.22. Después de 11 ejemplos evaluados nos encontramos con el nombre de un usuario que tiene un carácter Unicode que comparte con muchos otros el mismo carácter en mayúscula. Esto supone que la propiedad no se mantenga, el test falle y se detenga el contexto Spark no sin antes guardar el contraejemplo en el fichero. Imagen 4.22 Resultado de la ejecución del test. Imagen 4.23 Fichero que almacena los contraejemplos. 40 5. Conclusiones En esta sección concluyo la presente memoria añadiendo una valoración personal sobre el proyecto en el primer punto y el posible trabajo futuro que se podría realizar a partir de la extensión de la herramienta sscheck en el segundo punto. 5.1. Valoración personal Partiendo de los requisitos iniciales, que consistían en desarrollar una extensión de la herramienta sscheck que permitiese el testeo de programas Spark y Spark Streaming con la carga de casos de test desde ficheros avro y el guardado de contraejemplos en un fichero persistente, el proyecto ha cumplido los requisitos básicos establecidos al inicio del proyecto. Sin embargo, la funcionalidad del proyecto tiene algunas limitaciones, ya que por simplicidad se decidió guardar los datos en un fichero simple en local y debido a la curva de aprendizaje, que resultó ser más pronunciada de la esperada por el alumno, hubo que suprimir ciertas implementaciones: no se pudo probar en un entorno de flujo continuo de datos que simulase el uso de Spark Streaming. Todos los ejemplos realizados en este proyecto se basaron en la creación de lotes de datos desde Spark Core. Tampoco se pudo probar en un entorno real con datos reales y que pudiesen poner a prueba la utilidad de la herramienta. Como punto final, creo que este proyecto supone para mí un buen inicio de cara a introducirme de lleno en el mundo big data. He tenido la oportunidad de trabajar con un framework que cada vez está teniendo más aceptación por parte de muchas empresas, dado que, en ciertos entornos, es mucho más rápido que el entorno usado de facto, Hadoop 18 . Además, el haber tenido que aprender un lenguaje como Scala, sin ninguna noción de programación funcional, supuso poner a prueba los conocimientos adquiridos durante la carrera y también ver mi capacidad de adaptación y aprendizaje. Por último, el proyecto significó una primera toma de contacto con herramientas de testeo, creo que es algo a tener en cuenta, ya que como tal no se nos enseña en ninguna asignatura del grado y considero que es un valor añadido para mi currículo y de cara a enfrentarme al mundo laboral. 18 http://www.datastax.com/dev/blog/how-much-faster-is-spark-than-hadoop-in-datastax-enterprise