question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How do you shrink recursive records?

See original GitHub issue

💬 Question and Help

I am trying to port my property based test from jsverify to fast-check, which was easy to do, except for the shrinking of my record values. In jsverify I have to supply my own shrinking functions, which although a pain to write, are straightforward to write and understand. You just create smaller values in a lazy sequence. Unfortunately it seems the shrinking part of fast-check is undocumented, I had to look at the source code of some arbitraries. However they seem a bit daunting to translate to my use case.

I have simplified my use case to the following code:

import * as fc from "fast-check"
type Record = { contents: Content[] }
type Content = number | Record
const valueArb = fc.nat(999)
const recordArb = fc.memo(n => fc.record({ contents: n > 1 ? contentsArb() : fc.array(valueArb) }))
const contentsArb: fc.Memo<Content[]> = fc.memo(n => fc.array(fc.oneof(valueArb, recordArb(n))))
const includesBadValue = (content: Content) =>
  typeof content === "number" ? content === 666 : content.contents.some(includesBadValue)
fc.assert(fc.property(recordArb(5), record => !includesBadValue(record)))

And this produces errors like:

Error: Property failed after 1 tests
{ seed: -80021587, path: "0:3:4:6:5:7:9:8:19:18:16:14:12", endOnFailure: true }
Counterexample: [{"contents":[{"contents":[{"contents":[{"contents":[{"contents":[666]}]}]}]}]}]
Shrunk 12 time(s)
Got error: Property failed by returning false

The counter example could however be simplified to {"contents":[666]}, which it doesn’t do. Must I write my own fc.Arbitrary<Record> to make this happen? I did try this, but am unsure how to supply a list of potential shrinks:

class RecordArbitrary extends fc.Arbitrary<Record> {
  constructor(readonly n: number) {
    super()
  }

  generate(mrng: fc.Random): fc.Shrinkable<Record> {
    const recordShrinkable = recordArb(this.n).generate(mrng)
    return new fc.Shrinkable(recordShrinkable.value_, () =>
      recordShrinkable.shrink().join(
        // This obviously does not work, but conceptually is what I want:
        // record.contents.filter(content => typeof content === "object") as Record[]
      )
    )
  }
}

Is there any documentation I might have missed that can help me out, or some easier and more fitting examples than say ArrayArbitrary? Or am I going at it all wrong?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
msteencommented, Jan 5, 2021

Huray! The last idea was indeed the problem with my previous boolean().chain(...) approach, after chaining the contents, making sure the generated value is the same as in the nesting (it is what produced the failure after all), it is no longer left to chance, but will now consistently shrink in the same way, meaning [666]. This is the code that finally worked for me:

import * as fc from "fast-check"
type Record = { contents: Content[] }
type Content = number | Record
const valueArb = fc.nat(999)
const contentsArb: fc.Memo<Content[]> = fc.memo(n =>
  fc
    .array(
      fc.oneof(
        valueArb,
        (n > 1 ? contentsArb(n - 1) : fc.array(valueArb)).chain(contents =>
          fc.boolean().map(b => (b ? { contents } : contents)),
        ),
      ),
    )
    .map(arr => arr.flat()),
)
const includesBadValue = (content: Content) =>
  typeof content === "number" ? content === 666 : content.contents.some(includesBadValue)
fc.assert(fc.property(contentsArb(5), contents => !contents.some(includesBadValue)))

I will close the issue, as your suggestion ended up giving me a working solution, but I would love to know if there is a better way or if there is some more documentation on ArbitraryWithShrink on how to actually use it in practice, rather than the theory behind it (that is well enough documented in AdvancedArbitraries.md and HowItWorks.md). Or more to the point, how to add additional shrinkables to an existing arbitrary.

0reactions
msteencommented, Jan 5, 2021

I tried to implement ArbitraryWithShrink<Record>, but the shrink method is never called, so I am not quite sure how to use it. Thus I tried to implement Arbitrary<Record> and got it to shrink the recursion, except that other further shrinking is no longer done, which makes sense, as those shrink streams are lost without access to the Shrinkable that produced the values. Here is what I tried:

import * as fc from "fast-check"
type Record = { contents: Content[] }
type Content = number | Record
class RecordArbitrary extends fc.Arbitrary<Record> {
  constructor(readonly n: number) {
    super()
  }
  private shrinkableFor(value: Record): fc.Shrinkable<Record> {
    function* g(v: Record): IterableIterator<Record> {
      for (const content of v.contents) if (typeof content === "object") yield content
    }
    return new fc.Shrinkable(value, () => fc.stream(g(value)).map(v => this.shrinkableFor(v)))
  }
  generate(mrng: fc.Random): fc.Shrinkable<Record, Record> {
    return this.shrinkableFor(recordArb(this.n).generate(mrng).value_)
  }
}
const valueArb = fc.nat(999)
const recordArb = fc.memo(n => fc.record({ contents: n > 1 ? contentsArb() : fc.array(valueArb) }))
const contentsArb: fc.Memo<Content[]> = fc.memo(n => fc.array(fc.oneof(valueArb, new RecordArbitrary(n))))
const includesBadValue = (content: Content) =>
  typeof content === "number" ? content === 666 : content.contents.some(includesBadValue)
fc.assert(fc.property(new RecordArbitrary(5), record => !includesBadValue(record)))

My guess would be that if I wanted it to work with a custom Arbitrary, I will have to do something troublesome like keeping track of all contents arbitraries and creating and reimplementing most of array shrinking myself, like it seems to be done for CommandsArbitrary.ts. I hope that is not the case, as that is at the level that should remain implementation details of the tester, not something you have to write for adding some custom shrinkage.

In a last attempt I also tried the boolean().chain(...) trick, after thinking of a way to get it to flatten the recursion on false, but it does not do so consistently. While it does result in consistenly smaller values, it does not actually get as minimal as it could (that being [666]). This is how I went around doing so:

import * as fc from "fast-check"
type Record = { contents: Content[] }
type Content = number | Record
const valueArb = fc.nat(999)
const contentsArb: fc.Memo<Content[]> = fc.memo(n => {
  const contents = n > 1 ? contentsArb(n - 1) : fc.array(valueArb)
  return fc
    .array(
      fc.oneof(
        valueArb,
        fc.boolean().chain(b => (b ? fc.record({ contents }).map(r => [r]) : contents)),
      ),
    )
    .map(arr => arr.flat())
})
const includesBadValue = (content: Content) =>
  typeof content === "number" ? content === 666 : content.contents.some(includesBadValue)
fc.assert(fc.property(contentsArb(5), contents => !contents.some(includesBadValue)))

One last idea I have is to chain contents, as I think it does not quite yet do as I intended (the same exact contents should be either become a record, i.e. a nesting, or become part of the contents, i.e. remain flat).

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to restrict recursive CTE row count - sql - Stack Overflow
I have a function use to generate a record id, I want to use CTE to get batch of record id. Now the...
Read more >
Ready, SET, go -How does SQL Server handle Recursive CTE's
Split the CTE into anchor and recursive parts. Run the anchor member creating the first base result set (T0). Run the recursive member...
Read more >
SQL Recursion with CTE Part 1 | Quick Tips Ep59 - YouTube
In this video I show you how to execute a recursive CTE (Common Table Expression) against Employee table. I demonstrate recursion and walk ......
Read more >
Recursive queries in PostgreSQL - an introduction - CYBERTEC
Evaluate the recursive branch of the CTE, replacing the reference to the CTE with the working table. Add all resulting rows to the...
Read more >
What is recursive DNS? - Cloudflare
A recursive DNS lookup is where one DNS server communicates with several other DNS servers to hunt down an IP address and return...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found