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 a ENACS donde ubicar archivos Lisp.
  • en el parametro -l le indicamos interpretar el archivo Lisp.
  • 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 que INTEGRATION 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 en ACCEPTANCE.

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.

  1. crear otra función ejemplo grieta--gitea-auth-call.
  2. usar una variable global o función que nos entregue el api-key.
  3. 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 que INTEGRATION 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

  1. crear otra función ejemplo grieta--gitea-auth-issue
  2. usar una variable global o función que nos entregue el api-key.
  3. 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:

  1. especular
  2. ilustrar
  3. ajustar (+ importante no olvidar limpiar y aclarar lo escrito)

se realiza rápidamente en cuestión de segundos.