Emacs - Básico 1
GNU EMACS es un editor altamente extensible y vamos aprovechar esta posibilidad que nos ofrecen y construir un paquete que nos permita cubrir varios elementos (peticiones HTTP, SON, TDD, EMACS), el ejercicio entorna a implementar un módulo de EMACS que nos permita obtener el titulo de una incidencia, procediendo con:
- Autenticar a Gitea
- Solicitar id de incidencia
- Consultar titulo de incidencia indicado
- E insertar titulo en el árbol del documento org.
importante antes de empezar:
- ser usuario de EMACS.
- nociones en desarrollo de software.
- nociones del lenguaje LISP o uno de sus dialectos.
Vamos a usar el dialecto ELisp de EMACS el cual iremos conociendo poco a poco en el transcurso de la implementación.
Empecemos por crear la estructura mínima del paquete.
Makefile
.PHONY: test
test:
emacs --batch -L . -L tests -l grieta-tests.el -f ert-run-tests-batch
make test
Cannot open load file: No such file or directory, grieta-tests.el
make: *** [Makefile:3: test] Error 255
Empecemos a ilustrarnos a partir del error
Cannot open load file: No such file or directory, grieta-tests.el
no ubica el archivo grieta-tests.el
, debemos indicar el directorio donde se encuentra el archivo,
usamos los siguientes parametros.
- en el parametro
-L
le indicamos aENACS
donde ubicar archivosLisp
. - en el parametro
-l
le indicamos interpretar el archivoLisp
. - en el parametro
-f
le indicamos una función a ejecutar.
En otras palabras le estamos indicando a EMACS que ejecuta la función ert-run-tests-batch además que interprete el archivo grieta-tests.el
y que
posiblemente lo puede encontrar en el directorio tests
.
.
tests/grieta-tests.el
mkdir tests
touch tests/grieta-tests.el
make test
Running 0 tests (2023-07-10 21:29:29-0500, selector ‘t’)
Ran 0 tests, 0 results as expected, 0 unexpected (2023-07-10 21:29:29-0500, 0.000060 sec)
muy bien no mas errores por ahora, ahora vamos a proceder a iniciar con las pruebas y por medio de estas iremos realizando la implementación. Para automatizar las pruebas utilizaremos ERT.
Para empezar definimos como agrupar las pruebas:
UNIT
funciones que solo varían su comportamiento según los argumentos.INTEGRATION
funciones que varían su comportamiento según los argumentos u otros medios como variables globales o lecturas fuera del proceso (lectura disco, red, etc..).ACCEPTANCE
los mismo criterios queINTEGRATION
pero solo sobre las funciones publicadas es decir sobre las funciones que son accesible por el usuario u otros paquetes.MANUAL
pruebas que se deben realizar manualmente ya que el contexto no se puede reproduccir enACCEPTANCE
.
tests/grieta-tests.el
(require 'el-mock)
;; UNIT
;; INTEGRATION
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Cannot open load file: No such file or directory, el-mock
make: *** [Makefile:3: test] Error 255
Uumm ya hemos visto anteriormente este error, el-mock
es una dependencia
que utilizaremos para verificar el comportamiento en las pruebas de INTEGRATION
y ACCEPTANCE
.
curl -q https://raw.githubusercontent.com/rejeep/el-mock.el/master/el-mock.el --output tests/el-mock.el
make test
Running 0 tests (2023-07-10 21:46:25-0500, selector ‘t’)
Ran 0 tests, 0 results as expected, 0 unexpected (2023-07-10 21:46:25-0500, 0.000052 sec)
Excelente, ya no tenemos error al importar la dependencia el-mock
con (require ’el-mock).
tests/grieta-tests.el
(require 'el-mock)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Test grieta-test-grieta--gitea-issue-check-url-path condition:
(void-function grieta--gitea-issue)
FAILED 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000067 sec)
Iniciar las pruebas desde INTEGRATION
o ACCEPTANCE
nos facilita tener una perspectiva más cercana a su uso y empezar con anterioridad el flujo de la implementación, hemos decido empezar por confirmar que la petición este en la estructura
esperada por la API de gitea.
(void-function grieta--gitea-issue)
lo vamos a ver cuando no logra llamar la función, en nuestro caso es por que no existe.
tests/grieta-tests.el
(require 'el-mock)
(defun grieta--gitea-issue (host owner repo issue-id))
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
(mock-error not-called grieta--gitea-call)
FAILED 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000075 sec)
(mock-error not-called grieta--gitea-call)
nos indica que se esperaba que se invocará esa función pero no sucedió.
tests/grieta-tests.el
(require 'el-mock)
(defun grieta--gitea-call (host method path))
(defun grieta--gitea-issue (host owner repo issue-id)
(grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id)))
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Running 1 tests (2023-07-10 22:03:12-0500, selector ‘t’)
passed 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000062 sec)
Ran 1 tests, 1 results as expected, 0 unexpected (2023-07-10 22:03:12-0500, 0.000143 sec)
Muy bien hemos ajustado según lo ilustrado por la prueba, ahora juguemos un poco en
aclarar el código, las funciones que creamos grieta--gitea-issue
y grieta--gitea-call
están en las pruebas, movamos estas pruebas a su propio archivo.
grieta.el
(defun grieta--gitea-call (host method path))
(defun grieta--gitea-issue (host owner repo issue-id)
(grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
tests/grieta-tests.el
(require 'el-mock)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Test grieta-test-grieta--gitea-issue-check-url-path condition:
(void-function grieta--gitea-issue)
FAILED 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000073 sec)
(void-function grieta--gitea-issue)
ya hemos lo hemos vistos anteriormente, no logra ubicar
la función y claro es debido a que movimos la función a otro archivo, debemos
importar el archivo.
tests/grieta-tests.el
(require 'el-mock)
(require 'grieta)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Running 1 tests (2023-07-10 22:08:57-0500, selector ‘t’)
passed 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000060 sec)
Ran 1 tests, 1 results as expected, 0 unexpected (2023-07-10 22:08:57-0500, 0.000137 sec)
Muy bien volvimos a estar estables, ya que empezamos por validar la estructura de la URL podemos continuar sobre el mismo tema de validar elementos hacia la API externa, procedamos por ejemplo con traducir la respuesta JSON a una estructura interna.
tests/grieta-tests.el
(require 'el-mock)
(require 'grieta)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-json->issues ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
;; respuesta simplificada
(stub grieta--gitea-call => "{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
(should (equal
'(1 "Test")
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Test grieta-test-grieta--gitea-json->issues condition:
(ert-test-failed
((should
(equal '...
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))
:form
(equal
((1 "Test"))
"{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
:value nil :explanation
(different-types
((1 "Test"))
"{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")))
FAILED 2/2 grieta-test-grieta--gitea-json->issues (0.000051 sec)
miremos solo una parte
:form
(equal
((1 "Test"))
"{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
evidentemente los resultados no son iguales estamos retornando directamente la respuesta de la API, procedamos a implementar el traductor.
grieta.el
(defun grieta--gitea-call (host method path))
(defun grieta--gitea-issue (host owner repo issue-id)
(let* ((response (grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if response
(let ((remote-issue (json-parse-string response)))
(list (gethash "number" remote-issue)
(gethash "title" remote-issue)))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
make test
Running 2 tests (2023-07-10 22:35:19-0500, selector ‘t’)
passed 1/2 grieta-test-grieta--gitea-issue-check-url-path (0.000058 sec)
passed 2/2 grieta-test-grieta--gitea-json->issues (0.000062 sec)
Ran 2 tests, 2 results as expected, 0 unexpected (2023-07-10 22:35:19-0500, 0.000208 sec
Hemos logrado estar otra vez estables, pero en este caso adicionamos un if
lo cual nos invita que debemos verificar cual sera el comportamiento cuando no se obtenga
una respuesta desde el servidor.
tests/grieta-tests.el
(require 'el-mock)
(require 'grieta)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-json->issues-on-valid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
;; respuesta simplificada
(stub grieta--gitea-call => "{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
(should (equal
'(1 "Test")
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-json->issues-on-invalid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(stub grieta--gitea-call => nil)
(should (equal
nil ; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--gitea-call "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Running 3 tests (2023-07-10 22:39:39-0500, selector ‘t’)
passed 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000064 sec)
passed 2/3 grieta-test-grieta--gitea-json->issues-on-invalid-data (0.000030 sec)
passed 3/3 grieta-test-grieta--gitea-json->issues-on-valid-data (0.000067 sec)
Ran 3 tests, 3 results as expected, 0 unexpected (2023-07-10 22:39:39-0500, 0.000269 sec)
Ya con las pruebas en curso pasemos a completar la implementación es decir hacer la petición HTTP.
grieta.el
(defun grieta--gitea-call (host method path)
(let ((url-request-method method)
(url-request-data path))
(with-current-buffer
(url-retrieve-synchronously (concat "https://" host path))
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
(defun grieta--gitea-issue (host owner repo issue-id)
(let* ((response (grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if response
(let ((remote-issue (json-parse-string response)))
(list (gethash "number" remote-issue)
(gethash "title" remote-issue)))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
Verificamos manualmente que funcione la petición al servidor.
emacs --batch -l grieta.el --eval='(print (grieta--gitea-call "gitea.demo.org" "GET" "/api/v1/repos/bit4bit/prueba/issues/1"))'
"{\"errors\":null,\"message\":\"The target couldn't be found.\",\"url\":\"https://gitea.onecluster.org/api/swagger\"}
"
uuum, consultemos a un repositorio público
emacs --batch -l grieta.el --eval='(print (grieta--gitea-call "gitea.onecluster.org" "GET" "/api/v1/repos/OneTeam/trytondo-analytic_operations/issues/1"))'
"{\"id\":1846,\"url\":\"https://gitea.onecluster.org/api/v1/repos/OneTeam/trytondo-analytic_operations/issues/1\",\"html_url\":\"https://gitea.onecluster.org/OneTeam/trytondo-analytic_operations/issues/1\",\"number\":1,\"user\":{\"id\":5,\"login\":\"Rodia\",\"login_name\":\"\",\"full_name\":\"\",\"email\":\"rodia@noreply.onecluster.org\",\"avatar_url\":\"https://gitea.onecluster.org/avatar/2bb02a97b20d83fd00f4e7e044e9c1ef\",\"language\":\"\",\"is_admin\":false,\"last_login\":\"0001-01-01T00:00:00Z\",\"created\":\"2021-09-05T14:03:55-05:00\",\"restricted\":false,\"active\":false,\"prohibit_login\":false,\"location\":\"\",\"website\":\"\",\"description\":\"\",\"visibility\":\"public\",\"followers_count\":0,\"following_count\":0,\"starred_repos_count\":0,\"username\":\"Rodia\"},\"original_author\":\"\",\"original_author_id\":0,\"title\":\"Añadir Centro de Costos en Encabezado a Facturas de Cliente y de Proveedor\",\"body\":\"\",\"ref\":\"\",\"assets\":[],\"labels\":[],\"milestone\":null,\"assignee\":null,\"assignees\":null,\"state\":\"open\",\"is_locked\":false,\"comments\":0,\"created_at\":\"2023-07-01T00:01:50-05:00\",\"updated_at\":\"2023-07-01T00:01:50-05:00\",\"closed_at\":null,\"due_date\":null,\"pull_request\":null,\"repository\":{\"id\":2134,\"name\":\"trytondo-analytic_operations\",\"owner\":\"OneTeam\",\"full_name\":\"OneTeam/trytondo-analytic_operations\"}}
"
ajaa, investigando un poco sobre la API nos indica que para repositorios privados debemos que una Api Key, y ya ilustrados sobre este escenario procedamos a implementarlo, pero antes revisemos algunos caminos.
- crear otra función ejemplo
grieta--gitea-auth-call
. - usar una variable global o función que nos entregue el api-key.
- adicionar otro parámetro a la función
grieta-gitea-call
.
La primera ya que aunque puede ser la más “sencilla” tendríamos redundancia en la lógica que a posterior nos afectaría el mantenimiento, entonces la descartamos.
Ahora valoremos la segunda, especulemos sobre una posible implementación
(defvar grieta-gitea-api-key-call nil)
(defun grieta--gitea-call (host method path)
(let ((url-request-method method)
((url-request-data path))
(if grieta-gitea-api-key-call
(url-request-extra-headers
;; acoplamiento
(cons "Authorization" (cons "token " grieta-gitea-api-key-call)))))
(with-current-buffer
(url-retrieve-synchronously (concat "https://" host path))
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
al ejecutar las pruebas estas pasan, pero miremos algo, recordando lo siguiente:
UNIT
funciones que solo varían su comportamiento según los argumentos.INTEGRATION
funciones que varían su comportamiento según los argumentos u otros medios como variables globales o lecturas fuera del proceso (lectura disco, red, etc..).ACCEPTANCE
los mismo criterios queINTEGRATION
pero solo sobre las funciones publicadas es decir sobre las funciones que son accesible por el usuario u otros paquetes.MANUAL
pruebas que se deben realizar manualmente.
hasta ahora hemos realizado las pruebas en el grupo de INTEGRATION
donde
hemos verificado la interacción con grieta--gitea-call
por medio de un doble,
hasta ahora no nos hemos interesado en probar la función ya que esta es una pasarela un servicio
externo donde no tenemos mas lógica que la peticion al servicio pero ahora
la situación cambia ya que creamos una dependencia implícita (ya que solo leyendo la implementación podemos determinarla) a una variable que va ser
modificada por el usuario creando un acoplamiento de un mecanismo privado a un
mecanismo netamente publico, acá es de suma importancia mantener una clara separacion
entre capas, grieta--gitea-call
pertenece a una capa privada
mientras que grieta-gitea-api-key-call
pertenece a una capa publica,
por lo tanto necesitaríamos algún mecanismo que nos permita pasar la configuración
desde la capa publica a la privada.
ahora especulemos sobre adicionar un parámetro
grieta.el
(defun grieta--gitea-call (host api-key method path)
(let ((url-request-method method)
(url-request-data path)
(url-request-extra-headers
(list (cons "Authorization" (concat "token " api-key)))))
(with-current-buffer
(url-retrieve-synchronously (concat "https://" host path))
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
(defun grieta--gitea-issue (host owner repo issue-id)
(let* ((response (grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if response
(let ((remote-issue (json-parse-string response)))
(list (gethash "number" remote-issue)
(gethash "title" remote-issue)))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
al cambiar la firma de la función debemos actualizar todas las funciones dependientes Martin Fowler en Refactoring
propone una metodología estructura de realizar esto, ya que es algo que estamos haciendo constantemente y es de suma importancia aprender a
refactorizar para no tomar caminos mas “rápidos” que a largo plazo son perjudiciales, ahora si valoramos nuevamente donde
realizaríamos las pruebas estas irían en INTEGRATION
ya que realizan lecturas
fuera de proceso, y mirando un poco mas de cerca la función esta solo realiza actividades fuera de proceso osea
que si realizáramos pruebas tendríamos que usar dobles para simular el comportamiento de funciones como url-retrieve-synchronously
pero este tipo de pruebas seria falsas ya que no conocemos o participamos del ciclo de desarrollo de las funciones,
por lo cual este tipo de funciones las verificaremos indirectamente en comportamientos de INTEGRATION
o ACCEPTANCE
o bien MANUAL
.
emacs --batch -l grieta.el --eval='(print (grieta--gitea-call "gitea.demo.org" "oeuaeiaoi" "GET" "/api/v1/repos/bit4bit/prueba/issues/1"))'
"{\"id\":1861,\"url\":\"https://gitea.onecluster.org/api/v1/repos/bit4bit/prueba/issues/1\",\"html_url\":\"https://gitea.onecluster.org/bit4bit/prueba/issues/1\",\"number\":1,\"user\":{\"id\":10,\"login\":\"bit4bit\",\"login_name\":\"\",\"full_name\":\"\",\"email\":\"bit4bit@noreply.onecluster.org\",\"avatar_url\":\"https://gitea.onecluster.org/avatar/5984dea48f59d0cbf32c81e80cc81525\",\"language\":\"\",\"is_admin\":false,\"last_login\":\"0001-01-01T00:00:00Z\",\"created\":\"2021-11-24T14:59:21-05:00\",\"restricted\":false,\"active\":false,\"prohibit_login\":false,\"location\":\"\",\"website\":\"\",\"description\":\"\",\"visibility\":\"private\",\"followers_count\":0,\"following_count\":0,\"starred_repos_count\":0,\"username\":\"bit4bit\"},\"original_author\":\"\",\"original_author_id\":0,\"title\":\"prueba\",\"body\":\"\",\"ref\":\"\",\"assets\":[],\"labels\":[],\"milestone\":null,\"assignee\":null,\"assignees\":null,\"state\":\"open\",\"is_locked\":false,\"comments\":0,\"created_at\":\"2023-07-07T13:57:25-05:00\",\"updated_at\":\"2023-07-08T10:07:39-05:00\",\"closed_at\":null,\"due_date\":null,\"pull_request\":null,\"repository\":{\"id\":2098,\"name\":\"prueba\",\"owner\":\"bit4bit\",\"full_name\":\"bit4bit/prueba\"}}
"
make test
Running 3 tests (2023-07-14 20:36:23-0500, selector ‘t’)
passed 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000058 sec)
passed 2/3 grieta-test-grieta--gitea-json->issues-on-invalid-data (0.000029 sec)
passed 3/3 grieta-test-grieta--gitea-json->issues-on-valid-data (0.000061 sec)
Ran 3 tests, 3 results as expected, 0 unexpected (2023-07-14 20:36:23-0500, 0.000264 sec)
aaa pero que paso?? porque pasaron las pruebas si cambio la firma de la función?? esto es algo que suele ocurre cuando usamos dobles, por algo se le llama dobles no es el elemento de origen, esto puede aclarar un poco de porque se recomienda evitar los dobles sobre elementos que no sean implementados por el proyecto,
por ejemplo url-request-method
tiene un ciclo de desarrollo diferente.
ahora miremos un poco como podemos mitigar el efecto de los dobles
grieta.el
(defun grieta--gitea-call (host api-key method path)
(let ((url-request-method method)
(url-request-data path)
(url-request-extra-headers
(list (cons "Authorization" (concat "token " api-key)))))
(with-current-buffer
(url-retrieve-synchronously (concat "https://" host path))
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
(defun grieta--gitea-issue (host owner repo issue-id)
;; es una abstraccion sobre la comunicacion con gitea
(let* ((response (grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if response
;; pero aca tenemos un mecanismo de implementacion
(let ((remote-issue (json-parse-string response)))
;; nuevamente una abstraccion
(list (gethash "number" remote-issue)
(gethash "title" remote-issue)))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
antes que nada no debemos forzar los conceptos, si algo no cuadra es porque no va, revisando
nuevamente la función grieta--gitea-issue
se puede ver que ahí un brinco entre diferentes niveles de abstracción (grieta-gitea-call
json-parse-string
, gethash
),
vamos a llevar esto a un solo nivel.
grieta.el
(defun grieta--url-auth-request (method token path)
(let ((url-request-method method)
(url-request-data path)
(url-request-extra-headers
(list (cons "Authorization" (concat "token " token)))))
(with-current-buffer
(url-retrieve-synchronously path)
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
(defun grieta--gitea-call (host api-key method path)
(let ((response (grieta--url-auth-request method api-key (concat "https://" host path))))
(if response
(json-parse-string response)
nil)))
(defun grieta--gitea-issue (host owner repo issue-id)
(let* ((remote-issue (grieta--gitea-call host "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if remote-issue
(list (gethash "number" remote-issue)
(gethash "title" remote-issue))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
la mitigación se da aislando en lo posible la peticion HTTP grieta—url-auth-request
. Hemos aplanado los niveles de abstracción; por ejemplo grieta--gitea-issue
habla en términos de una petición
y la traducción a estructura interna, mientras que grieta--gitea-call
habla en términos de petición HTTP.
Como movimos las responsabilidades debemos actualizar los dobles usados en las pruebas.
tests/grieta-tests.el
(require 'el-mock)
(require 'grieta)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-json->issues-on-valid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
;; respuesta simplificada
(stub grieta--url-auth-request => "{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
(should (equal
'(1 "Test")
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-json->issues-on-invalid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(stub grieta--url-auth-request => nil)
(should (equal
nil ; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--url-auth-request "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Test grieta-test-grieta--gitea-json->issues-on-valid-data condition:
(wrong-number-of-arguments
(lambda
(host api-key method path)
(let
((response ...))
(json-parse-string response)))
3)
FAILED 3/3 grieta-test-grieta--gitea-json->issues-on-valid-data (0.000073 sec)
Ran 3 tests, 0 results as expected, 3 unexpected (2023-07-14 21:01:44-0500, 0.114529 sec)
3 unexpected results:
FAILED grieta-test-grieta--gitea-issue-check-url-path
FAILED grieta-test-grieta--gitea-json->issues-on-invalid-data
FAILED grieta-test-grieta--gitea-json->issues-on-valid-data
Excelente ya se nos manifestó el error, procedamos a actualizar.
grieta.el
(defun grieta--url-auth-request (method token path)
(let ((url-request-method method)
(url-request-data path)
(url-request-extra-headers
(list (cons "Authorization" (concat "token " token)))))
(with-current-buffer
(url-retrieve-synchronously path)
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
(defun grieta--gitea-call (host api-key method path)
(let ((response (grieta--url-auth-request method api-key (concat "https://" host path))))
(if response
(json-parse-string response)
nil)))
(defun grieta--gitea-issue (host owner repo issue-id)
;; TODO: api-key?
(let* ((remote-issue (grieta--gitea-call host nil "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if remote-issue
(list (gethash "number" remote-issue)
(gethash "title" remote-issue))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
make test
Test grieta-test-grieta--gitea-issue-check-url-path condition:
(mock-error
(grieta--url-auth-request "gitea.test.org" "GET" "/api/v1/repos/demo/grieta/issues/99")
(grieta--url-auth-request "GET" nil "https://gitea.test.org/api/v1/repos/demo/grieta/issues/99"))
FAILED 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000096 sec)
passed 2/3 grieta-test-grieta--gitea-json->issues-on-invalid-data (0.000044 sec)
passed 3/3 grieta-test-grieta--gitea-json->issues-on-valid-data (0.000082 sec)
Ran 3 tests, 2 results as expected, 1 unexpected (2023-07-14 21:11:49-0500, 0.040699 sec)
excelente nos estamos ilustrando.
tests/grieta-tests.el
(require 'el-mock)
(require 'grieta)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-json->issues-on-valid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
;; respuesta simplificada
(stub grieta--url-auth-request => "{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
(should (equal
'(1 "Test")
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-json->issues-on-invalid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(stub grieta--url-auth-request => nil)
(should (equal
nil ; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--url-auth-request "GET" "token" "https://gitea.test.org/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Test grieta-test-grieta--gitea-issue-check-url-path condition:
(mock-error
(grieta--url-auth-request "GET" "token" "/api/v1/repos/demo/grieta/issues/99")
(grieta--url-auth-request "GET" nil "https://gitea.test.org/api/v1/repos/demo/grieta/issues/99"))
FAILED 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000098 sec)
passed 2/3 grieta-test-grieta--gitea-json->issues-on-invalid-data (0.000043 sec)
passed 3/3 grieta-test-grieta--gitea-json->issues-on-valid-data (0.000080 sec)
Ran 3 tests, 2 results as expected, 1 unexpected (2023-07-14 21:14:39-0500, 0.041251 sec)
las pruebas nos esta ilustrando, diciéndonos que piensa hacer con el token? nuevamente tenemos varios caminos
- crear otra función ejemplo
grieta--gitea-auth-issue
- usar una variable global o función que nos entregue el api-key.
- adicionar otro parámetro a la función
grieta--gitea-issue
.
ya conocemos las implicaciones de las dos primeras, vayámonos directamente por la tercera.
invito hacer el ejerció de llegar al estado presentado desde al archivo anteriormente expuesto a este, haciendo cambios que no dejen fallar las pruebas
grieta.el
(defun grieta--url-auth-request (method token path)
(let ((url-request-method method)
(url-request-data path)
(url-request-extra-headers
(list (cons "Authorization" (concat "token " token)))))
(with-current-buffer
(url-retrieve-synchronously path)
(goto-char (point-min))
(search-forward-regexp "\n\n")
(buffer-substring (point) (point-max)))))
(defun grieta--gitea-call (host api-key method path)
(let ((response (grieta--url-auth-request method api-key (concat "https://" host path))))
(if response
(json-parse-string response)
nil)))
(defun grieta--gitea-issue (host api-key owner repo issue-id)
;; TODO: api-key?
(let* ((remote-issue (grieta--gitea-call host api-key "GET" (format "/api/v1/repos/%s/%s/issues/%d" owner repo issue-id))))
(if remote-issue
(list (gethash "number" remote-issue)
(gethash "title" remote-issue))
nil)))
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html
;; `provide` es necesario para luego importar con `require`
(provide 'grieta)
tests/grieta-tests.el
(require 'el-mock)
(require 'grieta)
;; UNIT
;; INTEGRATION
(ert-deftest grieta-test-grieta--gitea-json->issues-on-valid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
;; respuesta simplificada
(stub grieta--url-auth-request => "{\"id\": 1000, \"number\": 1, \"title\": \"Test\"}")
(should (equal
'(1 "Test")
(grieta--gitea-issue "gitea.test.org" "token" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-json->issues-on-invalid-data ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(stub grieta--url-auth-request => nil)
(should (equal
nil ; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales
(grieta--gitea-issue "gitea.test.org" "token" "demo" "grieta" 1)))))
(ert-deftest grieta-test-grieta--gitea-issue-check-url-path ()
(with-mock
;; https://try.gitea.io/api/swagger#/issue/issueGetIssue
(mock (grieta--url-auth-request "GET" "token" "https://gitea.test.org/api/v1/repos/demo/grieta/issues/99"))
(grieta--gitea-issue "gitea.test.org" "token" "demo" "grieta" 99)))
;; ACCEPTANCE
;; MANUAL
(provide 'grieta-tests)
make test
Running 3 tests (2023-07-14 21:19:48-0500, selector ‘t’)
passed 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000064 sec)
passed 2/3 grieta-test-grieta--gitea-json->issues-on-invalid-data (0.000033 sec)
passed 3/3 grieta-test-grieta--gitea-json->issues-on-valid-data (0.000069 sec)
Ran 3 tests, 3 results as expected, 0 unexpected (2023-07-14 21:19:48-0500, 0.000305 sec)
boom, nuevamente estables, el proceso que realizamos de:
- especular
- ilustrar
- ajustar (+ importante no olvidar limpiar y aclarar lo escrito)
se realiza rápidamente en cuestión de segundos.