-
-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
183 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/** | ||
* Basic state machine | ||
* Example of how fast-check can be derived for model based testing | ||
*/ | ||
export class MusicPlayer { | ||
private tracks: string[]; | ||
private isPlaying: boolean; | ||
private playingId: number; | ||
|
||
constructor(tracks: string[]) { | ||
this.tracks = [...tracks]; | ||
this.isPlaying = false; | ||
this.playingId = 0; | ||
} | ||
|
||
playing(): boolean { | ||
return this.isPlaying; | ||
} | ||
|
||
currentTrackName(): string | null { | ||
return this.playingId != null && this.playingId < this.tracks.length ? this.tracks[this.playingId] : null; | ||
} | ||
|
||
play(): void { | ||
this.isPlaying = true; | ||
} | ||
pause(): void { | ||
this.isPlaying = false; | ||
} | ||
|
||
addTrack(trackName: string, position: number): void { | ||
this.tracks = [...this.tracks.slice(0, position), trackName, ...this.tracks.slice(position)]; | ||
if (this.playingId >= position) { | ||
// comment this block to check bug detection | ||
++this.playingId; | ||
} | ||
} | ||
|
||
next(): void { | ||
if (++this.playingId === this.tracks.length) { | ||
this.playingId = 0; | ||
} | ||
} | ||
jump(position: number): void { | ||
this.playingId = position; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export interface Command<Model, Real> { | ||
// Check if the model is in the right state to apply the command | ||
// WARNING: does not change the model | ||
checkPreconditions(m: Model): void; | ||
|
||
// Apply the command on the model | ||
apply(m: Model): void; | ||
|
||
// Receive the non-updated model and the real or system under test | ||
// Performs the checks post-execution - Throw in case of invalid state | ||
run(m: Model, r: Real): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Command } from './Command'; | ||
|
||
type Setup<Model, Real> = () => { model: Model; real: Real }; | ||
export const CommandExecutor = <Model, Real>(s: Setup<Model, Real>, cmds: Command<Model, Real>[]): void => { | ||
const { model, real } = s(); | ||
for (const c of cmds) { | ||
if (c.checkPreconditions(model)) { | ||
c.run(model, real); | ||
c.apply(model); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import * as assert from 'assert'; | ||
import * as fc from 'fast-check'; | ||
import { Command } from './helpers/Command'; | ||
import { CommandExecutor } from './helpers/CommandExecutor'; | ||
|
||
import { MusicPlayer } from './MusicPlayer'; | ||
|
||
class MusicPlayerModel { | ||
isPlaying: boolean = false; | ||
numTracks: number = 0; | ||
tracksAlreadySeen: { [Key: string]: boolean } = {}; // our model forbid to append twice the same track | ||
} | ||
type MusicPlayerCommand = Command<MusicPlayerModel, MusicPlayer>; | ||
|
||
class PlayCommand implements MusicPlayerCommand { | ||
checkPreconditions(m: MusicPlayerModel) { | ||
return true; | ||
} | ||
apply(m: MusicPlayerModel) { | ||
m.isPlaying = true; | ||
} | ||
run(m: MusicPlayerModel, p: MusicPlayer) { | ||
p.play(); | ||
assert.ok(p.playing()); | ||
} | ||
toString() { | ||
return 'Play'; | ||
} | ||
} | ||
class PauseCommand implements MusicPlayerCommand { | ||
checkPreconditions(m: MusicPlayerModel) { | ||
return true; | ||
} | ||
apply(m: MusicPlayerModel) { | ||
m.isPlaying = false; | ||
} | ||
run(m: MusicPlayerModel, p: MusicPlayer) { | ||
p.pause(); | ||
assert.ok(!p.playing()); | ||
} | ||
toString() { | ||
return 'Pause'; | ||
} | ||
} | ||
class NextCommand implements MusicPlayerCommand { | ||
checkPreconditions(m: MusicPlayerModel) { | ||
return true; | ||
} | ||
apply(m: MusicPlayerModel) { | ||
/**/ | ||
} | ||
run(m: MusicPlayerModel, p: MusicPlayer) { | ||
const trackBefore = p.currentTrackName(); | ||
p.next(); | ||
assert.equal(p.playing(), m.isPlaying); | ||
if (m.numTracks === 1) { | ||
assert.equal(p.currentTrackName(), trackBefore); | ||
} else { | ||
assert.notEqual(p.currentTrackName(), trackBefore); | ||
} | ||
} | ||
toString() { | ||
return 'Next'; | ||
} | ||
} | ||
class AddTrackCommand implements MusicPlayerCommand { | ||
constructor(readonly position: number, readonly trackName: string) {} | ||
checkPreconditions(m: MusicPlayerModel) { | ||
return !m.tracksAlreadySeen[this.trackName]; | ||
} | ||
apply(m: MusicPlayerModel) { | ||
++m.numTracks; | ||
m.tracksAlreadySeen[this.trackName] = true; | ||
} | ||
run(m: MusicPlayerModel, p: MusicPlayer) { | ||
const trackBefore = p.currentTrackName(); | ||
p.addTrack(this.trackName, this.position % (m.numTracks + 1)); // old model | ||
assert.equal(p.playing(), m.isPlaying); | ||
assert.equal(p.currentTrackName(), trackBefore); | ||
} | ||
toString() { | ||
return `AddTrack(${this.position}, ${this.trackName})`; | ||
} | ||
} | ||
|
||
describe('MusicPlayer', () => { | ||
const TrackNameArb = fc.hexaString(1, 10); | ||
const CommandsArb: fc.Arbitrary<MusicPlayerCommand[]> = fc.array( | ||
fc.oneof( | ||
fc.constant(new PlayCommand()), | ||
fc.constant(new PauseCommand()), | ||
fc.constant(new NextCommand()), | ||
fc.record({ position: fc.nat(), trackName: TrackNameArb }).map(d => new AddTrackCommand(d.position, d.trackName)) | ||
) | ||
); | ||
it('should run fast-check on model based approach', () => | ||
fc.assert( | ||
fc.property(fc.set(TrackNameArb, 1, 10), CommandsArb, (initialTracks, commands) => { | ||
const real = new MusicPlayer(initialTracks); | ||
const model = new MusicPlayerModel(); | ||
model.numTracks = initialTracks.length; | ||
for (const t of initialTracks) { | ||
model.tracksAlreadySeen[t] = true; | ||
} | ||
CommandExecutor(() => ({ model, real }), commands); | ||
}) | ||
)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters