diff --git a/library/chemistry/src/Tests.qs b/library/chemistry/src/Tests.qs index e558af15e3c..357f81dc3fc 100644 --- a/library/chemistry/src/Tests.qs +++ b/library/chemistry/src/Tests.qs @@ -332,7 +332,7 @@ operation JordanWignerUCCDTermPQRSTest() : Unit { } @Config(Unrestricted) -// @Test() +@Test() operation JordanWignerUCCDTermPRQSTest() : Unit { let term0 = [NewJordanWignerInputState(2.0, 0.0, [2, 0, 4, 1])]; let state0 = [0., 0., 0., 0., 0.,-0.416147, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.909297, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; diff --git a/library/fixed_point/src/Operations.qs b/library/fixed_point/src/Operations.qs index 49f07c3f8ad..7d759576175 100644 --- a/library/fixed_point/src/Operations.qs +++ b/library/fixed_point/src/Operations.qs @@ -3,7 +3,6 @@ import Types.FixedPoint; import Init.PrepareFxP; -import Operations.AddFxP; import Signed.Operations.Invert2sSI, Signed.Operations.MultiplySI, Signed.Operations.SquareSI; import Facts.AssertPointPositionsIdenticalFxP; import Std.Arrays.Zipped; diff --git a/source/compiler/qsc/src/interpret.rs b/source/compiler/qsc/src/interpret.rs index 70832e5a52e..1cd1d6b30e1 100644 --- a/source/compiler/qsc/src/interpret.rs +++ b/source/compiler/qsc/src/interpret.rs @@ -440,7 +440,7 @@ impl Interpreter { /// Given a package ID, returns all the global items in the package. /// Note this does not currently include re-exports. - fn package_globals(&self, package_id: PackageId) -> Vec<(Vec>, Rc, Value)> { + fn package_globals(&self, package_id: PackageId) -> Vec { let mut exported_items = Vec::new(); let package = &self .compiler @@ -454,24 +454,25 @@ impl Interpreter { package: package_id, item: fir::LocalItemId::from(usize::from(term.id.item)), }; - exported_items.push(( - global.namespace, - global.name, - Value::Global(store_item_id, FunctorApp::default()), - )); + exported_items.push(PackageGlobal { + namespace: global.namespace, + name: global.name, + value: Value::Global(store_item_id, FunctorApp::default()), + is_test: term.is_test, + }); } } exported_items } /// Get the global callables defined in the user source passed into initialization of the interpreter as `Value` instances. - pub fn source_globals(&self) -> Vec<(Vec>, Rc, Value)> { + pub fn source_globals(&self) -> Vec { self.package_globals(self.source_package) } /// Get the global callables defined in the open package being interpreted as `Value` instances, which will include any items /// defined by calls to `eval_fragments` and the like. - pub fn user_globals(&self) -> Vec<(Vec>, Rc, Value)> { + pub fn user_globals(&self) -> Vec { self.package_globals(self.package) } @@ -1811,6 +1812,14 @@ pub enum CircuitGenerationMethod { Static, } +/// A global item as enumerated from a package. +pub struct PackageGlobal { + pub namespace: Vec>, + pub name: Rc, + pub value: Value, + pub is_test: bool, +} + /// A debugger that enables step-by-step evaluation of code /// and inspecting state in the interpreter. pub struct Debugger { diff --git a/source/compiler/qsc/src/interpret/circuit_tests.rs b/source/compiler/qsc/src/interpret/circuit_tests.rs index 76af72326a2..ad06feb4ed3 100644 --- a/source/compiler/qsc/src/interpret/circuit_tests.rs +++ b/source/compiler/qsc/src/interpret/circuit_tests.rs @@ -5,7 +5,7 @@ use super::{CircuitEntryPoint, Debugger, Interpreter}; use crate::{ - interpret::{CircuitGenerationMethod, Error}, + interpret::{CircuitGenerationMethod, Error, PackageGlobal}, target::Profile, }; use expect_test::expect; @@ -280,15 +280,15 @@ fn static_circuit_from_callable_with_callable_arg_matches_classical_eval() { let globals = interp.source_globals(); let invoke = globals .iter() - .find(|(_, n, _)| &**n == "InvokeWithQubits") + .find(|PackageGlobal { name, .. }| &**name == "InvokeWithQubits") .expect("InvokeWithQubits should be a source global") - .2 + .value .clone(); let all_h = globals .iter() - .find(|(_, n, _)| &**n == "AllH") + .find(|PackageGlobal { name, .. }| &**name == "AllH") .expect("AllH should be a source global") - .2 + .value .clone(); let args = Value::Tuple(vec![Value::Int(3), all_h].into(), None); diff --git a/source/compiler/qsc/src/interpret/tests.rs b/source/compiler/qsc/src/interpret/tests.rs index 664e7a5d8d8..3b951e13546 100644 --- a/source/compiler/qsc/src/interpret/tests.rs +++ b/source/compiler/qsc/src/interpret/tests.rs @@ -73,6 +73,8 @@ mod given_interpreter { use expect_test::expect; use indoc::indoc; + use crate::interpret::PackageGlobal; + use super::*; mod without_stdlib { @@ -674,17 +676,17 @@ mod given_interpreter { let items = interpreter.user_globals(); assert_eq!(items.len(), 2); // No namespace for top-level items - assert!(items[0].0.is_empty()); + assert!(items[0].namespace.is_empty()); expect![[r#" "Foo" "#]] - .assert_debug_eq(&items[0].1); + .assert_debug_eq(&items[0].name); // No namespace for top-level items - assert!(items[1].0.is_empty()); + assert!(items[1].namespace.is_empty()); expect![[r#" "Bar" "#]] - .assert_debug_eq(&items[1].1); + .assert_debug_eq(&items[1].name); } #[test] @@ -706,11 +708,11 @@ mod given_interpreter { "Foo", ] "#]] - .assert_debug_eq(&items[0].0); + .assert_debug_eq(&items[0].namespace); expect![[r#" "Bar" "#]] - .assert_debug_eq(&items[0].1); + .assert_debug_eq(&items[0].name); } #[test] @@ -737,29 +739,29 @@ mod given_interpreter { let items = interpreter.user_globals(); assert_eq!(items.len(), 4); // No namespace for top-level items - assert!(items[0].0.is_empty()); + assert!(items[0].namespace.is_empty()); expect![[r#" "Foo" "#]] - .assert_debug_eq(&items[0].1); + .assert_debug_eq(&items[0].name); // No namespace for top-level items - assert!(items[1].0.is_empty()); + assert!(items[1].namespace.is_empty()); expect![[r#" "Bar" "#]] - .assert_debug_eq(&items[1].1); + .assert_debug_eq(&items[1].name); // No namespace for top-level items - assert!(items[2].0.is_empty()); + assert!(items[2].namespace.is_empty()); expect![[r#" "Baz" "#]] - .assert_debug_eq(&items[2].1); + .assert_debug_eq(&items[2].name); // No namespace for top-level items - assert!(items[3].0.is_empty()); + assert!(items[3].namespace.is_empty()); expect![[r#" "Qux" "#]] - .assert_debug_eq(&items[3].1); + .assert_debug_eq(&items[3].name); } #[test] @@ -1105,7 +1107,13 @@ mod given_interpreter { interpreter .user_globals() .into_iter() - .find_map(|(_, global_name, value)| (global_name.as_ref() == name).then_some(value)) + .find_map( + |PackageGlobal { + name: global_name, + value, + .. + }| { (global_name.as_ref() == name).then_some(value) }, + ) .unwrap_or_else(|| panic!("{name} should be present in user globals")) } @@ -2952,11 +2960,11 @@ mod given_interpreter { "A", ] "#]] - .assert_debug_eq(&items[0].0); + .assert_debug_eq(&items[0].namespace); expect![[r#" "B" "#]] - .assert_debug_eq(&items[0].1); + .assert_debug_eq(&items[0].name); } #[test] diff --git a/source/compiler/qsc_frontend/src/resolve.rs b/source/compiler/qsc_frontend/src/resolve.rs index ce81a2a0bb5..73ea8da3dfb 100644 --- a/source/compiler/qsc_frontend/src/resolve.rs +++ b/source/compiler/qsc_frontend/src/resolve.rs @@ -1278,7 +1278,7 @@ impl GlobalTable { for global in global::iter_package(id, package).filter(|global| { global.visibility == hir::Visibility::Public - || matches!(&global.kind, global::Kind::Callable(t) if t.intrinsic) + || matches!(&global.kind, global::Kind::Callable(t) if t.is_intrinsic) }) { // If the namespace is `Main` and we have an alias, we treat it as the root of the package, so there's no // namespace prefix between the dependency alias and the defined items. @@ -1322,7 +1322,7 @@ impl GlobalTable { Res::Importable(Importable::Callable(term.id, global.status)), ); } - if term.intrinsic { + if term.is_intrinsic { self.scope.intrinsics.insert(global.name); } } diff --git a/source/compiler/qsc_hir/src/global.rs b/source/compiler/qsc_hir/src/global.rs index cb28ab420dc..1239564f175 100644 --- a/source/compiler/qsc_hir/src/global.rs +++ b/source/compiler/qsc_hir/src/global.rs @@ -3,7 +3,8 @@ use crate::{ hir::{ - Item, ItemId, ItemKind, ItemStatus, Package, PackageId, Res, SpecBody, SpecGen, Visibility, + Attr, Item, ItemId, ItemKind, ItemStatus, Package, PackageId, Res, SpecBody, SpecGen, + Visibility, }, ty::Scheme, }; @@ -48,7 +49,8 @@ pub struct Ty { pub struct Callable { pub id: ItemId, pub scheme: Scheme, - pub intrinsic: bool, + pub is_intrinsic: bool, + pub is_test: bool, } /// A lookup table used for looking up global core items for insertion in `qsc_passes`. @@ -144,7 +146,8 @@ impl PackageIter<'_> { kind: Kind::Callable(Callable { id: item_id, scheme: decl.scheme(), - intrinsic: decl.body.body == SpecBody::Gen(SpecGen::Intrinsic), + is_intrinsic: decl.body.body == SpecBody::Gen(SpecGen::Intrinsic), + is_test: decl.attrs.contains(&Attr::Test), }), }), (ItemKind::Callable(decl), None) => Some(Global { @@ -155,7 +158,8 @@ impl PackageIter<'_> { kind: Kind::Callable(Callable { id: item_id, scheme: decl.scheme(), - intrinsic: decl.body.body == SpecBody::Gen(SpecGen::Intrinsic), + is_intrinsic: decl.body.body == SpecBody::Gen(SpecGen::Intrinsic), + is_test: decl.attrs.contains(&Attr::Test), }), }), (ItemKind::Ty(name, _def), Some(ItemKind::Namespace(namespace, _))) => Some(Global { diff --git a/source/qdk_package/qdk/_context.py b/source/qdk_package/qdk/_context.py index ab764757a75..2adc8d4028e 100644 --- a/source/qdk_package/qdk/_context.py +++ b/source/qdk_package/qdk/_context.py @@ -252,12 +252,15 @@ def __init__( self_ref = weakref.ref(self) def make_callable_weak( - callable: GlobalCallable, namespace: List[str], callable_name: str + callable: GlobalCallable, + namespace: List[str], + callable_name: str, + is_test: bool, ) -> None: ctx = self_ref() if ctx is None or ctx._disposed: return - ctx._make_callable(callable, namespace, callable_name) + ctx._make_callable(callable, namespace, callable_name, is_test) def make_class_weak( qsharp_type: TypeIR, namespace: List[str], class_name: str @@ -433,7 +436,11 @@ def _get_code_module( return module def _make_callable( - self, callable: GlobalCallable, namespace: List[str], callable_name: str + self, + callable: GlobalCallable, + namespace: List[str], + callable_name: str, + is_test: bool, ): """Registers a Q# callable in this context's code module.""" module = self._get_code_module(namespace) @@ -452,12 +459,14 @@ def _callable_fn(*args): setattr(_callable_fn, "_qdk_context", self) setattr(_callable_fn, "__global_callable", callable) + setattr(_callable_fn, "__is_test__", is_test) + setattr(_callable_fn, "__name__", ".".join(namespace + [callable_name])) if module.__dict__.get(callable_name) is None: module.__setattr__(callable_name, _callable_fn) else: for key, val in module.__dict__.get(callable_name).__dict__.items(): - if key != "__global_callable": + if key != "__global_callable" and key != "__is_test__": _callable_fn.__dict__[key] = val module.__setattr__(callable_name, _callable_fn) diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index 41e4be1bb3f..7788caf3a41 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -156,7 +156,7 @@ class Interpreter: list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - make_callable: Optional[Callable[[GlobalCallable, List[str], str], None]], + make_callable: Optional[Callable[[GlobalCallable, List[str], str, bool], None]], make_class: Optional[Callable[[TypeIR, List[str], str], None]], trace_circuit: Optional[bool], ) -> None: diff --git a/source/qdk_package/qdk/test_utils.py b/source/qdk_package/qdk/test_utils.py index 02d83f86c75..b6359fb4990 100644 --- a/source/qdk_package/qdk/test_utils.py +++ b/source/qdk_package/qdk/test_utils.py @@ -1,10 +1,12 @@ -"""Helper functions for testing Q# code.""" +"""Utilities for testing Q# code.""" -from typing import Any - -from qdk._interpreter import _get_context_or_default +import re +import types +from collections.abc import Callable +from typing import Any, Optional from ._context import Context +from ._interpreter import _get_context_or_default, _get_default_context def dump_operation_on_state( @@ -13,7 +15,7 @@ def dump_operation_on_state( initial_state: list[float] | None = None, context: Context | None = None, ) -> list[complex]: - """Returns statevector after applying operation to the given state. + """Return the state vector after applying an operation to a given state. Uses big-endian convention for basis-state numbering. @@ -22,15 +24,15 @@ def dump_operation_on_state( a Q# callable. The callable must have signature ``(Qubit[] => Unit)``. num_qubits: Number of qubits the operation acts on. - initial_state: Initial state given by list of `2**num_qubits` real amplitudes. + initial_state: Initial state as a list of ``2**num_qubits`` real amplitudes. If the list is shorter, it will be padded with zeros. - If not provided, the initial state is zero state (|00..0>). - context: `qdk.Context` from which the operation was created (optional). If - not provided, will attempt to infer it from `op` and then fall back to - default context. + If not provided, the initial state is the zero state (|00..0>). + context: Optional ``qdk.Context`` in which to evaluate the operation. + If not provided, this function attempts to infer a context from ``op`` + and falls back to the default context. Returns: - The state vector as a list of `2**num_qubits` complex numbers. + The state vector as a list of ``2**num_qubits`` complex numbers. """ context = context or _get_context_or_default(op) if initial_state is None: @@ -68,3 +70,91 @@ def dump_operation_on_state( for index, amplitude in state.items(): statevector[index] = amplitude return statevector + + +def _get_test_callables(context: Context) -> list[Callable]: + """Return all Q# callables marked with the ``@Test`` attribute.""" + test_callables: list[Callable] = [] + + # Iterate through all the attributes of self.code and check if they are + # callables with the __is_test__ attribute set to True. + # Recursively check for nested modules as well. + def find_test_callables(module): + for attr_name in dir(module): + attr = getattr(module, attr_name) + if callable(attr) and getattr(attr, "__is_test__", False): + test_callables.append(attr) + elif isinstance(attr, types.ModuleType) or isinstance( + attr, types.SimpleNamespace + ): + find_test_callables(attr) + + find_test_callables(context.code) + return test_callables + + +def run_tests( + context: Optional[Context] = None, + seed: Optional[int] = None, + regex: Optional[str] = None, + verbose: int = 1, +) -> None: + """ + Discover and run ``@Test`` Q# callables in the selected context, with + optional name filtering and configurable verbosity. + + :param context: Optional `qdk.Context` to discover and run tests in. If not + provided, the default context is used. + :param seed: The seed to use for the random number generator in simulation, if any. + :param regex: Optional regular expression used to filter tests by fully + qualified test name (for example, ``MyNamespace.MyTest``). Only + matching tests are run. + :param verbose: Verbosity level. + 0 - Print nothing. + 1 - Print ``.`` for each successful test and suppress Q# output. + 2 - Print test names and suppress Q# output. + 3+ - Print test names and Q# output. + For ``verbose >= 1``, failures and a summary are printed at the end. + + :raises RuntimeError: If one or more tests fail. + """ + context = context or _get_default_context() + tests = _get_test_callables(context) + if regex is not None: + tests = [test for test in tests if re.search(regex, test.__name__) is not None] + + if verbose >= 1: + print(f"Running {len(tests)} tests...") + failed_tests = [] + failures = [] + for test in tests: + if verbose >= 2: + print(f"Running {test.__name__}...") + try: + context.run(test, 1, seed=seed, save_events=(verbose <= 2)) + if verbose == 1: + print(".", end="") + elif verbose >= 2: + print(f"PASSED: {test.__name__}") + except Exception as e: + if verbose >= 1: + print() + print(f"FAILED: {test.__name__}") + print(e) + failed_tests.append(test.__name__) + failures.append(str(e)) + + # Print summary. + if verbose >= 1: + num_passed = len(tests) - len(failed_tests) + print() + print(f"Finished tests: {num_passed} passed, {len(failed_tests)} failed.") + for test_name in failed_tests: + print(f" FAILED: {test.__name__}") + + # Construct descriptive error if there are any failures. + if len(failed_tests) > 0: + err = f"{len(failed_tests)} test(s) failed\n" + for test_name, failure in zip(failed_tests, failures): + err += f"FAILED: {test.__name__}\n{failure}\n" + raise RuntimeError(err) diff --git a/source/qdk_package/src/interpreter.rs b/source/qdk_package/src/interpreter.rs index 9505ab0a981..ac0390de7df 100644 --- a/source/qdk_package/src/interpreter.rs +++ b/source/qdk_package/src/interpreter.rs @@ -42,7 +42,7 @@ use pyo3::{ IntoPyObjectExt, create_exception, exceptions::{PyException, PyValueError}, prelude::*, - types::{PyDict, PyList, PyString, PyTuple, PyType}, + types::{PyBool, PyDict, PyList, PyString, PyTuple, PyType}, }; use qsc::{ LanguageFeatures, PackageType, SourceMap, @@ -51,7 +51,7 @@ use qsc::{ fir::{self}, hir::ty::{Prim, Ty}, interpret::{ - self, CircuitEntryPoint, PauliNoise, SimType, TaggedItem, Value, + self, CircuitEntryPoint, PackageGlobal, PauliNoise, SimType, TaggedItem, Value, output::{Error, Receiver}, }, openqasm::{ @@ -491,8 +491,14 @@ impl Interpreter { if let Some(make_callable) = &make_callable { // Add any global callables from the user source as Python functions to the environment. let exported_items = interpreter.source_globals(); - for (namespace, name, val) in exported_items { - create_py_callable(py, make_callable, &namespace, &name, val)?; + for PackageGlobal { + namespace, + name, + value, + is_test, + } in exported_items + { + create_py_callable(py, make_callable, &namespace, &name, value, is_test)?; } } if let Some(make_class) = &make_class { @@ -540,8 +546,14 @@ impl Interpreter { // This is safe because either the callable will be replaced with itself or a new callable with the // same name will shadow the previous one, which is the expected behavior. let new_items = self.interpreter.user_globals(); - for (namespace, name, val) in new_items { - create_py_callable(py, make_callable, &namespace, &name, val)?; + for PackageGlobal { + namespace, + name, + value, + is_test, + } in new_items + { + create_py_callable(py, make_callable, &namespace, &name, value, is_test)?; } } if let Some(make_class) = &self.make_class { @@ -667,8 +679,14 @@ impl Interpreter { // This is safe because either the callable will be replaced with itself or a new callable with the // same name will shadow the previous one, which is the expected behavior. let new_items = self.interpreter.user_globals(); - for (namespace, name, val) in new_items { - create_py_callable(py, make_callable, &namespace, &name, val)?; + for PackageGlobal { + namespace, + name, + value, + is_test, + } in new_items + { + create_py_callable(py, make_callable, &namespace, &name, value, is_test)?; } } value_to_pyobj(&self.interpreter, py, &value) @@ -1582,6 +1600,7 @@ fn create_py_callable( namespace: &[Rc], name: &str, val: Value, + is_test: bool, ) -> PyResult<()> { if namespace.is_empty() && name.starts_with(".lambda") { // We don't want to bind auto-generated lambda callables. @@ -1592,6 +1611,7 @@ fn create_py_callable( Py::new(py, GlobalCallable::from(val)).expect("should be able to create callable"), // callable id PyList::new(py, namespace.iter().map(ToString::to_string))?, // namespace as string array PyString::new(py, name), // name of callable + PyBool::new(py, is_test), // whether the callable has the @Test attribute ); // Call into the Python layer to create the function wrapping the callable invocation. diff --git a/source/qdk_package/tests-integration/test_libraries.py b/source/qdk_package/tests-integration/test_libraries.py new file mode 100644 index 00000000000..1e77cac27f3 --- /dev/null +++ b/source/qdk_package/tests-integration/test_libraries.py @@ -0,0 +1,31 @@ +from pathlib import Path + +import pytest +from qdk import Context +from qdk.test_utils import run_tests + +# Directory with all Q# libraries. +_LIB_DIR = str(Path(__file__).resolve().parents[3] / "library") + + +@pytest.mark.parametrize( + "library_name", + [ + "chemistry", + "fixed_point", + "qtest", + "rotations", + "signed", + "table_lookup", + ], +) +def test_library(library_name: str): + run_tests(Context(project_root=f"{_LIB_DIR}/{library_name}")) + + +# Use this test case for library development. +# Run with: +# pytest source/qdk_package/tests-integration/test_libraries.py::test_single -s +def test_single(): + ctx = Context(project_root=f"{_LIB_DIR}/table_lookup") + run_tests(ctx, verbose=3, regex="TestLookupMatchesStd") diff --git a/source/qdk_package/tests/test_interpreter.py b/source/qdk_package/tests/test_interpreter.py index db704d6bfe0..f2e3197a46f 100644 --- a/source/qdk_package/tests/test_interpreter.py +++ b/source/qdk_package/tests/test_interpreter.py @@ -28,7 +28,7 @@ def check_invoke(source: str, callable: str, expect: str): e = None f = None - def _make_callable(callable, namespace, callable_name): + def _make_callable(callable, namespace, callable_name, _is_test): nonlocal f f = callable @@ -436,7 +436,7 @@ def test_estimate_from_udt_returning_callable_matches_logical_counts_on_base_pro ): counted = None - def make_callable(callable_value, _namespace, callable_name): + def make_callable(callable_value, _namespace, callable_name, _is_test): nonlocal counted if callable_name == "Counted": counted = callable_value diff --git a/source/resource_estimator/src/counts/tests.rs b/source/resource_estimator/src/counts/tests.rs index 2df0e80ec9d..b510e50c9ed 100644 --- a/source/resource_estimator/src/counts/tests.rs +++ b/source/resource_estimator/src/counts/tests.rs @@ -9,7 +9,7 @@ use indoc::indoc; use miette::Report; use qsc::{ LanguageFeatures, PackageType, SourceMap, TargetCapabilityFlags, - interpret::{GenericReceiver, Interpreter, Value}, + interpret::{GenericReceiver, Interpreter, PackageGlobal, Value}, target::Profile, }; @@ -73,7 +73,13 @@ fn source_global(interpreter: &Interpreter, name: &str) -> Value { interpreter .source_globals() .into_iter() - .find_map(|(_, global_name, value)| (global_name.as_ref() == name).then_some(value)) + .find_map( + |PackageGlobal { + name: global_name, + value, + .. + }| (global_name.as_ref() == name).then_some(value), + ) .unwrap_or_else(|| panic!("{name} should be present in source globals")) }