Skip to content

Commit 5868df5

Browse files
authored
lsp-csharp.el: implement commands to run tests (#2574)
* Add lsp-csharp-run-test-at-point * Add lsp-csharp-run-test-in-buffer * Add lsp-csharp-run-all-tests-in-buffer
1 parent 555bd9f commit 5868df5

File tree

2 files changed

+198
-5
lines changed

2 files changed

+198
-5
lines changed

clients/lsp-csharp.el

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ Set this if you have the binary installed or have it built yourself."
4747
:group 'lsp-csharp
4848
:type '(string :tag "Single string value or nil"))
4949

50+
(defcustom lsp-csharp-test-run-buffer-name
51+
"*lsp-csharp test run*"
52+
"The name of buffer used for outputing lsp-csharp test run results."
53+
:group 'lsp-csharp
54+
:type 'string)
55+
5056
(defun lsp-csharp--version-list-latest (lst)
5157
(->> lst
5258
(-sort (lambda (a b) (not (version<= (substring a 1)
@@ -244,6 +250,184 @@ tarball or a zip file (based on a current platform) to TARGET-DIR."
244250
((&omnisharp:MsBuildProject :path) ms-build-project))
245251
(find-file path)))
246252

253+
(defun lsp-csharp--get-buffer-code-elements ()
254+
"Retrieve code structure by calling into the /v2/codestructure endpoint.
255+
Returns :elements from omnisharp:CodeStructureResponse."
256+
(-let* ((code-structure (lsp-request "o#/v2/codestructure"
257+
(lsp-make-omnisharp-code-structure-request :file-name (buffer-file-name))))
258+
((&omnisharp:CodeStructureResponse :elements) code-structure))
259+
elements))
260+
261+
(defun lsp-csharp--inspect-code-elements-recursively (fn elements)
262+
"Invoke FN for every omnisharp:CodeElement found recursively in ELEMENTS."
263+
(seq-each
264+
(lambda (el)
265+
(funcall fn el)
266+
(-let (((&omnisharp:CodeElement :children) el))
267+
(lsp-csharp--inspect-code-elements-recursively fn children)))
268+
elements))
269+
270+
(defun lsp-csharp--collect-code-elements-recursively (predicate elements)
271+
"Flatten the omnisharp:CodeElement tree in ELEMENTS matching PREDICATE."
272+
(let ((results nil))
273+
(lsp-csharp--inspect-code-elements-recursively (lambda (el)
274+
(when (funcall predicate el)
275+
(setq results (cons el results))))
276+
elements)
277+
results))
278+
279+
(lsp-defun lsp-csharp--l-c-within-range (l c (&omnisharp:Range :start :end))
280+
"Determine if L (line) and C (column) are within RANGE."
281+
(-let* (((&omnisharp:Point :line start-l :column start-c) start)
282+
((&omnisharp:Point :line end-l :column end-c) end))
283+
(or (and (= l start-l) (>= c start-c) (or (> end-l start-l) (<= c end-c)))
284+
(and (> l start-l) (< l end-l))
285+
(and (= l end-l) (<= c end-c)))))
286+
287+
(defun lsp-csharp--code-element-stack-on-l-c (l c elements)
288+
"Return omnisharp:CodeElement stack at L (line) and C (column) in ELEMENTS tree."
289+
(when-let ((matching-element (seq-find (lambda (el)
290+
(-when-let* (((&omnisharp:CodeElement :ranges) el)
291+
((&omnisharp:RangeList :full?) ranges))
292+
(lsp-csharp--l-c-within-range l c full?)))
293+
elements)))
294+
(-let (((&omnisharp:CodeElement :children) matching-element))
295+
(cons matching-element (lsp-csharp--code-element-stack-on-l-c l c children)))))
296+
297+
(defun lsp-csharp--code-element-stack-at-point ()
298+
"Return omnisharp:CodeElement stack at point as a list."
299+
(let ((pos-line (plist-get (lsp--cur-position) :line))
300+
(pos-col (plist-get (lsp--cur-position) :character)))
301+
(lsp-csharp--code-element-stack-on-l-c pos-line
302+
pos-col
303+
(lsp-csharp--get-buffer-code-elements))))
304+
305+
(lsp-defun lsp-csharp--code-element-test-method-p (element)
306+
"Return test method name and test framework for a given ELEMENT."
307+
(when element
308+
(-when-let* (((&omnisharp:CodeElement :properties) element)
309+
((&omnisharp:CodeElementProperties :test-method-name? :test-framework?) properties))
310+
(list test-method-name? test-framework?))))
311+
312+
(defun lsp-csharp--reset-test-buffer (present-buffer)
313+
"Create new or reuse an existing test result output buffer.
314+
PRESENT-BUFFER will make the buffer be presented to the user."
315+
(with-current-buffer (get-buffer-create lsp-csharp-test-run-buffer-name)
316+
(compilation-mode)
317+
(read-only-mode)
318+
(let ((inhibit-read-only t))
319+
(erase-buffer)))
320+
321+
(when present-buffer
322+
(display-buffer lsp-csharp-test-run-buffer-name)))
323+
324+
(defun lsp-csharp--start-tests (test-method-framework test-method-names)
325+
"Run test(s) identified by TEST-METHOD-NAMES using TEST-METHOD-FRAMEWORK."
326+
(if (and test-method-framework test-method-names)
327+
(let ((request-message (lsp-make-omnisharp-run-tests-in-class-request
328+
:file-name (buffer-file-name)
329+
:test-frameworkname test-method-framework
330+
:method-names (vconcat test-method-names))))
331+
(lsp-csharp--reset-test-buffer t)
332+
(lsp-session-set-metadata "last-test-method-framework" test-method-framework)
333+
(lsp-session-set-metadata "last-test-method-names" test-method-names)
334+
(lsp-request-async "o#/v2/runtestsinclass"
335+
request-message
336+
(-lambda ((&omnisharp:RunTestResponse))
337+
(message "lsp-csharp: Test run has started"))))
338+
(message "lsp-csharp: No test methods to run")))
339+
340+
(defun lsp-csharp--test-message (message)
341+
"Emit a MESSAGE to lsp-csharp test run buffer."
342+
(when-let ((existing-buffer (get-buffer lsp-csharp-test-run-buffer-name))
343+
(inhibit-read-only t))
344+
(with-current-buffer existing-buffer
345+
(save-excursion
346+
(goto-char (point-max))
347+
(insert message "\n")))))
348+
349+
(defun lsp-csharp-run-test-at-point ()
350+
"Start test run at current point (if any)."
351+
(interactive)
352+
(let* ((stack (lsp-csharp--code-element-stack-at-point))
353+
(element-on-point (car (last stack)))
354+
(test-method (lsp-csharp--code-element-test-method-p element-on-point))
355+
(test-method-name (car test-method))
356+
(test-method-framework (car (cdr test-method))))
357+
(lsp-csharp--start-tests test-method-framework (list test-method-name))))
358+
359+
(defun lsp-csharp-run-all-tests-in-buffer ()
360+
"Run all test methods in the current buffer."
361+
(interactive)
362+
(let* ((elements (lsp-csharp--get-buffer-code-elements))
363+
(test-methods (lsp-csharp--collect-code-elements-recursively 'lsp-csharp--code-element-test-method-p elements))
364+
(test-method-framework (car (cdr (lsp-csharp--code-element-test-method-p (car test-methods)))))
365+
(test-method-names (mapcar (lambda (method)
366+
(car (lsp-csharp--code-element-test-method-p method)))
367+
test-methods)))
368+
(lsp-csharp--start-tests test-method-framework test-method-names)))
369+
370+
(defun lsp-csharp-run-test-in-buffer ()
371+
"Run selected test in current buffer."
372+
(interactive)
373+
(when-let* ((elements (lsp-csharp--get-buffer-code-elements))
374+
(test-methods (lsp-csharp--collect-code-elements-recursively 'lsp-csharp--code-element-test-method-p elements))
375+
(test-method-framework (car (cdr (lsp-csharp--code-element-test-method-p (car test-methods)))))
376+
(test-method-names (mapcar (lambda (method)
377+
(car (lsp-csharp--code-element-test-method-p method)))
378+
test-methods))
379+
(selected-test-method-name (lsp--completing-read "Select test:" test-method-names 'identity)))
380+
(lsp-csharp--start-tests test-method-framework (list selected-test-method-name))))
381+
382+
(defun lsp-csharp-run-last-tests ()
383+
"Re-run test(s) that were run last time."
384+
(interactive)
385+
(if-let ((last-test-method-framework (lsp-session-get-metadata "last-test-method-framework"))
386+
(last-test-method-names (lsp-session-get-metadata "last-test-method-names")))
387+
(lsp-csharp--start-tests last-test-method-framework last-test-method-names)
388+
(message "lsp-csharp: No test method(s) found to be ran previously on this workspace")))
389+
390+
(lsp-defun lsp-csharp--handle-os-error (_workspace (&omnisharp:ErrorMessage :file-name :text))
391+
"Handle the 'o#/error' (interop) notification by displaying a message with lsp-warn."
392+
(lsp-warn "%s: %s" file-name text))
393+
394+
(lsp-defun lsp-csharp--handle-os-testmessage (_workspace (&omnisharp:TestMessageEvent :message))
395+
"Handle the 'o#/testmessage and display test message on lsp-csharp
396+
test output buffer."
397+
(lsp-csharp--test-message message))
398+
399+
(lsp-defun lsp-csharp--handle-os-testcompleted (_workspace (&omnisharp:DotNetTestResult
400+
:method-name
401+
:outcome
402+
:error-message
403+
:error-stack-trace
404+
:standard-output
405+
:standard-error))
406+
"Handle the 'o#/testcompleted' message from the server.
407+
408+
Will display the results of the test on the lsp-csharp test output buffer."
409+
(let ((passed (string-equal "passed" outcome)))
410+
(lsp-csharp--test-message
411+
(format "[%s] %s "
412+
(propertize (upcase outcome) 'font-lock-face (if passed 'success 'error))
413+
method-name))
414+
415+
(unless passed
416+
(lsp-csharp--test-message error-message)
417+
418+
(when error-stack-trace
419+
(lsp-csharp--test-message error-stack-trace))
420+
421+
(unless (seq-empty-p standard-output)
422+
(lsp-csharp--test-message "STANDARD OUTPUT:")
423+
(seq-doseq (stdout-line standard-output)
424+
(lsp-csharp--test-message stdout-line)))
425+
426+
(unless (seq-empty-p standard-error)
427+
(lsp-csharp--test-message "STANDARD ERROR:")
428+
(seq-doseq (stderr-line standard-error)
429+
(lsp-csharp--test-message stderr-line))))))
430+
247431
(lsp-defun lsp-csharp--action-client-find-references ((&Command :arguments?))
248432
"Read first argument from ACTION as Location and display xrefs for that location
249433
using the `textDocument/references' request."
@@ -255,10 +439,6 @@ using the `textDocument/references' request."
255439
(lsp-show-xrefs (lsp--locations-to-xref-items locations-found) nil t)
256440
(message "No references found")))
257441

258-
(lsp-defun lsp-csharp--handle-os-error (_workspace (&omnisharp:ErrorMessage :file-name :text))
259-
"Handle the 'o#/error' (interop) notification by displaying a message with lsp-warn."
260-
(lsp-warn "%s: %s" file-name text))
261-
262442
(lsp-register-client
263443
(make-lsp-client :new-connection (lsp-stdio-connection
264444
#'lsp-csharp--language-server-command
@@ -277,6 +457,8 @@ using the `textDocument/references' request."
277457
("o#/packagerestorefinished" 'ignore)
278458
("o#/unresolveddependencies" 'ignore)
279459
("o#/error" 'lsp-csharp--handle-os-error)
460+
("o#/testmessage" 'lsp-csharp--handle-os-testmessage)
461+
("o#/testcompleted" 'lsp-csharp--handle-os-testcompleted)
280462
("o#/projectconfiguration" 'ignore)
281463
("o#/projectdiagnosticstatus" 'ignore))
282464
:download-server-fn

lsp-protocol.el

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,18 @@ See `-let' for a description of the destructuring mechanism."
367367
(lsp-interface (omnisharp:ErrorMessage (:Text :FileName :Line :Column))
368368
(omnisharp:ProjectInformationRequest (:FileName))
369369
(omnisharp:MsBuildProject (:IsUnitProject :IsExe :Platform :Configuration :IntermediateOutputPath :OutputPath :TargetFrameworks :SourceFiles :TargetFramework :TargetPath :AssemblyName :Path :ProjectGuid))
370-
(omnisharp:ProjectInformation (:ScriptProject :MsBuildProject)))
370+
(omnisharp:ProjectInformation (:ScriptProject :MsBuildProject))
371+
(omnisharp:CodeStructureRequest (:FileName))
372+
(omnisharp:CodeStructureResponse (:Elements))
373+
(omnisharp:CodeElement (:Kind :Name :DisplayName :Children :Ranges :Properties))
374+
(omnisharp:CodeElementProperties () (:static :accessibility :testMethodName :testFramework))
375+
(omnisharp:Range (:Start :End))
376+
(omnisharp:RangeList () (:attributes :full :name))
377+
(omnisharp:Point (:Line :Column))
378+
(omnisharp:RunTestsInClassRequest (:MethodNames :RunSettings :TestFrameworkname :TargetFrameworkVersion :NoBuild :Line :Column :Buffer :FileName))
379+
(omnisharp:RunTestResponse (:Results :Pass :Failure :ContextHadNoTests))
380+
(omnisharp:TestMessageEvent (:MessageLevel :Message))
381+
(omnisharp:DotNetTestResult (:MethodName :Outcome :ErrorMessage :ErrorStackTrace :StandardOutput :StandardError)))
371382

372383
(lsp-interface (rls:Cmd (:args :binary :env :cwd) nil))
373384

0 commit comments

Comments
 (0)