-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Ide test integration #46
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,219 @@ | ||||||
## Abstract | ||||||
|
||||||
This proposal seeks technical coordination from the Haskell Fondation | ||||||
for improving integration between IDEs and testing libraries in Haskell. | ||||||
|
||||||
This work will involve two major implementation areas: testing library | ||||||
integration, and IDE integration. The author will aim to implement both | ||||||
of these features, though help is certainly welcome :). | ||||||
|
||||||
This undertaking requires buy-in and feedback from the maintainers of | ||||||
Haskell's testing frameworks, since we would like to have one interface | ||||||
that satisfies everyone's needs. | ||||||
|
||||||
## Background | ||||||
|
||||||
IDE integration for test suites is a feature that is expected in most | ||||||
if not all major programming languages. First class support exists for | ||||||
[JUnit with multiple IDEs](https://junit.org/junit5/docs/current/user-guide/#running-tests-ide), | ||||||
[Python with VSC](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiRyt_VwcD7AhUXIUQIHQe1CjUQFnoECBIQAQ&url=https%3A%2F%2Fcode.visualstudio.com%2Fdocs%2Fpython%2Ftesting&usg=AOvVaw0U1x8oEhgUTX1GeoPJqVqo), | ||||||
[.NET with VSC](https://marketplace.visualstudio.com/items?itemName=formulahendry.dotnet-test-explorer) | ||||||
and many others. | ||||||
|
||||||
Often these IDEs will supply inline hints for "Run this test" and | ||||||
"Re-run failed tests" as well as others. For users of mainstream | ||||||
languages, such features are no longer features, but instead | ||||||
expectations from a mature language. | ||||||
|
||||||
Haskell has a mature and well-maintained testing ecosystem, including | ||||||
the libraries: `tasty`, `HUnit`, and `hspec`. While these libraries | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about QuickCheck and friends? Is the intention that they are subsumed by the mention of Tasty here? |
||||||
themselves are mature, there is currently minimal | ||||||
integration between Haskell's testing libraries and HLS, as well as | ||||||
the primary IDEs used for Haskell (VSC;Emacs;Neovim). | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Attempts at integration between testing frameworks have focused on | ||||||
regex searches, with support for hspec: | ||||||
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. neotest uses https://tree-sitter.github.io/tree-sitter/ with https://github.com/tree-sitter/tree-sitter-haskell instead of regex. It still has its shortcomings to detect test cases, but at least in terms of syntax recognition it's much more robust as it uses a real parser. |
||||||
1. [neo-test](https://github.com/mrcjkb/neotest-haskell) | ||||||
2. [vim-test](https://github.com/vim-test/vim-test) | ||||||
|
||||||
While these are valuable contributions to the ecosystem, their choice | ||||||
of regex for the collection of the user's test tree, and support for only | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The term "test tree" is not necessarily understood by the audience for this proposal . Based on our discussion elsewhere, I think you made a good observation about how Haskell test libraries are typically (but not always) organized, and how it's different from something like JUnit-derived tests. In particular, JUnit-inspired libraries typically use the language's notion of a declaration or definition to bound tests, so a test is a class or a method, while Haskell libraries more frequently use an ordinary data structure to represent tests. This means that we'd need to be a bit creative about the UI, and draw inspiration from things like IntelliJ while still doing something a bit different that fits Haskell better. I think the proposal would be accessible to more readers if these assumptions and observations were made more explicit in this background section. |
||||||
specific frameworks are symptomatic of a lack of ecosystem coordination. | ||||||
|
||||||
## Motivation | ||||||
|
||||||
With the implementation of this proposal, the Haskell community will | ||||||
have gained a valuable IDE tool which will improve the user experience | ||||||
of Haskellers as well as make Haskellers more productive. | ||||||
|
||||||
Support for IDE integration of testing libraries will provide the following | ||||||
benefits: | ||||||
1. Ability for IDEs with Haskell support to provide inline hints for "Run | ||||||
Tests" style commands. | ||||||
Comment on lines
+51
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In visual studio code and other editors supporting the language server protocol, this is often accomplished with code-lens, in combination with custom client-side commands. Another example where they use the language-server but without code-lens would be https://github.com/microsoft/vscode-java-test |
||||||
2. Ability for IDEs with Haskell support and support for a test explorer | ||||||
view to display the user's test tree and run individual tests. | ||||||
3. A unified framework for testing libraries to provide integration with | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be able to offer a unified CLI interface also, perhaps? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be a really nice result! |
||||||
IDEs. When a new testing framework is developed, they will be able to | ||||||
provide the results in a Haskell datatype via serialization, which | ||||||
avoids framework-specific test tree discovery logic. | ||||||
4. Potential to share test result rendering logic between different testing | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Up until this point I thought this was mostly about test discovery, but now it sounds like you also want to standardise test reporting. That might be harder! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that running individual tests from the editor requires both. Discovery allows something like HLS to say "this is a test, want to run it?", and reporting allows HLS to say "It ran, and it passed". |
||||||
frameworks, whereas currently each testing framework has their own test result | ||||||
display logic. | ||||||
|
||||||
## Goals | ||||||
|
||||||
1. Create a new library, `hs-test`, with common datatypes for testing framework | ||||||
test trees, and test results. | ||||||
2. Generate test trees and test results that can be serialized into JSON from | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you have in mind concepts of "test tree" and "test results", it would really help if you made that explicit. The technical section below does that, but I think it would be helpful to have something in the initial prose, and maybe an argument that it's adequate for all usecases! |
||||||
Haskell's major testing frameworks, starting with `tasty`. | ||||||
Comment on lines
+67
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There would also be the Build Server Protocol which is adressing these requirements, but so far it didn't see much adaption so I guess JSON is be the safer/easier route. But maybe it's still useful to have a look at it and see how they modelled their solution. |
||||||
3. Integrate test tree output output with VSC's Haskell extension, via VSC's [Testing API](https://code.visualstudio.com/api/extension-guides/testing). | ||||||
4. Stretch goal: support IDEs other than VSC. | ||||||
5. Stretch goal: support test results caching, and features such as "Rerun only failed tests". | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't tell if this goal is a subgoal of number 3, or if it's something that should be supported by the other frameworks. Can you clarify it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is, is it a part of the JSON output format, or is it a VS Code extension feature?
Comment on lines
+49
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I am aware, this kind of functionality is often implemented on the client side/test-extension itself and doesn't need extra per language implementation. |
||||||
|
||||||
## Technical Proposal | ||||||
|
||||||
The `hs-test` library will contain the following data declarations: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that being concrete is great here, but you should add a bit of wiggle room in case you learn something in the process. What about:
Suggested change
|
||||||
|
||||||
```haskell | ||||||
-- | Datatype representing all the tests that exist in a test | ||||||
-- suite that could be run. | ||||||
-- | ||||||
-- `SrcLocs` are used to indicate where inline hints should be displayed | ||||||
-- in an IDE. | ||||||
data TestTree = | ||||||
TestGroup {name :: Text, subTrees :: [TestTree], location :: SrcLoc} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From where should the testing library get the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we definitely want the location to be optional. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're almost certainly going to want this to be extensible in some way. I think it's very likely that different testing frameworks will want to annotate the tree with framework-specific metadata of their own, and we should make supporting that a key design goal. |
||||||
| TestCase {name :: Text, location :: SrcLoc} | ||||||
|
||||||
data RunTestsRequest = | ||||||
RunTestsRequest {path :: NonEmpty Text} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing in Tasty guarantees that the names are unique, and if I have my Tasty test tree factored out into a variety of top-level definitions, then I could very easily end up accidentally reusing a name. I would then be surprised if the wrong test is run when I click on it, and it might make me miss a bug. Is there another way to represent the path? For instance, what about a redundant encoding of name-position pairs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Working out what I imagine would happen here:
Seems somewhat bad but maybe acceptable? |
||||||
|
||||||
-- | A lazy list in order to support arbitrary execution | ||||||
-- order and streaming between the test runner and IDE | ||||||
data TestResults = TestResults [TestCaseResult] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a lazy list rather than something in IO to support a hypothetical pure test runner? Does such a thing exist? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, the JUnit report format is already widely supported. For instance, Gitlab CI will provide detailed test suite summaries if the tests output this format. Why invent a new one here, rather than using the existing standard? (I'm not saying we shouldn't invent a new thing, but the proposal should say why novelty is necessary). |
||||||
|
||||||
data TestCaseResult = TestCaseResult { | ||||||
-- | Represents the path to get to this test case from the | ||||||
-- TestTree, where each element represents a node in the original | ||||||
-- TestTree. | ||||||
path :: NonEmpty Text, | ||||||
result :: TestResult, | ||||||
timeTaken :: Maybe Double | ||||||
} | ||||||
|
||||||
data TestResult = Success | Skipped | Failure FailureReason | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having timing information is great. Is it enough? Using Haskell datatypes as a means of thought is great, but I'd almost prefer the proposal to just define the JSON format. This would allow subtypes to be built (read: with extra fields) to allow the community to experiment with what additional information is useful on top of a common core. For instance, QuickCheck tests might want to show how many tests passed vs were skipped, and that would be useful feedback in my editor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is why I think this really needs to be extensible! |
||||||
|
||||||
newtype FailureReason = FailureReason Text | ||||||
``` | ||||||
|
||||||
It will also contain the logic to encode this datatype | ||||||
to JSON, for consumption by IDEs. | ||||||
|
||||||
------------ | ||||||
|
||||||
For testing frameworks that want to support IDE integration, | ||||||
they should provide support for the following commands: | ||||||
|
||||||
```bash | ||||||
./test-suite test-tree --output="FILE_NAME" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is clearly bikeshedding. Sorry! But I would expect that a protocol designed for machines would not take up an entry in the human-friendly subcommand namespace, and would instead do everything through |
||||||
./test-suite run-tests --input="FILE" --output="FILE" | ||||||
|
||||||
# Sample usages: | ||||||
> cabal test test-suite-name \ | ||||||
\--test-options='test-tree --output="output.tmp.json"' | ||||||
> cabal test test-suite-name \ | ||||||
\--test-options='run-tests --input="input.tmp.json" --output="output.tmp.json"' | ||||||
``` | ||||||
|
||||||
The `test-tree` command outputs a JSON-serialized `TestTree` to the given | ||||||
`output` file. | ||||||
|
||||||
The `run-tests` command reads a JSON-serialized `RunTestsRequest` from | ||||||
`input`, and, after executing tests, writes the result to the `output` file. | ||||||
|
||||||
`hs-test` will provide helper functions for these serialization tasks. | ||||||
|
||||||
------------- | ||||||
|
||||||
For IDEs, they will utilize these commands to: | ||||||
- find the test tree | ||||||
- execute a test run request, and display the results to the user. | ||||||
|
||||||
One problem that could occur is that having the IDE run `cabal test` to | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think having to actually run a cabal command to find this out is a weakness. Junit and friends can know most of this information statically, just by compiling the code, which is quite nice! That does mean that they have to rely on stuff like the hierarchy being implicit in static information (like the class hierarchy), but it is otherwise quite nice. I think we should at least think about whether we can do this more statically (or at least without cabal in the loop!!). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. e.g. HLS has some plugins that load code into the interpreter and run it, so we could plausibly compute the test tree without going through cabal, if we do need to run something. |
||||||
reindex the `TestTree` is too slow and incurs too large a latency for | ||||||
the user. However, this problem is no worse than the current situation, | ||||||
in which the user has to call `cabal test` anyway in order to run the tests. | ||||||
|
||||||
Furthermore, IDEs often having access to stale artifacts for code that used | ||||||
to compile but no longer compiles, therefore, the IDE could run the `test-tree` | ||||||
command while still displaying a mostly-correct test tree and inline hints | ||||||
in the user's IDE. Once the call to `test-tree` has completed, the new test | ||||||
tree can be displayed to the user. | ||||||
|
||||||
## Alternative Approaches | ||||||
|
||||||
### A typeclass driven approach with HLS integration | ||||||
|
||||||
Described in the following [discourse comment](https://discourse.haskell.org/t/hf-coordination-for-test-framework-ide-integration/5221/2?u=santiweight) | ||||||
|
||||||
In summary, take each function signature and detect whether said function | ||||||
is runnable as a test based on whether its type is a member of the | ||||||
proposed typeclass `IsTest`. Testing frameworks will each provide their | ||||||
own `IsTest` instances for their common testing types. | ||||||
|
||||||
I have chosen the option in this proposal for the following reasons: | ||||||
1. Such an approach would run Haskell code in GHCi, which is slower, and | ||||||
would therefore scale poorly for production codebases. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is actually true - the type class based approach seems to me like it could support compiled or interpreted mode, and that GHCi is an implementation strategy not forced by the design. Is there something I'm missing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also don't understand this. For running the tests you could still run them through cabal or whatever, assuming that you have some way of knowing how to do that. |
||||||
2. Such an approach requires integration with GHC's API, which the author | ||||||
is unfamiliar with and therefore cannot be confident in their ability to | ||||||
maintain the integration library. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that the design would require the IDE author to know the GHC API - hopefully this would be done once in HLS and then everyone else would get it for free. But the author of e.g. QuickCheck or Tasty would just need to write a four-line instance declaration. |
||||||
3. The author cannot see how to acquire the test tree that is common to | ||||||
IDE test integration, since it requires inspecting the typechecking artifacts | ||||||
of all files in the test suite. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a true limitation with respect to your proposal. Haskell test libraries really do organize things on a basis other than top-level declarations, and it would be good to not reinvent the wheel. I like that your proposal is highly compatible with what everyone's already using. |
||||||
4. Sometimes testing frameworks will provide conflicting `IsTest` instances for the | ||||||
the same type, which is a problem when using multiple testing frameworks in the | ||||||
same codebase. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the permanent problem of type classes. Such conflicting instances would always be orphans, right? I think this is no worse than any other Haskell type class. |
||||||
|
||||||
The typeclass proposal seems incredibly useful however for the typical `Run Program` | ||||||
command now provided, or as an additional tool for users. It is certainly a good | ||||||
idea for a future project. | ||||||
|
||||||
### Regex-based test discovery | ||||||
|
||||||
While regex discovery is a good and quick solution implementation wise, it does | ||||||
not scale to many real-world usecases. For example, many codebases will have helpers | ||||||
such as: | ||||||
|
||||||
```haskell | ||||||
testDecodeJsonFromFile fileName = testCase fileName $ readJsonFromFile fileName | ||||||
``` | ||||||
|
||||||
Since the fileName value is not known until runtime, regex-based approaches miss | ||||||
such cases, which is a non-starter for an ecosystem-wide tool. | ||||||
|
||||||
The proposal put forth by the author will also have to handle this use-case, by | ||||||
providing support in testing frameworks for users to define their own combinators | ||||||
that declare their location on use site. But this problem is a purely technical | ||||||
one with many solutions, such as the following pseudo-code: | ||||||
|
||||||
```haskell | ||||||
testDecodeJsonFromFile :: HasCallStack => TestTree | ||||||
testDecodeJsonFromFile = Tasty.customTestCase callstack fileName $ readJsonFromFile fileName | ||||||
``` | ||||||
|
||||||
## Future Work | ||||||
|
||||||
This proposal is purposefully minimal in scope, for a number | ||||||
of reasons: | ||||||
1. In order for testing frameworks to buy in, we will need a | ||||||
minimally-controversial interface. | ||||||
2. Support for more features should be done incrementally, since | ||||||
the design space here is rather large, and we will not know users' | ||||||
needs until after integration is alright out in the real world. | ||||||
|
||||||
Here are some features that have been neglected in this proposal: | ||||||
1. Distinction between "error" and "failure" when a test fails. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this not a mere matter of adding an additional constructor to |
||||||
2. Support for multiple filters when running a test. | ||||||
3. Support for options when running a test, such as: `--accept` when | ||||||
running golden tests, `-jN` to run tests in parallel, and options | ||||||
related to property-based tests. | ||||||
4. Support for localized options when running a test. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are localized options? In my head it would be something like replacing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, I think a screenshot would be very useful for Haskellers who don't use IntelliJ much. This one came up from a quick image search, for instance: https://i.stack.imgur.com/j5f1o.png