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

Test Explorer in VS Code Extension and @Test() Attribute #2059

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b79c31e
initial work on testing
sezna Dec 10, 2024
1c09783
Merge branch 'main' of github.com:microsoft/qsharp into alex/testHarness
sezna Dec 10, 2024
54eb209
progress on test explorer
sezna Dec 10, 2024
e6a0941
test collection works
sezna Dec 10, 2024
0495a6d
tests run
sezna Dec 10, 2024
eae1b4d
add todos
sezna Dec 10, 2024
759036f
switching to namespaces included with callable names
sezna Dec 10, 2024
cec4bf9
scoped test names
sezna Dec 10, 2024
ad7d3e2
deduplicate parent items
sezna Dec 12, 2024
d9f288d
make running child items work
sezna Dec 12, 2024
9ec9f41
auto-refresh test cases
sezna Dec 12, 2024
a03ed14
wip -- checkpoint
sezna Dec 12, 2024
c11476a
wip
sezna Dec 12, 2024
b247c35
Remove codelens stuff
sezna Dec 12, 2024
e75f65b
update libraries to use new testing harness
sezna Dec 12, 2024
131a341
remove bad imports
sezna Dec 12, 2024
b52ad25
document some functions
sezna Dec 12, 2024
180368e
Add comment
sezna Dec 12, 2024
e960916
Remove todos
sezna Dec 12, 2024
7d2aadc
Fix nested tests
sezna Dec 12, 2024
ac8bfd7
initial round of PR feedback
sezna Dec 13, 2024
c22d14d
move discovery of test items into compiler layer
sezna Dec 13, 2024
5a28ef1
Use a pass to detect test attribute errors and report them nicely
sezna Dec 16, 2024
16a2aed
use getActiveProgram
sezna Dec 16, 2024
214097c
remove unnecessary api in main.ts
sezna Dec 16, 2024
adbc4b2
Fmt
sezna Dec 16, 2024
5b6a1f8
fix lints
sezna Dec 16, 2024
7cee532
update tests
sezna Dec 16, 2024
be64105
filter out invalid test items
sezna Dec 16, 2024
e34b7d2
Merge branch 'main' of github.com:microsoft/qsharp into alex/testHarness
sezna Dec 18, 2024
fa1ca42
wip: start to add locations to the return type for test callables
sezna Dec 18, 2024
790c29b
get spans/ranges hooked up
sezna Dec 18, 2024
9d0190c
abstract compiler worker generation into a common singleton worker
sezna Dec 20, 2024
38e0f4b
wipz
sezna Dec 20, 2024
43f4e17
use updateDocument events for test discovery
sezna Dec 20, 2024
bcd6d34
it works, but i'd rather not have the tests collapse on auto refresh
sezna Dec 23, 2024
5421cb2
Fmt
sezna Dec 23, 2024
804fb0b
rename Vscode to VsCode
sezna Dec 23, 2024
ffab724
rename collectTestCallables to getTestCallables
sezna Dec 23, 2024
cca48b5
switch to debug event target; remove unnecessary result
sezna Dec 23, 2024
eea9581
rename test explorer to test discovery
sezna Dec 23, 2024
540de0b
fmt
sezna Dec 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions compiler/qsc_frontend/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,18 @@ impl With<'_> {
None
}
},
Ok(hir::Attr::Test) => {
// verify that no args are passed to the attribute
match &*attr.arg.kind {
ast::ExprKind::Tuple(args) if args.is_empty() => {}
_ => {
self.lowerer
.errors
.push(Error::InvalidAttrArgs("()".to_string(), attr.arg.span));
}
}
Some(hir::Attr::Test)
}
Err(()) => {
self.lowerer.errors.push(Error::UnknownAttr(
attr.name.name.to_string(),
Expand Down
51 changes: 51 additions & 0 deletions compiler/qsc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,53 @@ impl Display for Package {
}
}

impl Package {
/// Returns a collection of the fully qualified names of any callables annotated with `@Test()`
pub fn collect_test_callables(&self) -> std::result::Result<Vec<String>, String> {
sezna marked this conversation as resolved.
Show resolved Hide resolved
let items_with_test_attribute = self
.items
.iter()
.filter(|(_, item)| item.attrs.iter().any(|attr| *attr == Attr::Test));

let callables = items_with_test_attribute
.filter(|(_, item)| matches!(item.kind, ItemKind::Callable(_)));

let callable_names = callables
.filter_map(|(_, item)| -> Option<std::result::Result<String, String>>{
if let ItemKind::Callable(callable) = &item.kind {
if !callable.generics.is_empty() {
return Some(Err(format!("Callable {} has generic type parameters. Test callables cannot have generic type parameters.", callable.name.name)));
}
if callable.input.kind != PatKind::Tuple(vec![]) {
return Some(Err(format!("Callable {} has input parameters. Test callables cannot have input parameters.", callable.name.name)));
}
sezna marked this conversation as resolved.
Show resolved Hide resolved
// this is indeed a test callable, so let's grab its parent name
let name = match item.parent {
None => Default::default(),
Some(parent_id) => {
let parent_item = self
.items
.get(parent_id)
.expect("Parent item did not exist in package");
if let ItemKind::Namespace(ns, _) = &parent_item.kind {
format!("{}.{}", ns.name(), callable.name.name)
} else {
callable.name.name.to_string()
}
}
};

Some(Ok(name))
} else {
None
}
})
.collect::<std::result::Result<_, _>>()?;

Ok(callable_names)
}
}

/// An item.
#[derive(Clone, Debug, PartialEq)]
pub struct Item {
Expand Down Expand Up @@ -1359,6 +1406,8 @@ pub enum Attr {
/// Indicates that an intrinsic callable is a reset. This means that the operation will be marked as
/// "irreversible" in the generated QIR.
Reset,
/// Indicates that a callable is a test case.
Test,
}

impl Attr {
Expand All @@ -1376,6 +1425,7 @@ The `not` operator is also supported to negate the attribute, e.g. `not Adaptive
Attr::SimulatableIntrinsic => "Indicates that an item should be treated as an intrinsic callable for QIR code generation and any implementation should only be used during simulation.",
Attr::Measurement => "Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as \"irreversible\" in the generated QIR, and output Result types will be moved to the arguments.",
Attr::Reset => "Indicates that an intrinsic callable is a reset. This means that the operation will be marked as \"irreversible\" in the generated QIR.",
Attr::Test => "Indicates that a callable is a test case.",
}
}
}
Expand All @@ -1391,6 +1441,7 @@ impl FromStr for Attr {
"SimulatableIntrinsic" => Ok(Self::SimulatableIntrinsic),
"Measurement" => Ok(Self::Measurement),
"Reset" => Ok(Self::Reset),
"Test" => Ok(Self::Test),
_ => Err(()),
}
}
Expand Down
5 changes: 4 additions & 1 deletion compiler/qsc_lowerer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,10 @@ fn lower_attrs(attrs: &[hir::Attr]) -> Vec<fir::Attr> {
hir::Attr::EntryPoint => Some(fir::Attr::EntryPoint),
hir::Attr::Measurement => Some(fir::Attr::Measurement),
hir::Attr::Reset => Some(fir::Attr::Reset),
hir::Attr::SimulatableIntrinsic | hir::Attr::Unimplemented | hir::Attr::Config => None,
hir::Attr::SimulatableIntrinsic
| hir::Attr::Unimplemented
| hir::Attr::Config
| hir::Attr::Test => None,
})
.collect()
}
Expand Down
17 changes: 17 additions & 0 deletions compiler/qsc_parse/src/item/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2396,3 +2396,20 @@ fn top_level_nodes_error_recovery() {
]"#]],
);
}

#[test]
fn test_attribute() {
check(
parse,
"@Test() function Foo() : Unit {}",
&expect![[r#"
Item _id_ [0-32]:
Attr _id_ [0-7] (Ident _id_ [1-5] "Test"):
Expr _id_ [5-7]: Unit
Callable _id_ [8-32] (Function):
name: Ident _id_ [17-20] "Foo"
input: Pat _id_ [20-22]: Unit
output: Type _id_ [25-29]: Path: Path _id_ [25-29] (Ident _id_ [25-29] "Unit")
body: Block: Block _id_ [30-32]: <empty>"#]],
);
}
6 changes: 6 additions & 0 deletions compiler/qsc_passes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod measurement;
mod replace_qubit_allocation;
mod reset;
mod spec_gen;
mod test_attribute;

use callable_limits::CallableLimits;
use capabilitiesck::{check_supported_capabilities, lower_store, run_rca_pass};
Expand Down Expand Up @@ -52,6 +53,7 @@ pub enum Error {
Measurement(measurement::Error),
Reset(reset::Error),
SpecGen(spec_gen::Error),
TestAttribute(test_attribute::TestAttributeError),
}

#[derive(Clone, Copy, Debug, PartialEq)]
Expand Down Expand Up @@ -121,6 +123,9 @@ impl PassContext {
ReplaceQubitAllocation::new(core, assigner).visit_package(package);
Validator::default().visit_package(package);

let test_attribute_errors = test_attribute::validate_test_attributes(package);
Validator::default().visit_package(package);

callable_errors
.into_iter()
.map(Error::CallableLimits)
Expand All @@ -130,6 +135,7 @@ impl PassContext {
.chain(entry_point_errors)
.chain(measurement_decl_errors.into_iter().map(Error::Measurement))
.chain(reset_decl_errors.into_iter().map(Error::Reset))
.chain(test_attribute_errors.into_iter().map(Error::TestAttribute))
.collect()
}

Expand Down
45 changes: 45 additions & 0 deletions compiler/qsc_passes/src/test_attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use miette::Diagnostic;
use qsc_data_structures::span::Span;
use qsc_hir::{hir::Attr, visit::Visitor};
use thiserror::Error;

#[cfg(test)]
mod tests;

#[derive(Clone, Debug, Diagnostic, Error)]
pub enum TestAttributeError {
#[error("This callable has parameters. Tests cannot have parameters.")]
swernli marked this conversation as resolved.
Show resolved Hide resolved
CallableHasParameters(#[label] Span),
#[error("This callable has type parameters. Tests cannot have type parameters.")]
CallableHaSTypeParameters(#[label] Span),
sezna marked this conversation as resolved.
Show resolved Hide resolved
}

pub(crate) fn validate_test_attributes(
package: &mut qsc_hir::hir::Package,
) -> Vec<TestAttributeError> {
let mut validator = TestAttributeValidator { errors: Vec::new() };
validator.visit_package(package);
validator.errors
}

struct TestAttributeValidator {
errors: Vec<TestAttributeError>,
}

impl<'a> Visitor<'a> for TestAttributeValidator {
fn visit_callable_decl(&mut self, decl: &'a qsc_hir::hir::CallableDecl) {
if decl.attrs.iter().any(|attr| matches!(attr, Attr::Test)) {
if !decl.generics.is_empty() {
self.errors
.push(TestAttributeError::CallableHaSTypeParameters(decl.span));
}
if decl.input.ty != qsc_hir::ty::Ty::UNIT {
self.errors
.push(TestAttributeError::CallableHasParameters(decl.span));
}
}
}
}
79 changes: 79 additions & 0 deletions compiler/qsc_passes/src/test_attribute/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use expect_test::{expect, Expect};
use indoc::indoc;
use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags};
use qsc_frontend::compile::{self, compile, PackageStore, SourceMap};
use qsc_hir::{validate::Validator, visit::Visitor};

use crate::test_attribute::validate_test_attributes;

fn check(file: &str, expect: &Expect) {
let store = PackageStore::new(compile::core());
let sources = SourceMap::new([("test".into(), file.into())], None);
let mut unit = compile(
&store,
&[],
sources,
TargetCapabilityFlags::all(),
LanguageFeatures::default(),
);
assert!(unit.errors.is_empty(), "{:?}", unit.errors);

let errors = validate_test_attributes(&mut unit.package);
Validator::default().visit_package(&unit.package);
if errors.is_empty() {
expect.assert_eq(&unit.package.to_string());
} else {
expect.assert_debug_eq(&errors);
}
}

#[test]
fn callable_cant_have_params() {
check(
indoc! {"
namespace test {
@Test()
operation A(q : Qubit) : Unit {

}
}
"},
&expect![[r#"
[
CallableHasParameters(
Span {
lo: 33,
hi: 71,
},
),
]
"#]],
);
}

#[test]
fn callable_cant_have_type_params() {
check(
indoc! {"
namespace test {
@Test()
operation A<'T>() : Unit {

}
}
"},
&expect![[r#"
[
CallableHaSTypeParameters(
Span {
lo: 33,
hi: 66,
},
),
]
"#]],
);
}
1 change: 1 addition & 0 deletions language_service/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ fn collect_hardcoded_words(expected: WordKinds) -> Vec<Completion> {
),
Completion::new("Measurement".to_string(), CompletionItemKind::Interface),
Completion::new("Reset".to_string(), CompletionItemKind::Interface),
Completion::new("Test".to_string(), CompletionItemKind::Interface),
]);
}
HardcodedIdentKind::Size => {
Expand Down
8 changes: 3 additions & 5 deletions library/fixed_point/src/Tests.qs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import Std.Convert.IntAsDouble;
import Std.Math.AbsD;
import Operations.*;

operation Main() : Unit {
FxpMeasurementTest();
FxpOperationTests();
}

@Test()
operation FxpMeasurementTest() : Unit {
for numQubits in 3..12 {
for numIntBits in 2..numQubits {
Expand Down Expand Up @@ -43,6 +39,7 @@ operation TestConstantMeasurement(constant : Double, registerWidth : Int, intege
ResetAll(register);
}

@Test()
operation FxpOperationTests() : Unit {
for i in 0..10 {
let constant1 = 0.2 * IntAsDouble(i);
Expand All @@ -54,6 +51,7 @@ operation FxpOperationTests() : Unit {
TestSquare(constant1);
}
}

operation TestSquare(a : Double) : Unit {
Message($"Testing Square({a})");
use resultRegister = Qubit[30];
Expand Down
20 changes: 15 additions & 5 deletions library/qtest/src/Tests.qs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,34 @@

import Std.Diagnostics.Fact;

function Main() : Unit {
let sample_tests = [

function SampleTestData() : (String, () -> Int, Int)[] {
[
("Should return 42", TestCaseOne, 43),
("Should add one", () -> AddOne(5), 42),
("Should add one", () -> AddOne(5), 6)
];
]
}

@Test()
function ReturnsFalseForFailingTest() : Unit {
Fact(
not Functions.CheckAllTestCases(sample_tests),
not Functions.CheckAllTestCases(SampleTestData()),
"Test harness failed to return false for a failing tests."
);
}

@Test()
function ReturnsTrueForPassingTest() : Unit {
Fact(
Functions.CheckAllTestCases([("always returns true", () -> true, true)]),
"Test harness failed to return true for a passing test"
);
}

let run_all_result = Functions.RunAllTestCases(sample_tests);
@Test()
function RunAllTests() : Unit {
let run_all_result = Functions.RunAllTestCases(SampleTestData());

Fact(
Length(run_all_result) == 3,
Expand Down
7 changes: 2 additions & 5 deletions library/rotations/src/Tests.qs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import Std.Math.HammingWeightI, Std.Math.PI;

import HammingWeightPhasing.HammingWeightPhasing, HammingWeightPhasing.WithHammingWeight;

operation Main() : Unit {
TestHammingWeight();
TestPhasing();
}

@Test()
operation TestHammingWeight() : Unit {
// exhaustive
use qs = Qubit[4];
Expand Down Expand Up @@ -41,6 +37,7 @@ operation TestHammingWeight() : Unit {
}
}

@Test()
operation TestPhasing() : Unit {
for theta in [1.0, 2.0, 0.0, -0.5, 5.0 * PI()] {
for numQubits in 1..6 {
Expand Down
Loading
Loading