El lenguaje SPSL (abreviatura del nombre en inglés: Siette Processing Script Language) se usa en la evaluacón previa de las preguntas externas con fichero. Hay que tener en cuenta que las preguntas con ficheros no son mas que un tipo especial de preguntas externas, y como tales esperan del ítem externo una respuesta en forma de una cadena de caracteres como respuesta. El objetivo de este lenguaje es realizar una serie de pruebas mediante ejecución de instrucciones del sistema operativo, comprobar el resultado de las mismas y a partir de estos resultados determinar que etiqueta debe devolverse a Siette como respuesta.
Es un lenguaje muy simple, compuesto por una secuencia de filas, cada una de ellas representando una sentencia del lenguaje, que puede ser: una sentencia de control; o bien una sentencia de prueba; o bien una sentencia de repetición.
Las sentencias de prueba consisten en llamadas a instrucciones del sistema operativo y comprobación del resultado que éstas generan en la salida estándar.
Las sentencias de control comienzan siempre con el carácter @
, y sirven para configurar el modo en que se procesan las sentencias de prueba.
Todas las sentencias se van ejecutando secuencialmente una tras otra hasta llegar al final o hasta que se produzca un error en alguna sentencia de prueba, dependiendo del modo de ejecución según se haya definido mediante alguna de las sentencias de control del tipo @OnError
.
Todas las sentencias de control tienen efecto desde la línea en la que están definidas hasta el final del script, o bien hasta que otra sentencia de control modifique el comportamiento.
Finalmente el script de procesamiento obtendrá una cadena de caracteres que será el resultado que se pasará a Siette como respuesta.
A continuación se explicará el modo en que se procesa un script explicando una a una las sentencias del lenguaje. El orden de exposición de las sentencias irá introduciendo poco a poco los conceptos necesarios para entender el funcionamiento del lenguaje SPSL
Por ejemplo, supongamos que para probar un fichero, lo único que nos interesa saber es si un fichero test.txt
contiene la palabra function
, para lo cual se puede usar la siguiente instrucción del sistema operativo:
grep -l function test.txt
que devuelve test.txt
en caso de que el fichero contenga la palabra function
y no devuelve nada en otro caso.
La sentencia de control. A partir de esta instrucción se puede construir las siguientes sentencias de pruebas
Las sentencias de prueba completas se forman mediante una instrucción del sistema operativo, un valor esperado como resultado de la ejecución y dos valores a devolver a Siette, uno en caso de que la instrucción se cumpla y otro en caso en que no lo haga. Es decir, las sentencias de prueba siguen un esquema:
intrucción == resultado -> etiqueta positiva : etiqueta negativa
o bien:
intrucción <> resultado -> etiqueta positiva : etiqueta negativa
La prueba consiste en comprobar si el resultado de la ejecución de la instrucción encaja o no, dependiendo del signo de comparación, con un resultado esperado.
Por ejemplo:
grep -l function test.txt == test.txt -> A : B
que indica que debe ejercutar se la sentencia del sistema operativo, obtener su resultado, compararlo con la secuencia text.txt
y si es igual devolver como respuesta a Siette <!–A–>
; y si no devolverá <!–B–>
.
Nótese que el valor devuelto por el script se inserta dentro de un comentario HTML. Esto es así para evitar que el valor devuelto sea visible al mostrar los resultados en Siette.
La prueba de concordancia entre la instrucción del sistema operativo y el resultado se determina usando los mismos patrones que Siette utiliza para identificar las respuestas del alumno en las preguntas de respuesta libre.
Estas sentencias de control tienen la siguiente estructura:
@patron [exec|file
] :
(true
|false
) , (true
|false
) ,
(true
|false
) ,
(true
|false
)
Los cuatro elementos true
o false
que aparecen en esta sentencia de control corresponden a las posibles opciones de variabilidad de las respuestas, es decir, siguiendo el mismo orden:
La opción de Ignorar pequeños errores tipográficos está siempre deshabilitada en este contexto.
por ejemplo:
@SiettePattern: false, true, true, true
Indica que se usará el patrón de expresiones regulares Siette diferenciando entre mayusculas y minuúsculas, pero no teniendo en cuenta los acentos, ni los signos de puntuación, ni los espacios en blanco.
Finalmente, hay una expresión especial *
que se cumple siempre y por tanto nunca se comprueba. En el siguiente ejemplo, se ha añadido la instrucción rm Punto.class
para borrar el fichero .class
si existe. Si no existe esta instrucción daría un mensaje de error. Da igual el resultado, la sentencia de prueba siempre tiene éxito.
@SiettePattern: true, true, true, true rm Punto.class == * -> A javac Punto.java <> *error* -> OK : ERROR_COMPILACION java Punto
La opción file
que puede aparecer o no en esta sentencia de control indica que el valor devuelto por la instrucción debe compararse con el contenido del fichero que aparece detrás del signo de comparación. En este caso, el segundo campo no contiene el resultado en si, sino el nombre del fichero en donde está el resultado previsto. La comparación se establece entre el resultado de la ejecución de la instrucción y el contenido del fichero. A su vez, dependiendo del tipo de patrón de comparación que se utilice (Correspondencia, PatronSiette, etc.) y de las opciones de comparación, el contenido del fichero contendrá el patrón de respuesta.
Por ejemplo, las sentencias:
@SiettePattern file: false, true, true, true cat test.txt == test1.txt -> A : B
tienen como efecto comparar el contenido del fichero test.txt con el contenido del fichero test1.txt, o mejor dicho con la expresión regular contenida en este fichero, ya que se esta utilizando el patrón @SiettePattern
. No es una buena practica usar la comparación con ficheros de gran tamaño si se usan expresiones regulares, ya que es mucho mas ineficiente, y podría hacer que la prueba tardara demasiado. La opción file, en combinación con expresiones regulares solo esta indicada en ficheros pequeños.
La opción exec
indica que el valor devuelto por la instrucción que aparece a la izquierda del signo de comparación debe compararse con el resultado de la ejecución de la instrucción que aparece detrás del signo de comparación. La comparación se establece entre el resultado de la ejecución de la primera instrucción y el resultado de ejecución de la segunda, asumiendo en su caso que la segunda pudiera ser una expresión regular, (dependiendo del tipo de patrón).
Por ejemplo, las sentencias:
@SiettePattern: false, true, true, true javac wc.java <> *error* -> OK : ERROR_COMPILACION @Correspondencia exec: true, true, true, true java wc test.txt == wc test.txt -> A : B
Establece la compilación y ejecución de un fichero wc.java cuya ejecución debe dar el mismo resultado que la ejecución de la instrucción wc del sistema operativo, (salvo diferencias en espacios en blanco, etc.)
Si no hay ninguna sentencia de control para cambiar el patrón, por defecto se usa el patrón Java
La opción mas sencilla son las sentencias de prueba simples, en las que no se especifica ninguna condición, sino solamente se escribe la instrucción del sistema operativo tal cuál, es decir:
grep -l function test.txt
En este caso la sentencia de prueba devolverá a Siette directamente el valor que la ejecución de la instrucción devuelva por el flujo de datos de la salida estándar (stdout
). En este ejemplo la secuencia text.txt
, (en el caso de que el fichero contenga la palabra function
) o bien la cadena vacía.
Cuando no hay una sino varias sentencias de prueba la ejecución de las sentencias viene controlada por los distintos modelos de tratamiento de errores que se establecen mediante sentencias de control. Supongamos que existen estas dos sentencias de prueba para comprobar si un programa en Python es capaz de detectar si una secuencia de números esta ordenada. La primera prueba debe dar como resultado True
y la segunda False
@Correspondence: true, true, true, true python3 test.py 1 2 3 4 == True -> : Incorrecto1 python3 test.py 1 5 3 4 == False -> Correcto : Incorrecto2
Evidentemente en este caso interesa que se ejecuten ambas pruebas. Pueden ocurrir varias situaciones: (1) La primera prueba no da como resultado False
por lo que se devuelve a Siette la cadena <!–Incorrecto1–>
; (2) La primera
prueba se satisface, por lo que el script no termina, sino que continúa ejecutandose. Supongamos que la segunda preuba da un resultado True
cuando debería dar False
. La respuesta del sistema a Siette en este caso sería
<!–Incorrecto1–>
; (3) Supongamos que la primera prueba se cumple y la segunda tambien. En este caso la respuesta que devuelve a Siette será <!–Correcto–>
. En realidad, este script podria sustituirse por este otro mas simple:
@Correspondence: true, true, true, true python3 test.py 1 2 3 4 == True -> : Incorrecto python3 test.py 1 5 3 4 == False -> Correcto : Incorrecto
si no es necesario diferenciar entre uno u otro caso.
Cuando se trata de preguntas simples, Siette solo espera una respuesta tras la ejecución del script de verificación del fichero. Ahora bien, en el caso de preguntas compuestas, es necesario devolver una respuesta por cada una de las subpreguntas.
Cuando se usan preguntas compuestas con fichero, el script correspondiente a la pregunta compuesta se obtiene mediante la concatenación de los scripts de cada una de las subpreguntas. La etiqeuta @Answer
se usa para de separar cada uno los trozos de script pertenecientes a cada una de las subpreguntas.
Este proceso es transparente al usuario, ya que al construir la pregunta externa de fichero, si se trata de una pregunta compuesta, Siette se preocupa de añadir para cada una de las subpreguntas la opción @Answer label
correspondiente, de manera que cuando el script de procesamiento termina, ya sepa como comunicar la respuesta.
Podriamos decir que la sentencia @Answer
es un sentencia de control del sistema, ya que el usuario normalmente no tiene que usarla.
El modo descrito en los ejemplo anteriores es el comportamiento por defecto, que viene definido por la sentencia de control @OnError stop
, cuyo funcionamiento general es el siguiente:
Supongamos un conjunto de sentencias de prueba
@OnError stop instr<sub>1</sub> == res<sub>1</sub> -> A<sub>1</sub> : B<sub>1</sub> instr<sub>2</sub> == res<sub>2</sub> -> A<sub>2</sub> : B<sub>2</sub> ... instr<sub>j-1</sub> == res<sub>j-1</sub> -> A<sub>j-1</sub> : B<sub>j-1</sub> instr<sub>j</sub> == res<sub>j</sub> -> A<sub>j</sub> : B<sub>j</sub> instr<sub>n</sub> == res<sub>n</sub> -> A<sub>n</sub> : B<sub>n</sub>
El script ejecutara todas las instrucciones hasta llegar a una sentencia j que no se cumpla. En este momento debe decidir que etiqueta enviar a Siette. Si la etiqueta Bj no está vacía, utilizará esta etiqueta. En el caso en que Bj no exista, utilizará la última etiqueta correcta que haya alcanzado. Es decir, si existe Aj-1, enviará esta etiqueta, si no existe, enviará Aj-2 y así hasta llegar a A1. En caso de que tampoco exista enviará una etiqueta vacía.
En el caso en el que se mezclen sentencias de prueba completas y sentencias de prueba simples, se entenderá que las sentencias de prueba simples siempre se cumplen y solo tienen asignadas etiquetas para el caso positivo.
En el caso en el que todas las sentencias de prueba se satisfagan devolverá An si existe, y si no la etiqueta positiva mas reciente; y en el caso de que ninguna lo haga, devolverá B1, o la cadena vacía si ésta etiqueta no existiera.
Mediante la sentencia de control @OnError continue
, se puede cambiar el modo en que se evalúan las sentencias de pruebas
Supongamos un conjunto de sentencias de prueba
@OnError continue instr(1) == res(1) -> A(1) : B(1) instr(2) == res(2) -> A(2) : B(2) ... instr(j-1) == res(j-1) -> A(j-1) : B(j-1) instr(j) == res(j) -> A(j) : B(j) instr(n) == res(n) -> A(n) : B(n)
En el modo @OnError continue
se ejecutan todas las sentencias, y la respuesta a Siette viene determinada exclusivamente por la última sentencia, es decir que devolverá o bien An; o bien Bn
A primera vista parece que no tiene mucho sentido utilizar la opción @OnError continue
, ya que el resultado solo depende de la última sentencia de prueba, sin embargo, en combinación con otras opciones cobra especial sentido, por ejemplo con la sentencia de control @Evall add all
.
Esta sentencia de control se usa en combinación con la sentencia de control @OnError recover
. Cuando en un script se establece el modo @OnError skip
, si se produce un error en la ejecución de alguna de las sentencias de prueba el script no se interrumpe, sino que el flujo de control se reinicia y salta hasta encontrar la siguiente instrucción @OnError recover
. Por ejemplo, supongamos el siguiente script:
@OnError skip instr1 == res1 -> A1 : B1 instr2 == res2 -> A2 : B2 instr3 == res3 -> A3 : B3 @OnError recover instr4 == res4 -> A4 : B4 instr5 == res5 -> A5 : B5 @OnError recover instr6 == res6 -> A6 : B6 instr7 == res7 -> A7 : B7 ...
La ejecución comienza lanzando la instrucción instr1. Si la ejecución de instr1 da como resultado res1 el script de ejecución continúa con la instrucción instr2; pero si el resultado de la ejecución de instr1 no es el esperado, las instrucciones siguientes instr2 y instr3 no se ejecutarán. Al encontrar la sentencia de control @OnError recover
, el proceso se recupera, ejecutando ahora la instrucción instr4. Nuevamente si esta instrucción da un resultado correcto, el proceso seguirá con la instrucción siguiente, pero si la ejecución de
instr4 da error, la siguiente instrucción a ejecutar será instr6.
Nótese que la sentencia de control @OnError recover
no tendría ningún efecto si previamente no es hubiera establecido la sentencia de control @OnError skip
.
Un caso especial y muy frecuente del uso de estas sentencias de control es la ejecución de preguntas compuestas con fichero. Siette compone el script de ejecución añadiendo en la cabecera de cada subpregunta dos sentencias de control, una de tipo @Answer label
y otra @OnError recover
, por lo que solo sería necesario definir manualmente la sentencia de control @OnError skip
. En este caso, no es necesario incluir manualmente @OnError recover
ya que se añade automáticamente. El salto en la ejecución funcionará entre los (sub)scripts de las subpreguntas. Por ejemplo, supongamos que Siette ha compuesto el siguiente script:
@OnError skip instr(0,1) == res(0,1) -> A(0,1) : B(0,1) ... @Answer label(1) @OnError recover instr(1,1) == res(1,1) -> A(1,1) : B(1,1) instr(1,2) == res(2) -> A(2) : B(1,2) ... instr(1,j-1) == res(1,j-1) -> A(1,j-1) : B(1,j-1) instr(1,j) == res(1,j) -> A(1,j) : B(1,j) instr(1,n) == res(1,n) -> A(1,n) : B(1,n) @Answer label(2) @OnError recover instr(2,1) == res(2,1) -> A(2,1) : B(2,1) instr(2,2) == res(2) -> A(2) : B(2,2) ... instr(2,k-1) == res(2,k-1) -> A(2,k-1) : B(2,k-1) instr(2,k) == res(2,k) -> A(2,k) : B(2,k) instr(2,m) == res(2,m) -> A(2,m) : B(2,m) @Answer label(3) @OnError recover instr(3,1) == res(3,1) -> A(3,1) : B(3,1) ...
Si suponemos que las sentencias de pruebas fallan en la prueba instr1,j == res1,j, la ejecución saltara a la prueba instr2,1 == res2,1. Si suponemos que a partir de aquí todas las pruebas funcionan hasta la prueba instr2,k == res2,k, el flujo de control volverá a saltar hasta la siguiente instrucción OnError recover
situada justo detrás de @Answer label3, es decir a la prueba instr3,1 == res3,1, y así sucesivamente.
La instrucción @Answer label
devuelve en cada caso la etiqueta correspondiente al subscript que comienza a partir de esta sentencia, es decir, en el supuesto anterior, la pregunta externa de fichero devolverá a Siette el valor de la etiqueta B1,j (y si no existe esta etiqueta A1,j-1, y si no el anterior, y así sucesivamente), en el parámetro label1; y B1,k (o en su defecto la etiqueta que corresponda), como valor del parámetro label2.
Esta sentencia de control indica que cuando se devuelve un valor a Siette se deben sumar todas las etiquetas que se hayan producido, y no solamente utilizar la última. Evidentemente esto solo puede hacerse si las etiquetas tienen un valor numérico. Si no lo tienen o no se han escrito etiquetas en la sentencia de prueba, se tomara como valor 0.
Por ejemplo, supongamos que se quiere comprobar si un programa en Python es capaz de reconocer los numeros impares. El script podría ser el siguiente:
@OnError continue @Eval add all python3 test.py 1 == True -> 1 python3 test.py 2 == False -> 1 python3 test.py 3 == True -> 1 python3 test.py 4 == False -> 1 python3 test.py 5 == True -> 1 python3 test.py 6 == False -> 1
Cada una de estas instrucciones se ejecuta dando un resultado satisfactorio, o no, dependiendo de que se cumpla o no la igualdad. Puesto que el modo de evaluacion es @OnError continue
se ejecutarán todas las sentencias, pero solo algunas tendrán exito. El valor devuelto por Siette en este caso será <!–6–>
si y solo si, las seis sentencias de prueba tienen exito. Si por ejemplo el programa devuelve True
en todos los casos, la suma será de <!–3–>
. Puesto que esto no parece muy buena estrategia, el script podria mejorarse asi:
@OnError continue @Eval add all python3 test.py 1 == True -> 1 : -1 python3 test.py 2 == False -> 1 : -1 python3 test.py 3 == True -> 1 : -1 python3 test.py 4 == False -> 1 : -1 python3 test.py 5 == True -> 1 : -1 python3 test.py 6 == False -> 1 : -1
Con este script en el caso de que siempre devuelva True
, al sumar los resultados se obtiene como respuesta <!– 0 –>
Téngase en cuenta que, en general, para que esta opción funcione, es necesario activar también la opción @OnError continue
para que la ejecución no se detenga en el primer fallo.
Por otra parte, para que la evaluación tenga efecto habra que considerar todas las posibles combinaciones de valores. Por ejemplo, en el caso anterior las posibles opciones de respuesta son trece, que varían entre <!– -6 –>
y <!– 6 –>
, aunque quizas solo sea necesario contemplar las opciones <!– 1 –>
a <!– 6 –>
. No olvide seleccionar en este caso el modo de evaluación manual y asignar valores menores que uno a las respuestas parcialmente correctas, es decir las opciones de <!– 1 –>
a <!– 5 –>
.
Esta opción es similar a la anterior, pero solamente suma los valores corespondientes a las etiquetas de las sentencias de prueba que han dado un resultado positivas.
Toma como valor devuelto el de la última etiqueta no vacía de una sentencia de prueba que haya tenido éxito. Si no hay ejecucion correcta, devuelve la ultima etiqueta no vacia. El término max hace referencia a la última que se ejecuto, no al valor numérico.
En este caso, se toma como valor devuelto el de la última etiqueta no vacía de una sentencia de prueba haya tenido éxito o no. El término max hace referencia a la última que se ejecuto, no al valor numérico.
Este es el valor de la evaluación por defecto.
La sentencia de control @Scale
se usa para informar al sistema de evaluación que se va a utilizar una cierta escala para la evaluación en el caso de que la etiqueta que el script devolverá a Siette tenga un valor numérico. . La sintaxis de estas sentencias es la siguiente:
@timeout NUMERO1 NUMERO2
en donde NUMERO1 es un número entero positivo que representa el valor numérico máximo posible que resultaría como etiqueta final de la evaluación, y NUMERO2 el valor máximo de la escala que debería tener la etiqueta devuelta.
Por consiguiente, la etiqueta final, en el caso de usar una etiqueta numérica, no corresponde al valor que se muestra en el script, sino a su valor transformado en la escala [0,NUMERO2]. Este es el caso por ejemplo del script:
@OnError continue @Eval add correct @Scale 6 4 python3 test.py 1 == True -> 1 python3 test.py 2 == False -> 1 python3 test.py 3 == True -> 1 python3 test.py 4 == False -> 1 python3 test.py 5 == True -> 1 python3 test.py 6 == False -> 1
La instrucción @OnError continue
hace que la ejecución no se detenga tras la primera prueba fallida, sino que continue con la siguiente. La instrucción @Eval add all
informa a Siette de que debe sumar las etiquetas que hayan obtenido un resultado correcto, por lo que de las 6 lineas que hay en el script, si algunas resultan correctas y otras no, se pueden obtener como resultado de la evaluación las etiquetas “”, “1”, “2”, “3”, “4”, “5” y “6”.
La instrucción de escala @Scale 6 4
indica a Siette que el valor máximo que se puede obtener es 6, y que debe ser modificado en una escala que tenga como valor máximo 4. Esta operación dará como resultado siempre un número entero entre 0 y 4, truncando la parte decimal, por tanto los posibles valores por Siette son: “”, “0”, “1”, “2”, “2”, “3” y “4” para cada uno de los casos obtenidos anteriormente.
La gran ventaja de esta normalización es que el conjunto de etiquetas no depende del número de pruebas satisfactorias, ni de las posibles combinaciones de valores, que en algunos casos en donde hay muchas pruebas o las pruebas tienen distinto valor pueden dar lugar a un conjunto de etiquetas demasiado grande.
La sentencia de control @tmeout
sirve para imponer un limite de tiempo en la ejecución de cada una de las instrucciones del sistema operativo. La sintáxis de estas sentencias es la siguiente:
@timeout NUMERO
en donde NUMERO es un número entero positivo que representa el límite de tiempo en segundos.
Auqnue al ejecutar una pregunta con fichero puede establecerse un límite de tiempo global para el script completo (en la pestaña Avanzado), el mecanismo por el cual se limita el tiempo de ejecución del script no es tan preciso, ni tan fiable como el mecanismo puede establecerse mediante la sentencia de control @timeout
, ya que esta última impone un control desde el propio sistema operativo, mientras que la anterior lo hace sobre una hebra del programa Java que se encarga de ejecutar el script.
A bajo nivel, la instrucción @timeout
hace que la ejecución de las sentencias de prueba añadan a la instrucción del sistema operativo un prefijo con una instrucción de tipo timeout
. El valor exacto dependerá de la configuración de Siette. Un valor común de este prefijo es: timeout -k $1 $1
en donde $1
representa el tiempo límite. Por ejemplo, el script:
@OnError stop javac Punto.java <> *error* -> : ERROR_COMPILACION @timeout 10 java Punto 1 == 1,2 -> : ERROR_EJECUCION java Punto 2 == 3,4 -> : ERROR_EJECUCION java Punto 3 == 5,6 -> OK : ERROR_EJECUCION
al invocar la ejecución sobre el sistema operativo de las instrucciones de las sentencia de prueba ejecutará realmente las siguientes instrucciones, siempre que no se produzca un error que detenga el script. Estas instrucciones nunca se muestran realmente al usuario, ya que este proceso queda oculto.
javac Punto.java timeout -k 10 10 java Punto 1 timeout -k 10 10 java Punto 2 timeout -k 10 10 java Punto 3
En caso de que alguna de las sentencias no concluya su ejecución en 10 segundos, la instrucción timeout
forzará una señal kill
para detener el proceso por lo que presumiblemente el resultado de la ejecución será vacío y desde el punto de vista de la sentencia de control, se producirá un error.
En casos en los que se prevea la necesidad de usar límites de tiempo, por ejemplo, si se prevee que ejecución de una instrucción pudiera ocasionar un bucle sin final o un consumo excesivo de recursos, etc. es muy recomendable usar esta opción.
Esta sentencia de control sirve para indicar que las sentencias de prueba no se ejecutan sobre el servidor principal de Siette sino sobre un servidor remoto al cual tiene acceso el servidor de Siette. La sintáxis de estas sentencias es la siguiente:
@ssh user@host
en donde user@host es el usuario y la direccíón IP (simbolica o numérica), donde realmente se ejecutarán las sentencias de prueba.
Para que el mecanismo funcione, la máquina donde reside el servidor de Siette, y el usuario que ejecuta el proceso de la aplicación web Siette (normalmente el usuario tomcat
) debe tener acceso directo al host remoto, sin que sea necesaria la identificación mediante contraseña. 1)
Por ejemplo el siguiente script:
@OnError stop @ssh alumno@practicas.iaia.lcc.uma.es javac Punto.java <> *error* -> : ERROR_COMPILACION java Punto 1 == 1,2 -> : ERROR_EJECUCION java Punto 2 == 3,4 -> : ERROR_EJECUCION java Punto 3 == 5,6 -> OK : ERROR_EJECUCION
modifica internamente las llamadas a las instrucciones del sistema operativo de manera que las instrucciones que realmente se ejecutan son las siguientes:
ssh alumno@practicas.iaia.lcc.uma.es 'javac Punto.java' ssh alumno@practicas.iaia.lcc.uma.es 'java Punto 1' ssh alumno@practicas.iaia.lcc.uma.es 'java Punto 2' ssh alumno@practicas.iaia.lcc.uma.es 'java Punto 3'
A estas instrucciones hay que añadir otra que se genera automáticamente y que sirve para transferir todos los archivos del directorio de trabajo, entre los que se encuentran los ficheros que ha enviado el alumno, al servidor externo:
ssh alumno@practicas.iaia.lcc.uma.es 'mkdir -p dir' scp dir/* alumno@practicas.iaia.lcc.uma.es:dir
Estas dos instrucciones se ejecután en el punto en el que se sitúe la sentencia de control @ssh
, es decir, antes de las sentencias de prueba, como es lógico.
El uso de servidores remotos para la ejecución de las sentencias de prueba ofrece grandes ventajas frente a la seguridad. Si no se especifica ningún servidor externo, las instrucciones que ha incluido el profesor en el script se ejecutan sobre el propio servidor de Siette bajo el usuario que es porpietario del proceso del servidor de Siette (normalmente tomcat
2) ).
El script lo escriben los profesores, pero los ficheros los envían los alumnos. No es raro que un script compile y ejecute un archivo enviado por un alumno, y hay que prever un posible uso inapropiado o incluso malintencionado al ejecutar sobre el servidor uno de estos programas. Además de los controles del usuario tomcat
en Siette se han establecido algunos controles adicionales para que no se pueda acceder a otros directorios distintos al directorio de trabajo.
Aun así, el uso de servidores externos es mucho mas seguro ya que pueden emplearse máquinas virtuales cuyo funcionamiento no afecte al servidor principal, que pueden desecharse completamente una vez terminada la evaluación, generando otra nueva máquina virtual cada vez que haga falta.
Al finalizar la ejecución del script Siette borra del directorio de trabajo local todos los ficheros salvo los que hubiera enviado el alumno, pero deja intactos los ficheros en el directorio de trabajo remoto.
Esta sentencia de control indica al script si debe o no incluir en la respuesta que devuelve a Siette la instrucción del sistema operativo correspondiente a la última sentencia de prueba que se ejecutó. La sintaxis es la siguiente:
@Command (on
|off
)
Por ejemplo, sea el siguiente script de preocesamiento que comprueba si un programa en Python es capaz de reconocer los números impares:
@OnError stop @Eval max correct @Command on python3 test.py 1 == True -> 1 python3 test.py 2 == False -> 2 python3 test.py 3 == True -> 3 python3 test.py 4 == False -> 4 python3 test.py 5 == True -> 5 python3 test.py 6 == False -> 6
Si un alumno envia un fichero test.py
que da error por ejemplo en la tercera sentencia de prueba, siguiendo el criterio de detener la ejecución al encontrar el primer error y devolver la última etiqueta correcta, Siette devolverá como respuesta la etiqueta: <!–2–>
. Sin embargo, si está activa da la opción
@Command on
ademas de devolver la etiqueta añadirá la instrucción y el resultado de la instrucción como parte de la respuesta, es decir la respuesta completa será:
<!--2--> ><font color='red'>python3 test.py 3\nFalse</font>
Esto sirve para indicarle al alumno cuando se muestren los resultados de la evaluación que la respuesta que dió (al enviar el fichero), ha provocado que la ejecución del comando python3 test.py 3
diera como resultado False
. Esta información puede servir al alumno para señalarle en que casos se produce un error y permitirle refinar su código y volver a enviar una nueva respuesta en sucesivos intentos (si el test lo permite).
La opcion por defecto es @Command on
; pero en ocasiones mostrar la instrucción exacta que da lugar al error no es oportuno desde el punto de vista de la evaluación, ya que simplemente descubre cual es la prueba singular que falla y el alumno solo tendría que modificar su porgrama resolviendo este caso particular, y no modificando el programa convenientemente. En estos casos puede usarse la opción @Command off
que no añaden a la respuesta el resultado de la última ejecución. (Vea en el manual las consideraciones y consejos acerca de pruebas públicas y privadas).
En caso de que la respuesta se considere correcta, es decir, cuando la ejecución ha llegado al final del script de procesamiento, nunca se añade el resultado de la instrucción del sistema operativo. Solo se hace cuando como resultado de una prueba que ha dado error.
El lenguaje SPSL es muy simple y no contiene variables como en otros lenguajes de programación, pero si algunas macros que pueden usarse como tales al modo en que o hacen las macros #define
en el [wpes>Preprocesador_de_C| preprocesador del lenguaje C]], o mejor aún las variables del shell de Unix. Una variable se define mediante la siguiente sentencia de control:
@ $variable = valor
Por ejemplo, supongamos el siguiente script, estractado de un caso real de la asignatura Procesadores de lenguajes:
@ $file = repeat6 @Correspondencia: false, false, false, true @Command on java PLXC $file.plx $file.ctd == -> : ERROR-A ./ctd $file.ctd == 0 5 5 5 5 0 10 0 -> 0 : ERROR-B @Command off java PLXC $filev.plx $filev.ctd == -> 0 : 0 ./ctd $filev.ctd == 0 7 7 7 7 7 -> 10 : 0
En este script se ejecuta en Java un programa PLXC
que deberá compilar (traducir) un programa en un lenguaje plx
denominado repeat6.plx
para obtener un fichero objeto en código de tres direcciones repeat6.ctd
, que es a su vez la entrada de otro programa ctd
que interpreta el código generado y prueba que la ejecución de este fichero da como resultado 0 5 5 5 5 0 10 0
siendo esta una prueba pública visible por el alumno. Ademas, para comprobar que el compilador funciona correctamente utiliza una prueba privada con un fichero de entrada repeat6v.plx
al que aplica la misma secuencia de instrucciones, esperando obtener un resultado diferente.
La variable $file
es en realidad una macro que se sustituye por repeat6
, por lo que el funcionamiento del script anterior puede duplicarse, cambiando solamente los resultado de los casos de prueba:
@$file = repeat7 @Correspondencia: false, false, false, true @Command on java PLXC $file.plx $file.ctd == -> : ERROR-A ./ctd $file.ctd == 10 10 5 5 5 -> 0 : ERROR-B @Command off java PLXC $filev.plx $filev.ctd == -> 0 : 0 ./ctd $filev.ctd == 9 9 6 6 6 6 -> 10 : 0
Estas variables son útiles cuando algún elemento del lenguaje se repite varias veces a lo largo del script y facilitan la copia y modificación del script de manera mucho mas agil, y con menores probabilidad de cometer errores de transcripción. En los ejemplos anteriores por ejemplo solo será necesario cambiar el nombre de la variable una vez al principio y modificar los casos de prueba. Si no se hubieran empleado variables, habría que haber modificado todas las sentencias de prueba
Es normal que algunas sentencias de prueba se repitan varias veces en un script, usando distintos valores en las pruebas y esperando quizás distintos resultados. Para estos casos se ha diseñado una única sentencia de repetición cuya sintaxis es similar a la sentencia for del lenguaje BASH del shell de Unix:
for
variable in
listaValores do
lista de Sentencias
done
Por ejemplo, el script que se ha usado como ejemplo anteriormente para ilustrar el uso de @OnError continue
quedaría asi:
@OnError continue @Eval add all for i in 1 3 5 do python3 test.py $i == True -> 1 : -1 done for i in 2 4 6 do python3 test.py $i == False -> 1 : -1 done
No es necesario declarar previamente la variable del bucle for
. La variable se sustituyen en las sentencias de prueba antes de la ejecución y a todos los efectos es lo mismo que si se escribieran varias sentencias de prueba. El ejemplo anterior produce exactamente el mismo efecto que el siguiente trozo, ya que internamente el bucle for se pre-procesa y sustituye el trozo de los bucles por las correspondientes sentencias de prueba ya instanciadas.
@OnError continue @Eval add all python3 test.py 1 == True -> 1 : -1 python3 test.py 3 == True -> 1 : -1 python3 test.py 5 == True -> 1 : -1 python3 test.py 2 == False -> 1 : -1 python3 test.py 4 == False -> 1 : -1 python3 test.py 6 == False -> 1 : -1
Aunque no es obligatorio, el uso del bucle for
suele ir asociado a los modos de evaluación @Eval add all
y @OnError continue
, que realiza todas las pruebas y contabiliza las que obtiene un resultado positivo, descontando o no las pruebas con resultado negativo.
Cualquier línea que comience con el caracter #
se considera un comentario, y se ignora completamente. Los espacios en blanco no se tienen en cuanta mas que para separar los diferentes elemntos del lenguaje.
La siguiente gramática define y resume la sintaxis del lenguaje SPSL:
sentList → sentList sent | sent sent → controlSent LF | proofSent LF | forSent LF controlSent → patternType fileExecFlag “:” patternOptions | “@OnError” errorOption | “@Eval” evalOption | “@Scale” NUMBER NUMBER | “@Answer” label | “@ssh” USER@HOST | “@timeout” NUMBER | “@command” commandOption patternType → “@Correspondence” | “@SIETTEPattern” | “@JavaPattern” | “@MathPattern” fileExecFlag → “file” | "exec" | ε patternOptions → patternOptions “,” patternOption | patternOption patternOption → “true” | “false” errorOption → “skip” | “continue” | “stop” evalOption → “add correct” | “add all” | “max correct” | “max all” commandOption | “on” | "off" proofSent → COMMANDLINE comparison successCase failCase LF compararison → “==” RESULT | “<>” RESULT | ε successCase → “->” label | ε failCase → “:” label | ε forSent → “for” VARIABLE “in” valueList LF “do” LF sentList “done” valueList → valueList label | label label → TEXTLABEL | NUMBER
root