From 903a68517c0993522af7bc057a5aec6e1dbbb608 Mon Sep 17 00:00:00 2001 From: jwortmann Date: Wed, 26 Oct 2022 19:00:52 +0200 Subject: [PATCH] Add feature to run testitems (#18) --- .gitattributes | 3 + LSP-julia.sublime-commands | 4 + LSP-julia.sublime-settings | 3 + README.md | 25 + icons/LICENSE | 21 + icons/convert.bat | 9 + icons/failed.png | Bin 0 -> 324 bytes icons/failed.svg | 1 + icons/failed@2x.png | Bin 0 -> 477 bytes icons/failed@3x.png | Bin 0 -> 667 bytes icons/passed.png | Bin 0 -> 404 bytes icons/passed.svg | 1 + icons/passed@2x.png | Bin 0 -> 796 bytes icons/passed@3x.png | Bin 0 -> 1158 bytes icons/stopwatch.png | Bin 0 -> 381 bytes icons/stopwatch.svg | 1 + icons/stopwatch@2x.png | Bin 0 -> 790 bytes icons/stopwatch@3x.png | Bin 0 -> 1142 bytes img/testitem.png | Bin 0 -> 47973 bytes plugin.py | 501 ++++++++++++++++++-- server/Manifest.toml | 54 ++- testrunner/Project.toml | 7 + testrunner/VSCodeTestServer_license/LICENSE | 25 + testrunner/runtestitem.jl | 279 +++++++++++ 24 files changed, 891 insertions(+), 43 deletions(-) create mode 100644 icons/LICENSE create mode 100644 icons/convert.bat create mode 100644 icons/failed.png create mode 100644 icons/failed.svg create mode 100644 icons/failed@2x.png create mode 100644 icons/failed@3x.png create mode 100644 icons/passed.png create mode 100644 icons/passed.svg create mode 100644 icons/passed@2x.png create mode 100644 icons/passed@3x.png create mode 100644 icons/stopwatch.png create mode 100644 icons/stopwatch.svg create mode 100644 icons/stopwatch@2x.png create mode 100644 icons/stopwatch@3x.png create mode 100644 img/testitem.png create mode 100644 testrunner/Project.toml create mode 100644 testrunner/VSCodeTestServer_license/LICENSE create mode 100644 testrunner/runtestitem.jl diff --git a/.gitattributes b/.gitattributes index 1438600..6a67e50 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,6 @@ .gitattributes export-ignore pyproject.toml export-ignore +/icons/*.svg export-ignore +/icons/convert.bat export-ignore +/img export-ignore sublime-package.json linguist-language=jsonc diff --git a/LSP-julia.sublime-commands b/LSP-julia.sublime-commands index 0e466dc..5a095c3 100644 --- a/LSP-julia.sublime-commands +++ b/LSP-julia.sublime-commands @@ -37,4 +37,8 @@ "caption": "LSP-julia: Run Code Cell", "command": "julia_run_code_cell" }, + { + "caption": "LSP-julia: Run Testitem", + "command": "julia_run_testitem" + }, ] diff --git a/LSP-julia.sublime-settings b/LSP-julia.sublime-settings index 200e5f3..1491f30 100644 --- a/LSP-julia.sublime-settings +++ b/LSP-julia.sublime-settings @@ -14,6 +14,9 @@ // Language server configurations "command": ["$julia_exe", "--startup-file=no", "--history-file=no", "--project=$server_path", "--eval", "using LanguageServer; runserver()"], "selector": "source.julia", + "initializationOptions": { + "julialangTestItemIdentification": true + }, // Formatting options must be configured through a .JuliaFormatter.toml file, see // https://domluna.github.io/JuliaFormatter.jl/stable/config/ diff --git a/README.md b/README.md index 73f5305..15fb370 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,33 @@ LSP-julia provides additional commands which are available from the command pale | LSP-julia: Select Code Block | none | Select the function or code block at the current cursor position. For multiple active cursors, only the topmost cursor position is taken into account. | | LSP-julia: Run Code Block1 | Alt+Enter | If text is selected, run it in a Julia REPL. Otherwise, run the code block containing the current cursor position and move curser to the next block. | | LSP-julia: Run Code Cell1 | Alt+Shift+Enter | If text is selected, run it in a Julia REPL. Otherwise, run the code cell containing the current cursor position and move curser to the next cell. Code cells are signalized with a specially formatted comment at the start of a line: `##`, `#%%` or `# %%`. | +| LSP-julia: Run Testitem | none | Show a quick panel with all available `@testitem`s in Julia files (see description below). | Commands marked with a 1 are only available if you have the Terminus package installed. To add or adjust key bindings for the commands, edit the `.sublime-keymap` file for your OS in your `Packages/User` folder. For an example refer to the [Default.sublime-keymap](Default.sublime-keymap) file in this repository, and for the command names from this package see [LSP-julia.sublime-commands](LSP-julia.sublime-commands). + +### Run individual test items + +LSP-julia has a custom feature, similar to the Julia extension for VS Code, which allows to run individual testsets from a Julia package directly from the editor UI. + +For this to work, the tests must be contained within a `@testitem` block, which is basically a replacement for `@testset`. +For an example see the screenshot below or read the detailed description at https://github.com/julia-vscode/TestItemRunner.jl#writing-tests. + +A `@testitem` can be run via the "Run Test" link shown in an annotation on the righthand side of the editor, or via the "LSP-julia: Run Testitem" command from the command palette. +Possible test failures or errors will be shown as annotations at the position in the code where they occured. + +> **Note** +> The `@testitem` feature only works in [project environments](https://docs.julialang.org/en/v1/manual/code-loading/#Project-environments), i.e. you must have opened a folder in the sidebar which contains a *Project.toml* file with a `name` and `uuid` field. + +To completely disable this feature, you can toggle off the following entry in the *LSP-julia.sublime-settings* file: +```json +{ + "initializationOptions": { + "julialangTestItemIdentification": false + } +} +``` + +![Testitem preview](img/testitem.png) diff --git a/icons/LICENSE b/icons/LICENSE new file mode 100644 index 0000000..00e9069 --- /dev/null +++ b/icons/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/icons/convert.bat b/icons/convert.bat new file mode 100644 index 0000000..1943738 --- /dev/null +++ b/icons/convert.bat @@ -0,0 +1,9 @@ +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 16 -h 16 --export-filename=failed.png failed.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 32 -h 32 --export-filename=failed@2x.png failed.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 48 -h 48 --export-filename=failed@3x.png failed.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 16 -h 16 --export-filename=passed.png passed.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 32 -h 32 --export-filename=passed@2x.png passed.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 48 -h 48 --export-filename=passed@3x.png passed.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 16 -h 16 --export-filename=stopwatch.png stopwatch.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 32 -h 32 --export-filename=stopwatch@2x.png stopwatch.svg +inkscape --export-type=png --export-area=-1.23:-1.23:18.46:18.46 -w 48 -h 48 --export-filename=stopwatch@3x.png stopwatch.svg diff --git a/icons/failed.png b/icons/failed.png new file mode 100644 index 0000000000000000000000000000000000000000..68fa2b52be933739406181abdfb5b63cffa57866 GIT binary patch literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG|3R2>^7Y@0Ktah8 z*NBqf{Irtt#G+J&^73-M%)IR4}9Q!i+rsggB0z_)|-U1PBRJE01exCZM7kG%r}pT7#TU6EUGGwjvsUE$}g z_X(eN_~cZ}b-yt8$kZ5z(#dr(Z2Nuws8w9a>c|&nk3afU_shq5e-w^!$uyS!@MP7S zbd8DsfbXN%J0|MJtUmwh>J;5G`!zp3mi=>Ewd|UUy0gZ@aLd1Zb*&ps{SK+$Hi|a< T*Uf$&=s5;YS3j3^P6 diff --git a/icons/failed@2x.png b/icons/failed@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0942af7d7eca7bb79855a06e79304e5a93de339 GIT binary patch literal 477 zcmV<30V4j1P)8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10cuG^ zK~z|U?Upf412GUqpHU8g6a+_r1_`#pXJ*fV6X2~$29_fqNpdL>NxI3&MAzimB3413CBmQdCC$7| zcM*WF`dV-a z49$K4u4>jg^BIJnfvG)*z{oaM;kCjJk&pNiJ7qaAuPo*?qn@M-N#|yF(u_jw_&%X8 zG$s!T=t4v?d6j@RBC^P(U;I;Am5EeDBaa=y^4C!)9Yrd+B1iBe*w}XbcLaO_-5r9D TH1i2{00000NkvXXu0mjfkcGi5 literal 0 HcmV?d00001 diff --git a/icons/failed@3x.png b/icons/failed@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..916336cf7718d280e8828f59bef4cd666462edb7 GIT binary patch literal 667 zcmV;M0%ZM(P)*fKdUJ4MY)*0CfYUz^IF0$P(HWum_m24}PR}o>mG7GV7$J zO~RxL$m!r2WUQCI=fAj7b0q7%>-gS#POK+i+Z{>~97mEz(w-Az&(pt7#9-cdcO`yiy-%r@`Jw01 zvff}fVQBJBtiIm!lwZ-+>&}Gsne#MKC#|6s1BX_p8v_82D=G;llP&+tNsAX zQIujw_D0f`@x3zk!ggdSx0T!GnT`BB0jN5aIExhF)8?H@JZBmKh2qxboU=bps z$hQ%YQ$#fRwgPg9NJ3uH^4|~mUNLonufP-EL^ogz>;gYRoPDd&kx6zQlB@<%G`XqL02xzUQv((xLu=4T%NeU|! zjTk{boB475NfZ$W4m&gKIn3_aEvWn@hWhjtfF{rZdO#aE0V+QU6oCeC2HXQ%z!IuEnrs_K(EI)I#EOnp|%d4Z<7`wekJ9f^EjJ=XR!GHLTzlsk&j2lIUq|*Wb0000 diff --git a/icons/passed@2x.png b/icons/passed@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..753e0f0acd0323ae59663aa2b3cdcda4718a70af GIT binary patch literal 796 zcmV+%1LOROP)8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10;x$v zK~z|U?Uzey6hRQjf1AyoeQs9pfv2n&B^V`&S0kQG{01I_2SGthjQSOPVAcrg*`tD@ z!6Y6;e1RBG#-k7LamY$M7(@2&p}U!OGSf4&L2|L6nVIgcs()42R8^NGv6|Ges@j3h z0(^sr6kZgfbxXJxfyf;khw zv}Kqp==RWYxR{vC0IZ*lRS3TlUt9k`uSe|7}c1 zMBaJK+jAOCILBLdttqL`&Xr_-Q%5f)txbuI^LEdCCJF0@fUn^i$H49F`-g%gU6b)A4oxqwcwNBtvLt4#jSadUCr%08rF;G-cB0 zN>g!F(hn;$GZiDL$8*<&R#uG4GNoKH#i%zA0Axz1qwzLDlqFsF1NbR23!D zTogHus!S<&G^K(K2{LVz527g(OkDxRHA&|bow-vUY{{~k(u=@xqDVY_Q6J1QU@b|^!6Vb)weO`e;Aprk`R{d`H aAHW~VP9f_mA}>S$0000rZyq9l>l1d`9eIk!D`=j?mW<2E7CA1pZgoVC~f zt-aRS&xXoMM$d8+sEZZvzztonu>p7#*aZv%_W(Bo{lGNvFYqfc2Al-G0{-qoy9v~) zvR6H-&RGt#X}vu)+P)%HDm|orTat7PXVjf_9B5d46zT)s2VMjk5uGWLbompw1gr&a zBT1`uu_OSF051cVi;}royO^f~>W`6L{8@cn-J&)k^&9HeRBt?D=LhxfvJohvG^qZb z)8Kjaxi+L@(ol!hi5#1g>ZWcwFpxujUOlL;F56pytX4-;d!ZABBhl{TS{J9yj5<`4 zcql`u-7Ln zCf}6Npxx6|9M~HeIdAU7fte-2R|E0?d`3V5(sjcq4ch@@YXh6K??i z0D9B%IIx*yw(JIafER!}fkVJw5uGvM-n2Xi+(B#s7=+~6AbhWNF+EnPA7yNR-A4C7 zz~kwnDYPpne^IfgL#zTm1fI{l+tx<^^Pv22Ofhs|bLM#pI9GsNC*+X_c&kmFpGfu+ zdAwQ?gk&-kwlf`4;&JsC^-JSZf0#B+yjdXSvP1cWm|plII~#u5p@EAT-3gO*Q@6;Y zewL{-9n%X_s4w%}X>C4}dEN*d2L{q|yR;eUV7G}&ndh6LS~ViDBkh?iCe)2Jd%jcz z>ih$kYzIRpE_Z=^<$vIKrqYzfHY*RLx-q;{fqYNUF~?%M;Xg2zdF}_c6v)+6z!S6% zdriCxyjs9Uh^@da5ne3_9XJ`3pDfs5o)cBc13VR!Pee6?kgPX3w`pQL4QJ8N52|mg z&s0d8rZjBb_|K}h#`N=E9u0DMxCWUjG8DA`aXbCImwN&O6E59;pjSN?wEtLH2QV2z zvlv|>2S$T-Pj#vj`>@m2J@JC|^16uZvvp5AZ;d@M>fxX_W?b4+i4Tg@ZTGRylX$QBCvz9nyNcQ=qO>W;2~9?)Vav@En2hs7 zE*@=~Vtc_XE=;_X8KX zdOvVG$^Afoq|O3x76 Ye<4I%@H6Oo<^TWy07*qoM6N<$g306-p#T5? literal 0 HcmV?d00001 diff --git a/icons/stopwatch.png b/icons/stopwatch.png new file mode 100644 index 0000000000000000000000000000000000000000..c750ebee371933e9ba5d2f9bf779c2f0922b9cbb GIT binary patch literal 381 zcmV-@0fPRCP)M5pkRa3Koha~VVkL0o`)*q~M}(QHBS zD4b%lnMow#B$G4mnRDJb=bcPTYyaz0O%&T0ouS(66;1@R>VfV zDZO{B+sMm~P^oX~cA diff --git a/icons/stopwatch@2x.png b/icons/stopwatch@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eae492153c1587fe4daef270e9532df5fa20df34 GIT binary patch literal 790 zcmV+x1L^#UP)8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10;5Sp zK~z|U-Iq;>jZqlKe|Kh>!T8!x2qh&}BxagMLYSt+#==)gHdaPiNIuF^EbXO~ER>Jz zgi^jGk;N=9BMjx+wVI#BJ-0b-@45HBH<@_qbUM%T{Ganb&-jl-Ds*cIK)vv+-Nu*yB#o3yNz_|c=r4|yv zv1+fnJDR*`sSwLP1$8jk&gLQTJb+FoKyB)6f0v@#li_H zO0gUOu4ecSgnoL44amL?TS7maz%*y*Qe?|9lDBS_;ce(=B`}wPC(-zE&}B(E#!!kt zONQ7fe*Y1mI26 zTd`VeQg4MAUo0JNbrV267_mRn0D)r>{y;U~l%dU$qjWfss3#Kz^=QCo=>~0WgaGOs z^<}h@8&-dQbz1slWqF~_u0^`mqjU~%0azUA`hjb}>oA@PtOn*qJ`aISz^iJ(Q@fzD zwW^05Da>l}Sv^!M?Rhc~%M@TUupU^#$dNr1R(usWPtK=#-v5F>F8|!W;r~bA7re$@ U`SyYQSpWb407*qoM6N<$f*{CUivR!s literal 0 HcmV?d00001 diff --git a/icons/stopwatch@3x.png b/icons/stopwatch@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c336c14a27a3df05d13ea7cee42f2d9dcd2e7e GIT binary patch literal 1142 zcmV-+1d02JP)Asc9nGVQ_eB~dGzc?opVnO_h4+}pm}bIxApx@8!cKWy&4XRrNV zYwfl6*Euyna~WN;ZNRl+-hm8TBwdh1NtuxJR3d#X)Dq2sKP1%+a(@}+%t#f1-;Hu- z6X|OqRRr#o^t{~YE|v74*LGCW8MzeOBk5EMg)IQ7%VyfK`8w!ml{vo(rAncOqz#f* zOIj!CUa#$hq)|!7Bz>;b^B@wU1;A^-TfiMbTMc*&I1Efvh ztSJ-|*x@NZ1FX!RKL8vv8eN2O;Qoy7?u@^Q16BumA^}?F)f3tI|z5a;qhMCuy(TM%eJ$7R55Bg@}g(z=UBmzX!GhD++k-0-np{VweE#vqr!o zG+5%qFTf_NvaAdNCrXSTtiop%+?S*5L!hss@y*hQ%kvQXt8!U{)&Q3SMXz*dcvxNy zcrR!Ak~z=?91oPdpUC)T`5@r`Dbt(8fhTgvZ|Km_<-n)Fp{(ww)6apP$5S~l77(xR z(D;?Kuk)V;?T-d}j;3&6IWXgq9!_LDz(|hV5wCp)w=+li%MNoxl4{=nkIkEl+vMt! z?vdO4{7q_7(hE6#eEwRdKaLXb4hAFAFpNkS@DeadKFZelr9s3GSRzo?_@hU5)$L$6B zi%~K`4tW!58mJp4RS&QSn9BN10mIqzSl>GBH@i=+brO$y^)9(d7{e90%T8U=s_b`L z_B@77YR`z#BEdG4c(*E82J?2$l;x{H-+-lGOyGxrblr6_{-Hq6n6+>0z0fR5f6KC*G3;Z6OaDxfd3@WYwd3ldW7~f3c%Jx#%~RHXMl&Ra99Q3 z;A#v5-5nZU176SJtQ=jdU=Q$hj>02Uyu$)(fzceU5p4TKM-B+M5%@Ak@eJ;UwFOvI z(6=9WGLs9_5#S5lYs|{gtom}dFIzF5Rr_V9i7S#$%N+y$l-tN|m9#?nZ}N~dEa^o_ z|5h>Ne}>L-cq8yz&PZ0lDPTi~`a4RYu=E1YaP=}9!*SqgDlC;{CDr$VK}qW+t&ub! zcPw5k_hs^LNxw=OlQbgtogf)fIWTLO^PiaJ9hhy*J22b$521u!04s&=1poj507*qo IM6N<$f^xkbVE_OC literal 0 HcmV?d00001 diff --git a/img/testitem.png b/img/testitem.png new file mode 100644 index 0000000000000000000000000000000000000000..58af9bd4cc59ccccc53ebef8475ec3df975f1d59 GIT binary patch literal 47973 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 kIsdu+(w`2C8tb@15@4Vx+xSi)dkgr Optional[sublime.View]: for view in window.views(): @@ -85,12 +204,7 @@ def send_julia_repl(window: sublime.Window, code_block: str) -> None: window.focus_view(return_focus) -def versioned_text_document_position_params(view: sublime.View, location: int) -> Dict[str, Any]: - """ - Custom Julia-specific extension to the LSP. - - @see https://github.com/julia-vscode/LanguageServer.jl/blob/master/src/extensions/extensions.jl - """ +def versioned_text_document_position_params(view: sublime.View, location: int) -> VersionedTextDocumentPositionParams: position_params = text_document_position_params(view, location) return { "textDocument": position_params["textDocument"], @@ -141,15 +255,291 @@ def prepare_markdown(content: str) -> str: return content -class JuliaLanguageServer(AbstractPlugin): +def startupinfo(): + if sublime.platform() == "windows": + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + si.wShowWindow = 11 + return si + return None + + +class TestItemStorage: + + def __init__(self, window: sublime.Window) -> None: + self.window = window + self.pending_result = False + self.testitemparams = {} # type: Dict[str, Dict[str, Any]] + self.testitemdetails = {} # type: Dict[str, List[TestItemDetail]] + self.testitemstatus = {} # type: Dict[str, List[TestserverRunTestitemRequestParamsReturn]] + self.error_keys = {} # type: Dict[str, Set[str]] + + def update(self, uri: DocumentUri, params: PublishTestItemsParams) -> None: + # Use the filepath instead of the URI as the key for storing the testitems, because on Windows the language + # server sometimes uses uppercase and sometimes lowercase drive letters in the URI for the same file. + filepath = parse_uri(uri)[1] + details = params['testitemdetails'] + old_params = self.testitemparams.get(filepath) + if not details: + # If there are no testitems reported, just delete the key for the previously stored items if they existed. + if old_params: + del self.testitemparams[filepath] + del self.testitemdetails[filepath] + del self.testitemstatus[filepath] + self.render_testitems(uri) + return + if not params['package_path']: + for testitem in details: + testitem['error'] = "Unable to identify a Julia package for this test item.
Ensure you work in a Julia project environment with a Project.toml file." + status = [{ + 'status': TestItemStatus.Invalid if testitem.get('error') else TestItemStatus.Undetermined, + 'message': None, + 'duration': None + } for testitem in details] # type: List[TestserverRunTestitemRequestParamsReturn] + if not old_params or \ + any(old_params[key] != params[key] for key in ('project_path', 'package_path', 'package_name')): + # If there were no testitems for this file already stored, or one of the major parameters changed, copy the + # new parameters and testitems. + self.testitemparams[filepath] = { + 'uri': params['uri'], + 'version': params.get('version', 0), + 'project_path': params['project_path'], + 'package_path': params['package_path'], + 'package_name': params['package_name'] + } + else: + # If there are both new and old testitems, compare them and determine the unchanged items so that the old + # status can be retained. An old and a new testitem is considered the same if it has the same "id". + # Unfortunately the "id" field for the testitems is not necessarily unique, so there might be incorrect + # matches via this approach. Perhaps it should be considered as an additional requirement that the "code" + # property must also be the same (that would mean that testitems lose their status whenever there are + # changes in the particular testitem code)... + self.testitemparams[filepath]['uri'] = params['uri'] + self.testitemparams[filepath]['version'] = \ + params.get('version', self.testitemparams[filepath]['version'] + 1) + for old_idx, old_item in enumerate(self.testitemdetails[filepath]): + for new_idx, new_item in enumerate(details): + if new_item.get('error'): + continue + if old_item['id'] == new_item['id']: + # Copy old status into new status for this testitem + status[new_idx] = self.testitemstatus[filepath][old_idx] + break + self.testitemdetails[filepath] = details + self.testitemstatus[filepath] = status + self.render_testitems(uri) + + def stored_version(self, uri: DocumentUri) -> Optional[int]: + filepath = parse_uri(uri)[1] + params = self.testitemparams.get(filepath) + if params: + return params['version'] + return None + + def render_testitems(self, uri: DocumentUri, new_result_idx: Optional[int] = None) -> None: + filepath = parse_uri(uri)[1] + view = self.window.find_open_file(filepath) # This doesn't work if the tab was dragged out of the window... + if view and not view.is_loading(): + regions = { + TestItemStatus.Passed: [], + TestItemStatus.Failed: [], + TestItemStatus.Errored: [], + TestItemStatus.Undetermined: [], + TestItemStatus.Pending: [], + TestItemStatus.Invalid: [] + } # type: Dict[str, List[sublime.Region]] + if filepath not in self.testitemdetails: + for status in regions.keys(): + view.erase_regions('lsp_julia_testitem_{}'.format(status)) + return + annotations = { + TestItemStatus.Passed: [], + TestItemStatus.Failed: [], + TestItemStatus.Errored: [], + TestItemStatus.Undetermined: [], + TestItemStatus.Pending: [], + TestItemStatus.Invalid: [] + } # type: Dict[str, List[str]] + version = self.testitemparams[filepath]['version'] + error_annotation_color = view.style_for_scope(TESTITEM_SCOPES[TestItemStatus.Errored])['foreground'] + for idx, item, result in zip(itertools.count(), self.testitemdetails[filepath], self.testitemstatus[filepath]): + region = sublime.Region(point_to_offset(Point.from_lsp(item['range']['start']), view)) + annotation = 'Run Test'.format(html.escape(uri), idx, version) + duration = result['duration'] + if duration is not None: + if duration < 100: + annotation += " ({}ms)".format(round(duration)) + else: + annotation += " ({:0.2f}s)".format(duration/1000) + status = result['status'] + error = item.get('error') + if error and isinstance(error, str): + regions[TestItemStatus.Invalid].append(region) + annotations[TestItemStatus.Invalid].append(error) + continue + regions[status].append(region) + if status in (TestItemStatus.Passed, TestItemStatus.Undetermined): + annotations[status].append(annotation) + elif status == TestItemStatus.Pending: + annotations[status].append('Running…') + elif status in (TestItemStatus.Failed, TestItemStatus.Errored): + annotations[status].append(annotation) + if idx == new_result_idx and result['message'] is not None: + for error_idx, message in enumerate(result['message']): + location = message['location'] + if location: + regions_key = 'lsp_julia_testitem_error_{}_{}'.format(item['id'], error_idx) + view.add_regions( + regions_key, + [range_to_region(location['range'], view)], + flags=sublime.HIDE_ON_MINIMAP | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE, + annotations=["
".join(html.escape(message['message']).split("\n"))], + annotation_color=error_annotation_color, + on_close=partial(self.hide_annotation, uri, regions_key)) + self.error_keys[filepath].add(regions_key) + for status in regions.keys(): + regions_key = 'lsp_julia_testitem_{}'.format(status) + if regions[status]: + view.add_regions( + regions_key, + regions[status], + scope=TESTITEM_SCOPES[status], + icon=TESTITEM_ICONS[status], + flags=sublime.HIDE_ON_MINIMAP | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE, + annotations=annotations[status], + annotation_color=view.style_for_scope(TESTITEM_SCOPES[status])['foreground'], + on_navigate=None if status in (TestItemStatus.Pending, TestItemStatus.Invalid) else self.run_testitem) + else: + view.erase_regions(regions_key) + + def hide_annotation(self, uri: DocumentUri, key: str) -> None: + filepath = parse_uri(uri)[1] + view = self.window.find_open_file(filepath) + if view: + view.erase_regions(key) + self.error_keys[filepath].discard(key) + + def clear_error_annotations(self, uri: DocumentUri, testitem_id: Optional[str] = "") -> None: + filepath = parse_uri(uri)[1] + view = self.window.find_open_file(filepath) + if view: + for key in self.error_keys.setdefault(filepath, set()).copy(): + if key.startswith('lsp_julia_testitem_error_{}'.format(testitem_id)): + view.erase_regions(key) + self.error_keys[filepath].discard(key) + + def run_testitem_request_params(self, uri: DocumentUri, idx: int) -> Optional[TestserverRunTestitemRequestExtendedParams]: + filepath = parse_uri(uri)[1] + params = self.testitemparams.get(filepath) + if not params: + return None + testitem = self.testitemdetails[filepath][idx] + code_range = testitem.get('code_range') + if not code_range: + return None + code = testitem.get('code') + if not code: + return None + project_path = params['project_path'] + package_path = params['package_path'] + package_name = params['package_name'] + if not any([project_path, package_path, package_name]): + return None + line = code_range['start']['line'] + column = code_range['start']['character'] + if column == 0: + line -= 1 # Fix missmatch of start position between initial and subsequent reported testitem notifications + return { + 'uri': params['uri'], + 'name': testitem['label'], + 'packageName': package_name, + 'useDefaultUsings': testitem.get('option_default_imports') is not False, + 'line': line, + 'column': column, + 'code': code, + 'project_path': project_path, + 'package_path': package_path + } + + def run_testitem(self, href: str, focus_testitem: bool = False) -> None: + if self.pending_result: + self.window.status_message("Another testitem is already running") + return + uri, fragment = urldefrag(href) + filepath = parse_uri(uri)[1] + pq = parse_qs(fragment) + idx = int(pq['idx'][0]) + version = int(pq['version'][0]) + if version != self.stored_version(uri): + # Actually this should never happen in practice, because annotations for the corresponding view are redrawn + # on each julia/publishTestitems notification. + self.window.status_message("Version mismatch for testitem params") + return + params = self.run_testitem_request_params(uri, idx) + if params: + self.pending_result = True + self.testitemstatus[filepath][idx]['status'] = TestItemStatus.Pending + thread = threading.Thread(target=self.run_testitem_daemon_thread, args=(uri, idx, version, params), daemon=True) + thread.start() + if focus_testitem: + view = self.window.open_file("{}:{}".format(filepath, params['line'] + 1), flags=sublime.ENCODED_POSITION) + # In case the file wasn't open before and is still loading, add a small delay before drawing the + # annotations. + if view.is_loading(): + sublime.set_timeout(partial(self.render_testitems, uri), 50) + sublime.set_timeout(partial(self.clear_error_annotations, uri, params['name']), 50) + return + self.render_testitems(uri) + self.clear_error_annotations(uri, params['name']) + + def run_testitem_daemon_thread(self, uri: DocumentUri, idx: int, version: int, params: TestserverRunTestitemRequestExtendedParams) -> None: + try: + file_directory = os.path.dirname(parse_uri(params['uri'])[1]) + params_json = json.dumps(params, separators=(',', ':')) + result_json = subprocess.check_output([ + LspJuliaPlugin.julia_exe(), + "--startup-file=no", + "--history-file=no", + "--project={}".format(LspJuliaPlugin.testrunnerdir()), + os.path.join(LspJuliaPlugin.testrunnerdir(), "runtestitem.jl"), + params_json + ], cwd=file_directory, startupinfo=startupinfo()).decode("utf-8") + result = json.loads(result_json) + sublime.set_timeout(partial(self.on_result, uri, idx, version, result)) + except Exception: + self.pending_result = False + filepath = parse_uri(uri)[1] + self.testitemstatus[filepath][idx]['status'] = TestItemStatus.Invalid + self.testitemdetails[filepath][idx]['error'] = "The test process crashed while running this testitem.
Please check the console and consider to create an issue report in the LSP-julia GitHub repo." + traceback.print_exc() + sublime.set_timeout(partial(self.render_testitems, uri)) + + def on_result(self, uri: DocumentUri, idx: int, version: int, params: TestserverRunTestitemRequestParamsReturn) -> None: + self.pending_result = False + filepath = parse_uri(uri)[1] + if self.testitemparams[filepath]['version'] == version: + self.testitemstatus[filepath][idx] = params + self.render_testitems(uri, idx) + else: + # Ignore result if the language server has notified about a new version of testitems for this file in the + # meantime. The index of the stored testitem might have changed! Search through all testitems for an item + # with "pending" status and reset status if found. Don't draw new error annotations. + for idx, status in enumerate(self.testitemstatus[filepath]): + if status['status'] == TestItemStatus.Pending: + self.testitemstatus[filepath][idx]['status'] = TestItemStatus.Undetermined + self.render_testitems(uri) + + +class LspJuliaPlugin(AbstractPlugin): def __init__(self, weaksession) -> None: super().__init__(weaksession) + self.testitems = TestItemStorage(weaksession().window) # pyright: ignore[reportOptionalMemberAccess] if sublime.load_settings(SETTINGS_FILE).get("show_environment_status"): session = self.weaksession() if session: env_name = os.path.basename(session.working_directory) if session.working_directory else \ - JuliaLanguageServer.default_julia_environment() + LspJuliaPlugin.default_julia_environment() session.set_window_status_async(STATUS_BAR_KEY, "Julia env: {}".format(env_name)) @classmethod @@ -165,7 +555,11 @@ def additional_variables(cls) -> Optional[Dict[str, str]]: @classmethod def basedir(cls) -> str: - return os.path.join(cls.storage_path(), "LSP-julia") + return os.path.join(cls.storage_path(), "LSP-julia", "languageserver") + + @classmethod + def testrunnerdir(cls) -> str: + return os.path.join(cls.storage_path(), "LSP-julia", "testrunner") @classmethod def version_file(cls) -> str: @@ -181,14 +575,8 @@ def julia_exe(cls) -> str: @classmethod def julia_version(cls) -> str: - if sublime.platform() == "windows": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = 11 - return subprocess.check_output( - [cls.julia_exe(), "--version"], startupinfo=startupinfo).decode("utf-8").rstrip().split()[-1] - else: - return subprocess.check_output([cls.julia_exe(), "--version"]).decode("utf-8").rstrip().split()[-1] + return subprocess.check_output( + [cls.julia_exe(), "--version"], startupinfo=startupinfo()).decode("utf-8").rstrip().split()[-1] @classmethod def default_julia_environment(cls) -> str: @@ -197,7 +585,7 @@ def default_julia_environment(cls) -> str: @classmethod def server_version(cls) -> str: - return "9f6fdc0" # LanguageServer v4.3.2-DEV + return "c6fad5d" # LanguageServer v4.3.2-DEV @classmethod def needs_update_or_installation(cls) -> bool: @@ -215,10 +603,14 @@ def install_or_update(cls) -> None: shutil.rmtree(cls.basedir(), ignore_errors=True) try: os.makedirs(cls.basedir(), exist_ok=True) - for file in ["Project.toml", "Manifest.toml"]: + for file in ("Project.toml", "Manifest.toml"): ResourcePath.from_file_path( os.path.join(cls.packagedir(), "server", file)).copy(os.path.join(cls.basedir(), file)) # TODO Use cls.basedir() as DEPOT_PATH for language server + os.makedirs(cls.testrunnerdir(), exist_ok=True) + for file in ("Project.toml", "runtestitem.jl"): + ResourcePath.from_file_path( + os.path.join(cls.packagedir(), "testrunner", file)).copy(os.path.join(cls.testrunnerdir(), file)) returncode = subprocess.call([ cls.julia_exe(), "--startup-file=no", @@ -266,19 +658,25 @@ def on_server_response_async(self, method: str, response: Response) -> None: if response.result and isinstance(response.result["contents"], dict) and response.result["contents"].get("kind") == "markdown": # pyright: ignore response.result["contents"]["value"] = prepare_markdown(response.result["contents"]["value"]) # pyright: ignore + # Handles the julia/publishTestitems notification + def m_julia_publishTestitems(self, params: PublishTestItemsParams) -> None: + if params: + uri = params['uri'] + self.testitems.update(uri, params) + def plugin_loaded() -> None: - register_plugin(JuliaLanguageServer) + register_plugin(LspJuliaPlugin) def plugin_unloaded() -> None: - unregister_plugin(JuliaLanguageServer) + unregister_plugin(LspJuliaPlugin) class JuliaActivateEnvironmentCommand(LspWindowCommand): """ Can be invoked from the command palette to switch the active Julia project environment. - The active Julia project environment detemines the Julia packages used by the language server to provide + The active Julia project environment determines the Julia packages used by the language server to provide autocomplete suggestions and diagnostics. """ @@ -316,9 +714,6 @@ class EnvPathInputHandler(sublime_plugin.ListInputHandler): Used by JuliaActivateEnvironmentCommand to display the available Julia project environments the user can choose from. """ - KIND_DEFAULT_ENVIRONMENT = (sublime.KIND_ID_COLOR_YELLOWISH, "d", "default environment") - KIND_WORKSPACE_FOLDER = (sublime.KIND_ID_COLOR_PURPLISH, "f", "workspace folder") - def __init__(self, workspace_folders: List[WorkspaceFolder]) -> None: self.workspace_folders = workspace_folders @@ -327,11 +722,11 @@ def list_items(self) -> List[sublime.ListInputItem]: julia_env_home = os.path.expanduser(os.path.join("~", ".julia", "environments")) names = [env for env in reversed(os.listdir(julia_env_home)) if os.path.isdir(os.path.join(julia_env_home, env))] # collect all folder names in .julia/environments paths = [os.path.join(julia_env_home, env) for env in names] # the corresponding folder paths - items = [sublime.ListInputItem(name, path, kind=self.KIND_DEFAULT_ENVIRONMENT) for name, path in zip(names, paths)] + items = [sublime.ListInputItem(name, path, kind=KIND_DEFAULT_ENVIRONMENT) for name, path in zip(names, paths)] # add workspace folders on top of the list if they are valid Julia project environments for workspace_folder in reversed(self.workspace_folders): if workspace_folder.path not in paths and is_julia_environment(workspace_folder.path): - items.insert(0, sublime.ListInputItem(workspace_folder.name, workspace_folder.path, kind=self.KIND_WORKSPACE_FOLDER)) + items.insert(0, sublime.ListInputItem(workspace_folder.name, workspace_folder.path, kind=KIND_WORKSPACE_FOLDER)) # add option for folder picker dialog items.insert(0, sublime.ListInputItem("(pick a folder…)", "__select_folder_dialog")) return items @@ -671,3 +1066,45 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None, point: Optional[ window = self.view.window() if window: window.run_command("julia_search_documentation", {"word": word}) + + +class JuliaRunTestitemCommand(LspWindowCommand): + + session_name = SESSION_NAME + + def run(self) -> None: + session = self.session() + if not session: + return + plugin = cast(LspJuliaPlugin, session._plugin) + items = [] # type: List[sublime.QuickPanelItem] + self.hrefs = [] # type: List[str] + for filepath, details in plugin.testitems.testitemdetails.items(): + for idx, testitem in enumerate(details): + if not testitem.get('error'): + status = plugin.testitems.testitemstatus[filepath][idx]['status'] + kind = TESTITEM_KINDS.get(status, sublime.KIND_AMBIGUOUS) + details = ", ".join(testitem.get('option_tags') or []) + location = "{}:{}".format(filepath, testitem['range']['start']['line'] + 1) + items.append( + sublime.QuickPanelItem(testitem['label'], details=details, annotation=location, kind=kind)) + uri = plugin.testitems.testitemparams[filepath]['uri'] + version = plugin.testitems.testitemparams[filepath]['version'] + self.hrefs.append("{}#idx={}&version={}".format(uri, idx, version)) + session.window.show_quick_panel(items, on_select=partial(self._on_select, plugin), placeholder="Run @testitem") + + def is_enabled(self) -> bool: + session = self.session() + if session is None: + return False + plugin = cast(LspJuliaPlugin, session._plugin) + if plugin.testitems.pending_result: + return False + return any(testitem for testitems in plugin.testitems.testitemdetails.values() + for testitem in testitems + if not testitem.get('error')) + + def _on_select(self, plugin: LspJuliaPlugin, idx: int) -> None: + if idx > -1: + href = self.hrefs[idx] + plugin.testitems.run_testitem(href, focus_testitem=True) diff --git a/server/Manifest.toml b/server/Manifest.toml index a61a973..92d3bd2 100644 --- a/server/Manifest.toml +++ b/server/Manifest.toml @@ -2,6 +2,7 @@ [[ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.1" [[Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" @@ -23,13 +24,14 @@ version = "0.8.6" [[Compat]] deps = ["Dates", "LinearAlgebra", "UUIDs"] -git-tree-sha1 = "924cdca592bc16f14d2f7006754a621735280b74" +git-tree-sha1 = "3ca828fe1b75fa84b021a7860bd039eaea84d2f2" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.1.0" +version = "4.3.0" [[CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "0.5.2+0" [[Crayons]] git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" @@ -47,8 +49,17 @@ deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" [[Downloads]] -deps = ["ArgTools", "LibCURL", "NetworkOptions"] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.6.0" + +[[FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[Glob]] +git-tree-sha1 = "4df9f7e06108728ebf00a0a11edee4b29a482bb2" +uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" +version = "1.3.0" [[InteractiveUtils]] deps = ["Markdown"] @@ -67,15 +78,15 @@ uuid = "b9b8584e-8fd3-41f9-ad0c-7255d428e418" version = "1.3.4" [[JuliaFormatter]] -deps = ["CSTParser", "CommonMark", "DataStructures", "Pkg", "Tokenize"] -git-tree-sha1 = "6c0fd08be0dc54f11c224374ad6da743ce6a8ca0" +deps = ["CSTParser", "CommonMark", "DataStructures", "Glob", "Pkg", "Tokenize"] +git-tree-sha1 = "a5705d6e0a99bfb9465e7f2381783d3368f3b94b" uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899" -version = "1.0.8" +version = "1.0.14" [[LanguageServer]] -deps = ["CSTParser", "JSON", "JSONRPC", "JuliaFormatter", "Markdown", "Pkg", "REPL", "StaticLint", "SymbolServer", "Tokenize", "URIs", "UUIDs"] -git-tree-sha1 = "94e2554b551e12bed69a805e9bbfcadc3a97834b" -repo-rev = "9f6fdc0f3a40a56b40ba6cdeee355eb34bc7ef71" +deps = ["CSTParser", "JSON", "JSONRPC", "JuliaFormatter", "Markdown", "Pkg", "REPL", "StaticLint", "SymbolServer", "TestItemDetection", "Tokenize", "URIs", "UUIDs"] +git-tree-sha1 = "9627ae874d289072daab859ecd3938ec1af4bf16" +repo-rev = "c6fad5d954ed99cb69ce31dc12132fffddf75562" repo-url = "https://github.com/julia-vscode/LanguageServer.jl.git" uuid = "2b0e0bc5-e4fd-59b4-8912-456d1b03d8d7" version = "4.3.2-DEV" @@ -83,10 +94,12 @@ version = "4.3.2-DEV" [[LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.3" [[LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "7.84.0+0" [[LibGit2]] deps = ["Base64", "NetworkOptions", "Printf", "SHA"] @@ -95,6 +108,7 @@ uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" [[LibSSH2_jll]] deps = ["Artifacts", "Libdl", "MbedTLS_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.10.2+0" [[Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" @@ -113,19 +127,23 @@ uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" [[MbedTLS_jll]] deps = ["Artifacts", "Libdl"] uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.28.0+0" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" [[MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2022.2.1" [[NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.2.0" [[OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.20+0" [[OrderedCollections]] git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" @@ -134,13 +152,14 @@ version = "1.4.1" [[Parsers]] deps = ["Dates"] -git-tree-sha1 = "0044b23da09b5608b4ecacb4e5e6c6332f833a7e" +git-tree-sha1 = "6c01a9b494f6d2a9fc180a08b182fcb06f0958a0" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.3.2" +version = "2.4.2" [[Pkg]] deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.8.0" [[Printf]] deps = ["Unicode"] @@ -156,6 +175,7 @@ uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" [[Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -178,10 +198,18 @@ version = "7.2.1" [[TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.0" [[Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[TestItemDetection]] +deps = ["CSTParser"] +git-tree-sha1 = "8143bbfbe9ba25f080f3f82df76d4c275a71ceec" +uuid = "76b0de8b-5c4b-48ef-a724-914b33ca988d" +version = "0.1.1" [[Tokenize]] git-tree-sha1 = "2b3af135d85d7e70b863540160208fa612e736b9" @@ -203,15 +231,19 @@ uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [[Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.12+3" [[libblastrampoline_jll]] deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.1.1+0" [[nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.48.0+0" [[p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.4.0+0" diff --git a/testrunner/Project.toml b/testrunner/Project.toml new file mode 100644 index 0000000..1b2d806 --- /dev/null +++ b/testrunner/Project.toml @@ -0,0 +1,7 @@ +[deps] +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestEnv = "1e6cf692-eddd-4d53-88a5-2d735e33781b" +URIParser = "30578b45-9adc-5946-b283-645ec420af67" diff --git a/testrunner/VSCodeTestServer_license/LICENSE b/testrunner/VSCodeTestServer_license/LICENSE new file mode 100644 index 0000000..693328d --- /dev/null +++ b/testrunner/VSCodeTestServer_license/LICENSE @@ -0,0 +1,25 @@ +The MIT License (MIT) + +Copyright (c) 2012-2022 David Anthoff, Zac Nugent, Sebastian Pfitzner and +other contributors: + +https://github.com/JuliaLang/Julia.tmbundle/contributors +https://github.com/julia-vscode/julia-vscode/contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/testrunner/runtestitem.jl b/testrunner/runtestitem.jl new file mode 100644 index 0000000..3da8f80 --- /dev/null +++ b/testrunner/runtestitem.jl @@ -0,0 +1,279 @@ +import Pkg # stdlib +import Test # stdlib + +try + import JSON + import Suppressor + import TestEnv + import URIParser +catch + Pkg.instantiate(; io=devnull) + import JSON + import Suppressor + import TestEnv + import URIParser +end + + +TestserverRunTestitemRequestParamsReturn(status, message, duration) = Dict("status"=>status, "message"=>message, "duration"=>duration) +TestMessage(message, location) = Dict("message"=>message, "location"=>location) +Location(uri, range) = Dict("uri"=>uri, "range"=>range) +Range(start, stop) = Dict("start"=>start, "end"=>stop) +Position(line, character) = Dict("line"=>line, "character"=>character) + +struct TestserverRunTestitemRequestParams + uri::String + name::String + packageName::String + useDefaultUsings::Bool + line::Int + column::Int + code::String +end + +#==========================================================================================# +# Functions extracted from VSCodeTestServer.jl +# Keep in sync with https://github.com/julia-vscode/julia-vscode/blob/main/scripts/packages/VSCodeTestServer/src/VSCodeTestServer.jl + +function uri2filepath(uri::AbstractString) + parsed_uri = try + URIParser.URI(uri) + catch + return nothing + end + + if parsed_uri.scheme !== "file" + return nothing + end + + path_unescaped = URIParser.unescape(parsed_uri.path) + host_unescaped = URIParser.unescape(parsed_uri.host) + + value = "" + + if host_unescaped != "" && length(path_unescaped) > 1 + # unc path: file://shares/c$/far/boo + value = "//$host_unescaped$path_unescaped" + elseif length(path_unescaped) >= 3 && + path_unescaped[1] == '/' && + isascii(path_unescaped[2]) && isletter(path_unescaped[2]) && + path_unescaped[3] == ':' + # windows drive letter: file:///c:/far/boo + value = lowercase(path_unescaped[2]) * path_unescaped[3:end] + else + # other path + value = path_unescaped + end + + if Sys.iswindows() + value = replace(value, '/' => '\\') + end + + value = normpath(value) + + return value +end + +function filepath2uri(file::String) + isabspath(file) || error("Relative path `$file` is not valid.") + if Sys.iswindows() + file = normpath(file) + file = replace(file, "\\" => "/") + file = URIParser.escape(file) + file = replace(file, "%2F" => "/") + if startswith(file, "//") + # UNC path \\foo\bar\foobar + return string("file://", file[3:end]) + else + # windows drive letter path + return string("file:///", file) + end + else + file = normpath(file) + file = URIParser.escape(file) + file = replace(file, "%2F" => "/") + return string("file://", file) + end +end + +function withpath(f, path) + tls = task_local_storage() + hassource = haskey(tls, :SOURCE_PATH) + hassource && (path′ = tls[:SOURCE_PATH]) + tls[:SOURCE_PATH] = path + try + return f() + finally + hassource ? (tls[:SOURCE_PATH] = path′) : delete!(tls, :SOURCE_PATH) + end +end + +function format_error_message(err, bt) + try + return Base.invokelatest(sprint, Base.display_error, err, bt) + catch err + # TODO We could probably try to output an even better error message here that + # takes into account `err`. And in the callsites we should probably also + # handle this better. + return "Error while trying to format an error message" + end +end + +function run_testitem(params::TestserverRunTestitemRequestParams) + mod = Core.eval(Main, :(module $(gensym()) end)) + + if params.useDefaultUsings + try + Core.eval(mod, :(using Test)) + catch + return TestserverRunTestitemRequestParamsReturn( + "errored", + [ + TestMessage( + "Unable to load the `Test` package. Please ensure that `Test` is listed as a test dependency in the Project.toml for the package.", + Location( + params.uri, + Range(Position(params.line, 0), Position(params.line, 0)) + ) + ) + ], + nothing + ) + end + + if params.packageName!="" + try + Core.eval(mod, :(using $(Symbol(params.packageName)))) + catch err + bt = catch_backtrace() + error_message = format_error_message(err, bt) + + return TestserverRunTestitemRequestParamsReturn( + "errored", + [ + TestMessage( + error_message, + Location( + params.uri, + Range(Position(params.line, 0), Position(params.line, 0)) + ) + ) + ], + nothing + ) + end + end + end + + filepath = uri2filepath(params.uri) + + code = string('\n'^params.line, ' '^params.column, params.code) + + ts = Test.DefaultTestSet("$filepath:$(params.name)") + + Test.push_testset(ts) + + elapsed_time = UInt64(0) + + t0 = time_ns() + try + withpath(filepath) do + Base.invokelatest(include_string, mod, code, filepath) + elapsed_time = (time_ns() - t0) / 1e6 # Convert to milliseconds + end + catch err + elapsed_time = (time_ns() - t0) / 1e6 # Convert to milliseconds + + Test.pop_testset() + + bt = catch_backtrace() + st = stacktrace(bt) + + error_message = format_error_message(err, bt) + + if err isa LoadError + error_filepath = err.file + error_line = err.line + else + error_filepath = string(st[1].file) + error_line = st[1].line + end + + return TestserverRunTestitemRequestParamsReturn( + "errored", + [ + TestMessage( + error_message, + Location( + isabspath(error_filepath) ? filepath2uri(error_filepath) : "", + Range(Position(max(0, error_line - 1), 0), Position(max(0, error_line - 1), 0)) + ) + ) + ], + elapsed_time + ) + end + + ts = Test.pop_testset() + + try + Test.finish(ts) + + return TestserverRunTestitemRequestParamsReturn("passed", nothing, elapsed_time) + catch err + if err isa Test.TestSetException + failed_tests = Test.filter_errors(ts) + + return TestserverRunTestitemRequestParamsReturn( + "failed", + [TestMessage(sprint(Base.show, i), Location(filepath2uri(string(i.source.file)), Range(Position(i.source.line - 1, 0), Position(i.source.line - 1, 0)))) for i in failed_tests], + elapsed_time + ) + else + rethrow(err) + end + end +end + +#==========================================================================================# + +params_dict = JSON.parse(ARGS[1]) +package_name = params_dict["packageName"] +project_path = params_dict["project_path"] +package_path = params_dict["package_path"] + +params = TestserverRunTestitemRequestParams( + params_dict["uri"], + params_dict["name"], + package_name, + params_dict["useDefaultUsings"], + params_dict["line"], + params_dict["column"], + params_dict["code"] +) + +result = nothing + +Suppressor.@suppress begin + if project_path=="" + Pkg.activate(temp=true) + + Pkg.develop(path=package_path) + + TestEnv.activate(package_name) do + global result = run_testitem(params) + end + else + Pkg.activate(project_path) + + if package_name!="" + TestEnv.activate(package_name) do + global result = run_testitem(params) + end + else + global result = run_testitem(params) + end + end +end + +print(JSON.json(result))