-
Notifications
You must be signed in to change notification settings - Fork 76
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
Implement basics of test API #755
Conversation
027152b
to
c4f3d59
Compare
f023821
to
b49bf73
Compare
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.
Great work! I still have some questions, especially that I am not an expert in TS.
@gabro would you be able to take a look? Ignore me if you are busy. 😅 |
@tgodzik hey, I'm on vacation until tomorrow, but I can try taking a look on Monday! |
It's not blocking at all. I am just starting to think about the new release. Thanks! |
declare const sym: unique symbol; | ||
/** | ||
* Creates a newtype without any runtime overhead. It's important for ID to be both unique and descriptive. | ||
*/ | ||
export type newtype<A, ID extends string> = A & { | ||
readonly [sym]: ID; | ||
}; | ||
|
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.
I know it's a little bit magical to the person who is not familiar with the typescript type system & magic. However, these types created by newtype
produce compile errors and have meaningful names instead of just string
.
export type TargetName = newtype<string, "targetName">;
export type TargetUri = newtype<string, "targetUri">;
const x: TargetName = "app" as TargetName;
const y: TargetUri = x;
Type 'TargetName' is not assignable to type 'TargetUri'.
Type 'TargetName' is not assignable to type '{ readonly [sym]: "targetUri"; }'.
Types of property '[sym]' are incompatible.
Type '"targetName"' is not assignable to type '"targetUri"'.
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.
It seems like it's too much for poor Tomek :D
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.
I'm quite familiar with this encoding of newtypes, but I think the unique symbol should be the value, otherwise it's quite useless, the definition above is equivalent to:
export type newtype<A, ID extends string> = A & {
readonly theType: ID;
};
This way the uniqueness is only guaranteed by the string passed when defining the type, which can work ok, but you are not using the unique symbol.
An alternative encoding can just be:
export type Newtype<A, O> = A & O
type UserId = Newtype<number, { readonly UserId: unique symbol }>
type ThingId = Newtype<number, { readonly ThingId: unique symbol}>
Full example here:
(this is loosely based on https://github.com/gcanti/newtype-ts/blob/master/src/index.ts)
Also (suuuper tiny detail) this is not strictly a newtype, since a TargetName
can be passed where a string
is expected. The encoding in newtype-ts is a bit stricter in this sense, that's why I've relaxed it a bit
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.
I'm quite familiar with this encoding of newtypes, but I think the unique symbol should be the value, otherwise it's quite useless, the definition above is equivalent to:
Actually, I used that in this way intentionally. The symbol is declared as a unique one and is used as a key for unnamed property. Moreover, the symbol is not exported so only a newtype can use it. Thanks to that you cannot access this property or even fake it. What do I mean by "fake it"? Let's consider the following example:
type newtype<A, ID extends string> = A & {
readonly theType: ID;
};
type Type1 = newtype<string, "type1">;
type Type2 = string & {
theType: "type1";
};
const x: Type2 = "aezakmi" as Type2;
const y: Type1 = x; // works
I know that this is an almost never gonna happen scenario, but I try to be careful with structural typing.
Also (suuuper tiny detail) this is not strictly a newtype, since a TargetName can be passed where a string is expected. The encoding in newtype-ts is a bit stricter in this sense, that's why I've relaxed it a bit
Yeah, you're right. But this is more than fine in this case since I wanted to use "newtype" to distinguish between string types and for us, the name newtype
shouldn't be misleading.
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.
Thanks for the explanation, it make sense then! One minor thing is that not using a unique symbol in the value too means that you can accidentally do:
type Type1 = newtype<string, "type1">;
type Type2 = newtype<string, "type1">; // oops
and those two will be silently interchangeable.
It's probably ok if we don't have many cases, it's a mistake that can be caught in code review
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.
Really nice job, I've left a first pass of stylistic comments
src/test-provider/test-manager.ts
Outdated
run.started(test); | ||
const children = getTestItemChildren(test); | ||
children.forEach((c) => run.enqueued(c)); | ||
await commands |
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.
I would prefer not mixing async/await and the then
api of promises if possible.
Also in general this expression is very long and I think it would benefit in readability if broken down in multiple smaller expressions
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.
Ok, so followng is a good way to go?
try {
const debugSession = await commands...
const wasStarted = await ...
await analyzeResults()
} catch(error) {
}
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.
Some questions from me, excuse me if they don't make sense 😅 I am a typescript amateur.
createDebugAdapterTracker(session) { | ||
if (session.configuration.kind === testRunnerId) { | ||
return { | ||
onWillStartSession: () => testCache.setEmptySuiteResultsFor(session.id), |
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.
Can we run multiple tests at the same time?
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.
It's a good point, some mechanism that prevents users from starting multiple test runs has to be added.
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.
Actually, could we not allow it? Sorry for not wording it clearer. Is there anything that could break on multiple sessions?
const refreshTests = client.onRequest(CodeLensRefreshRequest.type, () => { | ||
testManager.discoverTestSuites(); | ||
}); |
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.
Test explorer should be refreshed just like code lense. This change is possible due to scalameta/metals#3355
} | ||
}; | ||
|
||
const callback = () => (this.isRunning = false); |
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.
I experimented a little bit and it seems like this + vscode built-in cancelation mechanism seems to work (more or less). When you are in the middle of test run and you'll click to run another test site this.isRunning
will prevent you from running the next suite. However, the first request is cancelled and is marked as a skipped/cancelled by vscode even though test-analyzer sets particular test items status. I think it's good enough for start and if this will be a problem we can spend more time on this mechanic. Ok @tgodzik ?
The config option was added which allows a user to switch tests UI. User can pick between Code Lenses (old way) and Test Explorer (new one). Changes are applied immediately and don't require restarting Metals server. `Test Explorer` enables to both run and debug test suites via the gutter icons (on the left of file) and via the testing panel.
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.
LGTM!
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.
This will be awesome!
There are some compilation errors which will be fixed in #754- done and mergedAfter the brainstorm, together with @tgodzik we concluded that using DAP Event to pass some structured data about test results (well-defined JSON) will be the best and the fastest approach.
For now, mentioned data is passed through an OutputEvent, more specifically as a
testResult
event which contains all necessary information about the whole suite and at the same time about every single test.