-
Notifications
You must be signed in to change notification settings - Fork 205
Circuit-Editor and Visualization Re-Architecting and Tests #3425
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
base: main
Are you sure you want to change the base?
Changes from 10 commits
c2cd30d
28ff077
384c38d
8568700
f0da218
a086285
9d26c36
099257a
e346b0e
5af8dea
2bcd93d
5739711
22d93c4
5d7db8a
f41ddcf
2cc4e21
f8cae3d
f535adb
935bc2e
d4c2059
57c6560
d8dd8b6
10e8bea
ca557f9
91a3f56
012db0a
37017d2
36e0331
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| // angleExpression tests — direct coverage for the two helpers in | ||
| // `angleExpression.ts` that drive the Edit Argument input prompt | ||
| // in [contextMenu.ts](../../ux/circuit-vis/editor/contextMenu.ts): | ||
| // | ||
| // - `isValidAngleExpression(expr)` is the predicate the prompt | ||
| // consults on every keystroke to enable/disable OK. Anything it | ||
| // accepts ends up persisted into the operation's `args`. | ||
| // - `normalizeAngleExpression(expr)` runs BEFORE validation: it | ||
| // trims surrounding whitespace and folds case-insensitive `pi` | ||
| // to `π`. The persisted value is the normalized form, so OK's | ||
| // enabled state must agree with what the user sees after Save. | ||
|
|
||
| // @ts-check | ||
|
|
||
| import { test } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { | ||
| isValidAngleExpression, | ||
| normalizeAngleExpression, | ||
| } from "../../dist/ux/circuit-vis/angleExpression.js"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // isValidAngleExpression — positive cases | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test("isValidAngleExpression: plain numbers (positive, negative, decimal) are valid", () => { | ||
| // The simplest path — the prompt should accept any literal | ||
| // numeric value the user types, including the "5." trailing-dot | ||
| // form (mirroring JavaScript's `parseFloat` tolerance). | ||
| assert.equal(isValidAngleExpression("0"), true); | ||
| assert.equal(isValidAngleExpression("5"), true); | ||
| assert.equal(isValidAngleExpression("-5"), true); | ||
| assert.equal(isValidAngleExpression("+5"), true); | ||
| assert.equal(isValidAngleExpression("3.5"), true); | ||
| assert.equal(isValidAngleExpression("-3.5"), true); | ||
| assert.equal(isValidAngleExpression("5."), true); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: bare π is valid in all four case forms", () => { | ||
| // The prompt's placeholder example is `"π / 2.0"`; the user | ||
| // can type π directly via the on-screen π button, or use any | ||
| // case variant of `pi` which `normalizeAngleExpression` folds | ||
| // to π before this check runs. | ||
| assert.equal(isValidAngleExpression("π"), true); | ||
| assert.equal(isValidAngleExpression("pi"), true); | ||
| assert.equal(isValidAngleExpression("Pi"), true); | ||
| assert.equal(isValidAngleExpression("PI"), true); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: signed π is valid", () => { | ||
| // The `-π` and `+π` forms are the unary-sign-on-pi-factor path | ||
| // through the parser; pin both. | ||
| assert.equal(isValidAngleExpression("-π"), true); | ||
| assert.equal(isValidAngleExpression("+π"), true); | ||
| assert.equal(isValidAngleExpression("-pi"), true); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: arithmetic combinations are valid", () => { | ||
| // The four supported binary operators, with both numbers and π. | ||
| // These mirror the prompt's example text ("π / 2.0", "2.0 * π"). | ||
| assert.equal(isValidAngleExpression("2 + 3"), true); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would you need addition?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seemed silly to support multiplication and division (which is useful in most expression with pi) without also supporting addition/subtraction and parens. Being able to support basic arithmetic will be useful when we add support for referencing things other than numbers here, but that is an idea for the future. |
||
| assert.equal(isValidAngleExpression("2 - 3"), true); | ||
| assert.equal(isValidAngleExpression("2 * 3"), true); | ||
| assert.equal(isValidAngleExpression("6 / 2"), true); | ||
| assert.equal(isValidAngleExpression("π / 2"), true); | ||
| assert.equal(isValidAngleExpression("2 * π"), true); | ||
| assert.equal(isValidAngleExpression("2.0 * π"), true); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: parentheses (including nesting) are valid", () => { | ||
| // Recursive descent through `parseFactor`'s `lpar` branch. | ||
| assert.equal(isValidAngleExpression("(π)"), true); | ||
| assert.equal(isValidAngleExpression("(2 + 3) * π"), true); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would find it reassuring to know that these expressions had been evaluated correctly.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As in, you want a test that putting in "(3 + 4) * 7" yields 49?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The evaluation of the arithmetic is not performed by the editor, as that is a Q# runtime task. |
||
| assert.equal(isValidAngleExpression("((π))"), true); | ||
| assert.equal(isValidAngleExpression("π / (2 * 3)"), true); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: leading/trailing whitespace is tolerated", () => { | ||
| // `normalizeAngleExpression` (called internally by the | ||
| // evaluator) trims; the prompt also normalizes the value | ||
| // BEFORE this predicate runs, so being lenient here matches | ||
| // what the user sees after the auto-trim. | ||
| assert.equal(isValidAngleExpression(" π "), true); | ||
| assert.equal(isValidAngleExpression("\tπ / 2\n"), true); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // isValidAngleExpression — negative cases | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test("isValidAngleExpression: empty / whitespace-only input is invalid", () => { | ||
| // The prompt's default OK-disabled state for an empty input. | ||
| // `evaluateAngleExpression` short-circuits on a falsy `expr` | ||
| // or a falsy post-normalize string. | ||
| assert.equal(isValidAngleExpression(""), false); | ||
| assert.equal(isValidAngleExpression(" "), false); | ||
| assert.equal(isValidAngleExpression("\t\n"), false); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: unknown characters are invalid", () => { | ||
| // Any character outside `[0-9.+\-*/()\sπ]` (after the `pi` → | ||
| // `π` fold) takes the tokenizer's "unknown character" fallthrough | ||
| // and returns undefined. Pin a few common typos. | ||
| assert.equal(isValidAngleExpression("π^2"), false); | ||
| assert.equal(isValidAngleExpression("sin(π)"), false); | ||
| assert.equal(isValidAngleExpression("π & 2"), false); | ||
| assert.equal(isValidAngleExpression("π,2"), false); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: malformed numbers are invalid", () => { | ||
| // The number tokenizer requires `\d+(\.\d*)?` — no leading dot, | ||
| // no multiple decimals. Both are rejected. | ||
| assert.equal( | ||
| isValidAngleExpression(".5"), | ||
| false, | ||
| "leading dot is not a valid number", | ||
| ); | ||
| assert.equal( | ||
| isValidAngleExpression("1.2.3"), | ||
| false, | ||
| "multiple decimal points are not a valid number", | ||
| ); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: unbalanced parentheses are invalid", () => { | ||
| // `parseFactor`'s `lpar` branch requires a matching `rpar` | ||
| // and returns undefined if it doesn't see one. The "extra | ||
| // rpar" case fails the trailing `k !== toks.length` guard. | ||
| assert.equal(isValidAngleExpression("(π"), false); | ||
| assert.equal(isValidAngleExpression("π)"), false); | ||
| assert.equal(isValidAngleExpression("((π)"), false); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: trailing / dangling operators are invalid", () => { | ||
| // `parseExpr` / `parseTerm` call `parseFactor` after consuming | ||
| // an operator; if the RHS factor is missing, the parse returns | ||
| // undefined. | ||
| assert.equal(isValidAngleExpression("π +"), false); | ||
| assert.equal(isValidAngleExpression("π *"), false); | ||
| assert.equal(isValidAngleExpression("2 +"), false); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: lone operators / empty parens are invalid", () => { | ||
| // No factor at all (operator only, empty parens) → parseFactor | ||
| // returns undefined on the first call. | ||
| assert.equal(isValidAngleExpression("+"), false); | ||
| assert.equal(isValidAngleExpression("-"), false); | ||
| assert.equal(isValidAngleExpression("*"), false); | ||
| assert.equal(isValidAngleExpression("()"), false); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: infinite results (division by zero) are invalid", () => { | ||
| // The evaluator's final `!isFinite(result)` guard rejects | ||
| // `1/0` and friends — the prompt should not allow the user | ||
| // to commit an angle that would evaluate to ±Infinity. | ||
| assert.equal(isValidAngleExpression("1 / 0"), false); | ||
| assert.equal(isValidAngleExpression("π / 0"), false); | ||
| assert.equal(isValidAngleExpression("-1 / 0"), false); | ||
| }); | ||
|
|
||
| test("isValidAngleExpression: adjacent factors without an operator are invalid", () => { | ||
| // Implicit multiplication isn't supported; "2π" is rejected | ||
| // even though it's a common math-notation shorthand. The | ||
| // parser leaves the `pi` token unconsumed after `parseFactor` | ||
| // returns `2`, then the trailing `k !== toks.length` guard | ||
| // trips. (If implicit-multiply support is added later, this | ||
| // test should be updated to expect `true`.) | ||
| assert.equal(isValidAngleExpression("2π"), false); | ||
| assert.equal(isValidAngleExpression("π2"), false); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // normalizeAngleExpression | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| test("normalizeAngleExpression: trims surrounding whitespace", () => { | ||
| // The prompt persists the normalized value, so leading/trailing | ||
| // whitespace must not survive into the operation's `args`. | ||
| assert.equal(normalizeAngleExpression(" π "), "π"); | ||
| assert.equal(normalizeAngleExpression("\tπ / 2\n"), "π / 2"); | ||
| assert.equal(normalizeAngleExpression(""), ""); | ||
| }); | ||
|
|
||
| test("normalizeAngleExpression: folds case-insensitive 'pi' → 'π'", () => { | ||
| // The π button on the prompt inserts the literal character, but | ||
| // users can also type any case variant of `pi`. Both must | ||
| // normalize to the same persisted form so `args` doesn't depend | ||
| // on which input path was used. | ||
| assert.equal(normalizeAngleExpression("pi"), "π"); | ||
| assert.equal(normalizeAngleExpression("Pi"), "π"); | ||
| assert.equal(normalizeAngleExpression("PI"), "π"); | ||
| assert.equal(normalizeAngleExpression("pI"), "π"); | ||
| }); | ||
|
|
||
| test("normalizeAngleExpression: folds 'pi' embedded inside an expression", () => { | ||
| // The fold is unanchored — every occurrence within an | ||
| // expression is replaced. Pin the common case (a `pi` factor | ||
| // inside an arithmetic expression). | ||
| assert.equal(normalizeAngleExpression("pi / 2"), "π / 2"); | ||
| assert.equal(normalizeAngleExpression("2 * Pi + PI"), "2 * π + π"); | ||
| assert.equal(normalizeAngleExpression("(pi)"), "(π)"); | ||
| }); | ||
|
|
||
| test("normalizeAngleExpression: leaves already-normalized π untouched", () => { | ||
| // Idempotency — passing the output back through the normalizer | ||
| // must be a no-op. The prompt re-runs the normalize step on | ||
| // every input event, so this property is what keeps the OK | ||
| // button's enabled state stable. | ||
| assert.equal(normalizeAngleExpression("π / 2"), "π / 2"); | ||
| assert.equal(normalizeAngleExpression("2 * π"), "2 * π"); | ||
| assert.equal( | ||
| normalizeAngleExpression(normalizeAngleExpression("PI / 2")), | ||
| "π / 2", | ||
| ); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to assert that these are not only valid but equivalent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what about Π? 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The angleExpression isn't evaluated here. Really, no part of the circuit is evaluated like that in the editor or visualizer. That's the job of the generated Q# code, which is very much separate from the circuit or editor code. That's why these tests very specifically focus only on the validation of the syntax. The actual expression is carried forward to the Q# and evaluated by the Q# runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capital Π is not supported because we don't expect users to be able to type that in. Lower-case is only supported because that's what other representations get converted into.