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

Update test callable discovery to work within the language service #2095

Open
wants to merge 62 commits into
base: main
Choose a base branch
from

Conversation

sezna
Copy link
Contributor

@sezna sezna commented Jan 7, 2025

This PR rewrites #2059 to trigger test discovery from within the language server, as opposed to externally in an entirely separate context. Please read the description in that PR for more detail.

It also adds tests for LS state in both the Rust-based LS tests and the JS-based basics.js npm API tests.

@sezna sezna marked this pull request as ready for review January 7, 2025 20:37
@sezna sezna changed the base branch from alex/testHarness to main January 7, 2025 23:41
Copy link
Member

@minestarks minestarks left a comment

Choose a reason for hiding this comment

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

Not done looking at all the files, posting all my comments for today.

compiler/qsc/src/lib.rs Outdated Show resolved Hide resolved
wasm/src/test_discovery.rs Outdated Show resolved Hide resolved
wasm/src/test_discovery.rs Outdated Show resolved Hide resolved

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "(callables: [string, ILocation][]) => void")]
Copy link
Member

Choose a reason for hiding this comment

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

Use the ITestCallable type you already defined in the other wasm file, instead of the [string, ILocation] tuple. Tuples are clunky in TS.

npm/qsharp/src/language-service/language-service.ts Outdated Show resolved Hide resolved
npm/qsharp/src/compiler/compiler.ts Outdated Show resolved Hide resolved
wasm/src/language_service.rs Show resolved Hide resolved
Copy link
Member

@minestarks minestarks left a comment

Choose a reason for hiding this comment

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

Finished looking. I really like the overall approach. Also I assume integration tests are coming up in a different PR.

Comment on lines +38 to +43
decl.name.span,
));
}
if decl.input.ty != qsc_hir::ty::Ty::UNIT {
self.errors
.push(TestAttributeError::CallableHasParameters(decl.name.span));
Copy link
Member

Choose a reason for hiding this comment

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

Instead of just the name, could you use the whole signature's span, so that the squiggles appear below the problematic code and not the name?

And now that I wrote that... I realize there may not be an easy way to get that span from the AST. I think I hit this issue before. Hmm...

Copy link
Member

Choose a reason for hiding this comment

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

As it was brought up in the team chat, I think it'd be useful to have a test case where a @Config() attribute causes a test callable to be excluded from the compilation. get_test_callables shouldn't return that callable.

Comment on lines +550 to +552
// TODO(sezna) verify encoding
crate::qsc_utils::into_location(
qsc::line_column::Encoding::Utf16,
Copy link
Member

Choose a reason for hiding this comment

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

For consistency, this should be the same position_encoding that the LanguageService was initialized with. You can add it as a parameter to CompilationStateUpdater::new and just keep it in this struct.

Comment on lines +2545 to +2552
// Trigger a document update for the second test file.
updater
.update_document(
"parent/src/test2.qs",
1,
"@Test() function Test2() : Unit {}",
)
.await;
Copy link
Member

Choose a reason for hiding this comment

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

It'd be interesting to test if the tests in test2.qs file get discovered WITHOUT triggering an update for test2.qs.

In theory, just poking test1.qs should cause the whole project to be loaded, which in turn will raise a notification about all the tests.

If you remove this line, I think you'll see in the below expect! that all the tests are still discovered. (Currently, the expect! output contains TWO notifications with the same contents, because the compilation is updated twice. After you remove the second update_document, the expect! output should contain ONE notification, which will still show all the tests).

I suggest just removing the second update. It'll make the test case both shorter and more "interesting".

@@ -37,15 +37,25 @@ import { registerQSharpNotebookCellUpdateHandlers } from "./notebook.js";
import { createReferenceProvider } from "./references.js";
import { createRenameProvider } from "./rename.js";
import { createSignatureHelpProvider } from "./signature.js";

export async function activateLanguageService(extensionUri: vscode.Uri) {
Copy link
Member

Choose a reason for hiding this comment

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

FWIW I had intentionally made this function take extensionUri instead of the whole context, since we never actually use anything but the extensionUri. I prefer the previous version.

Comment on lines +30 to +34
const compilerWorkerScriptPath = vscode.Uri.joinPath(
context.extensionUri,
"./out/compilerWorker.js",
).toString();
worker = getCompilerWorker(compilerWorkerScriptPath);
Copy link
Member

Choose a reason for hiding this comment

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

I'd love it if you pulled this into a common utility in vscode, since these two lines, including the hardcoded path, are repeated in 4-5 places now.

@Test()
operation MeasureSignedIntTests() : Unit {
let testCases = [
("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 6), 11),
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't actually pass for me .

false,
);

const testMetadata = new WeakMap<vscode.TestItem, number>();
Copy link
Member

Choose a reason for hiding this comment

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

Interesting, I don't think I've ever seen a WeakMap used in practice! Is the idea that the test controller owns these items, and not this map?

Copy link
Member

Choose a reason for hiding this comment

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

That's from the docs. Search on weakmap on https://code.visualstudio.com/api/extension-guides/testing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This idea came from this page: https://code.visualstudio.com/api/extension-guides/testing (search for weakmap) -- to be honest, I'm not 100% sure why they recommend a WeakMap versus a regular key/value store here.

Copy link
Member

Choose a reason for hiding this comment

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

to be honest, I'm not 100% sure why they recommend a WeakMap versus a regular key/value store here

Because a regular key/value store would hold a strong reference to the TestItem that is the key you put in it, so it would never get garbage collected (unless you manually scrubbed the map and removed items which no longer exist in the controller, which you don't in this code currently. You delete no longer needed items in line 182, but you don't remove them from the map, so they will remain held in memory by that reference if not using a WeakMap).

From the MDN docs: an object's presence as a key in a WeakMap does not prevent the object from being garbage collected. Once an object used as a key has been collected, its corresponding values in any WeakMap become candidates for garbage collection as well

Copy link
Member

Choose a reason for hiding this comment

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

In addition to the tests here I'd love it if you added a samples under samples/ showing how to use this new attribute.

#[derive(Debug)]
pub struct TestCallables {
pub callables: Vec<(String, Location)>,
pub version: Option<u32>,
Copy link
Member

Choose a reason for hiding this comment

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

Unused field (and rightly so!)

@minestarks
Copy link
Member

@sezna hitting this odd bug where you can't navigate to the test case after it fails.

test-explorer

.map(|(name, span)| {
(
name.clone(),
// TODO(sezna) verify encoding

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants