Skip to content
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

✨ Add fc.scheduler arbitrary #479

Merged
merged 47 commits into from Nov 27, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2984956
Very early draft for async scheduler
dubzzz Nov 1, 2019
3575fbd
add random scheduling
dubzzz Nov 1, 2019
7c185ad
Implement missing features
dubzzz Nov 12, 2019
4a78996
Add trivial e2e test
dubzzz Nov 13, 2019
0bdc23c
Merge branch 'master' into feat/async-scheduler
dubzzz Nov 13, 2019
6b6ad76
Add task id into the logs
dubzzz Nov 13, 2019
ea9587a
Add another race condition detection
dubzzz Nov 13, 2019
b568242
Merge branch 'master' into feat/async-scheduler
dubzzz Nov 13, 2019
fb07441
Add example based on React
dubzzz Nov 13, 2019
faa197d
Move deps fro codesandbox
dubzzz Nov 13, 2019
92eb8f9
Extract functions for the test example
dubzzz Nov 13, 2019
5289a88
Add a second test
dubzzz Nov 13, 2019
f7006ac
Add a more complex autocomplete implementation
dubzzz Nov 13, 2019
4cd38fb
Move to jsx
dubzzz Nov 13, 2019
136317a
Possible results have to be uniques
dubzzz Nov 13, 2019
7d63e58
Typo
dubzzz Nov 13, 2019
5a35cfb
Add disclaimer
dubzzz Nov 13, 2019
15180e6
Adapt example for CodeSandbox
dubzzz Nov 14, 2019
1e787f9
Add another AutocompleteField implementation
dubzzz Nov 14, 2019
4faad4d
Add global configuration for fast-check timeout and handle the CodeSa…
dubzzz Nov 14, 2019
373cd60
Add scheduling for sequences
dubzzz Nov 14, 2019
2fbf905
Update toString
dubzzz Nov 15, 2019
2ece1b0
Update toString
dubzzz Nov 15, 2019
4d02456
Update Autocomplete units
dubzzz Nov 18, 2019
d98f795
Add example for dependencyTree
dubzzz Nov 19, 2019
2cf992c
Unit tests for scheduleSequence
dubzzz Nov 25, 2019
a32eaf8
Add unit-tests for schedule
dubzzz Nov 25, 2019
9788de0
Add unit-tests for scheduleFunction
dubzzz Nov 25, 2019
14ae848
Add unit-tests to assess toString
dubzzz Nov 25, 2019
e0238f4
Unit-test cloneable feature of scheduler
dubzzz Nov 25, 2019
9774f18
Add test highlighting a bug in scheduleSequence
dubzzz Nov 25, 2019
894f977
Always wait the end of items taken from the sequence
dubzzz Nov 25, 2019
0e307ad
Replace `Promise.resolve` by real `delay` promises
dubzzz Nov 25, 2019
3147ea7
Clean some UnhandledPromiseRejectionWarning
dubzzz Nov 26, 2019
01aeb25
Remove comments about UnhandledPromiseRejectionWarning
dubzzz Nov 26, 2019
936a166
Fix e2e tests
dubzzz Nov 26, 2019
c3a8a76
Remove unneeded `export` and add `@hidden`
dubzzz Nov 26, 2019
99e4eb8
Add test showing sequence can also extract the name from the name of …
dubzzz Nov 26, 2019
17807f2
Add units to check waitOne and waitAll throw according to the spec
dubzzz Nov 26, 2019
1ea48a2
Fix linter issues
dubzzz Nov 26, 2019
245b676
Update README (1)
dubzzz Nov 26, 2019
db48526
Simpler example in Tips
dubzzz Nov 26, 2019
82e8240
Add section in Arbitraries.md doc
dubzzz Nov 26, 2019
65ac4ae
Add missing signature for SchedulerSequenceItem
dubzzz Nov 26, 2019
be73ed9
Bump ts-jest causing bug locally on Windows
dubzzz Nov 26, 2019
78d2f98
Add no regression test for scheduler
dubzzz Nov 26, 2019
5121fa6
Fix NoRegression test as it contained paths
dubzzz Nov 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/check/arbitrary/AsyncSchedulerArbitrary.ts
@@ -0,0 +1,151 @@
import { cloneMethod } from '../symbols';
import { Random } from '../../random/generator/Random';
import { Arbitrary } from './definition/Arbitrary';
import { Shrinkable } from './definition/Shrinkable';
import { stringify } from '../../utils/stringify';

// asyncScheduler
// taskScheduler
// asyncOrchestrator

// produce a scheduler
// with schedule and schedulable method
// plus waitAll and waitOne methods

export interface Scheduler {
/** Wrap a new task using the Scheduler */
schedule: <T>(task: Promise<T>) => Promise<T>;

/** Automatically wrap function output using the Scheduler */
scheduleFunction: <TArgs extends any[], T>(
asyncFunction: (...args: TArgs) => Promise<T>
) => (...args: TArgs) => Promise<T>;

/**
* Count of pending scheduled tasks
*/
count(): number;

/**
* Wait one scheduled task to be executed
* @throws Whenever there is no task scheduled
*/
waitOne: () => Promise<void>;

/**
* Wait all scheduled tasks,
* including the ones that might be greated by one of the resolved task
*/
waitAll: () => Promise<void>;
}

type ScheduledTask = {
original: PromiseLike<unknown>;
scheduled: PromiseLike<unknown>;
trigger: () => void;
label: string;
};

export class SchedulerImplem implements Scheduler {
private lastTaskId: number;
private readonly sourceMrng: Random;
private readonly scheduledTasks: ScheduledTask[];
private readonly triggeredTasksLogs: string[];

constructor(private readonly mrng: Random) {
dubzzz marked this conversation as resolved.
Show resolved Hide resolved
this.lastTaskId = 0;
// here we should received an already cloned mrng so that we can do whatever we want on it
this.sourceMrng = mrng.clone();
this.scheduledTasks = [];
this.triggeredTasksLogs = [];
}

private buildLog(taskId: number, meta: string, type: 'resolve' | 'reject' | 'pending', data: any[]) {
return `[task#${taskId}][${meta}][${type}] ${stringify(data)}`;
}

private log(taskId: number, meta: string, type: 'resolve' | 'reject' | 'pending', data: any[]) {
this.triggeredTasksLogs.push(this.buildLog(taskId, meta, type, data));
}

private scheduleInternal<T>(meta: string, task: PromiseLike<T>) {
let trigger: (() => void) | null = null;
const taskId = ++this.lastTaskId;
const scheduledPromise = new Promise<T>((resolve, reject) => {
trigger = () => {
task.then(
(...args) => {
this.log(taskId, meta, 'resolve', args);
return resolve(...args);
},
(...args) => {
this.log(taskId, meta, 'reject', args);
return reject(...args);
}
);
};
});
this.scheduledTasks.push({
original: task,
scheduled: scheduledPromise,
trigger: trigger!,
label: this.buildLog(taskId, meta, 'pending', [])
});
return scheduledPromise;
}

schedule<T>(task: Promise<T>) {
return this.scheduleInternal('promise', task);
}

scheduleFunction<TArgs extends any[], T>(
asyncFunction: (...args: TArgs) => Promise<T>
): (...args: TArgs) => Promise<T> {
return (...args: TArgs) =>
this.scheduleInternal(
`function::${asyncFunction.name}(${args.map(stringify).join(',')})`,
asyncFunction(...args)
);
}

count() {
return this.scheduledTasks.length;
}

async waitOne() {
if (this.scheduledTasks.length === 0) {
throw new Error('No task scheduled');
}
const taskIndex = this.mrng.nextInt(0, this.scheduledTasks.length - 1);
const [scheduledTask] = this.scheduledTasks.splice(taskIndex, 1);
scheduledTask.trigger(); // release the promise
await scheduledTask.scheduled; // wait for its completion
}

async waitAll() {
while (this.scheduledTasks.length > 0) {
await this.waitOne();
}
}

toString() {
return this.triggeredTasksLogs
.concat(this.scheduledTasks.map(t => t.label))
.map(log => `-> ${log}`)
.join('\n');
}

[cloneMethod]() {
return new SchedulerImplem(this.sourceMrng);
}
}

class SchedulerArbitrary extends Arbitrary<Scheduler> {
generate(mrng: Random) {
return new Shrinkable(new SchedulerImplem(mrng.clone()));
}
}

export function scheduler(): Arbitrary<Scheduler> {
return new SchedulerArbitrary();
}
4 changes: 4 additions & 0 deletions src/fast-check-default.ts
Expand Up @@ -87,6 +87,7 @@ import { ExecutionTree } from './check/runner/reporter/ExecutionTree';
import { cloneMethod } from './check/symbols';
import { Stream, stream } from './stream/Stream';
import { stringify } from './utils/stringify';
import { scheduler, Scheduler } from './check/arbitrary/AsyncSchedulerArbitrary';

// boolean
// floating point types
Expand Down Expand Up @@ -189,6 +190,9 @@ export {
asyncModelRun,
modelRun,
commands,
// scheduler
scheduler,
Scheduler,
// extend the framework
Arbitrary,
Shrinkable,
Expand Down
123 changes: 123 additions & 0 deletions test/e2e/AsyncScheduler.spec.ts
@@ -0,0 +1,123 @@
import * as fc from '../../src/fast-check';

const seed = Date.now();
describe(`AsyncScheduler (seed: ${seed})`, () => {
it('should detect trivial race conditions', async () => {
// The code below relies on the fact/expectation that fetchA takes less time that fetchB
// The aim of this test is to show that the code is wrong as soon as this assumption breaks
type CompoProps = { fetchHeroName: () => Promise<string>; fetchHeroes: () => Promise<{ name: string }[]> };
type CompoState = { heroName: string | undefined; heroes: { name: string }[] | undefined };
class Compo {
private props: CompoProps;
private state: CompoState;
constructor(props: CompoProps) {
this.props = props;
this.state = { heroName: undefined, heroes: undefined };
}
componentDidMount() {
this.props.fetchHeroName().then(heroName => (this.state = { ...this.state, heroName }));
this.props.fetchHeroes().then(heroes => (this.state = { ...this.state, heroes }));
}
render() {
const { heroName, heroes } = this.state;
if (!heroes) return null;
return `got: ${heroes.find(h => h.name === heroName!.toLowerCase())}`;
}
}
const out = await fc.check(
fc.asyncProperty(fc.scheduler(), async s => {
const fetchHeroName = s.scheduleFunction(function fetchHeroName() {
return Promise.resolve('James Bond');
});
const fetchHeroes = s.scheduleFunction(function fetchHeroesById() {
return Promise.resolve([{ name: 'James Bond' }]);
});
const c = new Compo({ fetchHeroName, fetchHeroes });
c.componentDidMount();
c.render();
while (s.count() !== 0) {
await s.waitOne();
c.render();
}
}),
{ seed }
);
expect(out.failed).toBe(true);
expect(out.counterexample![0].toString()).toMatchInlineSnapshot(`
"-> [task#2][function::fetchHeroesById()][resolve] [[{\\"name\\":\\"James Bond\\"}]]
-> [task#1][function::fetchHeroName()][pending] []"
`);
expect(out.error).toContain(`Cannot read property 'toLowerCase' of undefined`);
});

it('should detect race conditions leading to infinite loops', async () => {
// Following case is an example of code trying to scan all the dependencies of a given package
// In order to build a nice graph
type PackageDefinition = {
dependencies: { [packageName: string]: string };
};
type AllPackagesDefinition = { [packageName: string]: PackageDefinition };
const allPackages: AllPackagesDefinition = {
toto: {
dependencies: {
titi: '^1.0.0',
tata: '^2.0.0',
tutu: '^3.0.0'
}
},
titi: {
dependencies: {
noop: '^1.0.0',
tutu: '^3.0.0'
}
},
tata: {
dependencies: {
noop: '^1.0.0'
}
},
noop: {
dependencies: {}
},
tutu: {
dependencies: {
titi: '^1.0.0'
}
}
};
const buildGraph = async (
initialPackageName: string,
fetch: (packageName: string) => Promise<PackageDefinition>
) => {
const cache: AllPackagesDefinition = {};
// // Uncomment to remove the bug
//const cachePending = new Set<string>();
const feedCache = async (packageName: string) => {
// // Uncomment to remove the bug
// if (cachePending.has(packageName)) return;
// cachePending.add(packageName);
if (cache[packageName]) return;

const packageDef = await fetch(packageName); // cache miss
await Promise.all(Object.keys(packageDef.dependencies).map(dependencyName => feedCache(dependencyName)));
};
await feedCache(initialPackageName);
return cache; // we just return the cache instead of the garph for simplicity
};
const out = await fc.check(
fc.asyncProperty(fc.constantFrom(...Object.keys(allPackages)), fc.scheduler(), async (initialPackageName, s) => {
const originalFetch = (packageName: string) => Promise.resolve(allPackages[packageName]);
const fetch = s.scheduleFunction(originalFetch);
const handle = buildGraph(initialPackageName, fetch);
// Or: await s.waitAll();
while (s.count() !== 0) {
expect(s.count()).toBeLessThanOrEqual(Object.keys(allPackages).length);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might add a maxThreadPoolSize parameters with a quitehigh initial value or force the users to define one

await s.waitOne();
}
await handle; // nothing should block now
}),
{ seed }
);
expect(out.failed).toBe(true);
});
});