Add pragma for feature testing: @gate - #18581
Conversation
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 3b58bb6:
|
The `@gate` pragma declares under which conditions a test is expected to pass. If the gate condition passes, then the test runs normally (same as if there were no pragma). If the conditional fails, then the test runs and is *expected to fail*. An alternative to `it.experimental` and similar proposals. Examples -------- Basic: ```js // @gate enableBlocksAPI test('passes only if Blocks API is available', () => {/*...*/}) ``` Negation: ```js // @gate !disableLegacyContext test('depends on a deprecated feature', () => {/*...*/}) ``` Multiple flags: ```js // @gate enableNewReconciler // @gate experimental test('needs both useEvent and Blocks', () => {/*...*/}) ``` Logical operators (yes, I'm sorry): ```js // @gate experimental && (enableNewReconciler || disableSchedulerTimeoutBasedOnReactExpirationTime) test('concurrent mode, doesn\'t work in old fork unless Scheduler timeout flag is disabled', () => {/*...*/}) ``` Strings, and comparion operators No use case yet but I figure eventually we'd use this to gate on different release channels: ```js // @gate channel === "experimental" || channel === "modern" test('works in OSS experimental or www modern', () => {/*...*/}) ``` How does it work? I'm guessing those last two examples might be controversial. Supporting those cases did require implementing a mini-parser. The output of the transform is very straightforward, though. Input: ```js // @gate a && (b || c) test('some test', () => {/*...*/}) ``` Output: ```js _test_gate(ctx => ctx.a && (ctx.b || ctx.c, 'some test'), () => {/*...*/}); ``` It also works with `it`, `it.only`, and `fit`. It leaves `it.skip` and `xit` alone because those tests are disabled anyway. `_test_gate` is a global method that I set up in our Jest config. It works about the same as the existing `it.experimental` helper. The context (`ctx`) argument is whatever we want it to be. I set it up so that it throws if you try to access a flag that doesn't exist. I also added some shortcuts for common gating conditions, like `old` and `new`: ```js // @gate experimental test('experimental feature', () => {/*...*/}) // @gate new test('only passes in new reconciler', () => {/*...*/}) ``` Why implement this as a pragma instead of a runtime API? - Doesn't require monkey patching built-in Jest methods. Instead it compiles to a runtime function that composes Jest's API. - Will be easy to upgrade if Jest ever overhauls their API or we switch to a different testing framework (unlikely but who knows). - It feels lightweight so hopefully people won't feel gross using it. For example, adding or removing a gate pragma will never affect the indentation of the test, unlike if you wrapped the test in a conditional block.
|
I should maybe just swap out the tiny parser I wrote for actual JavaScript. I started out with a more restrictive syntax with ANDs and ORs but then eventually said fuck it and copied JS. |
There was a problem hiding this comment.
Ahaha forgot to give this a name
|
Sorry to barge in! I am curious if this was inspired by a very similar feature in another language. I know Sebastian mentioned taking inspiration from Rust in few occasions recently so I am wondering if you or the whole team are in the same boat |
|
@Mathspy I'm glad you find it interesting!
Hmm I'm sure I was inspired indirectly via exposure but I didn't have a specific feature in mind. It sounds like you're implying Rust has a similar feature. I am decently familiar with Rust but I actually haven't used it much. If you're aware of prior art, please let me know!
Neat! I was thinking that if we use this for a while and like it, we could try to upstream this to Jest. |
We patch console.error and console.warning to track unexpected calls in our tests. If there's an unexpected call, we usually throw inside an `afterEach` hook. However, that's too late for tests that we expect to fail, because our `_test_gate` runtime can't capture the error. So I also check for unexpected calls inside `_test_gate`.
e959688 to
f2055bd
Compare
|
Oh I see! #[cfg(target_os = "macos")]
fn macos_only() {
// ...
}This marks the module with a configuration predicate telling Rust to compile it only for the target of MacOS, otherwise it's completely removed. Another example: #[cfg(test)]
mod tests {
// ...
}This will be compiled only when running in test mode (similar A place where I can see this really shining in JS is actually: // marks module/function/statement to be conditionally compiled only in debug mode (AKA dev mode; not production)
#[cfg(debug_assertions)] I think that using a if (__DEV__) {
// dev mode assertions
}I am definitely going to try making |
c487127 to
4f4421c
Compare
|
Ah yes I was aware of that feature! I thought you meant more the "negative testing" aspect, where if the test doesn't pass the gating condition, you run it anyway and assert that it fails. That surely has prior art somewhere but I can't think of an example. |
|
Aha, I definitely share the feeling of "knowing" that specific aspect from somewhere too but just being unable to put my finger on it. If I happen to remember I will definitely leave a comment here! |
bvaughn
left a comment
There was a problem hiding this comment.
Didn't read through tokenize very closely. Read the meta tests though, and generally trust you with this sort of change.
| throw Error(errorMsg); | ||
| }; | ||
|
|
||
| // TODO: Deprecate these helpers in favor of @gate pragma |
There was a problem hiding this comment.
Why? What value does a custom pragma offer for iterating on a local focused test?
There was a problem hiding this comment.
Oh I meant it.experimental, not the focus helpers. Definitely not removing those!
There was a problem hiding this comment.
Ooh, I misunderstood that comment!
That makes sense then.
| console.error('Stop that!'); | ||
| throw Error('I told you to stop!'); | ||
| }); | ||
| }); |
Added some instructions for how the flags are set up and how to use them.
Receives same flags as the pragma. If we ever decide to revert the pragma, we can codemod them to use this instead.
|
I'm a little hesitant about the DSL aspect but I'm not too worried because I think in 99% percent of cases, you'll only use a a single flag or a combination of flags, no operators. So my exit strategy is:
|
* Add pragma for feature testing: @gate The `@gate` pragma declares under which conditions a test is expected to pass. If the gate condition passes, then the test runs normally (same as if there were no pragma). If the conditional fails, then the test runs and is *expected to fail*. An alternative to `it.experimental` and similar proposals. Examples -------- Basic: ```js // @gate enableBlocksAPI test('passes only if Blocks API is available', () => {/*...*/}) ``` Negation: ```js // @gate !disableLegacyContext test('depends on a deprecated feature', () => {/*...*/}) ``` Multiple flags: ```js // @gate enableNewReconciler // @gate experimental test('needs both useEvent and Blocks', () => {/*...*/}) ``` Logical operators (yes, I'm sorry): ```js // @gate experimental && (enableNewReconciler || disableSchedulerTimeoutBasedOnReactExpirationTime) test('concurrent mode, doesn\'t work in old fork unless Scheduler timeout flag is disabled', () => {/*...*/}) ``` Strings, and comparion operators No use case yet but I figure eventually we'd use this to gate on different release channels: ```js // @gate channel === "experimental" || channel === "modern" test('works in OSS experimental or www modern', () => {/*...*/}) ``` How does it work? I'm guessing those last two examples might be controversial. Supporting those cases did require implementing a mini-parser. The output of the transform is very straightforward, though. Input: ```js // @gate a && (b || c) test('some test', () => {/*...*/}) ``` Output: ```js _test_gate(ctx => ctx.a && (ctx.b || ctx.c, 'some test'), () => {/*...*/}); ``` It also works with `it`, `it.only`, and `fit`. It leaves `it.skip` and `xit` alone because those tests are disabled anyway. `_test_gate` is a global method that I set up in our Jest config. It works about the same as the existing `it.experimental` helper. The context (`ctx`) argument is whatever we want it to be. I set it up so that it throws if you try to access a flag that doesn't exist. I also added some shortcuts for common gating conditions, like `old` and `new`: ```js // @gate experimental test('experimental feature', () => {/*...*/}) // @gate new test('only passes in new reconciler', () => {/*...*/}) ``` Why implement this as a pragma instead of a runtime API? - Doesn't require monkey patching built-in Jest methods. Instead it compiles to a runtime function that composes Jest's API. - Will be easy to upgrade if Jest ever overhauls their API or we switch to a different testing framework (unlikely but who knows). - It feels lightweight so hopefully people won't feel gross using it. For example, adding or removing a gate pragma will never affect the indentation of the test, unlike if you wrapped the test in a conditional block. * Compatibility with console error/warning tracking We patch console.error and console.warning to track unexpected calls in our tests. If there's an unexpected call, we usually throw inside an `afterEach` hook. However, that's too late for tests that we expect to fail, because our `_test_gate` runtime can't capture the error. So I also check for unexpected calls inside `_test_gate`. * Move test flags to dedicated file Added some instructions for how the flags are set up and how to use them. * Add dynamic version of gate API Receives same flags as the pragma. If we ever decide to revert the pragma, we can codemod them to use this instead.
Alternative to #18574
The
@gatepragma declares under which conditions a test is expected to pass.If the gate condition passes, then the test runs normally (same as if there were no pragma).
If the conditional fails, then the test is expected to fail. This ensures that all tests run in all environments and reduces the likelihood of a test being completely skipped.
An alternative to
it.experimentaland similar proposals.Examples
Basic:
Negation:
Multiple flags:
Logical operators (yes, I'm sorry):
Strings, and comparison operators
No use case yet but I figure eventually we'd use this to gate on different release channels:
How does it work?
The output of the transform is pretty straightforward:
Input:
Output:
It also works with
it,it.only, andfit. It leavesit.skipandxitalone because those tests are disabled anyway._test_gateis a global method that I set up in our Jest config. It works about the same as the existingit.experimentalhelper.The context (
ctx) argument is whatever we want it to be. I set it up so that it throws if you try to access a flag that doesn't exist. I also added some shortcuts for common gating conditions, likeexperimentalandstable; andoldandnew:Why implement this as a pragma instead of a dynamic API?