Full text
Generador de retroalimentación detallada para DOMjudge Detailed feedback generator for DOMjudge Trabajo Fin de Grado Curso 2021-2022 Autores Ricardo Enrique Freire Sacco Adrián González Cabanillas Erik Karlgren Domercq Félix Redondo Manzanares Directores Enrique Martín Martín Manuel Montenegro Montes Grado en Ingeniería Informática Facultad de Informática Universidad Complutense de Madrid
2
3 Resumen DOMfeed es una aplicación web que ofrece retroalimentación a los participantes de un concurso de programación que se lleve a cabo mediante el sistema DOMjudge. DOMjudge no ofrece la posibilidad de mostrar qué casos de prueba de un problema han fallado, ya que únicamente informa al usuario de si una solución es válida o no. En DOMfeed, sin embargo, para cada entrega inválida por parte de un usuario se muestra el caso de prueba erróneo. De esta forma, el usuario podrá agilizar el proceso de depuración de su solución. No obstante, el administrador del concurso deberá describir el formato de los ficheros de entrada y salida para que se almacenen en la base de datos de DOMfeed. Para ello definimos un lenguaje de especificaciones que permite, dado un problema, determinar cómo separar los casos de prueba en la entrada y la salida del mismo. Palabras Clave DOMjudge, retroalimentación, juez de programación, React, Node.js, aplicación web, DOMfeed, web scraping, Express
4 Abstract DOMfeed is a web application that provides feedback to the participants of a programming contest conducted using the DOMjudge system. DOMjudge does not offer the possibility to show which test cases of a problem have failed, as it only informs the user whether a solution is valid or not. In DOMfeed, however, for each invalid submission by a user, the wrong test case is displayed. In this way, the user can speed up the debugging process of their solution. However, the contest administrator must describe the format of the input and output files to be stored in the DOMfeed database. For this purpose, we define a specification language that allows, given a problem, to determine how to separate the test cases in the input and output of the problem. Keywords DOMjudge, feedback, programming judge, React, Node.js, web application, DOMfeed, web scraping, Express
Índice general 1. Introducción 13 1.1. Motivación................................ 13 1.2. Objetivos ................................ 14 1.3. Plandetrabajo............................. 15 1.4. Organización de la memoria . . . . . . . . . . . . . . . . . . . . . . 17 2. Introduction 19 2.1. Motivation................................ 19 2.2. Goals................................... 20 2.3. Workplan................................ 21 2.4. Reportorganisation........................... 22 3. Preliminares 25 3.1. Aprendizaje de la programación y el uso de problemas . . . . . . . 25 3.2. Jueces de programación y su importancia en el aprendizaje . . . . . 26 3.3. Limitaciones de los jueces de programación . . . . . . . . . . . . . . 27 3.3.1. COMPILER-ERROR . . . . . . . . . . . . . . . . . . . . . . 27 3.3.2. WRONG-ANSWER . . . . . . . . . . . . . . . . . . . . . . 27 3.4. DOMjudge como juez de programación y su API . . . . . . . . . . . 27 3.5. Gramáticas formales . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.5.1. Definición ............................ 29 3.5.2. Ejemplos............................. 29 3.6. Tecnologíasusadas ........................... 30 3.6.1. JavaScript............................ 30 3.6.2. Node.js ............................. 30 3.6.3. Express.js ............................ 32 3.6.4. React .............................. 32 3.6.5. ReactRouter .......................... 32 3.6.6. Bootstrap ............................ 34 3.6.7. React Bootstrap . . . . . . . . . . . . . . . . . . . . . . . . 34 3.6.8. MySQL ............................. 34 3.6.9. PEG.js.............................. 36 3.6.10.Jest ............................... 37 5
6ÍNDICE GENERAL 4. Organización del sistema 39 4.1. DOMjudge................................ 39 4.2. Aplicación web o frontend ....................... 40 4.2.1. Vistas del administrador . . . . . . . . . . . . . . . . . . . . 40 4.2.2. Vistas del usuario . . . . . . . . . . . . . . . . . . . . . . . . 40 4.2.3. Máximo de correcciones . . . . . . . . . . . . . . . . . . . . 42 4.3. Servidor o backend ........................... 42 4.3.1. npm ............................... 42 4.3.2. Peticiones del usuario . . . . . . . . . . . . . . . . . . . . . . 43 4.3.3. Datos de DOMjudge . . . . . . . . . . . . . . . . . . . . . . 43 4.3.4. Datos de la base de datos . . . . . . . . . . . . . . . . . . . 44 4.3.5. Procesamiento de los datos . . . . . . . . . . . . . . . . . . . 44 4.4. Basededatos.............................. 44 4.5. Analizador de casos de prueba . . . . . . . . . . . . . . . . . . . . . 45 4.5.1. Comprobación de la validez de una especificación . . . . . . 45 4.5.2. Separación de casos de prueba . . . . . . . . . . . . . . . . . 46 5. Analizador de casos de prueba 47 5.1. Lenguaje de especificación . . . . . . . . . . . . . . . . . . . . . . . 47 5.1.1. Requisitos del lenguaje . . . . . . . . . . . . . . . . . . . . . 47 5.1.2. Descripción del lenguaje . . . . . . . . . . . . . . . . . . . . 49 5.1.3. Ejemplos de especificaciones . . . . . . . . . . . . . . . . . . 52 5.1.3.1. Ejemplo1....................... 52 5.1.3.2. Ejemplo2....................... 52 5.1.3.3. Ejemplo3....................... 52 5.1.3.4. Ejemplos problemáticos . . . . . . . . . . . . . . . 53 5.1.4. API de comprobación de la validez de una especificación: tryToParseGrammar() ..................... 54 5.2. Separación de los casos de prueba . . . . . . . . . . . . . . . . . . . 54 5.2.1. Implementación de separateCases() ............. 55 5.2.1.1. API de separación de casos: separateCases() . . . 55 5.2.1.2. Función auxiliar: _separateCases() ........ 56 5.2.2. TokenStream .......................... 59 5.2.3. Reglas de procesamiento . . . . . . . . . . . . . . . . . . . . 60 5.2.3.1. Especificaciones aplicables a cadenas vacías . . . . 60 5.2.3.2. Evaluación de una expresión aritmética . . . . . . . 61 5.2.3.3. Definiciones de las reglas . . . . . . . . . . . . . . . 62 5.3. Verificación del correcto funcionamiento del analizador . . . . . . . 65 6. Servidor principal: frontend ybackend 67 6.1. Frontend ................................. 67 6.1.1. Iniciodesesión ......................... 68 6.1.2. Cabecera............................. 68 6.1.3. Páginas del administrador . . . . . . . . . . . . . . . . . . . 69 6.1.3.1. Vista de problemas . . . . . . . . . . . . . . . . . . 69
ÍNDICE GENERAL 7 6.1.3.2. Vista de especificaciones . . . . . . . . . . . . . . . 71 6.1.3.3. Vista de entregas . . . . . . . . . . . . . . . . . . . 72 6.1.4. Páginas del usuario . . . . . . . . . . . . . . . . . . . . . . . 73 6.1.4.1. Vista de entregas . . . . . . . . . . . . . . . . . . . 73 6.1.5. Vista de correcciones . . . . . . . . . . . . . . . . . . . . . . 75 6.1.6. Código de interés . . . . . . . . . . . . . . . . . . . . . . . . 77 6.1.6.1. Mantener la sesión . . . . . . . . . . . . . . . . . . 77 6.1.6.2. Páginas protegidas . . . . . . . . . . . . . . . . . . 78 6.1.6.3. Notificaciones . . . . . . . . . . . . . . . . . . . . . 81 6.2. Backend ................................. 83 6.2.1. Basededatos.......................... 84 6.2.1.1. Estructura de la base de datos . . . . . . . . . . . 84 enabled_submissions . . . . . . . . . . . . . . . . . . 84 problem_specifications . . . . . . . . . . . . . . . . . 85 max_contest_attempts . . . . . . . . . . . . . . . . . 86 max_problem_attempts . . . . . . . . . . . . . . . . 86 6.2.1.2. Acceso a la base de datos . . . . . . . . . . . . . . 86 6.2.2. API de DOMfeed . . . . . . . . . . . . . . . . . . . . . . . . 87 6.2.2.1. Midlewares ...................... 87 6.2.2.2. Obtención de datos de DOMjudge . . . . . . . . . 88 Concursos por equipos . . . . . . . . . . . . . . . . . 89 Problemas por concurso . . . . . . . . . . . . . . . . . 89 Entregas por equipos . . . . . . . . . . . . . . . . . . 89 Entregas por problemas . . . . . . . . . . . . . . . . . 89 6.2.2.3. Gestión del número de correcciones de problemas yconcursos ...................... 89 Número de correcciones máximo de un concurso . . . 89 Número de correcciones máximo de un problema . . . 90 Número de correcciones disponibles de un equipo para unproblema................. 90 6.2.2.4. Gestión de especificaciones de un problema . . . . 90 Análisis de especificaciones . . . . . . . . . . . . . . . 90 6.2.2.5. Separación de casos de prueba . . . . . . . . . . . . 91 Obtención de casos de prueba . . . . . . . . . . . . . 91 Separación de los ficheros . . . . . . . . . . . . . . . . 92 6.2.2.6. Gestión de correcciones . . . . . . . . . . . . . . . 92 Habilitación de entregas . . . . . . . . . . . . . . . . 92 Corrección de una entrega . . . . . . . . . . . . . . . 92 6.2.3. Web scraping .......................... 92 7. Conclusiones y trabajo futuro 95 7.1. Objetivos alcanzados . . . . . . . . . . . . . . . . . . . . . . . . . . 95 7.2. Dificultades encontradas . . . . . . . . . . . . . . . . . . . . . . . . 96 7.2.1. Grupo1............................. 96 7.2.2. Grupo2............................. 96
8ÍNDICE GENERAL 7.3. Trabajofuturo ............................. 97 7.3.1. Lenguaje de especificación . . . . . . . . . . . . . . . . . . . 97 7.3.2. Implementación de la separación de casos de prueba . . . . . 97 7.3.3. Frontend ............................. 98 7.3.4. Backend ............................. 98 8. Conclusions and Future Work 99 8.1. Objectives achieved . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 8.2. Difficulties encountered . . . . . . . . . . . . . . . . . . . . . . . . . 100 8.2.1. Group1 .............................100 8.2.2. Group2 .............................100 8.3. Futurework...............................101 8.3.1. Specification language . . . . . . . . . . . . . . . . . . . . . 101 8.3.2. Implementation of test case separation . . . . . . . . . . . . 101 8.3.3. Frontend.............................101 8.3.4. Backend .............................102 9. Contribuciones personales 103 9.1. Félix Redondo Manzanares . . . . . . . . . . . . . . . . . . . . . . . 103 9.1.1. Scraping .............................103 9.1.2. Algoritmo de búsqueda de caso erróneo . . . . . . . . . . . . 103 9.1.3. API de DOMfeed . . . . . . . . . . . . . . . . . . . . . . . . 104 9.1.4. Páginas de la interfaz web . . . . . . . . . . . . . . . . . . . 104 9.1.5. Sistema de notificaciones . . . . . . . . . . . . . . . . . . . . 104 9.1.6. Páginas privadas . . . . . . . . . . . . . . . . . . . . . . . . 105 9.1.7. Mantenimiento de la sesión del usuario . . . . . . . . . . . . 105 9.1.8. Segurización de la API . . . . . . . . . . . . . . . . . . . . . 105 9.1.9. Llamada a la API de DOMjudge . . . . . . . . . . . . . . . 105 9.2. Ricardo Enrique Freire Sacco . . . . . . . . . . . . . . . . . . . . . 105 9.2.1. API de DOMfeed . . . . . . . . . . . . . . . . . . . . . . . . 106 9.2.2. Basededatos..........................106 9.2.3. Gestión del número de correcciones . . . . . . . . . . . . . . 106 9.2.4. Gestión de especificaciones . . . . . . . . . . . . . . . . . . . 107 9.2.5. Interfazweb...........................107 9.2.6. Integración del separador de casos en la aplicación web . . . 107 9.3. Adrián González Cabanillas . . . . . . . . . . . . . . . . . . . . . . 107 9.3.1. Gramática del lenguaje de especificación . . . . . . . . . . . 107 9.3.2. Implementación de reglas . . . . . . . . . . . . . . . . . . . . 108 9.3.3. Variables y estados . . . . . . . . . . . . . . . . . . . . . . . 108 9.3.4. Especificaciones que enlazan la entrada con la salida . . . . . 108 9.3.5. Elaboración de tests . . . . . . . . . . . . . . . . . . . . . . 109 9.3.6. Agrupación de casos de prueba separados . . . . . . . . . . . 109 9.3.7. Manual de especificaciones . . . . . . . . . . . . . . . . . . . 109 9.4. Erik Karlgren Domercq . . . . . . . . . . . . . . . . . . . . . . . . . 109 9.4.1. TokenStream ..........................109
ÍNDICE GENERAL 9 9.4.2. Gramática del lenguaje de especificación . . . . . . . . . . . 110 9.4.3. Reglas de procesamiento . . . . . . . . . . . . . . . . . . . . 110 9.4.4. Separación de casos de la entrada . . . . . . . . . . . . . . . 110 9.4.5. Elaboración de tests . . . . . . . . . . . . . . . . . . . . . . 111 9.4.6. Documentos...........................111 A. Ejemplos de especificaciones i A.1. Obtener el mínimo de un vector . . . . . . . . . . . . . . . . . . . . i A.1.1.Enunciado............................ i A.1.2.Especificaciones......................... ii A.2.ElGrande................................ iii A.2.1.Enunciado............................ iv A.2.2.Especificaciones......................... v Bibliografía ix
16 CAPÍTULO 1. INTRODUCCIÓN Figura 1.1: Diagrama de Gantt de las tareas de cada grupo a lo largo del curso 2021/2022 Desarrollo durante el segundo cuatrimestre (1 de febrero de 2022 - 31 de marzo de 2022) El grupo 1 completó el desarrollo del lenguaje de especificación y la separación de casos, cuyos resultados pueden consultarse en el capítulo 5. Para probar que la implementación de ambos era la esperada, se crearon múltiples tests usando Jest (ver sección 5.3). El grupo 2 se centró en perfeccionar todo el trabajo realizado en el primer cuatrimestre y en crear tanto las pantallas de gestión del administrador, como la base de datos necesaria para su funcionamiento. También se modificó el algoritmo de búsqueda del caso erróneo, pues se introdujeron nuevos cambios en la especificación. El resultado final del trabajo del grupo 2 se puede consultar en el capítulo 6. Integración del trabajo de ambos grupos (1 de abril de 2022 - 30 de abril de 2022) Tras el desarrollo en paralelo de ambos grupos, se lleva a cabo la integración del analizador de casos de prueba con la aplicación web. El resultado de dicha integración se explica en el capítulo 4. El objetivo principal de esta fase es comprobar que la especificación introducida es válida, así como realizar la separación de los casos de prueba utilizando el trabajo realizado por el grupo 1. Además, el grupo 2 hace nuevas tareas de web scraping para obtener los ficheros de prueba del problema a tratar, pues hasta ese momento se estaban utilizando ficheros guardados de forma local. Elaboración de la memoria (1 de mayo de 2022 - 30 de mayo de 2022) Elaboramos conjuntamente este documento con ayuda de nuestros tutores, que se encargaron de revisarlo en varias ocasiones.
1.4. ORGANIZACIÓN DE LA MEMORIA 17 1.4. Organización de la memoria La estructura de la memoria es la siguiente: 1. Preliminares: En el capítulo 3 explicamos todos los conceptos necesarios para entender el resto de esta memoria y del proyecto. 2. Organización del sistema: Explicamos cómo están interconectados los componentes de DOMfeed y qué tecnologías utilizan en el capítulo 4. 3. Analizador de casos de prueba: Definimos el lenguaje de especificación y describimos la separación de los casos de prueba en el capítulo 5. 4. Backend y frontend: Explicamos cómo funciona el servidor. Esto incluye la página web, la base de datos, cómo se utiliza el analizador de casos y cómo se actualizan los casos de prueba con separadores, así como la petición de datos a DOMjudge y su posterior tratamiento mediante la utilización de su API y el uso de web scraping para los casos donde la API de DOMjudge no proporciona los datos que necesitamos. Ver capítulo 6. 5. Conclusiones y trabajo futuro: Recapitulamos los objetivos conseguidos, discutimos hasta qué punto se han logrado y comentamos qué más cosas querríamos añadir a DOMfeed en el capítulo 7(la sección Conclusions and future work es idéntica pero en inglés, capítulo 8). 6. Contribuciones personales: Mencionamos las tareas que ha llevado a cabo cada uno de nosotros en el capítulo 9. 7. (Apéndice) Ejemplos de especificación: Por último, presentamos ejemplos de especificaciones para problemas reales de programación en el apéndice A.
18 CAPÍTULO 1. INTRODUCCIÓN
Capítulo 2 Introduction In this report we describe the process of designing and creating a detailed feedback generator for DOMjudge which we have decided to call DOMfeed. We will start this section by mentioning what motivated us to create DOMfeed and what our goals were with this project. Finally, we will detail both our work plan and report structure. 2.1. Motivation DOMjudge is a programming judge mainly intended for programming competitions, but it is also frequently used in certain subjects taken in the Faculty of Computer Science at the Complutense University of Madrid. Most of these subjects are oriented towards the teaching of algorithms and data structures, for which it is essential that students practice with programming problems that test their knowledge. When a user submits a solution to a given problem to DOMjudge, DOMjudge compiles and executes the solution, checking its validity through a series of test cases stored on the server. DOMjudge captures the output emitted by the solution, and checks that it matches the expected output for that problem. If it does, the judge responds to the user with a CORRECT veredict. Conversely, if there is any discrepancy between the output obtained and the expected output, a WRONG-ANSWER verdict is returned. However, the latter verdict merely informs the student that his solution is incorrect, so the student is not given any clues that might help him to fix his solution. This forces them either to think about what they may have done wrong, or to ask their teacher what problem their solution presents or in which test case it fails. While we believe that a student needs to spend time thinking about what might be going wrong with their solution, we also believe that some automatic way of giving them a hint would be desirable. Furthermore, although we have found some programming judges that show more detailed feedback than DOMjudge (e.g. Jutge.org [10, 17]), we have not found a modified version of DOMjudge or an application that interacts with DOMjudge that shows more information for wrong 19
20 CAPÍTULO 2. INTRODUCTION solutions. This is why we decided to create DOMfeed, a web application whose role is to provide students with more detailed feedback than DOMjudge for wrong solutions. 2.2. Goals When developing DOMfeed, as mentioned above, we wanted to provide better feedback to the student when a solution developed by him or his team did not generate the expected output. To do so, we considered it convenient to show him the test cases for which the solution failed, which forced us to: 1. Find a way to separate test cases. 2. Given separate test cases, check which test cases the student fails. The second step was the simplest, because once we have all the separate test cases, it is relatively trivial to compare the expected output and the student’s output on a case-by-case basis until we find one where they differ. However, separating test cases from a file is a significantly more complex problem, as not all programming problems represent test cases in the same way. Therefore, we decided to create a language for specifying test cases, and it is the task of the teacher to define a correct specification for the input and output of a problem. Such a language will need a way to delimit where each test case starts and where it ends. On the other hand, we decided that DOMfeed would be a web application like DOMjudge. Therefore, we need to create a web page with which both the student and the teacher would interact (the frontend), and a backend to interact with DOMjudge together with its own database. The interaction with DOMjudge is necessary to be able to consult the deliveries and problems with their respective files with the input and output test cases, thus avoiding the teacher having to upload these files to DOMfeed on his own. In fact, it also allows us to control that only students and teachers who are registered in DOMjudge using the same credentials can access DOMfeed. The database, on the other hand, is needed to store information that we cannot get from DOMjudge, such as the specifications of each problem provided by the teachers. In this web application, the student would be able to see the failed DOMjudge submissions and discover the test cases in which each failed. However, to prevent the student from abusing this system, we decided that the teacher could limit the number of times the student could view this information per problem and per quiz. Also, teachers would use the web application to provide the specifications for each problem, as mentioned above. In conclusion, we have defined the following main goals: The definition of a test case specification language and the implementation of a mechanism to separate the test cases of a problem from a specification.
2.3. WORK PLAN 21 The development of a web application that allows the student to see in which test case the student’s delivery has failed and the administrator to record the necessary specifications so that the test cases can be separated from the problem. 2.3. Work plan At the beginning of the 2021/2022 academic year, we divided into two groups, so that Erik Karlgren Domercq and Adrián González Cabanillas (Group 1) would have the objective of achieving the separation of test cases, while Félix Redondo Manzanares and Ricardo Enrique Freire Sacco would be in charge of the web application (Group 2). For both parts, we established work goals every two weeks, at which time we met with the project tutors (Enrique Martín Martín and Manuel Montenegro Montes) to show the work we had progressed. We also discussed the problems that had arisen and how we had solved them. At the end of these meetings, new objectives were set for the following two weeks if we had completed the pending tasks. Summarising, the project has been divided into several phases, indicating which objectives have been achieved in each phase and in which chapters of the report they are explained. The milestones achieved are shown in figure 2.1 and are described below: Figura 2.1: Gantt chart of the tasks of each group during the academic year 2021/2022 Project analysis (20 September 2021 - 7 October 2021) We established the goals and scope of the project. We began by investigating the technologies
22 CAPÍTULO 2. INTRODUCTION we would use in the project. Our conclusions are detailed in the section 3.6. Group 1 was in charge of performing proofs of concept with the PEG.js library while group 2 spent time familiarising themselves with the DOMjudge interface as well as the DOMjudge API. Development during the first four-month period (8 October 2021 - 31 January 2022)During this stage, group 1 was implementing the most basic features of the specification language and test case separation, adding new functionalities every two weeks. On the other hand, group 2 was working on a first version of the DOMfeed web application, where the first iteration of the failed test case search algorithm, the first DOMfeed API endpoints, and the development of the issue display, delivery and fix pages were developed. All the necessary web scraping was also performed to enable the download of the output obtained from the user. Development during the second four-month period (1 February 2022 - 31 March 2022) Group 1 completed the development of the specification language and the separation of cases, the results of which can be found in chapter 5. To test that the implementation of both was as expected, multiple tests were created using Jest (see section 5.3). Group 2 focused on refining all the work done in the first term and creating both the administrator management screens and the database necessary for their operation. The search algorithm for the erroneous case was also modified, as new changes were made to the specification. The final result of group 2’s work can be found in chapter 6. Integration of the work of both groups (1 April 2022 - 30 April 2022) After the parallel development of both groups, the integration of the test case analyser with the web application is carried out. The result of this integration is explained in the chapter 4. The main goal of this phase is to check that the specification introduced is valid, as well as to carry out the separation of the test cases using the work done by group 1. In addition, group 2 performs new web scraping tasks to obtain the test files for the problem to be dealt with, as up to this point, locally stored files were being used. Report (1 May 2022 - 30 May 2022) We jointly developed this document with the help of our tutors, who were responsible for revising it on several occasions. 2.4. Report organisation The structure of the report is as follows: 1. Preliminaries: In chapter 3 we explain all the concepts necessary to understand the rest of this report and the project.
2.4. REPORT ORGANISATION 23 2. System organisation: We explain how DOMfeed’s components are interconnected and which technologies they use in chapter 4. 3. Test case analyser: We define the specification language and describe the separation of the test cases in chapter 5. 4. Backend y frontend: We explain how the server works. This includes the web page, the database, how the case parser is used and how test cases are updated with separators, as well as requesting data from DOMjudge and then processing it using its API and using web scraping for cases where the DOMjudge API does not provide the data we need. See chapter 6. 5. Conclusions and future work: We recap the objectives achieved, discuss to what extent they have been achieved and discuss what else we would like to add to DOMfeed in chapter 7(section Conclusions and future work is identical but in English, chapter 8). 6. Personal contributions: We mention the tasks carried out by each of us in chapter 9. 7. (Appendix) Specification examples: Finally, we present examples of specifications for real programming problems in the appendix A.
24 CAPÍTULO 2. INTRODUCTION
Capítulo 3 Preliminares En esta sección se listan y se explican los conceptos necesarios para entender el proyecto, al igual que se presentan las tecnologías usadas para el desarrollo del mismo. 3.1. Aprendizaje de la programación y el uso de problemas A la hora de aprender a programar es fundamental la resolución de problemas para poner a prueba los conocimientos adquiridos. En programación existen muchos tipos de problemas, pero para todos ellos hay tres conceptos principales: El problema a resolver en sí, que determina lo que se quiere calcular, y para el cual se necesita definir un algoritmo. Las entradas del problema, que son los parámetros u opciones necesarios para que el problema se pueda resolver. Y las salidas, que corresponden al resultado del problema una vez que se ha ejecutado teniendo en cuenta las entradas. Supongamos el siguiente problema: dados dos números enteros, tenemos que calcular su suma. Para ello definimos un programa o algoritmo (por ejemplo, en JavaScript) que recibe dos parámetros, entrada1 yentrada2, y devuelve el resultado de sumar esos dos valores, resultado, como podemos ver en la figura 3.1: 1function sumar (var entrada1, var entrada2 ) { 2var resultado = entrada1 + entrada2 ; 3return resultado ; 4} Figura 3.1: Ejemplo de un problema de programación 25
32 CAPÍTULO 3. PRELIMINARES es una gran ventaja, pues ofrece la posibilidad de utilizar la misma tecnología tanto en el front-end como en el back-end. Node.js ofrece una gran cantidad de librerías que facilitan el desarrollo de aplicaciones, las cuales son fácilmente gestionables mediante el gestor de paquetes de node: npm. Además, Node.js es un lenguaje asíncrono basado en callbacks o promesas, permitiendo así atender múltiples conexiones o llamadas simultáneamente. Esto también permite que tareas que puedan tomar cierto tiempo pasen a segundo plano, como el caso de llamadas a la API o a la base de datos y que así no se detenga el flujo de la aplicación. 3.6.3. Express.js Express.js [7] es un framework para Node.js que permite estructurar y enrutar aplicaciones web y crear APIs. Además también ofrece otras funciones como la gestión de sesiones y cookies. Debido a su sencillez y a que está programado en Node, se adapta perfectamente a todos los requisitos ofreciendo gran flexibilidad y rendimiento. Además Express.js ofrece gran sencillez a la hora de desarrollar cualquier tipo de APIs, pues con una simple línea de código, se puede poner en funcionamiento un servidor para que reciba peticiones de clientes. Así mismo, ofrece multitud de métodos para crear y manejar correctamente las rutas y peticiones al servidor. En la figura 3.3 se puede ver un claro ejemplo de la simplicidad de Express.js, donde se está creando un servidor en pocas líneas de código. Para el tratamiento de las sesiones, cookies, permisos, y en definitiva, el análisis de las peticiones y la gestión de errores, se usan los middlewares. Un middleware es una función que se sitúa entre la solicitud inicial y la ruta final a la que se pretende llegar y que puede ejecutar cualquier código, cambiar la petición inicial, modificar la respuesta o terminar la petición impidiendo que llegue a su ruta final. 3.6.4. React React[20] es una biblioteca de JavaScript que se utiliza para facilitar la creación de interfaces de usuario en el front-end (o navegador web). Es un lenguaje declarativo basado en componentes reutilizables que se renderizan y actualizan automáticamente cuando ocurre un cambio en la información que muestra la aplicación. Además de resultar cómodo para el desarrollo de la aplicación, React es una tecnología ampliamente utilizada en la actualidad, lo cual ayuda a maximizar la mantenibilidad del sistema. 3.6.5. React Router React Router [19] es una librería de enrutamiento en JavaScript basada en componentes para React. Esta librería permite crear un enrutamiento dinámico en
3.6. TECNOLOGÍAS USADAS 33 1var express = require(’express’) 2var app = express () 3 4// middleware que muestra un mensaje por pantalla cada vez que llega una peticion al servidor enviando una respuesta y permitiendo que la llamada continue 5app .use ( function (req , res , next) { 6console . log (’Ha llegado una peticion ’) 7next () 8}) 9 10 // Se crea un routa / hello que devuelve ’hello world ’ al cliente 11 app.get (’/ hello ’,function (req , res ) { 12 res. send(’hello world ’) 13 }) 14 15 // middleware que maneja los posibles errores que se hayan producido y devuelve un mensaje de error 16 app .use ( function (err , req , res , next ) { 17 console . error ( err . stack ) 18 res . status (500) . send (’Algo ha salido mal ’) 19 }) 20 21 // Se pone en marcha un servidor que escucha por el puerto 3000 22 app.listen(3000) Figura 3.3: Ejemplo de servidor en Express.js
34 CAPÍTULO 3. PRELIMINARES la parte del servidor, realizar redirecciones, pasar parámetros en las rutas, tener acceso al historial de navegación y manejar las páginas de errores. En esencia, esta tecnología permite cargar unos componentes u otros dinámicamente dependiendo de la ruta de la aplicación en la que estemos. 3.6.6. Bootstrap Bootstrap [2] es un framework CSS que ofrece diversas funcionalidades y plantillas para el diseño del front-end de aplicaciones web. El framework combina tanto CSS como JavaScript para dar estilo a la web. El principal objetivo de Bootstrap es crear una interfaz web adaptable (responsive), es decir, que la web se adapte dependiendo del dispositivo y tamaño de pantalla desde donde se esté accediendo. Así mismo, también contiene una gran cantidad de componentes ya creados como alertas, barras de progreso y menús de navegación, entre muchos otros. También añade una gran biblioteca de cientos de iconos para poder utilizar en la web muy fácilmente. La decisión de utilizar Bootstrap para el diseño del proyecto se debe a la intención de replicar el diseño de la aplicación de DOMjudge, la cual está desarrollada con Bootstrap. 3.6.7. React Bootstrap React Bootstrap [18] reemplaza completamente la parte de JavaScript de Bootstrap eliminando JQuery, creando componentes totalmente optimizados para React y mucho mas fiables. En la figura 3.4 se puede ver un ejemplo. Como se puede ver en la figura, al utilizar React Bootstrap en vez de Bootstrap, se obtiene un código mucho más declarativo y fácil de escribir y leer. En vez de tener que utilizar clases para definir el estilo de un componente ’div’ en Bootstrap, en React Bootstrap se inserta un componente, el cual internamente ya tiene definido todo lo necesario para su correcto funcionamiento: clases, estilos, código JavaScript, lógica funcional, etc. Lo único que hay que definir, en este caso, al usar React Bootstrap, es el componente que se va a usar, Alert, que es de tipo danger y que además puede cerrarse añadiendo el atributo dismissible. También hay que especificar que se quiere una cabecera, Alert.Heading, y el cuerpo de la alerta, p. React Bootstrap proporciona un código mucho más limpio y legible frente a Bootstrap. Además, si nos fijamos, nos estamos ahorrando la creación del botón porque el atributo dismissible lo inserta internamente, sin contar todas las clases que no se añaden porque ya se incluyen por defecto al crear el componente Alert. 3.6.8. MySQL MySQL es un sistema utilizado para la administración y gestión de bases de datos relacionales. Un sistema gestor de bases de datos relacional es una aplicación que se encarga de almacenar y ofrecer fácil y rápido acceso a un conjunto de datos de
3.6. TECNOLOGÍAS USADAS 35 1 2// BOOTSTRAP 3 4import React from ’react ’; 5 6function Example () { 7return ( 8<div class =" alert alert - danger alert - dismissible 9fade show" role=" alert "> 10 <strong >Oh snap ! You got an error ! </ strong > 11 <p> 12 Change this and that and try again . 13 </p> 14 <button type ="button" class =" close " 15 data - dismiss =" alert " aria - label =" Close "> 16 <span aria - hidden =" true" >& times ; </ span> 17 </button > 18 </div> 19 ) 20 } 21 22 // REACT - BOOTSTRAP 23 24 import React , { Component } from ’react ’; 25 import Alert from ’react - bootstrap / Alert ’; 26 27 function Example () { 28 return ( 29 <Alert dismissible variant =" danger " > 30 <Alert .Heading > 31 Oh snap ! You got an error ! 32 </ Alert .Heading> 33 <p> 34 Change this and that and try again . 35 </p> 36 </Alert> 37 ) 38 } Figura 3.4: Comparativa de Bootstrap con React-Bootstrap
36 CAPÍTULO 3. PRELIMINARES 1start 2= additive 3 4additive 5= left:multiplicative "+" right : additive { return left + right ; } 6/ multiplicative 7 8multiplicative 9= left : primary "*" right:multiplicative { return left * right ; } 10 / primary 11 12 primary 13 = integer 14 /"(" additive:additive ")" {return additive ; } 15 16 integer "integer" 17 = digits :[0 -9]+ { return parseInt ( digits . join ("") , 10); } Figura 3.5: Gramática para expresiones aritméticas para PEG.js manera estructurada y ordenada, agrupándolos mediante tablas y relacionándolos entre sí. Para nuestro proyecto esta tecnología resulta de gran utilidad a la hora de almacenar información que no podemos obtener a partir de la API de DOMjudge, como las especificaciones de los problemas o el número de revisiones asociado a cada problema. 3.6.9. PEG.js PEG.js es una librería desarrollada en JavaScript para generar analizadores sintácticos o parsers [16]. Es especialmente adecuada para procesar lenguajes de programación e incluso acabar construyendo intérpretes o compiladores. Además, está basada en gramáticas de expresión de análisis sintáctico (en inglés Parsing Expression Grammar, y de ahí las siglas PEG) en vez de gramáticas LL(k)o LR(k). Al descargarnos la librería podemos instalar también un comando pegjs con el que generar analizadores sintácticos o parsers a partir de ficheros como el de la figura 3.5, el cual es un ejemplo proveniente de la documentación de PEG.js [15]. En este fichero se describe una gramática para reconocer expresiones aritméticas de números enteros que soporta el reconocimiento de sumas, multiplicaciones y paréntesis. Como podemos observar, la estructura de estos ficheros sigue una estructura muy similar a la usada normalmente para definir gramáticas, lo cual facilita enormemente el uso de la librería. No obstante, PEG.js también nos permite realizar cálculos o procesamientos con las cadenas de caracteres que reconoce cada regla usando JavaScript. De hecho, en el fichero de la figura 3.5, la regla integer hace lo siguiente:
3.6. TECNOLOGÍAS USADAS 37 1. Reconoce una cadena de caracteres con solo dígitos ([0-9]+). 2. Asigna esa cadena a una variable digits. 3. Usa esa variable en un fragmento de código en JavaScript para convertir la cadena a un número entero. Nótese que la variable digits contiene un array de caracteres en vez de un string de JavaScript, y por eso necesitamos usar el método join() para convertirlo a un string. El resto de reglas funcionan de forma parecida, pero se usan para calcular el valor de la expresión aritmética partiendo de los enteros reconocidos por la regla integer. Por ello, al terminar de procesar una expresión aritmética reconocible por la gramática que define este fichero, el analizador sintáctico devuelve el valor de dicha expresión. Para más detalles, véase la documentación [15]. 3.6.10. Jest Jest [9] es una librería implementada en JavaScript para ejecutar tests que funciona con Node.js, entre otras tecnologías. El principal atractivo de esta librería es que funciona sin tener que configurar nada y que incluye muchas funcionalidades en un solo paquete. Entre ellas se encuentran: tests, mocking de objetos, análisis de cobertura del código (code coverage), y mensajes muy informativos en caso de que un test falle (véase la figura 3.6, donde al intentar sumar 1y2se espera un 3 pero se obtiene un 4). Al ser una librería tan completa hemos decidido usarla con algunas partes de nuestro proyecto.
38 CAPÍTULO 3. PRELIMINARES Figura 3.6: Ejecución de Jest para una implementación no válida de una función de suma. El comando npm test llama internamente a Jest en este proyecto.
Capítulo 4 Organización del sistema El sistema está dividido en dos partes. Por un lado, se encuentra la aplicación web desarrollada DOMfeed y, por otro lado, el juez de programación DOMjudge. DOMfeed a su vez está compuesto por una aplicación web con la que interactúa el usuario, un servidor, una base de datos relacional y un analizador (o parser) de casos de prueba. En la figura 4.1 se muestra un diagrama explicativo de cómo el sistema está estructurado. A continuación, se detallan cada una de las partes y sus funciones principales de forma específica. Figura 4.1: Diagrama explicativo con los diferentes componentes del sistema 4.1. DOMjudge DOMjudge es la base principal de DOMfeed. Todas las funcionalidades desarrolladas en DOMfeed dependen directamente de llamadas a la API de DOMjudge [5]. La API de DOMjudge es limitada, pues no provee de todas las llamadas que 39
40 CAPÍTULO 4. ORGANIZACIÓN DEL SISTEMA se necesitan para que funcione DOMfeed. Por ello, hemos tenido que recurrir al scraping a la hora de descargar los ficheros de prueba y salidas del usuario El scraping es una técnica que sirve para extraer información de páginas web de manera automatizada. Existen ciertas herramientas que facilitan el uso del scraping, pero en este caso, ya que su uso es puntual, no se ha utilizado ninguna. Más adelante, se detalla su uso en el proyecto (ver sección 6.2.3). 4.2. Aplicación web o frontend El frontend es la parte de la aplicación con la que interactúa el usuario. Es la encargada de mostrar la información que se solicite. La aplicación está dividida en dos partes independientes según el rol del usuario: Una parte de administración, a la que accede el profesor (de ahora en adelante el ‘administrador’). Este tiene acceso a la configuración de todos los concursos. Otra parte para el alumno (de ahora en adelante el ‘usuario’), donde este puede consultar las entregas de los problemas del equipo al que pertenezca. El frontend está programado en su totalidad en React [20]. Debido a que la aplicación requiere de diferentes páginas para los distintos componentes o funcionalidades, hemos utilizado React Router [19] para facilitar el enrutado. La aplicación web requiere de múltiples peticiones al servidor para obtener todos los datos necesarios. Para ello, decidimos utilizar la librería Axios [1], la cual provee de grandes ayudas a la hora de hacer peticiones al servidor. En cuanto a la maquetación y CSS, hemos utilizado React Bootstrap [18], el cual reemplaza a Bootstrap [2] como framework. A diferencia de Bootstrap, React Bootstrap está completamente optimizado para React, permitiendo así mejoras en rendimiento y en velocidad de desarrollo. 4.2.1. Vistas del administrador Las pantallas del administrador se basan en la gestión y control de DOMfeed. El administrador tiene privilegios totales de visualización, pudiendo acceder a todas las entregas o submissions de cualquier equipo. La tarea principal del administrador es la de suministrar, para cada uno de los problemas que hay registrados en DOMjudge, una especificación válida y un número máximo de veces que un equipo puede ver la solución a una entrega para un problema determinado y concursos. En la sección 4.2.3 se explica detenidamente en qué consiste esta parte. 4.2.2. Vistas del usuario El rol principal del usuario en DOMfeed es visualizar, para cualquier problema donde DOMjudge haya mostrado un veredicto de WRONG-ANSWER, la salida
4.2. APLICACIÓN WEB O FRONTEND 41 Figura 4.2: Vista de administrador esperada frente a la salida obtenida de ejecutar la solución proporcionada por el usuario. Para ello hay que solicitar al servidor esos datos. De esta forma, una vez hecha la petición, se mostrará una pantalla explicativa donde se pueden ver los casos de entrada, la salida esperada y la salida obtenida por la solución del usuario. El caso de prueba en el que difiere la salida esperada aparece de color rojo, para indicar al usuario donde está el error en su solución del problema. Un usuario solamente puede ver los concursos a los que pertenezca, así como las entregas que haya hecho él mismo o su equipo. Además, solo podrá ver las soluciones en el caso de que tenga correcciones restantes o cuando ya ha solicitado la ayuda para dicha entrega previamente. Este punto se explicará con más detalle en la sección 6.1.4.1. Figura 4.3: Vista de usuario
48 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA Información previa Caso de prueba 1 Caso de prueba 2 ... Caso de prueba n Fin de fichero Figura 5.1: Esquema de un fichero de casos de prueba de entrada Información previa <separador> Caso de prueba 1 <separador> Caso de prueba 2 <separador> ... <separador> Caso de prueba n <separador> Fin de fichero Figura 5.2: Esquema de un fichero de casos de prueba de entrada con separadores Además, debemos tener en cuenta que los ficheros de muchos problemas tienen algo de información adicional al principio o al final del propio fichero que no pertenece a ningún caso de prueba, como muestra el esquema de la figura 5.1. La información presente al principio del fichero suele indicar el número de casos de prueba que contiene, mientras que la parte final se suele usar para indicar que no hay más casos de prueba. Por consiguiente, hemos considerado que un caso de prueba debe tener siempre un separador antes y otro después, de modo que la información del principio del fichero solo tendrá un separador después, y la del fin del fichero tendrá el separador antes de la misma (véase la figura 5.2). De esta forma podemos siempre distinguir qué parte de un fichero es un caso de prueba o no. Hasta el momento hemos visto qué criterio aplicar a la hora de poner separadores. No obstante, necesitamos también especificar qué estructura tienen los casos de prueba y la información del principio y del final del fichero. Para ello primero necesitamos introducir el concepto de token ocomponente/token léxico. Un token es una cadena de caracteres al que se le asigna un significado. Por ejemplo, la cadena ‘1234’ se puede tratar como un número entero y podríamos llamarlo un token entero. Por otro lado, la cadena ‘x’ podría ser tratada como el nombre de una variable o de una función en un lenguaje de programación y podríamos denominarlo como un token identificador. El concepto de token es fundamental para poder describir la estructura de los casos de prueba de un fichero, por lo cual decidimos incorporarlo a nuestro lenguaje. De hecho, muchos problemas de programación trabajan con entradas formadas
5.1. LENGUAJE DE ESPECIFICACIÓN 49 únicamente por números enteros. A raíz de esto, era indispensable incluir un token de tipo número entero a nuestro lenguaje de especificación. No obstante, los problemas de programación también pueden esperar cadenas de caracteres o strings, por lo que decidimos también incluir un token string. Este token serviría para representar cadenas de caracteres delimitadas por caracteres de espacio, tabuladores o fin de línea. Además, en algunos problemas parte de esas cadenas son siempre las mismas, por lo que convenía que nuestro lenguaje de especificación soportase literales. Un literal es un valor constante, como puede ser ‘1’,‘abc’ o‘hello world’ (nótese que el último ejemplo incluye un espacio, por lo que equivaldría a dos tokens string). Por último, decidimos también incluir un token para representar líneas de texto y otro para representar el fin de fichero al que llamaremos EOF (del inglés End Of File). Nuestro lenguaje además necesitaba poder reconocer expresiones formadas por secuencias de tokens separadas por espacios, pero también disyunciones de estas mismas expresiones. Por ejemplo, en algunos problemas el alumno tiene que escribir en la salida o bien un string o bien otro (por ejemplo, ‘SI’ o ‘NO’), así que decidimos incluir disyunciones en el lenguaje. Por último, incluimos la posibilidad de que una expresión se repita, o bien un número indeterminado de veces, o bien una cantidad constante. De hecho, como en la información del principio de un fichero está presente en muchas ocasiones el número de casos de prueba, decidimos añadir la posibilidad de que el número de repeticiones dependa de un token entero anterior, de modo que una especificación pueda indicar que el primer token es un entero n, y que a continuación hay ncasos de prueba. 5.1.2. Descripción del lenguaje La gramática de la figura 5.3 puede usarse como documentación del lenguaje de especificación e incluye todas las características que comentamos en la sección anterior. No obstante, es ambigua, es decir, se pueden usar sus reglas de varias formas distintas para describir una misma especificación, lo cual complica enormemente su uso a la hora de implementar un analizador o parser del lenguaje. Por tanto, hemos definido la gramática de la figura 5.4, la cual no es ambigua y describe la prioridad a la hora de aplicar unas reglas sobre otras, algo de lo que carecía la gramática de la figura 5.3. Por ejemplo, veamos la especificación <int>∧(2 * 4 + 3). La gramática ambigua (figura 5.3) no sería capaz de discernir si la expresión 2 * 4 + 3 se procesaría como (2 * 4) + 3 o2 * (4 + 3), es decir, no sabe qué operación es más prioritaria entre la multiplicación y la suma. Esto es así porque las reglas expr ::= expr + expr yexpr ::= expr * expr pueden usarse en cualquier orden. No obstante, la gramática no ambigua (figura 5.4) no presenta tal problema, pues la regla A1 ::= A2 * A1 siempre se tiene que aplicar después de la regla A0 ::= A1 + A0. Sin embargo, la gramática no ambigua es significativamente más sencilla, por lo que sigue siendo útil para documentar las características del lenguaje.
50 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA Inicio ::=S EOF Fichero de casos de prueba S::=Token Token |S S Secuencia |S ‘∥’ S Disyunción |S ‘∧’ rep Repetición |‘–––’ Separador de casos rep ::=‘*’ Número indeterminado |‘n’Número entero |‘x’Variable |‘(’ exp ‘)’ exp ::=‘n’Número entero n |‘x’Variable |exp ‘+’ exp Suma |exp ‘-’ exp Resta |exp ‘*’ exp Multiplicación |exp ‘/’ exp División |‘+’ exp Más unario |‘-’ exp Menos unario |‘(’ exp ‘)’ Expresión entre paréntesis Token ::=int |string |line |literal int ::=‘<int>’ Entero |‘<x:int>’ Entero asignado a una variable x string ::=‘<string>’ Cadena de caracteres |‘<x:string>’ line ::=‘<line>’ Línea de texto |‘<x:line>’ literal ::=‘”s”’Literal sentre comillas dobles Figura 5.3: Gramática ambigua del lenguaje de especificación
5.1. LENGUAJE DE ESPECIFICACIÓN 51 Inicio ::=Disyunción EOF Fichero de casos de prueba Disyunción ::=Secuencia ‘∥’ Disyunción |Secuencia Secuencia ::=Repetición Secuencia |Repetición Repetición ::=S ‘∧’ rep |S S::=Token |‘(’ Disyunción ‘)’ |‘–––’ Separador de casos rep ::=‘*’ Número indeterminado |exp exp ::=A3 Expresión aritmética A0 ::=A1 ‘+’ A0 Suma |A1 ‘-’ A0 Resta |A1 A1 ::=A2 ‘*’ A1 Multiplicación |A2 ‘/’ A1 División |A2 A2 ::=‘+’ A3 Más unario |‘-’ A3 Menos unario |A3 A3 ::=‘n’Número entero n |‘x’Variable |‘(’ A0 ‘)’ Expresión entre paréntesis Token ::=int |string |line |literal int ::=‘<int>’ Entero |‘<x:int>’ Entero asignado a una variable x string ::=‘<string>’ Cadena de caracteres |‘<x:string>’ line ::=‘<line>’ Línea de texto |‘<x:line>’ literal ::=‘”s”’Literal sentre comillas dobles Figura 5.4: Gramática desambiguada del lenguaje de especificación
52 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA 5.1.3. Ejemplos de especificaciones Para que el lector se familiarice con el lenguaje de especificaciones descrito en las figuras 5.3 y 5.4, conviene que presentemos algunos ejemplos sencillos. Para ejemplos más avanzados, consulte el apéndice A, el cual contiene especificaciones para problemas reales de programación. 5.1.3.1. Ejemplo 1 Empecemos por un problema que siempre tiene un único caso de prueba. Si dicho caso de prueba consiste de tres números enteros, podríamos usar la siguiente especificación: --- <int> <int> <int> --- La cual se puede abreviar de la siguiente manera: --- <int>∧3 --- Aquí <int> representa un entero cualquiera, y --- representa el separador de casos de prueba (lo que en las figuras 5.1 y 5.2 se denomina <separador>). Como mencionamos en la sección 5.1.1, un caso de prueba debe tener un separador antes y otro después. 5.1.3.2. Ejemplo 2 Veamos un uso típico de variables. Tenemos un problema donde la entrada empieza con un número nseguido de ncasos de prueba, de modo que cada uno consiste en otros dos números enteros. Una especificación válida podría ser la siguiente: <n:int> --- (<int> <int> ---)∧n Con el token <n:int>, el valor del primer entero de la entrada se guarda para poder referenciarse posteriormente con la variable n. De hecho, se usa este valor npara describir cuántos casos de prueba tiene la entrada, usando ∧n. Además, al igual que en el ejemplo anterior, cada caso de prueba queda delimitado por dos separadores, mientras que <n:int> tiene solo uno después al tratarse de información previa a los casos de prueba. 5.1.3.3. Ejemplo 3 Veamos esta vez un problema donde cada caso de prueba de la entrada es, o bien una cadena "comprar" seguida de un entero, o bien una cadena "vender" seguida también de un entero. Además, supongamos que el número de casos es indeterminado. Entonces podríamos usar la siguiente especificación: --- (("comprar" || "vender") <int> ---)∧* ("comprar" || "vender") indica que puede aparecer, al igual que dice el enunciado, tanto la cadena ‘comprar’ como la cadena ‘vender’. Por otro lado, ∧*indica que puede haber 0 o más casos de prueba.
5.1. LENGUAJE DE ESPECIFICACIÓN 53 5.1.3.4. Ejemplos problemáticos Sea un problema de programación donde tenemos un número indeterminado de casos de prueba, de modo que cada uno consiste de dos cadenas de caracteres sin espacios, y delimitamos el fin del fichero con la cadena ‘-fin fichero-’ (es un literal). Una posible especificación sería la siguiente: --- (<string> <string> ---)∧* "-fin fichero-" Esta especificación presenta un problema de indeterminismo debido a los requisitos del enunciado. A menos que ningún caso de prueba pudiera tener las cadenas ‘-fin’ y‘fichero-’, si al ir leyendo la entrada encontramos el literal ‘-fin fichero-’, no podemos asegurar que hayamos llegado al final del fichero si no seguimos leyéndolo. Esto supone un problema a la hora de procesar un fichero de entrada junto a esta especificación, pues nos fuerza a comprobar si el literal ‘-fin fichero-’ son dos cadenas de un mismo caso de prueba, y en caso contrario tenemos que volver atrás y procesarlo como un literal. Este problema de indeterminismo no solo existe entre tokens string y literales, pues un número como el 0 puede ser aceptado tanto por "0" como <int>, como ocurre en las siguientes especificaciones: --- (<int> ---)∧* "0" --- ("0" ---)∧* "0" Además, las siguientes especificaciones siempre presentan los problemas anteriores, independientemente de que los casos de prueba tengan siempre un contenido distinto al del fin de fichero: --- (<string> ---)∧* <string> --- (<int> ---)∧* <int> De hecho, podemos generalizar todos los casos anteriores. Dadas las especificaciones R,SyT, la especificación R S∧* T necesitará el mecanismo de vuelta atrás mencionado anteriormente si existe una cadena xque sea aceptada tanto por la especificación Scomo por T. Eso sí, en el caso de una especificación --- (S’ ---)∧* T, donde S′es la especificación de un caso de prueba, dejará de suponer un problema si dicha cadena xno aparece nunca como caso de prueba. Esta vuelta atrás complica enormemente el procesamiento, y como veremos en la sección 5.2, hemos decidido optar por implementar un mecanismo alternativo para resolver el indeterminismo. Si tenemos dos especificaciones S1yS2,S1acepta la cadena vacía, y una cadena xes aceptada por ambas especificaciones, a la hora de procesar la especificación S1S2optaremos por descartar la especificación S1y proseguir con S2. Este mecanismo de procesamiento, no obstante, tiene a su vez el problema de que algunas especificaciones dejan de ser aplicables a algunos ficheros. Por ejemplo, volvamos a considerar la siguiente especificación:
54 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA --- (<int> ---)∧* <int> Debido al mecanismo anterior, esta especificación solo podría describir ficheros con un único número entero, el cual serviría como fin de fichero. Por tanto, la especificación anterior equivaldría a --- <int>. Por otro lado, volvamos a considerar también la siguiente especificación: --- (<int> ---)∧* "0" Esta especificación serviría para describir cualquier fichero que consista solo de números enteros siempre y cuando el 0 solo aparezca al final. En cambio, para un fichero cuyo contenido sea 12 0 3 1 0, el primer 0 se trata como fin de fichero y se ignoran el resto de enteros, lo cual no ocurriría si hubiéramos implementado la vuelta atrás. 5.1.4. API de comprobación de la validez de una especificación: tryToParseGrammar() Para comprobar la validez de una especificación dada por un profesor, nuestra biblioteca dispone de la función tryToParseGrammar() (ver figura 5.5), la cual recibe una especificación y devuelve no solo si es válida de acuerdo a la gramática de la figura 5.4, sino también qué errores hay en caso de que no lo sea. 1function tryToParseGrammar(spec) { 2try { 3parseSpecification ( spec); 4return { valid : true, errorMessage: undefined }; 5}catch ( error ) { 6return { valid : false, errorMessage : error . message }; 7} 8} Figura 5.5: tryToParseGrammar(): Función para comprobar la validez de una especificación 5.2. Separación de los casos de prueba En esta sección se explica cómo funciona la separación de casos de prueba a alto nivel. Nos centraremos primero en la implementación de la función separateCases(), que se encarga de ir separando los casos de prueba de entrada y de salida. Posteriormente, veremos un tipo abstracto de datos llamado TokenStream que hemos creado para el tratamiento de los tokens de un fichero, y finalmente describiremos unas reglas usadas para procesar la separación de casos. No explicaremos la función separateInputCases(), pues funciona igual que separateCases(), pero separando solo los casos de prueba de entrada.
5.2. SEPARACIÓN DE LOS CASOS DE PRUEBA 55 5.2.1. Implementación de separateCases() En esta sección presentaremos dos funciones: separateCases(), que es la API que usa el servidor para separar los casos de prueba de entrada y de salida de sus respectivos ficheros. _separateCases(), que tiene la implementación real de la separación de los casos de prueba y es llamada internamente por separateCases(). 5.2.1.1. API de separación de casos: separateCases() Para empezar, los parámetros de entrada de la función separateCases() son: inputFilename: El nombre del fichero con los casos de prueba de entrada. inputSpec: La especificación que describe cómo separar los casos de prueba de entrada. outputFilename: El nombre del fichero con los casos de prueba de salida. outputSpec: La especificación que describe cómo separar los casos de prueba de salida. separator: Una cadena de caracteres que se vaya a usar para separar los casos de prueba. Es un argumento opcional, por lo que si no se especifica se usa por defecto la cadena ‘<==========>’. A continuación explicamos el código en sí de separateCases() (ver figura 5.6). En primer lugar, al tratarse de una API, se comprueba que todos los argumentos anteriormente descritos sean válidos, y que en el caso de no elegir un separador para los casos de prueba se use el separador por defecto. Después, en la función auxiliar createCasesFile(), que es llamada tanto para la entrada como para la salida, creamos una carpeta nueva donde se guardarán los ficheros con los casos de prueba separados, verificamos la especificación y construimos un TokenStream para tokenizar o separar en tokens el contenido de un fichero (ver sección 5.2.2 para más información). Si no ha ocurrido ningún error durante la llamada a createCasesFile() (por una especificación inválida, por ejemplo), se crea un diccionario para guardar el estado de las variables definidas en las especificaciones. A este diccionario nos referiremos de ahora en adelante como estado. En informática un diccionario es una colección de pares (clave, valor) cuyas claves no se repiten. La clave puede ser de cualquier tipo de datos. En nuestro caso, es de tipo ‘string’ porque corresponde al nombre de una variable. Por otro lado, los enteros y literales son los únicos tipos de datos soportados en nuestro diccionario. La necesidad de tener un diccionario es debido a que se pueden referenciar variables en las especificaciones, y un diccionario es una manera muy efectiva de guardar y consultar posteriormente su valor. Por último, tras haber realizado la separación de casos, se comprueba si ha habido algún error en el proceso en la función checkForErrors(). Finalmente, se
56 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA devuelve la ruta de los ficheros de entrada y salida separados junto con el separador usado. 1async function separateCases ( inputFilename , inputSpec , outputFilename , outputSpec , separator = undefined ) { 2checkInputs ( inputFilename , inputSpec , outputFilename , outputSpec , separator ); 3 4if (! separator ) { 5const config = await getConfiguration (); 6separator = config . default_separator ; 7} 8 9const input = await createCasesFile ( inputFilename , inputSpec , separator ); 10 const output = await createCasesFile ( outputFilename , outputSpec , separator ); 11 let currentState = new Map (); 12 13 // Separamos los casos 14 _separateCases ( input , output , currentState ); 15 checkForErrors ( input , output , currentState ); 16 17 return { 18 inputSeparatedCasesFilename : input . separatedCasesFilename , 19 outputSeparatedCasesFilename : output . separatedCasesFilename , 20 separator 21 }; 22 } Figura 5.6: Implementación de la función separateCases() 5.2.1.2. Función auxiliar: _separateCases() En esta función (véase el código en la figura 5.7), el TokenStream junto con las especificaciones se irán consumiendo basándose en unas reglas de procesamiento en la función step(). Estas reglas son descritas más adelante en la sección 5.2.3, pero, a modo de introducción, debemos mencionar que dichas reglas controlan no solo cómo se deben consumir una especificación y el TokenStream al que describe, sino también la inserción de separadores. Así pues, comenzamos con el grueso de la separación de los casos de prueba, que se trata de un bucle que termina cuando tanto la especificación de entrada como la de salida se consuman o si ha ocurrido un error inesperado. Mientras no se cumplan las condiciones previas, procesamos primero la entrada paso a paso con llamadas a step() hasta que: Se encuentre un separador. Esto indica el fin del caso de prueba actual y el comienzo del siguiente.
5.2. SEPARACIÓN DE LOS CASOS DE PRUEBA 57 Se encuentre que la especificación no es válida y, por tanto, no se puede aplicar al fichero de entrada representado por su respectivo TokenStream. Se consuma completamente la especificación, es decir, se quede vacía. Después procesaríamos la salida hasta que se diera cualquiera de las condiciones anteriores. Es decir, hasta que no se termina de separar el primer caso de prueba de salida, no se comienza con el segundo caso de prueba de entrada, y lo mismo ocurre con los siguientes casos de prueba. Las llamadas a step() se encargan de: Devolver el nuevo estado de las variables en caso de ser modificado. Devolver la especificación actualizada tras aplicarle las reglas de procesamiento. Actualizar el TokenStream, consumiendo tokens según indiquen las reglas de procesamiento. Devolver si sigue siendo válido aplicar la especificación al TokenStream. Y, finalmente, devolver si se ha encontrado el separador que indica el final del caso actual. Después de procesar la entrada, el nuevo estado actual σes usado para procesar la salida. No obstante, este segundo procesamiento crea un nuevo estado σ′que hemos decidido ignorar y, por tanto, para el siguiente caso de entrada usamos el estado σ. Este detalle es fundamental para el uso de variables comunes entre la especificación de entrada y la de salida. Implica que cualquier variable definida en la especificación de entrada puede usarse tanto en la especificación de entrada como en la de salida, pero las variables definidas en la especificación de salida solo pueden usarse en dicha especificación. Por ejemplo, veamos la siguiente especificación de entrada: <n:int> --- (<string> <int> ---)∧n En esta especificación hemos definido una variable npara definir el número de casos de prueba de entrada. Podemos volver a utilizarla en una especificación de salida como la siguiente: --- (<int> ---)∧n Nótese que en esta especificación no hemos definido la variable nen ningún sitio. De hecho, podemos intentar redefinir una variable en la especificación de salida previamente definida en la de entrada, pero esto originará un comportamiento a primera vista inesperado. Lo que ocurrirá es que cualquier redefinición de dicha variable solo afectará al caso de prueba actual, y fuera de este volverá a tener el valor anterior. De hecho, a continuación se muestran varias especificaciones de salida que intentan redefinir ny a qué especificación equivale cada una teniendo en cuenta el comportamiento que hemos descrito anteriormente:
64 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA 7. Si la especificación es una disyunción S1∥S2, no podemos dar un paso de procesamiento solo con S1(es decir, S1,In, σ −→) y se puede dar un paso de procesamiento S2,In, σ −→ S′ 2,In′, σ′, descartamos S1y seguimos el procesamiento con S′ 2,In′yσ′. S2,In, σ −→ S′ 2,In′, σ′S1,In, σ −→ S1∥S2,In, σ −→ S′ 2,In′, σ′ 8. Si la especificación es una secuencia S1S2, la especificación S1no acepta la cadena vacía y se puede dar el paso de procesamiento S1,In, σ −→ S′ 1,In′, σ′, entonces seguimos el procesamiento con S′ 1S2,In′yσ′. ¬nilP(S1, σ)S1,In, σ −→ S′ 1,In′, σ′ S1S2,In, σ −→ S′ 1S2,In′, σ′ 9. Si la especificación es una secuencia S1S2, la especificación S1acepta la cadena vacía y se puede dar el paso de procesamiento S2,In, σ −→ S′ 2,In′, σ′, entonces descartamos la especificación S1y seguimos el procesamiento con S′ 2,In′yσ′. Puede ocurrir que se hubiera podido dar el paso de procesamiento S1,In, σ −→ S′ 1,In′′, σ′′, y que por no haberlo hecho la especificación S1S2 deje de ser aplicable a In tras haber dado varios pasos con la especificación S′ 2. No obstante, esto complicaría sustancialmente el procesamiento, por lo que hemos decidido mantener esta limitación. Véase la sección 5.1.3.4 para ver ejemplos de este tipo de especificaciones. nilP(S1, σ)S2,In, σ −→ S′ 2,In′, σ′ S1S2,In, σ −→ S′ 2,In′, σ′ 10. Si la especificación es una secuencia S1S2, la especificación S1acepta la cadena vacía, y no se puede dar el paso de procesamiento solo con S2(es decir, S2,In, σ −→), pero sí se puede dar el paso S1,In, σ −→ S′ 1,In′, σ′, entonces seguimos el procesamiento con S′ 1S2,In′yσ′. nilP(S1, σ)S1,In, σ −→ S′ 1,In′, σ′S2,In, σ −→ S1S2,In, σ −→ S′ 1S2,In′, σ′ 11. Si la especificación es S∧∗(es decir, la especificación Saparece cero o más veces seguidas), probamos si se puede dar el paso de procesamiento SS∧∗,In, σ −→ S′,In′, σ′. Si es así, entonces seguimos el procesamiento con S′,In′yσ′. Usamos la especificación SS∧∗porque así comprobamos si se puede aplicar S al TokenStream In, y este proceso se repetirá todas las veces que podamos seguir aplicando S. SS∧∗,In, σ −→ S′,In′, σ′ S∧∗,In, σ −→ S′,In′, σ′
5.3. VERIFICACIÓN DEL CORRECTO FUNCIONAMIENTO DEL ANALIZADOR65 12. Si la especificación es S∧exp, el valor de exp dado el estado σes nynes estrictamente mayor que 0, probamos si se puede dar el paso de procesamiento SS∧(n−1),In, σ −→ S′,In′, σ′. Si es así, entonces seguimos el procesamiento con S′,In′yσ′. Usamos la especificación SS∧(n−1) porque así comprobamos si se puede aplicar una vez Sal TokenStream In y este proceso se repetirá otras n−1veces, cuando tengamos la especificación S∧0(ver siguiente regla). n=evaluate(exp, σ)n > 0SS∧(n−1),In, σ −→ S′,In′, σ′ S∧exp, In, σ −→ S′,In′, σ′ 13. Si la especificación es S∧exp y el valor de exp dado el estado σes 0, se consume la especificación sin alterar ni el TokenStream In ni el estado σ. evaluate(exp, σ)=0 S∧exp, In, σ −→ ε, In, σ 14. Si la especificación es --- (un separador), entonces consumimos la especificación y escribimos un separador en el TokenStream In′. No alteramos el estado σ. In′=writeSeparator(In) –––,In, σ −→ ε, In′, σ 5.3. Verificación del correcto funcionamiento del analizador Por último, tras la implementación de la separación de casos y del lenguaje de especificaciones generado, decidimos incluir la biblioteca Jest [9] para generar conjuntos de tests. De esta forma podemos asegurar que ambas funcionalidades se ejecutan sin errores. En la figura 5.8 podemos observar que tenemos 11 suites incluyendo un total de 129 tests que se han pasado con éxito. La mayor parte de estas pruebas se centran en el procesamiento de tokens, más en concreto, en la clase TokenStream, debido a su complejidad. En menor medida, se han comprobado tanto las reglas utilizadas para el procesamiento de los ficheros como la gramática utilizada para especificarlos.
66 CAPÍTULO 5. ANALIZADOR DE CASOS DE PRUEBA Figura 5.8: Resultado de ejecutar los tests del analizador de casos de prueba
Capítulo 6 Servidor principal: frontend y backend En este capítulo presentaremos el funcionamiento del frontend. Explicaremos una a una todas las páginas de las que se compone la aplicación y cómo se usan, separándolas en páginas del administrador y páginas del usuario. Veremos que algunas de las vistas son comunes para ambos, eliminando ciertas restricciones para los administradores. Al final de la sección explicaremos el frontend, donde se explicarán algunos aspectos del código que puedan ser de interés. También veremos detenidamente la parte del backend. Explicaremos cómo está desarrollado, los algoritmos utilizados, las llamadas a la API de DOMjudge, cómo se transforman y se tratan los datos de las diferentes llamadas y cómo se está haciendo el scraping necesario. De la misma manera, también se explicará nuestra API a la que se accede desde el frontend y los requisitos o permisos necesarios para la obtención de los diferentes datos. La explicación de las diferentes páginas junto con su código será de una manera progresiva y continua, es decir, iremos siguiendo los diferentes pasos que un usuario debería ir haciendo desde el primer momento en que accede a la aplicación hasta que se desconecta de ella. 6.1. Frontend Como ya explicamos en la sección 4.2), el frontend es la parte visual de DOMfeed con la que el usuario interactúa. Se compone de dos partes: la parte del administrador, orientada a la gestión y control, y la parte del usuario, orientada a la consulta de las entregas de los problemas con resultado erróneo. Como se explicó anteriormente, el frontend está programado con React como framework de JavaScript, React-Bootstrap para encapsular Bootstrap en componentes nativos de React para optimizar y simplificar el código, y posteriormente bibliotecas como Axios para hacer las correspondientes llamadas a la API de DOMfeed. 67
68 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND 6.1.1. Inicio de sesión La página del inicio de sesión es la primera que aparecerá al acceder a DOMfeed, a no ser que haya iniciado sesión previamente en la aplicación. Es una página común tanto para el administrador como para el usuario, y cuya única función es permitir que un usuario pueda acceder DOMfeed. La página está compuesta de un formulario sencillo con dos campos: el primero para introducir un usuario y el segundo una contraseña. Se puede observar que no existe ninguna opción de registro, pues todo usuario que tiene una cuenta en DOMjudge, también la tiene en DOMfeed automáticamente. Es el profesor correspondiente quién debe dar de alta al usuario en DOMjudge. Si ocurre algún error realizando el inicio de sesión, aparecerá una notificación con el mensaje de error correspondiente. En la figura 6.1 se muestra un ejemplo. La parte de la izquierda representa la página de inicio de sesión y la parte derecha se muestra el mensaje de error al introducir incorrectamente las credenciales. Figura 6.1: Página de inicio de sesión 6.1.2. Cabecera Al iniciar la sesión, el usuario accederá a la página principal. Para los usuarios administradores será la página de problemas (ver sección 6.1.3.1), mientras que para el resto de usuarios será la página de entregas (sección 6.1.3.3), y estará seleccionado el primer concurso al que pertenezca el usuario. De esta manera, el usuario podrá acceder a la cabecera de la aplicación, la cual es común a todos los usuarios y a todas las páginas. Mediante la cabecera, el usuario podrá realizar 3 acciones: Ir a la página principal, la cual depende del tipo de usuario, como se ha explicado anteriormente. Cerrar la sesión, y volver a la página de inicio de sesión (sección 6.1.1).
6.1. FRONTEND 69 Seleccionar un concurso, para así poder acceder a toda su información relevante (problemas, entregas, correcciones). Toda esa información va ligada al concurso en el que se encuentre el usuario. Un mismo problema puede estar en varios concursos, y un mismo equipo puede estar también en varios concursos, por lo que para un mismo problema se van a mostrar entregas diferentes para un mismo equipo, dependiendo del concurso previamente seleccionado. Para ir a la página principal se dispone de un botón Home a la izquierda del todo, para cerrar la sesión se dispone de otro botón más a la derecha con el texto Cerrar sesión, y finalmente, para seleccionar el concurso, a la derecha del todo existe un menú desplegable que contiene todos los concursos donde el usuario que ha iniciado sesión tiene permisos para acceder. Para seleccionar un concurso simplemente hay que desplegar el control de los concursos y hacer clic sobre el concurso deseado. En la figura 6.2 se muestra una captura de la cabecera de DOMfeed. Figura 6.2: Cabecera con el concurso demo seleccionado 6.1.3. Páginas del administrador Las diferentes páginas del administrador están orientadas a la gestión de DOMfeed. Desde ellas, el administrador podrá configurar los problemas, añadiendo las especificaciones correspondientes y el número de correcciones de la solución que el usuario tendrá disponibles para dicho problema. El administrador también podrá ver todas las entregas de todos los grupos pertenecientes al concurso previamente seleccionado, así como sus correcciones o casos fallidos. 6.1.3.1. Vista de problemas Esta página es exclusiva de los administradores y se accede a ella tras seleccionar el concurso deseado en el desplegable destinado a ello. Tras elegir el concurso se mostrarán todos los problemas correspondientes. Es una página tanto informativa como de gestión, pues es el lugar donde el administrador especifica el número de correcciones máximas para un problema y donde también se accede a la página de especificación. La página está compuesta de una tabla donde se muestran todos los problemas relativos al concurso seleccionado. Esta tabla contiene una breve información de cada problema que explicamos a continuación: id, es el identificador del problema. Es único para cada problema. Se crea automáticamente en DOMjudge al registrarse un problema nuevo.
70 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND Problema, corresponde al nombre del problema. Suele ser un nombre descriptivo que indique la temática del problema. Abbr, una abreviación para el nombre del problema. Especificación, al hacer clic en la celda correspondiente al problema, se accederá a la página de definición de las especificaciones. Se explica detenidamente en la siguiente sección (6.1.3.2). Número de correcciones, corresponde al número de correcciones disponibles que tiene el usuario o equipo para dicho problema. Aparte de la tabla de los problemas, en la parte superior de la página se puede ver otro campo con el texto Número de correcciones disponibles para este concurso, que indica el número de correcciones máximas que un usuario o equipo tiene para ese concurso. Como ya se ha comentado en la sección 4.2.3, este límite sirve para impedir que el usuario solicite infinitas correcciones. Como ya se ha mencionado anteriormente, en esta página el administrador puede modificar el número máximo de correcciones disponibles que tendrá un usuario para cierto problema o concurso. Para ello, el administrador tiene que hacer clic sobre el icono correspondiente, en forma de lápiz, junto al número de correcciones de dicho problema. De esta forma, se habilitará la edición en la celda de las correcciones del problema en cuestión. Cuando el administrador desee guardar el valor nuevo, debe hacer clic al icono de check que ha aparecido donde antes estaba el lápiz de edición. Si desea cancelar la edición del campo, simplemente tendrá que hacer clic sobre el icono de cruz ubicado a la izquierda del check. De manera análoga, para editar el número máximo de correcciones en el concurso, hay que hacer clic en el icono situado a la derecha del valor actual. En el caso que un problema o concurso no tenga límite de correcciones para el usuario, aparecerá indicado con el símbolo del infinito como valor actual. Para indicar que un problema o concurso no tiene límite de correcciones, simplemente hay que dejar el campo vacío a la hora de editar. También se puede acceder a la página de las especificaciones que se explica en la sección 6.1.3.2 para crear la especificación del problema seleccionado. La forma de acceder es mediante el icono correspondiente situado en la columna ‘especificación’ de la tabla de los problemas. También al hacer clic en una fila de la tabla de los problemas, el profesor puede acceder a una nueva página donde se mostrarán todas las entregas que han realizado todos los grupos del concurso. Esta nueva página se explica en detalle en la sección 6.1.3.3. Además, al hacer una edición, ya sea del número de correcciones de un problema o del número de correcciones para el concurso, se mostrará una notificación indicando si dicha operación ha tenido éxito o si, de lo contrario, ha ocurrido algún error. Todas estas funcionalidades se pueden ver reflejadas en la figura 6.3.
6.1. FRONTEND 71 Figura 6.3: Página de problemas 6.1.3.2. Vista de especificaciones Para cada problema existe una página de especificaciones, cuya función es añadir las dos especificaciones de los casos de prueba, una para el fichero de entrada y otra para el fichero de salida. Estas especificaciones son necesarias para poder separar los casos de prueba y así poder obtener una corrección para las entregas de ese mismo problema. Al escribir una especificación, esta se analiza continuamente, obteniéndose así una salida en tiempo real de la compilación de la especificación introducida. En el caso de que la especificación introducida sea incorrecta, la compilación muestra si existe un error en la sintaxis de la especificación introducida o si, en su defecto, la sintaxis es correcta, pero ocurrió un error a la hora de usar la especificación para generar el fichero de salida tras dividir los casos de prueba. Por otra parte, en el caso de que la especificación introducida sea correcta, se mostrará un ejemplo de cómo queda un fichero de entrada o salida separado utilizando la especificación introducida. Esta página dispone de 4 componentes principales: dos campos de texto editables para escribir las especificaciones de los problemas y dos campos de texto no editables donde se muestra la salida de la compilación de los mismos. Cada vez que se edita una de las especificaciones se muestra el resultado de la compilación de dicha especificación en el campo de texto correspondiente a dicha especificación. Este campo se coloreará de verde o de rojo en función de si la especificación introducida es correcta o no. Esto se puede ver claramente en la figura 6.4. Además, al final de la página se dispone de dos botones: Uno para volver a la página de problemas.
72 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND Otro para guardar y aplicar la especificación, el cual solo tendrá efecto si ambas especificaciones son correctas. Finalmente, al guardar las especificaciones, aparecerá en la parte superior de la página una notificación que indica si la operación se ha realizado con éxito. Figura 6.4: Página de especificaciones 6.1.3.3. Vista de entregas En esta página el administrador puede visualizar las entregas que han realizado todos los grupos para un problema y concurso concreto. Es una vista meramente informativa donde la única interacción que existe en la página es hacer clic en una entrega para acceder a la siguiente página de correcciones. La página consta de una tabla central, similar a la tabla de los problemas, que contiene todas las entregas de todos los grupos o usuarios para el problema seleccionado. La información de la tabla es la siguiente: id, identificador de la entrega. Es único para cada entrega. Se crea automáticamente en DOMjudge en el momento que un usuario realiza una entrega a un problema. Hora, muestra a qué hora el usuario ha realizado la entrega del problema en DOMjudge. Problema, corresponde al nombre del problema. Equipo, indica qué equipo ha realizado la entrega. Un equipo puede tener muchos usuarios. Lenguaje, en qué lenguaje de programación se ha programado el código.
6.1. FRONTEND 73 Resultado, muestra el estado de error resultado de ejecutar el problema en DOMjudge con los casos de prueba correspondientes. En este proyecto, solo estamos contemplando las entregas que DOMjudge haya valorado con WRONG-ANSWER. El resto de valoraciones se están ignorando, pues no entran dentro del alcance de DOMfeed, ya que en esos casos no se podrá generar una retroalimentación detallada del caso de prueba que ha fallado. Esto significa que solamente se está mostrando las entregas donde la salida de DOMjudge sea WA, es decir, WRONG-ANSWER. En la figura 6.5 podemos ver un ejemplo. La forma en la que se accede a la corrección de la entrega es la que explicamos a continuación. El administrador debe hacer clic sobre la fila de la tabla correspondiente a la entrega que desee ver. Automáticamente se mostrará la siguiente página, que explicamos en la sección 6.1.5 Figura 6.5: Página de entregas para administradores 6.1.4. Páginas del usuario Las páginas del usuario están destinadas a mostrar información sobre las entregas realizadas con resultado WRONG-ANSWER. Es por ello que el usuario no tiene acceso a la página de problemas como tiene el administrador, ya que esta es una página dedicada a la gestión. Las únicas funcionalidades que tiene el usuario en DOMfeed es ver el listado de entregas realizado y solicitar las correcciones sobre dichas entregas, siempre que se puedan solicitar. 6.1.4.1. Vista de entregas La vista de entregas del usuario es prácticamente igual a la explicada anteriormente en la sección del administrador. Las diferencias que encontramos son descriptivas, tal y como explicamos a continuación. La página está compuesta de una tabla central donde se muestran todas las entregas que ha realizado el alumno o grupo para cualquier problema. Esto ya es
80 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND 1import Container from ’react - bootstrap / Container ’ 2import VistaProtegida from ’@/ components / VistaProtegida ’ 3 4export default function Layout () { 5 6return ( 7<VistaProtegida > 8<Container fluid> 9... 10 </ Container > 11 </ VistaProtegida > 12 ) 13 } Figura 6.12: Componente Layout general 1import { Navigate , useLocation } from ’react - router - dom ’ 2import { useAuth } from ’@/ hooks / useAuth ’ 3 4const VistaProtegida = ({ children }) => { 5const auth = useAuth () 6const location = useLocation () 7const milliseconds = new Date () . getTime () 8 9// Si el usuario no ha iniciado sesion o si esta ha caducado 10 if (! auth . user || auth .user . hasta < milliseconds ) { 11 return <Navigate to=’/ login ’ state ={{ from: location }} /> 12 } 13 14 return children 15 } 16 17 export default VistaProtegida Figura 6.13: Componente vista protegida el usuario inicie sesión correctamente, se le volverá de redirigir automáticamente a la página a la que quería acceder inicialmente. Para implementar páginas de acceso restringido, hemos creado el componente VistaProtegida. Este componente envuelve todas las rutas de la aplicación, realizando una comprobación de autenticación cada vez que un usuario cambia de ruta en DOMfeed. Para ello, el componente VistaProtegida hace uso del hook useAuth que hemos explicado en la sección anterior. El componente es muy sencillo, simplemente recupera el objeto auth del contexto de la aplicación mediante el hook useAuth. Ese objeto contiene los datos referentes al usuario que ha iniciado sesión. En el caso de que no exista el objeto del usuario o si el tiempo que el usuario lleva logueado ha excedido el tiempo máximo permitido, entonces se le redirige a la vista de inicio de sesión. Se puede ver el código referente a este apartado en las figuras 6.12 y 6.13.
6.1. FRONTEND 81 1<Route path=’/ problems ’ element ={< Layout />}> 2<Route index element ={< Problems />} /> 3<Route path=’: contestId / ’ element ={ < Problems / >} /> 4... 5</Route> Figura 6.14: Ejemplo de enrutado 6.1.6.3. Notificaciones En DOMfeed hay algunas páginas en las cuales se muestran notificaciones tras realizar una acción. Son algunas de las pantallas de gestión del administrador: pantalla de problemas y pantalla de especificaciones. Con la idea de que las notificaciones sean un componente general y único para toda la aplicación con el objetivo de que el código no se repita en cada pantalla donde se necesite una notificación, hemos creado un hook para mostrar las notificaciones de una forma sencilla en todas las páginas en las que se necesite. La forma que decidimos para realizar los componentes de notificación es crear un contexto a nivel de enrutado. En la figura 6.14 podemos ver una parte del enrutado de DOMfeed. Como se puede observar, existe una ruta padre /problems que renderiza del componente <Layout/>. Dentro de dicha ruta tenemos las rutas hijas, que serían, por ejemplo de la siguiente forma /problems/:contestId/ donde contestId corresponde a un número entero positivo. Esta ruta concreta renderiza el componente <Problems/>. El enrutado descrito funciona de la siguiente manera. Cuando un usuario accede a la ruta /problems/3 para continuar con el ejemplo, se renderiza el componente <Layout/>. Dicho componente renderiza a su vez otro componente que se llama <Outlet/>. En esencia el componente <Outlet/> sería sustituido por el componente que renderice la ruta hija del padre, en este caso <Problems/>. El componente <Outlet/> acepta un parámetro context el cual se utiliza para crear un contexto a nivel de Outlet tal y como habíamos mencionado anteriormente. La creación de un contexto es lo que permite, tal y como vimos en la sección anterior, poder utilizar un objeto en cualquiera de los componentes hijos. En este caso, al ser un contexto que proporciona el propio componente <Outlet/> no hay que crearlo manualmente como hicimos con el contexto del usuario (ver sección 6.1.6.1). Simplemente se debe pasar el objeto a compartir como valor del parámeto context en el componente <Outlet/> y posteriormente utilizarlo en el componente que renderice Outlet independientemente de cual sea. En la figura 6.15 se muestra un ejemplo de uso, donde se crea un estado que sirve mostrar las notificaciones de la aplicación. A nosotros lo que nos interesa es que se pueda modificar el valor a mostrar en la notificación en cualquier componente que se renderice. Es por eso que pasamos el método setNotification como valor con contexto. La forma de utilizar las notificaciones en un componente que las necesite sería la mostrada en la figura 6.16. Para ello es necesario importar el custom hook useNotification. El código de dicho hook esta mostrado en la figura 6.17. Es un hook muy simple, el cual devuelve el objeto useOutletContext que devuelve el
82 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND 1import { useState } from ’react ’ 2import Container from ’react - bootstrap / Container ’ 3import Header from ’@/ components / header / Header ’ 4import Notificacion from ’@/ components / Notificacion ’ 5import {Outlet } from ’react -router - dom ’ 6import VistaProtegida from ’@/ components / VistaProtegida ’ 7 8export default function Layout () { 9const [alert ,setAlert] = useState ({ type : ’’, message: ’’ }) 10 11 const setNotification = notification => { 12 setAlert(notification) 13 setTimeout (() => { 14 setAlert({ type: ’’, message: ’’ }) 15 }, 3000) 16 } 17 18 return ( 19 <VistaProtegida > 20 <Container fluid> 21 <Header /> 22 <Notificacion alert={ alert } /> 23 <Outlet context ={{ setNotification }} /> 24 </ Container > 25 </ VistaProtegida > 26 ) 27 } Figura 6.15: Contexto de las notificaciones
6.2. BACKEND 83 1 2 3export default function Problem ({ contestId , id , newValue }) { 4const { setNotification } = useNotification () 5 6problemServices . setProblemMaxAttempts ( contestId , id , newValue ) 7.then (() => { 8setNotification ({ type: ’success’, message: ’Se ha actualizado el numero de correcciones ’ }) 9}) 10 .catch ( err => { 11 console . error ( err . response . data ) 12 setNotification ({ type: ’danger’, message: ’No se ha podido actualizar el numero de correcciones ’ }) 13 }) 14 } 15 16 return ( 17 <tr role=’button’ className = ’table - row ’ {... props }> 18 <td className =’fw - normal ’ onClick ={() => onClick (id) }>{ id } </ td > 19 ... 20 </tr > 21 ) 22 } Figura 6.16: Ejemplo de uso de las notificaciones 1import { useOutletContext } from ’react - router - dom ’ 2 3export function useNotification () { 4return useOutletContext() 5} Figura 6.17: Hook useNotification contexto del Outlet, en este caso el método setNotification para poder editar la notificación. 6.2. Backend El servidor o backend es la parte de DOMfeed que se encarga de procesar las peticiones y los datos obtenidos de DOMjudge, tal y como explicamos anteriormente en la sección 4.3. En esta sección explicaremos las partes más importantes del backend y mostraremos ejemplos de cómo se han programado las partes más relevantes en caso que fuera necesario. Además, explicaremos todas las funciones que ofrece la API de DOMfeed indicando los parámetros que espera y los datos que devuelve al cliente. En caso de que fuera necesario, se explicará cómo se ha desarrollado el código para el cálculo de los datos solicitados. Es importante men-
84 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND cionar que todas las peticiones a la API de DOMjudge se realizan con un usuario y contraseña administrador codificado en base64 y pasado en la petición mediante la cabecera Authorization. Recordemos que el servidor está desarrollado en su totalidad en Node.js [12], utilizando Express [7] para crear la API necesaria para la solicitud de datos al servidor por parte del frontend, y mysql2 como biblioteca necesaria para conectar e interactuar con la base de datos. 6.2.1. Base de datos Como ya se ha mencionado en apartados anteriores, DOMfeed tiene una base de datos propia, donde se almacena la información que no se puede obtener mediante la API de DOMjudge, pero es necesaria para las funcionalidades de la aplicación. Dado que la mayor parte de la información se encuentra en DOMjudge, la base de datos por sí sola posee muy poca información, y las relaciones entre las tablas están limitadas. La información almacenada es la siguiente: El número de correcciones disponibles para un concurso. El número de correcciones disponibles para un problema. Las especificaciones de los problemas. Las entregas cuya retroalimentación se ha solicitado. 6.2.1.1. Estructura de la base de datos La base de datos está compuesta por cuatro tablas, las cuales se explican detalladamente a continuación. En la figura 6.18 se representa un esquema de cómo están relacionadas las diferentes tablas. enabled_submissions En esta tabla se almacenan todas aquellas entregas que han sido habilitadas por un usuario para ser visualizadas mediante el canje de una corrección. Todas las entregas que se encuentran en esta tabla se consideran habilitadas y podrán ser visualizadas por el usuario que la haya canjeado. Por otra parte, aquellas entregas que no se encuentren en esta tabla solo podrán ser visibles por usuarios administradores. contest_id: Un número entero que simboliza el identificador del concurso al cual pertenece la entrega que se habilita. Este valor se corresponde con el identificador que tiene el concurso en DOMjudge. submission_id: Un número entero que simboliza el identificador de la entrega que se habilita. Este valor se corresponde con el identificador que tiene la entrega en DOMjudge.
6.2. BACKEND 85 Figura 6.18: Esquema de la base de datos problem_id: Un número entero que simboliza el identificador del problema al cual pertenece la entrega que se habilita. Este valor se corresponde con el identificador que tiene el problema en DOMjudge. Además, tiene una relación con el campo problem_id de la tabla problem_specifications, dado que cada problema habilitado debe estar asociado a una especificación. team_id: Un número entero que simboliza el identificador del equipo que ha habilitado la entrega. Este valor se corresponde con el identificador que tiene el equipo en DOMjudge. date: Fecha que indica el momento en que se habilitó la entrega. Es importante señalar que solo con los campos problem_id ysubmission_id ya se puede identificar la entrega, pero se añade a la clave el contest_id, dado que un mismo problema puede estar presente en varios concursos, pero una corrección podría estar habilitada para únicamente uno de ellos. problem_specifications En esta tabla se almacenan las especificaciones de los problemas introducidas por los profesores mediante los siguientes campos: problem_id: Un número entero que simboliza el identificador del problema cuya especificación se almacena. Este valor se corresponde con el identificador que tiene el problema en DOMjudge. input: Una cadena variable de 500 caracteres que simboliza la especificación para los ficheros de entrada correspondientes al problem_id
86 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND output: Una cadena variable de 500 caracteres que simboliza la especificación para los ficheros de salida correspondientes al problem_id max_contest_attempts En esta tabla se almacena el número de correcciones disponibles para cada concurso. Se considera que cualquier concurso que no se encuentre en esta tabla no tiene un número definido de correcciones, por lo que se considerará que este número es infinito. contest_id: Un número entero que simboliza el concurso. Este valor se corresponde con el identificador que tiene el concurso en DOMjudge. max_attempts: Un número entero que se corresponde con el número de correcciones asignado al concurso. Este valor puede tomar el valor NULL, en caso de que el concurso tenga una cantidad infinita de intentos. max_problem_attempts En esta tabla se almacena el número de correcciones disponibles para cada problema. Se considera que cualquier problema que no se encuentre en esta tabla no tiene un número definido de correcciones, por lo que se considerará que este número es infinito. contest_id: Un número entero que simboliza el concurso al que pertenece el problema. Este valor se corresponde con el identificador que tiene el concurso en DOMjudge. problem_id: Un número entero que simboliza el el problema. Este valor se corresponde con el identificador que tiene el problema en DOMjudge. max_attempts: Un número entero que se corresponde con el número de correcciones asignado al problema. Este valor puede tomar el valor NULL, en caso de que el problema tenga una cantidad infinita de intentos. Al igual que con los identificadores de entregas, el campo problem_id ya es suficiente para identificar a un problema, pero se añade a la clave el campo contest_id, debido a que un mismo problema puede estar presente en varios concursos, y se puede llegar a especificar un número de intentos para un problema en función del concurso en el que se encuentre. 6.2.1.2. Acceso a la base de datos Para el acceso a la base de datos se ha utilizado la librería mysql2 de Node.js. Mediante esta librería se crea un pool de conexiones, el cual se encarga de suministrar conexiones a la base de datos para las peticiones de los usuarios. En la figura 6.19 se muestra un ejemplo de creación de un pool de conexiones utilizando la librería antes mencionada.
6.2. BACKEND 87 1import mysql from ’mysql2’ 2 3const pool = mysql . createPool ({ 4host: ’localhost ’, 5user: ’root ’, 6password: ’’, 7database: ’domfeed’ 8}) 9 10 const promisePool = pool . promise () 11 12 export { promisePool } Figura 6.19: Creación del pool de conexiones Para hacer las peticiones a la base de datos se escribe una consulta SQL parametrizada, donde cada uno de los parámetros correspondientes será analizado internamente y posteriormente sustituido en el lugar que corresponda para así sanear y evitar ataques de inyección SQL. Cada una de las consultas a la base de datos está separada en un método individual que sigue el esquema mostrado en la figura 6.20. 1import { promisePool } from ’ ../../ utils / db_connection . js ’ 2 3const setContestMaxAttempts = ( contestId , attempts ) => { 4return promisePool . query (‘ 5INSERT INTO max_contest_attempts ( contest_id , max_attempts ) 6VALUES ( ? , ? ) 7ON DUPLICATE KEY UPDATE max_attempts = ?‘, [ contestId , attempts , attempts ]) 8} Figura 6.20: Ejemplo de consulta SQL utilizando el pool de conexiones 6.2.2. API de DOMfeed Como ya mencionamos en la sección 4.3.2, DOMfeed dispone de una API realizada mediante la biblioteca Express.js [7]. La API es la base de DOMfeed pues todas las peticiones del usuario, tanto para solicitar datos como para modificarlos se realizan a través de esta API. Todos los endpoints de la API siguen la estructura que podemos ver en la figura 6.21. 6.2.2.1. Midlewares Una parte muy importante de las peticiones al servidor son los middlewares. Los middlewares son funciones intermedias que interceptan las peticiones del usuario antes de llegar a su endpoint destino para realizar algún trabajo. Posteriormente
88 CAPÍTULO 6. SERVIDOR PRINCIPAL: FRONTEND YBACKEND 1router . post (’/: contestId / maxAttempts ’, express . json () , (req , res , next ) => { 2const { contestId } = req . params 3const { maxAttempts } = req . body 4 5contestHelper . setMaxAttempts ( contestId , maxAttempts ) 6.then (() => res . status (200) .end ()) 7.catch ( err => next (err)) 8}) Figura 6.21: Estructura de los endpoints de la API de DOMfeed pueden hacer que la petición siga su curso o devolver una respuesta al usuario. Mediante estas funciones se pueden modificar los parámetros de la petición, añadir o borrar datos del cuerpo, etc. En DOMfeed estamos utilizando un middleware que se ejecuta en cada una de las solicitudes del usuario para extraer la información de un token de JWT que se envía en la cabecera de las peticiones en el campo Authorization. Ese token se crea y se envía al frontend en el momento en el que inicia sesión un usuario. Una vez extraído el token se guarda como objeto en el request de la petición. El código se puede ver en la siguiente figura 6.22. 1const extractToken = ( request , response , next) => { 2const token = request . get (’authorization ’) 3const extractToken = ! token || ! token . toLowerCase () . startsWith ( ’ bearer ’) ? null : token . substring (7) 4request . token = extractToken 5 6next () 7} Figura 6.22: Middleware para extraer el token El token JWT contiene información acerca del usuario que ha realizado la petición: los roles de los que forma parte, identificador, username y expiración del token. Al enviarse de vuelta ese token tras cada llamada a la API de DOMfeed, se comprueba que los parámetros de la petición correspondan a los permisos que tiene el usuario que vienen introducidos en dicho token. Imaginemos que un usuario solicita las entregas realizadas para el concurso con identificador 5, pero ese usuario en la información recibida del token dice que pertenece al concurso 3. Entonces se producirá un error de permisos bloqueando la solicitud. Esta funcionalidad, está realizada mediante otro middleware llamado verifyTokenAndRequestPermissions que se ejecuta en todas las peticiones. 6.2.2.2. Obtención de datos de DOMjudge Para obtener los datos de DOMjudge se ha utilizado la librería Axios [1] la cual nos ayuda a realizar peticiones a servidores de una manera sencilla.
6.2. BACKEND 89 Concursos por equipos La llamada a GET /api/team/:teamId/contests sirve para obtener todos los concursos a los que pertenece un equipo. Sin embargo, la API de DOMjudge no contempla esta petición, si no que, por el contrario, solamente se pueden obtener los equipos a los que pertenece un concurso. Es por ello que a partir de los datos que obtenemos de DOMjudge, obtenemos los datos que necesita DOMfeed. Problemas por concurso Mediante la llamada GET /api/problems/:contestId se obtienen los problemas pertenecientes al concurso contestId. En este caso, la API de DOMjudge sí que dispone de un endpoint específico, por lo que no hay que hacer tratamiento especial para devolver dichos datos. Entregas por equipos Al invocar al endpoint GET /api/submissions/:contestId/:teamId de DOMfeed, se obtienen las entregas que ha realizado el equipo con identificador teamId para el concurso contestId junto con el resultado obtenido por DOMjudge. Obtener estos valores de DOMjudge no es trivial pues no existe una llamada que los proporcione directamente. Para obtenerlos hemos tenido que hacer uso de varias llamadas a la API de DOMjudge. Primero debemos realizar una llamada para obtener todos las entregas para el concurso con identificador contestId. DOMjudge no proporciona las entregas filtradas por equipos, por lo que hemos tenido que hacerlo nosotros una vez recibimos los datos. Además, se ha tenido que hacer para cada entrega ya filtrada otra llamada a DOMjudge para obtener el resultado de su ejecución. Aparte, se devuelve también si esa entrega ya ha sido solicitada por el equipo, por lo que hay que hacer otra llamada a nuestra base de datos. Entregas por problemas La llamada GET /api/submissions/:contestId/problem/:problemId devuelve las entregas pertenecientes al problema con identificador problemId para el concurso contestId. Los problemas y soluciones presentadas en el cálculo de este endpoint son los mismos que los explicados en el caso anterior. El código también es prácticamente idéntico. 6.2.2.3. Gestión del número de correcciones de problemas y concursos Número de correcciones máximo de un concurso Cómo se ha explicado previamente, los concursos pueden tener asignados un número máximo de correcciones, el cual se almacena en la base de datos. Mediante la llamada GET /api/contests/:contestId/maxAttempts se puede obtener el número máximo de correcciones del concurso con identificador contestId.
96 CAPÍTULO 7. CONCLUSIONES Y TRABAJO FUTURO usuario que difiere de la solución esperada. Al introducir la especificación de un problema, se lleva a cabo en tiempo real la comprobación de la sintaxis en la vista del administrador, así como la generación de un caso de prueba con separadores para verificar su correcto funcionamiento. 7.2. Dificultades encontradas Durante el desarrollo del proyecto nos surgieron varias dificultades, a veces derivadas del uso de tecnologías que no conocíamos previamente, y otras veces por problemas que no fuimos capaces de prever. Otro inconveniente que afectó a todo el equipo fue que casi todos tuvimos menos tiempo el primer cuatrimestre que el segundo, por lo que el desarrollo del proyecto fue más lento al principio. Además, a continuación se exponen las dificultades y problemas que afectaron a cada grupo durante el desarrollo del proyecto. 7.2.1. Grupo 1 El grupo 1 tuvo que aprender a manejar el lenguaje de gramáticas de PEG.js. Por tanto, antes de crear el lenguaje de especificación de DOMfeed, sus miembros estuvieron experimentando con gramáticas más sencillas y leyendo la documentación [15]. Al principio la función separadora de casos de prueba funcionaba de forma síncrona, pues el grupo 1 tenía un conocimiento limitado de JavaScript, y además nunca había trabajado con Node.js. Por tanto, no fue hasta el segundo cuatrimestre que consiguieron un separador de casos asíncrono. Las dificultades surgieron principalmente por no estar familiarizados con la programación dirigida por eventos característica de Node.js. 7.2.2. Grupo 2 Al iniciar el proyecto, el grupo 2 tenía muy poca experiencia trabajando con React y Node.js, por lo que se tuvo que familiarizar y aprender a utilizar dichas herramientas para la elaboración de la aplicación web. Durante el mes de diciembre, cuando el proyecto ya tenía una base sólida, DOMjudge lanzó una nueva versión de su API. Sin embargo, esta nueva actualización eliminaba el dato team_id en la respuesta de la llamada a la API con los datos del usuario. Sin este dato no se podía obtener la información de concursos y problemas del usuario, por lo que la aplicación no funcionaba correctamente. Para solucionar este problema se contactó con los desarrolladores de DOMjudge, quienes confirmaron que, efectivamente, se trataba de un error y lo solucionaron.
7.3. TRABAJO FUTURO 97 Las limitaciones de la API de DOMjudge, al no ofrecer los casos de prueba así como la salida de un usuario a un problema determinado, nos provocaron la búsqueda de alternativas. Llegamos a la conclusión de que la única forma de poder obtener los ficheros de una manera automatizada era mediante scraping. Hubo que aprender algunos conceptos del scraping para poder utilizarlo correctamente. La API de DOMjudge no siempre proporciona los datos de la forma en la que los necesitamos, por lo que hemos tenido que procesar muchos de los datos obtenidos. En algunos casos se han tenido que hacer hasta tres llamadas a la API de DOMjudge para obtener todos los datos necesarios para construir el objeto que necesitamos. 7.3. Trabajo futuro Los objetivos iniciales han sido alcanzados. No obstante, creemos que el proyecto tiene capacidad de mejora, por lo que a continuación se exponen algunas ideas para ello: 7.3.1. Lenguaje de especificación Introducir un token de fin de línea: En el enunciado de muchos problemas de programación se indica, por ejemplo, que un caso de prueba tiene dos líneas, donde la primera tiene un entero y la segunda tiene varios. Ahora mismo, nuestro lenguaje de especificación tiene dos formas de describir casos de prueba como el anterior: usando tokens <int> o tokens <line>. Sin embargo, no hay ninguna manera de especificar que una línea tiene un solo entero. Es por ello que consideramos útil que se pueda especificar dónde acaba y, por tanto, empieza la siguiente línea usando un nuevo token. Introducir un token para números reales: Consideramos que por completitud debería añadirse un token para números reales, pues ya hay uno para números enteros. Ahora mismo una especificación tendría que usar un token string para reconocer un número real. 7.3.2. Implementación de la separación de casos de prueba Reducir el consumo de memoria del TokenStream: Para crear un TokenStream, es necesario haber leído todo el fichero con los casos de prueba y pasarle un string a la constructora. Esto puede llegar a incurrir en un gran consumo de memoria para ficheros inusualmente grandes, y además supone que no podemos tokenizar el fichero hasta haberlo leído completamente. Se sugiere buscar una manera sencilla de ir leyendo el fichero por partes sin complicar excesivamente la lógica de las funciones que interactúan directamente con la implementación de un TokenStream.
98 CAPÍTULO 7. CONCLUSIONES Y TRABAJO FUTURO Escribir más pruebas o tests:Al tener una especificación que no se pueda aplicar a un fichero de casos de prueba, se imprime un mensaje de error. Este mensaje debería imprimir la fila y la columna correcta del fichero donde se encontró el error, pero no se está verificando que este comportamiento funcione todavía. Además, convendría realizar más pruebas que cubran las reglas de procesamiento y la separación de casos, pues la cobertura de las pruebas existentes no supera más del 60 % del código en ambos casos. 7.3.3. Frontend Se podría valorar la posibilidad de proporcionar el código del usuario que ha propiciado la salida errónea. Existe un endpoint en la API de DOMjudge que devuelve el código entregado como solución a un problema para cierta entrega. Las tablas de la aplicación actualmente no tienen ningún tipo de paginación, por lo que en el momento en el que un concurso tenga muchos problemas o un problema tenga muchas entregas, habrá un problema grave de rendimiento. Sería interesante añadir una paginación a las tablas de veinte filas, por ejemplo. Actualmente, el frontend no contiene ningún test para comprobar funcionalidades y evitar que estas se rompan al añadir nuevas o modificar existentes. Se podría utilizar la biblioteca Cypress [4] para realizar este trabajo. Existen ciertas páginas en DOMfeed que tienen una apariencia demasiado simple como es la página 404. Podría definirse una hoja de estilos para estas páginas. Se podría crear un icono para la aplicación, el cual se pueda colocar en la página de inicio de sesión, en la cabecera y en el favicon de la página web. Se podría añadir el manual de las especificaciones a la aplicación web, para que así los profesores tengan un fácil y rápido acceso a la hora de redactar especificaciones. 7.3.4. Backend Implementar un log de errores para la aplicación, donde se lleve registro de todos los imprevistos ocurridos en el servidor. Actualmente, solo se devuelven los errores al cliente y se muestran o por consola o como notificación. Se podría mejorar la gestión del sistema de ficheros. Actualmente, se descargan las salidas obtenidas por las ejecuciones de los usuarios cada vez que se pide una corrección y nunca se borran. Solo se sustituyen cuando se vuelve a pedir la corrección. Se podría evitar volver a descargar un fichero ya existente, o borrar periódicamente aquellos ficheros que ya no sean necesarios o que no se utilicen.
Capítulo 8 Conclusions and Future Work In this section we explain to what extent the main objectives have been met, the difficulties encountered during the development of the project and what improvements can be added in the future. 8.1. Objectives achieved The main objectives of this project were: The definition of a test case specification language and the implementation of a mechanism to separate the test cases of a problem with a specification. The development of a web application that allows the student to see in which test case the student’s submission has failed and the administrator to record the necessary specifications so that the test cases can be separated from the problem. We can say that both goals have been met. First, the specification language developed is intuitive, and we have not found any problem for which there is no specification to describe the input and output. In addition, we have written a user’s guide for teachers to familiarize themselves with this language, which, however, we have not been able to add to the website in the end. On the other hand, we have developed more than 100 tests to ensure the correct functioning of both the grammar that recognizes the specification language and the separation of test cases, and the parser has passed all of them (see section 5.3). Regarding the second objective, the web application is quite similar to DOMjudge, as this way we manage to maintain external consistency and make it easier for new users to familiarize themselves with the environment. The access credentials to the application are the same as for DOMjudge, so there is no registration process. For each problem, the user will have a number of corrections set by the contest administrator, both for the contest and the problem. In the correction view, the first test case of the user’s solution that differs from the expected solution is displayed. When entering a problem specification, syntax checking is performed in 99
100 CAPÍTULO 8. CONCLUSIONS AND FUTURE WORK real time in the administrator’s view, as well as the generation of a test case with separators to verify correct operation. 8.2. Difficulties encountered During the development of the project we encountered several difficulties, sometimes derived from the use of technologies that we were not previously familiar with, and other times due to problems that we were not able to foresee. Another inconvenience that affected the whole team was that almost all of us had less time in the first quarter than in the second, so the development of the project was slower at the beginning. In addition, the following are the difficulties and problems that affected each group during the development of the project. 8.2.1. Group 1 Group 1 had to learn how to handle the PEG.js grammar language. Therefore, before creating the DOMfeed specification language, its members were experimenting with simpler grammars and reading the documentation. [15]. At first the test case separator function worked synchronously, as group 1 had limited knowledge of JavaScript, and also had never worked with Node.js. Therefore, it was not until the second quarter that they got an asynchronous case separator. The difficulties arose mainly because they were not familiar with the event-driven programming characteristic of Node.js. 8.2.2. Group 2 At the beginning of the project, group 2 had very little experience working with React and Node.js, so they had to familiarize themselves with and learn how to use these tools for the development of the web application. During the month of December, when the project already had a solid base, DOMjudge released a new version of its API. However, this new update eliminated the team_id data in the API call response with the user’s data. Without this data, it was not possible to obtain the information of contests and problems from the user, so the application did not work correctly. To solve this problem, we contacted DOMjudge developers, who confirmed that it was indeed a bug and fixed it. The limitations of the DOMjudge API, by not offering the test cases as well as the output of a user to a given problem, caused us to search for alternatives. We came to the conclusion that the only way we could get the files in an automated way was by scraping. We had to learn some concepts of scraping to be able to use it correctly.
8.3. FUTURE WORK 101 The DOMjudge API does not always provide the data the way we need it, so we have had to process a lot of the data obtained. In some cases, we have had to make up to three DOMjudge API calls to get all the data needed to build the object we need. 8.3. Future work The initial objectives have been achieved. However, we believe that the project has room for improvement, so here are some ideas for it: 8.3.1. Specification language Creating an end-of-line token: In the statement of many programming problems it is stated, for example, that a test case has two lines, where the first has one integer and the second has several. Right now, our specification language has two ways to describe test cases like the above: using tokens <int> or tokens <line>. However, there is no way to specify that a line has a single integer. This is why we find it useful to be able to specify where the next line ends and thus begins using a new token. Creating a token for real numbers: We believe that for completeness a token for real numbers should be added, as there is already one for integers. Right now, a specification would have to use a string token to recognize a real number. 8.3.2. Implementation of test case separation Reducing TokenStream memory consumption: In order to create a TokenStream, it is necessary to have read the whole file with the test cases and pass a string to the constructor. This can be very memory intensive for unusually large files, and also means that we cannot tokenize the file until it has been read completely. It is suggested to find a simple way to read the file in parts without overcomplicating the logic of the functions that interact directly with the implementation of a TokenStream. Writing more tests: When a specification cannot be applied to a test case file, an error message is printed. This message should print the correct row and column of the file where the error was encountered, but this behavior is not being verified to work yet. In addition, more tests covering processing rules and case separation would be desirable, as the existing test coverage does not exceed more than 60 % of the code in both cases. 8.3.3. Frontend It could be considered to provide the user code that caused the erroneous output. There is an endpoint in the DOMjudge API that returns the code
102 CAPÍTULO 8. CONCLUSIONS AND FUTURE WORK delivered as a solution to a problem for a certain submission. The application tables currently do not have any pagination, so the moment a contest has many problems or a problem has many submissions, there will be a serious performance problem. It would be interesting to add pagination to tables of twenty rows, for example. Currently, the frontend does not contain any tests to check functionality and prevent it from breaking when adding new or modifying existing functionality. The Cypress [4] library could be used to do this job. There are certain pages in DOMfeed that have a too simple appearance, such as the 404 page. A stylesheet could be defined for these pages. An icon could be created for the application, which can be placed on the login page, in the header and in the favicon of the web page. The specifications manual could be added to the web application, so teachers can have an easy and quick access when writing specifications. 8.3.4. Backend Implement an error log for the application, where all unforeseen events occurring on the server are logged. Currently, only errors are returned to the client and are displayed either by console or as a notification. The management of the file system could be improved. Currently, the outputs obtained by user runs are downloaded each time a correction is requested and are never deleted. They are only replaced when the correction is requested again. It would be possible to avoid re-downloading an existing file, or periodically deleting files that are no longer needed or used.
Capítulo 9 Contribuciones personales En esta sección se describen las contribuciones de cada componente del grupo. Es importante recordar que el equipo se dividió en dos subgrupos, de modo que el grupo conformado por Erik Karlgren Domercq y Adrián González Cabanillas se encargó de la separación de los casos de prueba, mientras que el grupo formado por Félix Redondo Manzanares y Ricardo Enrique Freire Sacco se encargó de la aplicación web. 9.1. Félix Redondo Manzanares Félix ya tenía conocimientos iniciales tanto en Javascript como en Node.js y React, por lo que tuvo un inicio más rápido en el desarrollo de DOMfeed. Los primeros días estuvo familiarizándose con DOMjudge y todo lo relativo a su API. Durante el primer cuatrimestre, Félix asumió todo el trabajo referente a la parte correspondiente al frontend ybackend ya que Ricardo no tenía disponibilidad. En el segundo cuatrimestre fue Ricardo quien realizó casi todo el trabajo restante. 9.1.1. Scraping Una de las primeras tareas fue poder descargar la salida producida por el alumno para cierto problema. Félix se dio cuenta de que no se podía obtener dicha salida por medio de la API, así que tuvo que buscar una alternativa. La única forma que encontró fue mediante una llamada a DOMfeed desde una de las pantallas de gestión del administrador, por lo que era necesario poder iniciar sesión como administrador y descargar el archivo. Para ello tuvo que recurrir al scraping, por el cual consiguió iniciar sesión en DOMjudge y extraer el token de inicio de sesión necesario para poder descargar la salida del usuario. En la sección 6.2.3 se explica detalladamente el proceso seguido. 9.1.2. Algoritmo de búsqueda de caso erróneo Una vez obtenido la salida del alumno había que compararla con la salida esperada. Inicialmente, los casos de prueba del problema (casos de entrada y salida 103
104 CAPÍTULO 9. CONTRIBUCIONES PERSONALES esperada para dichos casos), estaban almacenados en su local. Félix realizó una primera iteración del algoritmo que posteriormente tuvo que modificarlo dos veces más a lo largo del desarrollo a causa de variaciones en los requerimientos. 9.1.3. API de DOMfeed Félix desarrolló los siguientes endpoints de la API: POST /api/login: para el inicio de sesión del usuario. GET /api/team/:teamId/contests: para obtener los concursos de un usuario o equipo. GET /api/problems/:contestId: que devuelve todos los problemas de un concurso. GET /api/submissions/:contestId/:teamId: retorna las entregas de un usuario o equipo para un concurso. GET /api/submissions/:contestId/problem/:problemId: entrega todas las entregas de todos los equipos para un problema de un concurso. GET /api/submissions/:contestId/:submissionId/resultados: devuelve los casos de prueba erróneos. 9.1.4. Páginas de la interfaz web La mayor parte de la interfaz web fue desarrollada por Félix, pues tenía más experiencia. Las páginas junto con la lógica de estas que desarrolló son las siguientes: Inicio de sesión: vista para el inicio de sesión del usuario. Problemas: página que muestra los problemas por concursos. Entregas: vista que muestra las entregas de un equipo para un concurso. Correcciones: página donde se muestran el caso erróneo de la entrega. 9.1.5. Sistema de notificaciones Félix desarrolló un componente de React para mostrar notificaciones al usuario como feedback tras realizar ciertas acciones. El componente se puede utilizar desde cualquier página de la web, pues está colocado en un nivel inicial del árbol de componentes que componen la aplicación, y cuya función o mecanismo para mostrar y cambiar el texto de la notificación está guardado en el contexto de React, por lo que cualquier componente hijo puede acceder a él.
9.2. RICARDO ENRIQUE FREIRE SACCO 105 9.1.6. Páginas privadas La única página de DOMfeed donde un usuario puede acceder sin iniciar sesión es la vista de login. El resto de páginas requieren de una autenticación previa del usuario. Para ello, Félix desarrolló un hook que comprueba en cada pantalla que el usuario está logueado. El hook se almacena a nivel de contexto de aplicación, por lo que se puede acceder desde cualquier vista de la web. 9.1.7. Mantenimiento de la sesión del usuario Para evitar que el usuario perdiera la sesión tras refrescar el navegador y que tuviera que volver a iniciar sesión en DOMfeed, Félix desarrolló un método donde se guardaba un token de sesión en el localStorage del navegador. En esencia, cada vez que se refresca el navegador, se comprueba que existe ese token y se agrega al contexto de la aplicación para indicar la existencia de un usuario logueado. 9.1.8. Segurización de la API Para asegurar que todas las peticiones a la API de DOMfeed fueran de usuarios autenticados, Félix tuvo que desarrollar un sistema de seguridad. Este, en esencia, consiste en un token con información relativa al usuario que ha iniciado sesión que manda al frontend al inicio de esta, y posteriormente se devuelve en cada llamada a la API. Básicamente, la seguridad se consigue comprobando la existencia del token en cada llamada a la API por medio de un middleware y, en el caso de que exista y no esté alterado ni corrupto, hacer otra serie de comprobaciones para validar que el usuario que realiza la petición tiene permisos para obtener los datos solicitados. 9.1.9. Llamada a la API de DOMjudge Tanto las llamadas a la API como el posterior tratamiento de los datos obtenidos fueron desarrolladas por Félix, puesto que esta parte fue trabajo del primer cuatrimestre. Tras cada llamada corresponde un tratamiento de los datos para adaptarlos a lo solicitado por el usuario a través de nuestra API de DOMfeed. En algunas ocasiones tuvo que hacer varias llamadas a DOMjudge y cruzar los datos obtenidos para poder conseguir los datos necesarios. 9.2. Ricardo Enrique Freire Sacco Al iniciar el proyecto, Ricardo no tenía ninguna experiencia trabajando con tecnologías como React o Node.js, por lo que los primeros meses se centró principalmente en aprender los conceptos básicos de dichas tecnologías. Además, por temas de disponibilidad no pudo aportar al proyecto durante el primer cuatrimestre, por lo que el segundo cuatrimestre asumió la mayor parte de la carga de trabajo.
Apéndice A Ejemplos de especificaciones En este apéndice se presentan ejemplos de especificaciones de entrada y de salida para problemas de programación reales y se muestra cómo quedarían separados los ficheros de casos de prueba. Estos problemas han sido usados en exámenes o prácticas del grado de Ingeniería Informática de la Universidad Complutense de Madrid. A.1. Obtener el mínimo de un vector Este problema apareció en el examen final de septiembre de 2014 de la asignatura de Fundamentos de Algoritmia. A.1.1. Enunciado Consideramos un vector V[N]de números enteros, cuyos valores se han obtenido aplicando una rotación sobre un vector ordenado en orden estrictamente decreciente. Implementa un algoritmo que calcule el mínimo del vector con una complejidad O(log(n)). El número de elementos sobre los que se aplica la rotación para obtener el vector de entrada es un valor entre 0 y Ny no se conoce. Por ejemplo, un posible vector de entrada sería el vector 70 55 13 4 100 80, obtenido desplazando los dos primeros elementos del vector 100 80 70 55 13 4 al final del mismo. Requisitos de implementación. Se debe implementar una función recursiva (resolver) que dado el vector, con los datos de entrada ya leídos, obtenga el mínimo en tiempo logarítmico respecto al número de elementos del vector. Se pueden utilizar más parámetros si se considera necesario. Entrada La entrada consta de una serie de casos de prueba. Cada caso de prueba consta de dos líneas. En la primera se indica el número de elementos del vector y en la i
ii APÉNDICE A. EJEMPLOS DE ESPECIFICACIONES 1Entrada Salida 2 34 2 48 6 4 2 2 54 2 62 8 6 4 2 74 1 84 2 8 6 1 94 4 10 6 4 2 8 11 5 12 8 5 3 1 10 13 5 14 5 3 1 10 8 15 6 16 70 55 13 4 100 80 Figura A.1: Mínimo de un vector: casos de prueba sin separar segunda los valores del vector. Salida Para cada caso de prueba se escribe en una línea diferente el mínimo del vector. Véase la figura A.1 para ver el ejemplo de entrada y de salida del enunciado de este problema. A.1.2. Especificaciones Para definir las especificaciones solo necesitamos prestar atención a las secciones de Entrada y de Salida. Como bien dice el enunciado, para cada caso de prueba de entrada tenemos dos líneas, teniendo la primera un entero con el número de elementos de la siguiente línea, que a su vez son también enteros. No se menciona en ningún sitio que haya algo de información previa a todos los casos de prueba, como la cantidad de los mismos, ni de que se marque el fin de la entrada de alguna forma. Por tanto, tenemos dos posibles especificaciones para la entrada: --- (<n:int> <int>^n ---)^* --- (<line>^2 ---)^* No obstante, hay que tener en cuenta que en la primera especificación no importa que cada caso de prueba esté en dos líneas, en una, o si todos los casos de prueba se encuentran en la misma línea. Por otro lado, la segunda especificación no nos serviría en el caso de que los datos de cada caso de prueba de la entrada no siguieran de manera estricta las condiciones del enunciado y los datos de un mismo caso estuvieran en una o más de dos líneas. Por último, hay también dos posibles especificaciones para la salida:
A.2. EL GRANDE iii 1Entrada Salida 2 3--- --- 44 2 58 6 4 2 --- 6--- 2 74 --- 82 8 6 4 2 9--- --- 10 4 2 11 4 2 8 6 --- 12 --- 1 13 4 --- 14 6 4 2 8 1 15 --- --- 16 5 4 17 8 5 3 1 10 --- 18 --- 19 5 20 5 3 1 10 8 21 --- 22 6 23 70 55 13 4 100 80 24 --- Figura A.2: Mínimo de un vector: casos de prueba separados --- (<int> ---)^* --- (<line> ---)^* Ambas especificaciones tienen las mismas peculiaridades que sus respectivas especificaciones para la entrada, pero al igual que estas, si las condiciones sobre la entrada y la salida se siguen de manera estricta, servirían para separar los casos de prueba apropiadamente. De hecho, los casos de prueba que aparecen en el enunciado (figura A.1) se separan correctamente usando cualquiera de las especificaciones de entrada y de salida anteriores, como puede observarse en la figura A.2. A.2. El Grande Este problema apareció en una práctica de la asignatura de Estructuras de Datos. Fue creado por Manuel Montenegro, uno de los tutores del TFG. En este problema se menciona que hay que implementar un TAD, que son las siglas de Tipo Abstracto de Datos.
iv APÉNDICE A. EJEMPLOS DE ESPECIFICACIONES A.2.1. Enunciado El Grande es un juego de mesa ambientado en la España medieval. En este juego, los jugadores tienen que colocar estratégicamente a sus caballeros por las distintas regiones del país. Decimos que un jugador predomina en una región cuando tiene estrictamente más caballeros en dicha región que cualquiera de los restantes jugadores. Por otro lado, decimos que una región está en disputa si está ocupada por al menos un caballero, pero ningún jugador predomina en la misma. En particular, las regiones vacías (esto es, sin caballeros) no están en disputa. En este ejercicio trataremos de implementar un TAD que almacene la información correspondiente a una partida de este juego. Las operaciones a implementar son: anyadir_jugador(jugador). Añade un jugador a la partida. Si el jugador (de tipo string) ya estaba inscrito en ella, se lanza una excepción domain_error con el mensaje Jugador existente. colocar_caballero(jugador, region). Indica que el jugador (que es de tipo string) coloca un caballero en la region (de tipo string) indicada. Si el jugador no se encuentra inscrito en la partida, se lanza la excepción domain_error con el mensaje Jugador no existente. Si la region no existe, se dará de alta. puntuacion(jugador). Devuelve el número de regiones en las que predomina el jugador pasado como parámetro. Si el jugador no se encuentra inscrito en la partida, se lanza la excepción domain_error con el mensaje Jugador no existente. regiones_en_disputa(). Devuelve un vector<string> con la lista de regiones que están en disputa. La lista ha de estar ordenada ascendentemente, en orden alfabético según el nombre de la región. expulsar_caballeros(region). Elimina a todos los caballeros de la region pasada como parámetro. Si la región no existe o no tiene ningún caballero, se lanza la excepción domain_error con el mensaje Region vacia. En este ejercicio se pide: 1. Implementar las operaciones descritas en el TAD. Ninguna de ellas debe realizar operaciones de E/S. El manejo de E/S debe hacerse en la función tratar_caso(). 2. Indicar el coste de cada operación. Las operaciones deben implementarse de la manera más eficiente desde el punto de vista del coste asintótico en tiempo.
A.2. EL GRANDE v Entrada La entrada consta de una serie de casos de prueba. Cada caso está formado por una serie de líneas en las que se muestran las operaciones a llevar a cabo, una por cada línea: el nombre de la operación seguido de sus argumentos. La palabra FIN en una línea indica el final de cada caso. Los nombres de jugadores y regiones son cadenas de caracteres sin espacios en blanco. Salida Las operaciones que generan salida son: puntuacion J, que debe escribir una línea con el mensaje Puntuacion de J: X, donde Jes el jugador del cual quiere obtenerse la puntuación, y Xes la puntuación del mismo. regiones_en_disputa, que debe escribir una línea con el mensaje Regiones en disputa:, seguida de los nombres de las regiones que están en disputa, uno por línea. Si una operación produce un error, entonces se escribirá una línea con el mensaje ERROR:, seguido del mensaje de la excepción que lanza la operación, y no se escribirá nada más para esa operación. Cada caso termina con una línea con tres guiones (---) Véase la figura A.3 para ver el ejemplo de entrada y de salida del enunciado de este problema. A.2.2. Especificaciones La salida y la entrada de este problema es significativamente más compleja que la del problema anterior. Aun así, podemos usar especificaciones bastante sencillas para describirlas. Veamos las siguientes: Entrada: --- (<line>^* "FIN" ---)^* Salida: --- (<line>^* "---" ---)^* Aquí estamos aprovechando que el analizador de casos de prueba, dada una especificación S^* T y una cadena x, intentará primero comprobar si la cadena x es aceptada por la especificación T, y solo si no es el caso, comprobará si es aceptada por la especificación S. Por eso, en el caso de la especificación de entrada, si se lee una cadena ‘FIN’ no se trata como un token de línea, sino como el literal “FIN”. Nótese además que ‘‘---’’ no se trata como un separador en la especificación al tratarse de un literal. Véase la figura A.4 para ver cómo quedan los casos de prueba separados.
vi APÉNDICE A. EJEMPLOS DE ESPECIFICACIONES 1Entrada Salida 2 3anyadir_jugador jug1 Puntuacion de jug1: 1 4anyadir_jugador jug2 Puntuacion de jug2: 0 5colocar_caballero jug1 Granada Puntuacion de jug1: 0 6colocar_caballero jug2 Granada Puntuacion de jug2: 0 7colocar_caballero jug1 Granada Regiones en disputa : 8puntuacion jug1 Aragon 9puntuacion jug2 Granada 10 colocar_caballero jug2 Granada --- 11 puntuacion jug1 ERROR : Jugador no existente 12 puntuacion jug2 Puntuacion de jug1: 2 13 colocar_caballero jug2 Aragon Puntuacion de jug1: 1 14 colocar_caballero jug1 Aragon ERROR : Region vacia 15 regiones_en_disputa --- 16 FIN 17 colocar_caballero jug1 Sevilla 18 anyadir_jugador jug1 19 colocar_caballero jug1 Valencia 20 colocar_caballero jug1 Galicia 21 puntuacion jug1 22 expulsar_caballeros Valencia 23 puntuacion jug1 24 expulsar_caballeros Valencia 25 FIN Figura A.3: El Grande: casos de prueba sin separar
A.2. EL GRANDE vii 1Entrada Salida 2 3<====================> <====================> 4anyadir_jugador jug1 Puntuacion de jug1: 1 5anyadir_jugador jug2 Puntuacion de jug2: 0 6colocar_caballero jug1 Granada Puntuacion de jug1: 0 7colocar_caballero jug2 Granada Puntuacion de jug2: 0 8colocar_caballero jug1 Granada Regiones en disputa : 9puntuacion jug1 Aragon 10 puntuacion jug2 Granada 11 colocar_caballero jug2 Granada --- 12 puntuacion jug1 <==================== > 13 puntuacion jug2 ERROR : Jugador no existente 14 colocar_caballero jug2 Aragon Puntuacion de jug1: 2 15 colocar_caballero jug1 Aragon Puntuacion de jug1: 1 16 regiones_en_disputa ERROR : Region vacia 17 FIN --- 18 <====================> <====================> 19 colocar_caballero jug1 Sevilla 20 anyadir_jugador jug1 21 colocar_caballero jug1 Valencia 22 colocar_caballero jug1 Galicia 23 puntuacion jug1 24 expulsar_caballeros Valencia 25 puntuacion jug1 26 expulsar_caballeros Valencia 27 FIN 28 <====================> Figura A.4: El Grande: casos de prueba separados
viii APÉNDICE A. EJEMPLOS DE ESPECIFICACIONES
Bibliografía [1] Axios, Promise based HTTP client.url:https://axios-http.com/. [2] Bootstrap, the world’s most popular framework for building responsive sites. url:https://getbootstrap.com/. [3] Classical Logic.url:https://plato.stanford.edu/archives/win2009/ entries/logic-classical/. [4] Cypress, frontend testing.url:https://www.cypress.io/. [5] DOMjudge API.url:https://www.domjudge.org/demoweb/api/doc. [6] DOMjudge documentation.url:https : / / www . domjudge . org / docs / manual/8.0/index.html. [7] Express.js, A API framework for Node.js.url:https://expressjs.com/ es/. [8] Russ Ferguson. Beginning Javascript: The Ultimate Guide to Modern JavaScript Development. Apress, 2019. isbn: 1484243943. [9] Jest - Delightful JavaScript Testing.url:https://jestjs.io/. [10] Jutge, The Virtual Learning Environment for Computer Programming.url: https://jutge.org/. [11] Mooshak, automatic judge.url:https://mooshak.dcc.fc.up.pt/. [12] Node.js, A JavaScript runtime built.url:https://nodejs.org/es/. [13] npm, The world’s largest software registry.url:https://www.npmjs.com/. [14] Package.json, Node and npm config file.url:https://docs.npmjs.com/ cli/v7/configuring-npm/package-json. [15] PEG.js - Documentation.url:https://pegjs.org/documentation. [16] PEG.js - Parser Generator for JavaScript.url:https://pegjs.org/. [17] J. Petit y col. «Jutge.org: Characteristics and Experiences». En: IEEE Transactions on Learning Technologies 11.3 (2017), págs. 321-333. doi:https:// doi.org/10.1109/TLT.2017.2723389. [18] React Bootstrap, the world’s most popular framework rebuilt for React.url: https://react-bootstrap.github.io/. [19] React Router, A routing library for React.url:https://reactrouter. com/. ix
xBIBLIOGRAFÍA [20] React, A JavaScript library for building user interfaces.url:https://es. reactjs.org/. [21] stream.Writable | Node.js v18.2.0 Documentation.url:https://nodejs. org/api/stream.html#class-streamwritable. [22] Basarat Ali Syed. Beginning Node.js. Apress, 2014. isbn: 9781484201886.