Skip to content

Commit

Permalink
Improve tests for Subsequence Indices
Browse files Browse the repository at this point in the history
Original tests:
- When `arr` and `sub` are arbitrary,
check that if `subsequenceIndices(arr, sub)` returns non-null,
then the returned indices are correct.
- When `sub` is guaranteed to be a subsequence of `arr`,
check that `subsequenceIndices(arr, sub)` never returns null.

New tests:
- When `sub` is guaranteed to be a subsequence of `arr`,
check `subsequenceIndices(arr, sub)` returns non-null, correct indices.
- When `arr` and `sub` are arbitrary,
check that if `subsequenceIndices(arr, sub)` returns non-null,
then the returned indices are correct.

Adding the indices correctness check to both tests allows us to
use the guaranteed-subsequence test as the primary test
for the success case (non-null return), with the arbitrary-inputs test
primarily accounting for the failure case now.
This allows us to vary the inputs to the arbitrary-inputs test more,
since we are no longer depending on it for coverage of the success case.

Also, the inputs to the guaranteed-subsequence test are now
decoupled from the inputs to the arbitrary-inputs test,
and are even more varied.
  • Loading branch information
shape-warrior-t committed Jul 10, 2024
1 parent ed733bd commit fcb35d6
Showing 1 changed file with 43 additions and 33 deletions.
76 changes: 43 additions & 33 deletions typescript-challenges/code/subsequence-indices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
* Tests for Subsequence Indices, ensuring that the output of
* `subsequenceIndices(arr, sub)` is always correct.
*
* Properties tested:
* a. If `subsequenceIndices(arr, sub)` is a list of indices `[i_0, i_1, ...]`,
* then the list is strictly ascending
* and `[arr[i_0], arr[i_1], ...]` represents the same list as `sub`.
* b. If `subsequenceIndices(arr, sub)` is null, then `sub` is not a subsequence of `arr` --
* equivalently, if `sub` is a subsequence of `arr`,
* then `subsequenceIndices(arr, sub)` is not null.
* If `subsequenceIndices(arr, sub)` returns a list of indices `[i_0, i_1, ...]`,
* we can always verify correctness by checking that the list is strictly ascending
* and that `[arr[i_0], arr[i_1], ...]` represents the same list as `sub`.
* Based on that, we have the following tests:
* - When `sub` is guaranteed to be a subsequence of `arr`,
* `subsequenceIndices(arr, sub)` returns the correct list of indices and never returns null.
* - When `arr` and `sub` are arbitrary,
* `subsequenceIndices(arr, sub)` either returns the correct list of indices or returns null.
*
* NOTE: generating indices `[i_0, i_1, ...]` and testing that
* `subsequenceIndices(arr, [arr[i_0], arr[i_1], ...])` is the same as `[i_0, i_1, ...]`
Expand All @@ -23,44 +24,53 @@ import fc from 'fast-check';
import { pairwise } from 'itertools';
import subsequenceIndices from './subsequence-indices';

/**
* Arbitrary for list inputs to `subsequenceIndices`.
*
* Elements are taken from a set of size 2 in order to maximize the chances that,
* given randomly generated `sub` and `arr`,
* `sub` is a subsequence of `arr` and also has a reasonably high length.
*/
function list(maxLength: number): fc.Arbitrary<string[]> {
return fc.array(fc.constantFrom('a', 'b'), { maxLength });
function expectIndicesAreCorrect<T>(
indices: number[],
arr: T[],
sub: T[],
): void {
expectStrictlyAscending(indices);
expect(indices.map((i) => arr[i])).toStrictEqual(sub);
}

test('a. list returns are valid', () => {
fc.assert(
fc.property(list(10), list(5), (arr, sub) => {
const indices = subsequenceIndices(arr, sub);
if (indices === null) return;
expectStrictlyAscending(indices);
const subFromIndices = indices.map((i) => arr[i]);
expect(subFromIndices).toStrictEqual(sub);
}),
);
});

function expectStrictlyAscending(arr: number[]): void {
for (const [a, b] of pairwise(arr)) {
expect(a).toBeLessThan(b);
}
}

/** Arbitrary for a list together with a random subsequence of that list. */
const listAndSubsequence: fc.Arbitrary<[string[], string[]]> = list(10).chain(
(arr) => fc.subarray(arr).map((sub) => [arr, sub]),
);
const listAndSubsequence: fc.Arbitrary<[number[], number[]]> = fc
.array(fc.integer({ min: 0, max: 9 }))
.chain((arr) => fc.subarray(arr).map((sub) => [arr, sub]));

test('b. returns non-null for subsequences', () => {
test('`sub` subsequence of `arr`', () => {
fc.assert(
fc.property(listAndSubsequence, ([arr, sub]) => {
expect(subsequenceIndices(arr, sub)).not.toBeNull();
const indices = subsequenceIndices(arr, sub);
expect(indices).not.toBeNull();
expectIndicesAreCorrect(indices!, arr, sub);
}),
);
});

/**
* Arbitrary for list inputs to `subsequenceIndices`.
*
* Elements are taken from a set of size 3 in order to raise the chances of
* `sub` being a subsequence or near-subsequence of `arr`.
*/
function list(maxLength: number): fc.Arbitrary<string[]> {
return fc.array(fc.constantFrom('a', 'b', 'c'), { maxLength });
}

test('`arr` and `sub` arbitrary', () => {
fc.assert(
fc.property(list(10), list(5), (arr, sub) => {
const indices = subsequenceIndices(arr, sub);
if (indices !== null) {
expectIndicesAreCorrect(indices, arr, sub);
}
}),
);
});

0 comments on commit fcb35d6

Please sign in to comment.