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

Add Qtest library that uses class constraints #2013

Merged
merged 3 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions library/qtest/qsharp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"author": "Microsoft",
"license": "MIT",
"files": [
"src/Functions.qs",
"src/Main.qs",
"src/Operations.qs",
"src/Tests.qs",
"src/Util.qs"
]
}
60 changes: 60 additions & 0 deletions library/qtest/src/Functions.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import Util.TestCaseResult, Util.OutputMessage;
import Std.Arrays.Mapped, Std.Arrays.All;

/// # Summary
/// Runs a number of test cases and returns true if all tests passed, false otherwise.
/// Prints a report of what passed and what failed as output.
///
/// For a more flexible test running function, see `RunAllTestCases` which returns
/// test results instead of printing out to output.
///
/// # Input
/// Takes a list of test cases. A test case is a tuple of `(String, () -> T, 'T)`, where
/// the first String is the name of the test, the function is the test case itself, and the
/// final element of the tuple is the expected return value from the test case.
///
/// # Example
/// ```qsharp
/// CheckAllTestCases([("Should return 42", () -> 42, 42)]);
/// ```
function CheckAllTestCases<'T : Eq + Show>(test_cases : (String, () -> 'T, 'T)[]) : Bool {
let test_results = RunAllTestCases(test_cases);

OutputMessage(test_results);
All(test_case -> test_case.did_pass, test_results)
}

/// # Summary
/// Runs all given test cases and returns a `TestCaseResult` for each test, representing whether or not it passed
/// and what the failure message, if any.
/// This is a good alternative to `CheckAllTestCases` when you want custom output based on the results of your tests,
/// or more control over how test results are rendered.
/// # Input
/// Takes a list of test cases. A test case is a tuple of `(String, () -> T, 'T)`, where
/// the first String is the name of the test, the function is the test case itself, and the
/// final element of the tuple is the expected return value from the test case.
///
/// # Example
/// ```qsharp
/// RunAllTestCases([("Should return 42", () -> 42, 42)]);
/// ```
function RunAllTestCases<'T : Eq + Show>(test_cases : (String, () -> 'T, 'T)[]) : TestCaseResult[] {
let num_tests = Length(test_cases);

Mapped((name, case, result) -> TestCase(name, case, result), test_cases)
}

/// Internal (non-exported) helper function. Runs a test case and produces a `TestCaseResult`
function TestCase<'T : Eq + Show>(name : String, test_case : () -> 'T, expected : 'T) : TestCaseResult {
let result = test_case();
if result == expected {
new TestCaseResult { did_pass = true, message = "" }
} else {
new TestCaseResult { did_pass = false, message = $"{name}: expected: {expected}, got: {result}" }
}
}

export CheckAllTestCases, RunAllTestCases;
4 changes: 4 additions & 0 deletions library/qtest/src/Main.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export Util.TestCaseResult;
80 changes: 80 additions & 0 deletions library/qtest/src/Operations.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import Util.TestCaseResult, Util.OutputMessage;
import Std.Arrays.Mapped, Std.Arrays.All;

/// # Summary
/// Runs a number of test cases and returns true if all tests passed, false otherwise.
/// Prints a report of what passed and what failed as output.
///
/// For a more flexible test running function, see `RunAllTestCases` which returns
/// test results instead of printing out to output.
///
/// # Input
/// Takes a list of test cases. A test case is a tuple of `(String, () -> T, 'T)`, where
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: (String, () => T, 'T)

/// the first String is the name of the test, the function is the test case itself, and the
/// final element of the tuple is the expected return value from the test case.
///
/// # Example
sezna marked this conversation as resolved.
Show resolved Hide resolved
/// ```qsharp
/// CheckAllTestCases([("Should return 42", () -> 42, 42)]);
/// ```
operation CheckAllTestCases<'T : Eq + Show>(test_cases : (String, Int, (Qubit[]) => (), (Qubit[]) => 'T, 'T)[]) : Bool {
let test_results = RunAllTestCases(test_cases);

OutputMessage(test_results);

All(test_case -> test_case.did_pass, test_results)

}

/// # Summary
/// Runs all given test cases and returns a `TestCaseResult` for each test, representing whether or not it passed
/// and what the failure message, if any.
/// This is a good alternative to `CheckAllTestCases` when you want custom output based on the results of your tests,
/// or more control over how test results are rendered.
/// # Input
/// Takes a list of test cases. A test case is a tuple of `(String, () -> T, 'T)`, where
Copy link
Contributor

@Manvi-Agrawal Manvi-Agrawal Nov 15, 2024

Choose a reason for hiding this comment

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

Nit: Similarly here. (String, () => T, 'T) and the corresponding example update

/// the first String is the name of the test, the function is the test case itself, and the
/// final element of the tuple is the expected return value from the test case.
///
/// # Example
/// ```qsharp
/// RunAllTestCases([("Should return 42", () -> 42, 42)]);
/// ```
operation RunAllTestCases<'T : Eq + Show>(test_cases : (String, Int, (Qubit[]) => (), (Qubit[]) => 'T, 'T)[]) : TestCaseResult[] {
let num_tests = Length(test_cases);

let num_tests = Length(test_cases);

MappedOperation((name, num_qubits, prepare_state, case, result) => {
use qubits = Qubit[num_qubits];
prepare_state(qubits);
let res = TestCase(name, qubits, case, result);
ResetAll(qubits);
res
}, test_cases)
}

/// Helper function, copy of `Std.Arrays.Mapped` which works on operations instead
/// of functions.
operation MappedOperation<'T, 'U>(mapper : ('T => 'U), array : 'T[]) : 'U[] {
mutable mapped = [];
for element in array {
set mapped += [mapper(element)];
}
mapped
}

/// Internal (non-exported) helper function. Runs a test case and produces a `TestCaseResult`
operation TestCase<'T : Eq + Show>(name : String, qubits : Qubit[], test_case : (Qubit[]) => 'T, expected : 'T) : TestCaseResult {
let result = test_case(qubits);
if result == expected {
new TestCaseResult { did_pass = true, message = "" }
} else {
new TestCaseResult { did_pass = false, message = $"{name}: expected: {expected}, got: {result}" }
}
}

export CheckAllTestCases, RunAllTestCases;
41 changes: 41 additions & 0 deletions library/qtest/src/Tests.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import Std.Diagnostics.Fact;

function Main() : Unit {
let sample_tests = [
("Should return 42", TestCaseOne, 43),
("Should add one", () -> AddOne(5), 42),
("Should add one", () -> AddOne(5), 6)
];

Fact(
not Functions.CheckAllTestCases(sample_tests),
"Test harness failed to return false for a failing tests."
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: for a failing test/for the failing test(s).

);

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);

Fact(
Length(run_all_result) == 3,
"Test harness did not return results for all test cases."
);
sezna marked this conversation as resolved.
Show resolved Hide resolved

Fact(run_all_result[0].did_pass, "test one passed when it should have failed");
Fact(run_all_result[1].did_pass, "test two failed when it should have passed");
Fact(run_all_result[2].did_pass, "test three passed when it should have failed");
}

function TestCaseOne() : Int {
42
}

function AddOne(x : Int) : Int {
x + 1
}
26 changes: 26 additions & 0 deletions library/qtest/src/Util.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import Std.Arrays.Filtered;
import Std.Diagnostics.Fact;

struct TestCaseResult {
did_pass : Bool,
message : String,
}


function OutputMessage(test_results : TestCaseResult[]) : Unit {
let num_tests = Length(test_results);
let failed_tests = Filtered((item -> not item.did_pass), test_results);
let num_passed = Std.Arrays.Count((item -> item.did_pass), test_results);
let num_failed = Length(failed_tests);

Fact((num_passed + num_failed) == num_tests, "invariant failed in test harness: passed plus failed should equal total");

let test_word = if num_tests == 1 or num_tests == 0 { "test" } else { "tests" };
Message($"{num_passed} of {num_tests} {test_word} passed. ({num_failed} failed)");
for failed_test in failed_tests {
Message($"{failed_test.message}");
}
}
8 changes: 8 additions & 0 deletions library/signed/qsharp.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
"ref": "3195043",
"path": "library/unstable"
}
},
"Qtest": {
"github": {
"owner": "Microsoft",
"repo": "qsharp",
"ref": "486616a4",
"path": "library/qtest"
}
}
},
"files": [
Expand Down
51 changes: 14 additions & 37 deletions library/signed/src/Tests.qs
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,24 @@ import Measurement.MeasureSignedInteger;
/// This entrypoint runs tests for the signed integer library.
operation Main() : Unit {
UnsignedOpTests();
MeasureSignedIntTests();
Fact(Qtest.Operations.CheckAllTestCases(MeasureSignedIntTests()), "SignedInt tests failed");
SignedOpTests();
sezna marked this conversation as resolved.
Show resolved Hide resolved

}

operation MeasureSignedIntTests() : Unit {
use a = Qubit[4];

// 0b0001 == 1
X(a[0]);
let res = MeasureSignedInteger(a, 4);
Fact(res == 1, $"Expected 1, received {res}");

// 0b1111 == -1
X(a[0]);
X(a[1]);
X(a[2]);
X(a[3]);
let res = MeasureSignedInteger(a, 4);
Fact(res == -1, $"Expected -1, received {res}");

// 0b01000 == 8
use a = Qubit[5];
X(a[3]);
let res = MeasureSignedInteger(a, 5);
Fact(res == 8, $"Expected 8, received {res}");

// 0b11110 == -2
X(a[1]);
X(a[2]);
X(a[3]);
X(a[4]);
let res = MeasureSignedInteger(a, 5);
Fact(res == -2, $"Expected -2, received {res}");

// 0b11000 == -8
X(a[3]);
X(a[4]);
let res = MeasureSignedInteger(a, 5);
Fact(res == -8, $"Expected -8, received {res}");

function MeasureSignedIntTests() : (String, Int, (Qubit[]) => (), (Qubit[]) => Int, Int)[] {
sezna marked this conversation as resolved.
Show resolved Hide resolved
[
("0b0001 == 1", 4, (qs) => X(qs[0]), (qs) => MeasureSignedInteger(qs, 4), 1),
Copy link
Contributor

@Manvi-Agrawal Manvi-Agrawal Nov 15, 2024

Choose a reason for hiding this comment

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

@sezna , it seems these tests should not pass since the signature of QTest.Operations.TestCases was changed. Do the current library tests get checked in CI or am I missing something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey Manvi, I'm not seeing an issue here. The function call here does indeed use the latest api.

Copy link
Contributor

Choose a reason for hiding this comment

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

@sezna, I realized it now, that this still accepts a tuple containing 5 values. I think I got confused by 2 factors:

  • Incorrect doc string : this seems to be copy paste of the functions namespace. Could you please fix it?
  • Unnecessary braces around (Qubit[]) in method signature: Its written as (Qubit[])=> () here and in Operations.qs. This can be written as Qubit[] => ().

("0b1111 == -1", 4, (qs) => { X(qs[0]); X(qs[1]); X(qs[2]); X(qs[3]); }, (qs) => MeasureSignedInteger(qs, 4), -1),
("0b01000 == 8", 5, (qs) => X(qs[3]), (qs) => MeasureSignedInteger(qs, 5), 8),
("0b11110 == -2", 5, (qs) => {
X(qs[1]);
X(qs[2]);
X(qs[3]);
X(qs[4]);
}, (qs) => MeasureSignedInteger(qs, 5), -2),
("0b11000 == -8", 5, (qs) => { X(qs[3]); X(qs[4]); }, (qs) => MeasureSignedInteger(qs, 5), -8)
]
}

operation SignedOpTests() : Unit {
Expand Down
Loading