Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Sep 20, 2025

Problem

The filter method on arbitraries could loop indefinitely when the filter predicate was too restrictive, while fc.pre had a proper rejection mechanism that would fail fast and be handled by the property testing framework.

Consider this example that would hang indefinitely before this fix:

// This would loop forever - no integer from 1-10 is greater than 20
const impossibleFilter = fc.integer({ min: 1, max: 10 }).filter(x => x > 20);

const property = fc.property(impossibleFilter, (x) => {
  return true; // never reached due to infinite generation loop
});

fc.check(property, { numRuns: 1 }); // hangs forever

In contrast, fc.pre handles impossible conditions gracefully:

const property = fc.property(fc.integer({ min: 1, max: 10 }), (x) => {
  fc.pre(x > 20); // impossible condition
  return true;
});

fc.check(property, { numRuns: 1, maxSkipsPerRun: 10 }); // fails fast with "too many skips"

Solution

Modified FilterArbitrary.generate() to count failed filter attempts and throw PreconditionFailure after 100 attempts (aligning with the framework's default maxSkipsPerRun of 100). This connects filter to the same rejection mechanism that fc.pre uses.

The key changes in packages/fast-check/src/check/arbitrary/definition/Arbitrary.ts:

generate(mrng: Random, biasFactor: number | undefined): Value<U> {
  // Limit the number of attempts to avoid infinite loops
  // This connects filter to the same rejection mechanism as fc.pre
  const maxAttempts = 100;
  let attempts = 0;
  
  while (attempts < maxAttempts) {
    const g = this.arb.generate(mrng, biasFactor);
    if (this.refinementOnValue(g)) {
      return g;
    }
    attempts++;
  }
  
  // After too many failed attempts, throw PreconditionFailure to be handled 
  // by the same rejection mechanism that handles fc.pre
  throw new PreconditionFailure();
}

Benefits

  • Prevents infinite loops: Filter with impossible conditions now fails fast instead of hanging
  • Consistent behavior: Filter now behaves like fc.pre when conditions are impossible to satisfy
  • Framework integration: Uses the existing rejection mechanism (PreconditionFailure) that the framework already handles properly
  • Minimal change: Only 13 lines of code changed, preserving all existing functionality for reasonable filters

Testing

Added comprehensive tests demonstrating that:

  1. Impossible filters now complete quickly and fail gracefully (no more infinite loops)
  2. Reasonable filters continue to work efficiently as before
  3. The behavior is now consistent between filter and fc.pre for rejection scenarios

All existing tests continue to pass, ensuring no regressions were introduced.


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

@changeset-bot
Copy link

changeset-bot bot commented Sep 20, 2025

⚠️ No Changeset found

Latest commit: 0c41ee4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

…ditionFailure after max attempts

Co-authored-by: dubzzz <[email protected]>
Copilot AI changed the title [WIP] Connect filter to rejection mechanisms like fc.pre does Connect filter to rejection mechanisms like fc.pre does Sep 20, 2025
Copilot AI requested a review from dubzzz September 20, 2025 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants