branch: externals/plz-media-type
commit 6ad13f4320cbf57e5f81a9cbee4a012ba29ae20a
Author: Roman Scherer <ro...@burningswell.com>
Commit: r0man <ro...@burningswell.com>

    Don't fail on blank lines when parsing application/x-ndjson
---
 plz-media-type.el                                  | 25 ++++++---
 tests/plz-media-type-test.el                       |  2 +
 .../application/x-ndjson/ollama-blank-lines.txt    | 37 +++++++++++++
 .../application/x-ndjson/ollama-broken.txt         |  9 ++++
 tests/test-plz-media-type.el                       | 62 +++++++++++++++++++++-
 5 files changed, 128 insertions(+), 7 deletions(-)

diff --git a/plz-media-type.el b/plz-media-type.el
index 2bd2afc43d..bcf330e54c 100644
--- a/plz-media-type.el
+++ b/plz-media-type.el
@@ -277,6 +277,15 @@ STRING which is output just received from the process."
         (when moving
           (goto-char (process-mark process)))))))
 
+(defconst plz-media-type--blank-line-regexp
+  (rx (+ space) (or "\r\n" "\n" "\r"))
+  "Regular expression matching a blank line.")
+
+(defun plz-media-type--delete-blank-lines ()
+  "Delete the next blank lines following point."
+  (while (looking-at plz-media-type--blank-line-regexp)
+    (delete-region (match-beginning 0) (match-end 0))))
+
 ;; Content Type: application/octet-stream
 
 (defclass plz-media-type:application/octet-stream (plz-media-type)
@@ -461,7 +470,8 @@ will always be set to nil.")
   "Parse a single line of the newline delimited JSON MEDIA-TYPE."
   (when (looking-at plz-media-type:application/x-ndjson--line-regexp)
     (prog1 (plz-media-type--parse-json-object media-type)
-      (delete-region (match-beginning 0) (match-end 0)))))
+      (when (< (match-beginning 0) (match-end 0))
+        (delete-region (match-beginning 0) (match-end 0))))))
 
 (defun plz-media-type:application/x-ndjson--parse-stream (media-type)
   "Parse all lines of the newline delimited JSON MEDIA-TYPE in the PROCESS 
buffer."
@@ -470,11 +480,14 @@ will always be set to nil.")
       (unless plz-media-type--position
         (setq-local plz-media-type--position (point)))
       (goto-char plz-media-type--position)
-      (when-let (object (plz-media-type:application/x-ndjson--parse-line 
media-type))
-        (while object
-          (setq-local plz-media-type--position (point))
-          (push object objects)
-          (setq object (plz-media-type:application/x-ndjson--parse-line 
media-type))))
+      (plz-media-type--delete-blank-lines)
+      (condition-case nil
+          (when-let (object (plz-media-type:application/x-ndjson--parse-line 
media-type))
+            (while object
+              (setq-local plz-media-type--position (point))
+              (push object objects)
+              (setq object (plz-media-type:application/x-ndjson--parse-line 
media-type))))
+        (json-end-of-file))
       objects)))
 
 (cl-defmethod plz-media-type-process
diff --git a/tests/plz-media-type-test.el b/tests/plz-media-type-test.el
index 7c36aae39f..5ec0c1df74 100644
--- a/tests/plz-media-type-test.el
+++ b/tests/plz-media-type-test.el
@@ -63,8 +63,10 @@ If running httpbin locally, set to \"http://localhost\".";)
     ;; that something funny is going on...
     (cl-loop for i upto times ;; 10 seconds
              while (equal 'run (process-status process))
+             ;; TODO: sleep-for or sit-for?
              do (sleep-for seconds))))
 
+
 (cl-defmacro plz-deftest (name () &body docstring-keys-and-body)
   "Like `ert-deftest', but defines tests for both HTTP/1.1 and HTTP/2.
 Also defines local function `url' which returns its argument
diff --git a/tests/response/application/x-ndjson/ollama-blank-lines.txt 
b/tests/response/application/x-ndjson/ollama-blank-lines.txt
new file mode 100644
index 0000000000..1bd5835041
--- /dev/null
+++ b/tests/response/application/x-ndjson/ollama-blank-lines.txt
@@ -0,0 +1,37 @@
+HTTP/1.1 200 OK
+Content-Type: application/x-ndjson
+Date: Tue, 12 Mar 2024 12:05:13 GMT
+Transfer-Encoding: chunked
+
+{"model":"llama2","created_at":"2024-03-12T12:05:13.747334659Z","response":"Hello","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:13.814191426Z","response":" 
there","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:13.880926587Z","response":"!","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:13.947866055Z","response":" 
It","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.015054376Z","response":"'","done":false}
+
+
+{"model":"llama2","created_at":"2024-03-12T12:05:14.082471215Z","response":"s","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.148577108Z","response":" 
nice","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.214802148Z","response":" 
to","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.281459481Z","response":" 
meet","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.350610212Z","response":" 
you","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.419490326Z","response":".","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.486487527Z","response":" 
Is","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.553190097Z","response":" 
there","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.623595043Z","response":" 
something","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.694458171Z","response":" 
I","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.76547139Z","response":" 
can","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.833659175Z","response":" 
help","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.903078162Z","response":" 
you","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:14.97368534Z","response":" 
with","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.046102396Z","response":" 
or","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.117115422Z","response":" 
would","done":false}
+
+
+
+{"model":"llama2","created_at":"2024-03-12T12:05:15.18784764Z","response":" 
you","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.259555212Z","response":" 
like","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.328392358Z","response":" 
to","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.398189056Z","response":" 
chat","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.467785437Z","response":"?","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:15.535938819Z","response":"","done":true,"context":[518,25580,29962,3532,14816,29903,29958,5299,829,14816,29903,6778,13,13,10994,518,29914,25580,29962,13,10994,727,29991,739,29915,29879,7575,304,5870,366,29889,1317,727,1554,306,508,1371,366,411,470,723,366,763,304,13563,29973],"total_duration":3569916695,"load_duration":782537698,"prompt_eval_count":21,"prompt_eval_duration":998427000,"eval_count":27,"eval_duration":1788606000}
diff --git a/tests/response/application/x-ndjson/ollama-broken.txt 
b/tests/response/application/x-ndjson/ollama-broken.txt
new file mode 100644
index 0000000000..6f194344e3
--- /dev/null
+++ b/tests/response/application/x-ndjson/ollama-broken.txt
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Content-Type: application/x-ndjson
+Date: Tue, 12 Mar 2024 12:05:13 GMT
+Transfer-Encoding: chunked
+
+{"model":"llama2","created_at":"2024-03-12T12:05:13.747334659Z","response":"Hello","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:13.814191426Z","response":" 
there","done":false}
+{"model":"llama2","created_at":"2024-03-12T12:05:13.880926587Z","response":"!","done":false}
+{"model":"llama2","created_at":
diff --git a/tests/test-plz-media-type.el b/tests/test-plz-media-type.el
index 07b7cb2d75..374f77e695 100644
--- a/tests/test-plz-media-type.el
+++ b/tests/test-plz-media-type.el
@@ -226,13 +226,73 @@
                        (response . "Hello")
                        (done . :json-false))
                      (seq-elt objects 26)))
-      ;; TODO: Fix parsing of last line :/
       (should (equal '((model . "llama2")
                        (created_at . "2024-03-12T12:05:15.467785437Z")
                        (response . "?")
                        (done . :json-false))
                      (seq-elt objects 1))))))
 
+(ert-deftest 
test-plz-media-type-request:application/x-ndjson:ollama-blank-lines ()
+  (plz-media-type-test-with-mock-response (plz-media-type-test-response 
"application/x-ndjson/ollama-blank-lines.txt")
+    (let* ((else) (finally) (then) (objects)
+           (process (plz-media-type-request 'get "MOCK-URL"
+                      :as `(media-types ((application/x-ndjson
+                                          . 
,(plz-media-type:application/x-ndjson
+                                              :handler (lambda (object) (push 
object objects))))))
+                      :else (lambda (object) (push object else))
+                      :finally (lambda () (push t finally))
+                      :then (lambda (object) (push object then)))))
+      (plz-media-type-test-wait process)
+      (should (null else))
+      (should (equal '(t) finally))
+      (should (equal 1 (length then)))
+      (seq-doseq (response then)
+        (should (plz-response-p response))
+        (should (equal 200 (plz-response-status response)))
+        (should (null (plz-response-body response))))
+      (should (equal 27 (length objects)))
+      (should (equal '((model . "llama2")
+                       (created_at . "2024-03-12T12:05:13.747334659Z")
+                       (response . "Hello")
+                       (done . :json-false))
+                     (seq-elt objects 26)))
+      (should (equal '((model . "llama2")
+                       (created_at . "2024-03-12T12:05:15.467785437Z")
+                       (response . "?")
+                       (done . :json-false))
+                     (seq-elt objects 1))))))
+
+(ert-deftest test-plz-media-type-request:application/x-ndjson:ollama-broken ()
+  (plz-media-type-test-with-mock-response (plz-media-type-test-response 
"application/x-ndjson/ollama-broken.txt")
+    (let* ((else) (finally) (then) (objects)
+           (process (plz-media-type-request 'get "MOCK-URL"
+                      :as `(media-types ((application/x-ndjson
+                                          . 
,(plz-media-type:application/x-ndjson
+                                              :handler (lambda (object)
+                                                         (push object 
objects))))))
+                      :else (lambda (object) (push object else))
+                      :finally (lambda () (push t finally))
+                      :then (lambda (object) (push object then)))))
+      (plz-media-type-test-wait process)
+      (should (null else))
+      (should (equal '(t) finally))
+      (should (equal 1 (length then)))
+      (seq-doseq (response then)
+        (should (plz-response-p response))
+        (should (equal 200 (plz-response-status response)))
+        (should (null (plz-response-body response))))
+      (should (equal 3 (length objects)))
+      (should (equal '((model . "llama2")
+                       (created_at . "2024-03-12T12:05:13.747334659Z")
+                       (response . "Hello")
+                       (done . :json-false))
+                     (seq-elt objects 2)))
+      (should (equal '((model . "llama2")
+                       (created_at . "2024-03-12T12:05:13.880926587Z")
+                       (response . "!")
+                       (done . :json-false))
+                     (seq-elt objects 0))))))
+
 (ert-deftest test-plz-media-type-request:application/x-ndjson:proxy-http ()
   (plz-media-type-test-with-mock-response (plz-media-type-test-response 
"application/x-ndjson/proxy-http.txt")
     (let* ((else) (finally) (then) (objects)

Reply via email to