Skip to content
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

Merged
merged 1 commit into from
Dec 28, 2021
Merged

Implement basics of test API #755

merged 1 commit into from
Dec 28, 2021

Conversation

kpodsiad
Copy link
Member

@kpodsiad kpodsiad commented Nov 9, 2021

There are some compilation errors which will be fixed in #754 - done and merged

After 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.

Copy link
Contributor

@tgodzik tgodzik left a 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.

src/test-provider/util.ts Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
src/test-provider/analyze-test-run.ts Outdated Show resolved Hide resolved
src/test-provider/types.ts Outdated Show resolved Hide resolved
@tgodzik
Copy link
Contributor

tgodzik commented Dec 10, 2021

@gabro would you be able to take a look? Ignore me if you are busy. 😅

@gabro
Copy link
Member

gabro commented Dec 10, 2021

@tgodzik hey, I'm on vacation until tomorrow, but I can try taking a look on Monday!
In case it's blocking a release don't mind waiting for me :)

@tgodzik
Copy link
Contributor

tgodzik commented Dec 10, 2021

It's not blocking at all. I am just starting to think about the new release. Thanks!

Comment on lines +8 to +15
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;
};

Copy link
Member Author

@kpodsiad kpodsiad Dec 10, 2021

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"'.

Copy link
Member Author

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

Copy link
Member

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:

https://www.typescriptlang.org/play?ssl=1&ssc=13&pln=1&pc=20#code/KYDwDg9gTgLgBDAnmYcBywDuSUB4CCANHAPIB8cAvHPnAGSkBQjOqAqgM7BQCSAJlXRZWuAHYBXALYAjbsQDecKMACGfCKIA2iOJ278AXHHGiAlgEdxqDohkRNcAL5kWyVABUAFqdEBzfoIY2G5iUrJQCkqq6lo6Xj7+fEYmFlZwNnaazsx8wADGmirKcABmJnkwphqlABTiXLxJug38AJRGAG4QpnwA3Iy5BUWoeRoc8PX6TXqN-YOFxaOi4wjefoZw8et9zCU1MGuJrYx7k42tQA

(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

Copy link
Member Author

@kpodsiad kpodsiad Dec 15, 2021

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.

Copy link
Member

@gabro gabro Dec 15, 2021

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

src/test-provider/util.ts Outdated Show resolved Hide resolved
Copy link
Member

@gabro gabro left a 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/extension.ts Outdated Show resolved Hide resolved
src/extension.ts Outdated Show resolved Hide resolved
src/test-provider/analyze-test-run.ts Outdated Show resolved Hide resolved
src/test-provider/analyze-test-run.ts Outdated Show resolved Hide resolved
src/test-provider/test-manager.ts Outdated Show resolved Hide resolved
run.started(test);
const children = getTestItemChildren(test);
children.forEach((c) => run.enqueued(c));
await commands
Copy link
Member

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

Copy link
Member Author

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) {
}

Copy link
Contributor

@tgodzik tgodzik left a 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.

src/extension.ts Outdated Show resolved Hide resolved
src/extension.ts Outdated Show resolved Hide resolved
createDebugAdapterTracker(session) {
if (session.configuration.kind === testRunnerId) {
return {
onWillStartSession: () => testCache.setEmptySuiteResultsFor(session.id),
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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?

src/test-explorer/test-manager.ts Show resolved Hide resolved
src/test-explorer/test-manager.ts Show resolved Hide resolved
src/test-explorer/test-manager.ts Show resolved Hide resolved
Comment on lines +637 to +639
const refreshTests = client.onRequest(CodeLensRefreshRequest.type, () => {
testManager.discoverTestSuites();
});
Copy link
Member Author

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);
Copy link
Member Author

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.
@kpodsiad
Copy link
Member Author

I squashed commits to a single one with a little bit more description.
@tgodzik @gabro have you got any comments left? I would like to put the "waiting for a metals release" label on this PR as fast as possible :D

Copy link
Member

@gabro gabro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@tgodzik tgodzik left a 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!

@tgodzik tgodzik merged commit b7c86a6 into scalameta:main Dec 28, 2021
@kpodsiad kpodsiad deleted the test-api branch January 21, 2022 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants