Skip to content

Commit d8fb385

Browse files
committed
Introduce declarative test project definition
Test data is currently often in some 'testdata' directory in 'test/'. It can easily get quite messy with many files. Especially since 'lsp-test' should load only exactly what is needed to speed up tests and avoid test flakiness. A subdirectory per test file is quite overkill and increases the required boilerplate to write tests. Thus, we introduce a declarative test project specification that runs lsp-test in a temporary directory. The first advantage is that we can load only exactly what we need, and create more accurate projects.
1 parent 3ffde0d commit d8fb385

File tree

4 files changed

+475
-11
lines changed

4 files changed

+475
-11
lines changed

hls-test-utils/hls-test-utils.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ library
2828
exposed-modules:
2929
Test.Hls
3030
Test.Hls.Util
31+
Test.Hls.FileSystem
3132

3233
hs-source-dirs: src
3334
build-depends:

hls-test-utils/src/Test/Hls.hs

Lines changed: 238 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,26 @@ module Test.Hls
2020
defaultTestRunner,
2121
goldenGitDiff,
2222
goldenWithHaskellDoc,
23+
goldenWithHaskellDocInTmpDir,
2324
goldenWithHaskellAndCaps,
25+
goldenWithHaskellAndCapsInTmpDir,
2426
goldenWithCabalDoc,
2527
goldenWithHaskellDocFormatter,
28+
goldenWithHaskellDocFormatterInTmpDir,
2629
goldenWithCabalDocFormatter,
30+
goldenWithCabalDocFormatterInTmpDir,
2731
def,
2832
-- * Running HLS for integration tests
2933
runSessionWithServer,
3034
runSessionWithServerAndCaps,
3135
runSessionWithServerFormatter,
3236
runSessionWithCabalServerFormatter,
37+
runSessionWithServerInTmpDir,
38+
runSessionWithServerAndCapsInTmpDir,
39+
runSessionWithServerFormatterInTmpDir,
40+
runSessionWithCabalServerFormatterInTmpDir,
3341
runSessionWithServer',
42+
runSessionWithServerInTmpDir',
3443
-- * Helpful re-exports
3544
PluginDescriptor,
3645
IdeState,
@@ -90,11 +99,13 @@ import GHC.Stack (emptyCallStack)
9099
import GHC.TypeLits
91100
import Ide.Logger (Doc, Logger (Logger),
92101
Pretty (pretty),
93-
Priority (Debug),
102+
Priority (..),
94103
Recorder (Recorder, logger_),
95104
WithPriority (WithPriority, priority),
96105
cfilter, cmapWithPrio,
97-
makeDefaultStderrRecorder)
106+
logWith,
107+
makeDefaultStderrRecorder,
108+
(<+>))
98109
import Ide.Types
99110
import Language.LSP.Protocol.Capabilities
100111
import Language.LSP.Protocol.Message
@@ -105,9 +116,12 @@ import System.Directory (getCurrentDirectory,
105116
setCurrentDirectory)
106117
import System.Environment (lookupEnv)
107118
import System.FilePath
119+
import System.IO.Extra (newTempDir, withTempDir)
108120
import System.IO.Unsafe (unsafePerformIO)
109121
import System.Process.Extra (createPipe)
110122
import System.Time.Extra
123+
import qualified Test.Hls.FileSystem as FS
124+
import Test.Hls.FileSystem
111125
import Test.Hls.Util
112126
import Test.Tasty hiding (Timeout)
113127
import Test.Tasty.ExpectedFailure
@@ -116,11 +130,26 @@ import Test.Tasty.HUnit
116130
import Test.Tasty.Ingredients.Rerun
117131
import Test.Tasty.Runners (NumThreads (..))
118132

119-
newtype Log = LogIDEMain IDEMain.Log
133+
data Log
134+
= LogIDEMain IDEMain.Log
135+
| LogTestHarness LogTestHarness
120136

121137
instance Pretty Log where
122138
pretty = \case
123-
LogIDEMain log -> pretty log
139+
LogIDEMain log -> pretty log
140+
LogTestHarness log -> pretty log
141+
142+
data LogTestHarness
143+
= LogTestDir FilePath
144+
| LogCleanup
145+
| LogNoCleanup
146+
147+
148+
instance Pretty LogTestHarness where
149+
pretty = \case
150+
LogTestDir dir -> "Test Project located in directory:" <+> pretty dir
151+
LogCleanup -> "Cleaned up temporary directory"
152+
LogNoCleanup -> "No cleanup of temporary directory"
124153

125154
-- | Run 'defaultMainWithRerun', limiting each single test case running at most 10 minutes
126155
defaultTestRunner :: TestTree -> IO ()
@@ -144,6 +173,18 @@ goldenWithHaskellDoc
144173
-> TestTree
145174
goldenWithHaskellDoc = goldenWithDoc "haskell"
146175

176+
goldenWithHaskellDocInTmpDir
177+
:: Pretty b
178+
=> PluginTestDescriptor b
179+
-> TestName
180+
-> VirtualFileTree
181+
-> FilePath
182+
-> FilePath
183+
-> FilePath
184+
-> (TextDocumentIdentifier -> Session ())
185+
-> TestTree
186+
goldenWithHaskellDocInTmpDir = goldenWithDocInTmpDir "haskell"
187+
147188
goldenWithHaskellAndCaps
148189
:: Pretty b
149190
=> ClientCapabilities
@@ -165,6 +206,27 @@ goldenWithHaskellAndCaps clientCaps plugin title testDataDir path desc ext act =
165206
act doc
166207
documentContents doc
167208

209+
goldenWithHaskellAndCapsInTmpDir
210+
:: Pretty b
211+
=> ClientCapabilities
212+
-> PluginTestDescriptor b
213+
-> TestName
214+
-> VirtualFileTree
215+
-> FilePath
216+
-> FilePath
217+
-> FilePath
218+
-> (TextDocumentIdentifier -> Session ())
219+
-> TestTree
220+
goldenWithHaskellAndCapsInTmpDir clientCaps plugin title tree path desc ext act =
221+
goldenGitDiff title (vftTestDataRoot tree </> path <.> desc <.> ext)
222+
$ runSessionWithServerAndCapsInTmpDir plugin clientCaps tree
223+
$ TL.encodeUtf8 . TL.fromStrict
224+
<$> do
225+
doc <- openDoc (path <.> ext) "haskell"
226+
void waitForBuildQueue
227+
act doc
228+
documentContents doc
229+
168230
goldenWithCabalDoc
169231
:: Pretty b
170232
=> PluginTestDescriptor b
@@ -198,6 +260,27 @@ goldenWithDoc fileType plugin title testDataDir path desc ext act =
198260
act doc
199261
documentContents doc
200262

263+
goldenWithDocInTmpDir
264+
:: Pretty b
265+
=> T.Text
266+
-> PluginTestDescriptor b
267+
-> TestName
268+
-> VirtualFileTree
269+
-> FilePath
270+
-> FilePath
271+
-> FilePath
272+
-> (TextDocumentIdentifier -> Session ())
273+
-> TestTree
274+
goldenWithDocInTmpDir fileType plugin title tree path desc ext act =
275+
goldenGitDiff title (vftTestDataRoot tree </> path <.> desc <.> ext)
276+
$ runSessionWithServerInTmpDir plugin tree
277+
$ TL.encodeUtf8 . TL.fromStrict
278+
<$> do
279+
doc <- openDoc (path <.> ext) fileType
280+
void waitForBuildQueue
281+
act doc
282+
documentContents doc
283+
201284
-- ------------------------------------------------------------
202285
-- Helper function for initialising plugins under test
203286
-- ------------------------------------------------------------
@@ -308,6 +391,90 @@ runSessionWithServerFormatter plugin formatter conf fp act = do
308391
fp
309392
act
310393

394+
runSessionWithServerInTmpDir :: Pretty b => PluginTestDescriptor b -> VirtualFileTree -> Session a -> IO a
395+
runSessionWithServerInTmpDir plugin tree act = do
396+
recorder <- pluginTestRecorder
397+
runSessionWithServerInTmpDir' (plugin recorder) def def fullCaps tree act
398+
399+
runSessionWithServerAndCapsInTmpDir :: Pretty b => PluginTestDescriptor b -> ClientCapabilities -> VirtualFileTree -> Session a -> IO a
400+
runSessionWithServerAndCapsInTmpDir plugin caps tree act = do
401+
recorder <- pluginTestRecorder
402+
runSessionWithServerInTmpDir' (plugin recorder) def def caps tree act
403+
404+
runSessionWithServerFormatterInTmpDir :: Pretty b => PluginTestDescriptor b -> String -> PluginConfig -> VirtualFileTree -> Session a -> IO a
405+
runSessionWithServerFormatterInTmpDir plugin formatter conf tree act = do
406+
recorder <- pluginTestRecorder
407+
runSessionWithServerInTmpDir'
408+
(plugin recorder)
409+
def
410+
{ formattingProvider = T.pack formatter
411+
, plugins = M.singleton (PluginId $ T.pack formatter) conf
412+
}
413+
def
414+
fullCaps
415+
tree
416+
act
417+
418+
-- | Host a server, and run a test session on it.
419+
--
420+
-- Creates a temporary directory, and materializes the VirtualFileTree
421+
-- in the temporary directory.
422+
--
423+
-- To debug test cases and verify the file system is correctly set up,
424+
-- you should set the environment variable 'HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP=1'.
425+
-- Further, we log the temporary directory location on startup. To view
426+
-- the logs, set the environment variable 'HLS_TEST_HARNESS_STDERR=1'.
427+
--
428+
-- Example invocation to debug test cases:
429+
--
430+
-- @
431+
-- HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP=1 HLS_TEST_HARNESS_STDERR=1 cabal test <plugin-name>
432+
-- @
433+
--
434+
-- Don't forget to use 'TASTY_PATTERN' to debug only a subset of tests.
435+
--
436+
-- For plugin test logs, look at the documentation of 'mkPluginTestDescriptor'.
437+
--
438+
-- Note: cwd will be shifted into a temporary directory in @Session a@
439+
runSessionWithServerInTmpDir' ::
440+
-- | Plugins to load on the server.
441+
--
442+
-- For improved logging, make sure these plugins have been initalised with
443+
-- the recorder produced by @pluginTestRecorder@.
444+
IdePlugins IdeState ->
445+
-- | lsp config for the server
446+
Config ->
447+
-- | config for the test session
448+
SessionConfig ->
449+
ClientCapabilities ->
450+
VirtualFileTree ->
451+
Session a ->
452+
IO a
453+
runSessionWithServerInTmpDir' plugins conf sessConf caps tree act = withLock lockForTempDirs $ do
454+
(recorder, _) <- initialiseTestRecorder
455+
["LSP_TEST_LOG_STDERR", "HLS_TEST_HARNESS_STDERR", "HLS_TEST_LOG_STDERR"]
456+
457+
-- Do not clean up the temporary directory if this variable is set to anything but '0'.
458+
-- Aids debugging.
459+
cleanupTempDir <- lookupEnv "HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP"
460+
let runTestInDir = case cleanupTempDir of
461+
Just val
462+
| val /= "0" -> \action -> do
463+
(tempDir, _) <- newTempDir
464+
a <- action tempDir
465+
logWith recorder Debug $ LogNoCleanup
466+
pure a
467+
468+
_ -> \action -> do
469+
a <- withTempDir action
470+
logWith recorder Debug $ LogCleanup
471+
pure a
472+
473+
runTestInDir $ \tmpDir -> do
474+
logWith recorder Info $ LogTestDir tmpDir
475+
_fs <- FS.materialiseVFT tmpDir tree
476+
runSessionWithServer' plugins conf sessConf caps tmpDir act
477+
311478
goldenWithHaskellDocFormatter
312479
:: Pretty b
313480
=> PluginTestDescriptor b -- ^ Formatter plugin to be used
@@ -352,6 +519,50 @@ goldenWithCabalDocFormatter plugin formatter conf title testDataDir path desc ex
352519
act doc
353520
documentContents doc
354521

522+
goldenWithHaskellDocFormatterInTmpDir
523+
:: Pretty b
524+
=> PluginTestDescriptor b -- ^ Formatter plugin to be used
525+
-> String -- ^ Name of the formatter to be used
526+
-> PluginConfig
527+
-> TestName -- ^ Title of the test
528+
-> VirtualFileTree -- ^ Virtual representation of the test project
529+
-> FilePath -- ^ Path to the testdata to be used within the directory
530+
-> FilePath -- ^ Additional suffix to be appended to the output file
531+
-> FilePath -- ^ Extension of the output file
532+
-> (TextDocumentIdentifier -> Session ())
533+
-> TestTree
534+
goldenWithHaskellDocFormatterInTmpDir plugin formatter conf title tree path desc ext act =
535+
goldenGitDiff title (vftTestDataRoot tree </> path <.> desc <.> ext)
536+
$ runSessionWithServerFormatterInTmpDir plugin formatter conf tree
537+
$ TL.encodeUtf8 . TL.fromStrict
538+
<$> do
539+
doc <- openDoc (path <.> ext) "haskell"
540+
void waitForBuildQueue
541+
act doc
542+
documentContents doc
543+
544+
goldenWithCabalDocFormatterInTmpDir
545+
:: Pretty b
546+
=> PluginTestDescriptor b -- ^ Formatter plugin to be used
547+
-> String -- ^ Name of the formatter to be used
548+
-> PluginConfig
549+
-> TestName -- ^ Title of the test
550+
-> VirtualFileTree -- ^ Virtual representation of the test project
551+
-> FilePath -- ^ Path to the testdata to be used within the directory
552+
-> FilePath -- ^ Additional suffix to be appended to the output file
553+
-> FilePath -- ^ Extension of the output file
554+
-> (TextDocumentIdentifier -> Session ())
555+
-> TestTree
556+
goldenWithCabalDocFormatterInTmpDir plugin formatter conf title tree path desc ext act =
557+
goldenGitDiff title (vftTestDataRoot tree </> path <.> desc <.> ext)
558+
$ runSessionWithCabalServerFormatterInTmpDir plugin formatter conf tree
559+
$ TL.encodeUtf8 . TL.fromStrict
560+
<$> do
561+
doc <- openDoc (path <.> ext) "cabal"
562+
void waitForBuildQueue
563+
act doc
564+
documentContents doc
565+
355566
runSessionWithCabalServerFormatter :: Pretty b => PluginTestDescriptor b -> String -> PluginConfig -> FilePath -> Session a -> IO a
356567
runSessionWithCabalServerFormatter plugin formatter conf fp act = do
357568
recorder <- pluginTestRecorder
@@ -363,7 +574,22 @@ runSessionWithCabalServerFormatter plugin formatter conf fp act = do
363574
}
364575
def
365576
fullCaps
366-
fp act
577+
fp
578+
act
579+
580+
runSessionWithCabalServerFormatterInTmpDir :: Pretty b => PluginTestDescriptor b -> String -> PluginConfig -> VirtualFileTree -> Session a -> IO a
581+
runSessionWithCabalServerFormatterInTmpDir plugin formatter conf tree act = do
582+
recorder <- pluginTestRecorder
583+
runSessionWithServerInTmpDir'
584+
(plugin recorder)
585+
def
586+
{ cabalFormattingProvider = T.pack formatter
587+
, plugins = M.singleton (PluginId $ T.pack formatter) conf
588+
}
589+
def
590+
fullCaps
591+
tree
592+
act
367593

368594
-- | Restore cwd after running an action
369595
keepCurrentDirectory :: IO a -> IO a
@@ -374,6 +600,12 @@ keepCurrentDirectory = bracket getCurrentDirectory setCurrentDirectory . const
374600
lock :: Lock
375601
lock = unsafePerformIO newLock
376602

603+
604+
{-# NOINLINE lockForTempDirs #-}
605+
-- | Never run in parallel
606+
lockForTempDirs :: Lock
607+
lockForTempDirs = unsafePerformIO newLock
608+
377609
-- | Host a server, and run a test session on it
378610
-- Note: cwd will be shifted into @root@ in @Session a@
379611
runSessionWithServer' ::
@@ -390,7 +622,7 @@ runSessionWithServer' ::
390622
FilePath ->
391623
Session a ->
392624
IO a
393-
runSessionWithServer' plugins conf sconf caps root s = withLock lock $ keepCurrentDirectory $ do
625+
runSessionWithServer' plugins conf sconf caps root s = withLock lock $ keepCurrentDirectory $ do
394626
(inR, inW) <- createPipe
395627
(outR, outW) <- createPipe
396628

0 commit comments

Comments
 (0)