diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..40d6d3386f --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/internal/testing/parser/Parser.js diff --git a/package-lock.json b/package-lock.json index b48cc4ae29..d9039e39e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@reactivex/rxjs", - "version": "6.0.0-tactical-rc.1", + "version": "6.0.0-turbo-rc.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -142,6 +142,18 @@ "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=", "dev": true }, + "JSONSelect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/JSONSelect/-/JSONSelect-0.4.0.tgz", + "integrity": "sha1-oI7cxn6z/L6Z7WMIVTRKDPKCu40=", + "dev": true + }, + "JSV": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", + "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=", + "dev": true + }, "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", @@ -1861,6 +1873,15 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, + "cjson": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.0.tgz", + "integrity": "sha1-5kObkHA9MS/24iJAl76pLOPQKhQ=", + "dev": true, + "requires": { + "jsonlint": "1.6.0" + } + }, "cli-cursor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", @@ -2833,6 +2854,12 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "ebnf-parser": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/ebnf-parser/-/ebnf-parser-0.1.10.tgz", + "integrity": "sha1-zR9rpHfFY4xAyX7ZtXLbW6tdgzE=", + "dev": true + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -6297,6 +6324,74 @@ } } }, + "jison": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/jison/-/jison-0.4.18.tgz", + "integrity": "sha512-FKkCiJvozgC7VTHhMJ00a0/IApSxhlGsFIshLW6trWJ8ONX2TQJBBz6DlcO1Gffy4w9LT+uL+PA+CVnUSJMF7w==", + "dev": true, + "requires": { + "JSONSelect": "0.4.0", + "cjson": "0.3.0", + "ebnf-parser": "0.1.10", + "escodegen": "1.3.3", + "esprima": "1.1.1", + "jison-lex": "0.3.4", + "lex-parser": "0.1.4", + "nomnom": "1.5.2" + }, + "dependencies": { + "escodegen": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz", + "integrity": "sha1-8CQBb1qI4Eb9EgBQVek5gC5sXyM=", + "dev": true, + "requires": { + "esprima": "1.1.1", + "estraverse": "1.5.1", + "esutils": "1.0.0", + "source-map": "0.1.43" + } + }, + "esprima": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz", + "integrity": "sha1-W28VR/TRAuZw4UDFCb5ncdautUk=", + "dev": true + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", + "dev": true + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "jison-lex": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/jison-lex/-/jison-lex-0.3.4.tgz", + "integrity": "sha1-gcoo2E+ESZ36jFlNzePYo/Jux6U=", + "dev": true, + "requires": { + "lex-parser": "0.1.4", + "nomnom": "1.5.2" + } + }, "jpm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jpm/-/jpm-1.0.0.tgz", @@ -6805,6 +6900,16 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, + "jsonlint": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.0.tgz", + "integrity": "sha1-iKpGvCiaesk7tGyuLVihh6m7SUo=", + "dev": true, + "requires": { + "JSV": "4.0.2", + "nomnom": "1.5.2" + } + }, "jsonpointer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", @@ -6927,6 +7032,12 @@ "type-check": "0.3.2" } }, + "lex-parser": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/lex-parser/-/lex-parser-0.1.4.tgz", + "integrity": "sha1-ZMTwJfF/1Tv7RXY/rrFvAVp0dVA=", + "dev": true + }, "lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", @@ -7778,6 +7889,30 @@ "dev": true, "optional": true }, + "nomnom": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.5.2.tgz", + "integrity": "sha1-9DRUSKhTz71cDSYyDyR3qwUm/i8=", + "dev": true, + "requires": { + "colors": "0.5.1", + "underscore": "1.1.7" + }, + "dependencies": { + "colors": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", + "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=", + "dev": true + }, + "underscore": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.1.7.tgz", + "integrity": "sha1-QLq4S60Z0jAJbo1u9ii/8FXYPbA=", + "dev": true + } + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -11060,6 +11195,62 @@ } } }, + "rollup-plugin-commonjs": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.0.tgz", + "integrity": "sha512-NrfE0g30QljNCnlJr7I2Xguz+44mh0dCxvfxwLnCwtaCK2LwFUp1zzAs8MQuOfhH4mRskqsjfOwGUap/L+WtEw==", + "dev": true, + "requires": { + "estree-walker": "0.5.1", + "magic-string": "0.22.5", + "resolve": "1.7.1", + "rollup-pluginutils": "2.0.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.1.tgz", + "integrity": "sha512-7HgCgz1axW7w5aOvgOQkoR1RMBkllygJrssU3BvymKQ95lxXYv6Pon17fBRDm9qhkvXZGijOULoSF9ShOk/ZLg==", + "dev": true + }, + "magic-string": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", + "dev": true, + "requires": { + "vlq": "0.2.3" + } + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "rollup-pluginutils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz", + "integrity": "sha1-fslbNXP2VDpGpkYb2afFRFJdD8A=", + "dev": true, + "requires": { + "estree-walker": "0.3.1", + "micromatch": "2.3.11" + }, + "dependencies": { + "estree-walker": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.3.1.tgz", + "integrity": "sha1-5rGlHPcpJSTnI3wxLl/mZgwc4ao=", + "dev": true + } + } + } + } + }, "rollup-plugin-inject": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-2.0.0.tgz", diff --git a/package.json b/package.json index 0a9773a975..b9863c655c 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "build_spec": "npm-run-all build_cjs generate_packages copy_for_tests", "build_spec_full": "npm-run-all compat_build_cjs compile_legacy_reexport compat_generate_packages build_spec", "build_spec_browser": "webpack --config spec/support/webpack.mocha.config.js", + "build_marble_parser": "jison ./src/internal/testing/parser/grammar.jison --outfile ./src/internal/testing/parser/Parser.js --module-type commonjs", "clean_dist": "shx rm -rf ./dist", "clean_dist_cjs": "shx rm -rf ./dist/cjs", "clean_dist_esm5": "shx rm -rf ./dist/esm5", @@ -97,11 +98,11 @@ "clean_dist_esm2015": "shx rm -rf ./dist/esm2015", "clean_dist_global": "shx rm -rf ./dist/global", "commit": "git-cz", - "compile_dist_cjs": "tsc -p ./tsconfig/tsconfig.cjs.json", - "compile_dist_esm5": "tsc -p ./tsconfig/tsconfig.esm5.json", - "compile_dist_esm2015": "tsc -p ./tsconfig/tsconfig.esm2015.json", + "compile_dist_cjs": "tsc -p ./tsconfig/tsconfig.cjs.json && shx cp -r ./dist/src/internal/testing/parser ./dist/cjs/internal/testing/parser", + "compile_dist_esm5": "tsc -p ./tsconfig/tsconfig.esm5.json && shx cp -r ./dist/src/internal/testing/parser ./dist/esm5/internal/testing/parser", + "compile_dist_esm2015": "tsc -p ./tsconfig/tsconfig.esm2015.json && shx cp -r ./dist/src/internal/testing/parser ./dist/esm2015/internal/testing/parser", "compile_dist_esm2015_for_docs": "tsc ./dist/src/internal/Rx.ts ./dist/src/add/observable/of.ts ./dist/src/MiscJSDoc.ts -m es2015 --sourceMap --outDir ./dist/es6 --target es2015 -d --diagnostics --pretty --noImplicitAny --noImplicitReturns --noImplicitThis --suppressImplicitAnyIndexErrors --moduleResolution node", - "compile_dist_esm5_for_rollup": "tsc -p ./tsconfig/tsconfig.esm5.rollup.json", + "compile_dist_esm5_for_rollup": "tsc -p ./tsconfig/tsconfig.esm5.rollup.json && shx cp -r ./dist/src/internal/testing/parser ./dist/esm5_for_rollup/internal/testing/parser", "compile_legacy_reexport": "tsc -p ./tsconfig/tsconfig.legacy-reexport.json", "copy_sources": "mkdirp dist && shx cp -r ./src/ ./dist/src", "copy_for_tests": "shx rm -rf ./spec-build && shx cp -r ./spec/ ./spec-build/ && mkdirp ./spec-build/node_modules && shx cp -r ./dist/package/ ./spec-build/node_modules/rxjs && shx cp -r ./dist-compat/package/ ./spec-build/node_modules/rxjs-compat", @@ -221,6 +222,7 @@ "gzip-size": "4.1.0", "http-server": "0.11.1", "husky": "0.14.3", + "jison": "^0.4.18", "klaw-sync": "3.0.2", "lint-staged": "3.2.5", "lodash": "4.17.5", @@ -237,6 +239,7 @@ "promise": "8.0.1", "protractor": "3.1.1", "rollup": "0.36.3", + "rollup-plugin-commonjs": "8.3.0", "rollup-plugin-inject": "2.0.0", "rollup-plugin-node-resolve": "2.0.0", "rx": "latest", diff --git a/spec/testing/index-spec.ts b/spec/testing/index-spec.ts index c3707d5f47..bd4fbdbb57 100644 --- a/spec/testing/index-spec.ts +++ b/spec/testing/index-spec.ts @@ -1,8 +1,211 @@ -import * as index from 'rxjs/testing'; import { expect } from 'chai'; +import { run, marbles } from 'rxjs/testing'; +import { + asyncScheduler, Observable, Scheduler, VirtualTimeScheduler, + VirtualAction, of, Subscription, SchedulerAction, range +} from 'rxjs'; +import { + Event, NodeKind, MarbleDiagram, Next, Error, Complete, MacroTask, + MicroTask, Queue, Subscribe, Unsubscribe, TimeProgression, DurationUnit +} from 'rxjs/internal/testing/ast'; describe('index', () => { - it('should export TestScheduler', () => { - expect(index.TestScheduler).to.exist; + describe('run()', () => { + it('should handle a single synchronous emission', () => { + const output$ = of(1); + const actual = run(output$); + const expected = marbles('a|', { + a: [1] + }); + + expect(actual).to.deep.equal(expected); + }); + + it('should handle multiple synchronous emissions', () => { + const output$ = of(1, 2, 3); + const actual = run(output$); + const expected = marbles('a|', { + a: [1, 2, 3] + }); + + expect(actual).to.deep.equal(expected); + }); + + it.only('should handle macrotasks', () => { + const output$ = range(1, 3, asyncScheduler); + const actual = run(output$); + const expected = marbles('-a-b-c-|', { + a: [1], + b: [2], + c: [3] + }); + + expect(actual).to.deep.equal(expected); + }); + }); + + it('should support identifiers as marbles', () => { + const a = marbles('a'); + expect(a).to.deep.equal([ + new Next('a') + ]); + + const abc = marbles('abc'); + expect(abc).to.deep.equal([ + new Next('abc') + ]); + + const a2 = marbles('a', { + a: [1] + }); + expect(a2).to.deep.equal([ + new Next(1) + ]); + + const a3 = marbles('a', { + a: [1, 2, 3] + }); + expect(a3).to.deep.equal([ + new Next(1), + new Next(2), + new Next(3) + ]); + }); + + it('should support # as errors', () => { + const a = marbles('#'); + expect(a).to.deep.equal([ + new Error() + ]); + + const error = new TypeError('wow'); + const b = marbles('#', {}, error); + expect(b).to.deep.equal([ + new Error(error) + ]); + }); + + it('should support | as complete', () => { + const a = marbles('|'); + expect(a).to.deep.equal([ + new Complete() + ]); + }); + + it('should support - as a macrotask', () => { + const a = marbles('-'); + expect(a).to.deep.equal([ + new MacroTask() + ]); + + const b = marbles('---'); + expect(b).to.deep.equal([ + new MacroTask(), + new MacroTask(), + new MacroTask() + ]); + }); + + it('should support ~ as a microtask', () => { + const a = marbles('~'); + expect(a).to.deep.equal([ + new MicroTask() + ]); + + const b = marbles('~~~'); + expect(b).to.deep.equal([ + new MicroTask(), + new MicroTask(), + new MicroTask() + ]); + }); + + it('should support + as a queue', () => { + const a = marbles('+'); + expect(a).to.deep.equal([ + new Queue() + ]); + + const b = marbles('+++'); + expect(b).to.deep.equal([ + new Queue(), + new Queue(), + new Queue() + ]); + }); + + it('should support ^ as a subscribe', () => { + const a = marbles('^'); + expect(a).to.deep.equal([ + new Subscribe() + ]); + }); + + it('should support ! as an unsubscribe', () => { + const a = marbles('!'); + expect(a).to.deep.equal([ + new Unsubscribe() + ]); + }); + + it('should support time durations', () => { + const a = marbles('1ms 99s 100m'); + expect(a).to.deep.equal([ + new TimeProgression(1, DurationUnit.MILLISECONDS), + new TimeProgression(99, DurationUnit.SECONDS), + new TimeProgression(100, DurationUnit.MINUTES) + ]); + }); + + it('should support various combinations', () => { + const a = marbles('a-b--c~d~~e 1ms f 99s g - 100m ~#'); + + expect(a).to.deep.equal([ + new Next('a'), + new MacroTask(), + new Next('b'), + new MacroTask(), + new MacroTask(), + new Next('c'), + new MicroTask(), + new Next('d'), + new MicroTask(), + new MicroTask(), + new Next('e'), + new TimeProgression(1, DurationUnit.MILLISECONDS), + new Next('f'), + new TimeProgression(99, DurationUnit.SECONDS), + new Next('g'), + new MacroTask(), + new TimeProgression(100, DurationUnit.MINUTES), + new MicroTask(), + new Error() + ]); + + const b = marbles('a-b--^c~d~~e 1ms f 99s g - ! 100m ~|'); + + expect(b).to.deep.equal([ + new Next('a'), + new MacroTask(), + new Next('b'), + new MacroTask(), + new MacroTask(), + new Subscribe(), + new Next('c'), + new MicroTask(), + new Next('d'), + new MicroTask(), + new MicroTask(), + new Next('e'), + new TimeProgression(1, DurationUnit.MILLISECONDS), + new Next('f'), + new TimeProgression(99, DurationUnit.SECONDS), + new Next('g'), + new MacroTask(), + new Unsubscribe(), + new TimeProgression(100, DurationUnit.MINUTES), + new MicroTask(), + new Complete() + ]); }); }); diff --git a/src/internal/Scheduler.ts b/src/internal/Scheduler.ts index 30ca9574d0..449f93a344 100644 --- a/src/internal/Scheduler.ts +++ b/src/internal/Scheduler.ts @@ -2,6 +2,17 @@ import { Action } from './scheduler/Action'; import { Subscription } from './Subscription'; import { SchedulerLike, SchedulerAction } from './types'; +export enum SchedulerKind { + ASYNC = 'ASYNC', + ASAP = 'ASAP', + QUEUE = 'QUEUE', + VIRTUAL = 'VIRTUAL', + TEST = 'TEST', + NEW = 'NEW', + ANIMATION_FRAME = 'ANIMATION_FRAME', + UNKNOWN = 'UNKNOWN' +} + /** * An execution context and a data structure to order tasks and schedule their * execution. Provides a notion of (potentially virtual) time, through the @@ -22,13 +33,16 @@ import { SchedulerLike, SchedulerAction } from './types'; * {@link SchedulerLike} */ export class Scheduler implements SchedulerLike { + public static delegate?: Scheduler; /** @nocollapse */ public static now: () => number = Date.now ? Date.now : () => +new Date(); + public kind = SchedulerKind.UNKNOWN; + private _now: () => number; - constructor(private SchedulerAction: typeof Action, - now: () => number = Scheduler.now) { - this.now = now; + constructor(protected SchedulerAction: typeof Action, + now: () => number = Scheduler.now) { + this._now = now; } /** @@ -39,7 +53,18 @@ export class Scheduler implements SchedulerLike { * have a relation to wall-clock time. May or may not refer to a time unit * (e.g. milliseconds). */ - public now: () => number; + public now(): number { + if (Scheduler.delegate) { + return Scheduler.delegate.now(); + } else { + return this._now(); + } + } + + + protected _schedule(originalScheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription { + return new this.SchedulerAction(this, work).schedule(state, delay); + } /** * Schedules a function, `work`, for execution. May happen at some point in @@ -59,6 +84,10 @@ export class Scheduler implements SchedulerLike { * the scheduled work. */ public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription { - return new this.SchedulerAction(this, work).schedule(state, delay); + if (Scheduler.delegate) { + return Scheduler.delegate._schedule(this, work, delay, state); + } else { + return this._schedule(this, work, delay, state); + } } } diff --git a/src/internal/scheduler/AnimationFrameScheduler.ts b/src/internal/scheduler/AnimationFrameScheduler.ts index c550429f17..a83ad5369e 100644 --- a/src/internal/scheduler/AnimationFrameScheduler.ts +++ b/src/internal/scheduler/AnimationFrameScheduler.ts @@ -1,7 +1,10 @@ import { AsyncAction } from './AsyncAction'; import { AsyncScheduler } from './AsyncScheduler'; +import { SchedulerKind } from '../Scheduler'; export class AnimationFrameScheduler extends AsyncScheduler { + public kind = SchedulerKind.ANIMATION_FRAME; + public flush(action?: AsyncAction): void { this.active = true; diff --git a/src/internal/scheduler/AsapScheduler.ts b/src/internal/scheduler/AsapScheduler.ts index 659aa5823c..0b74e2ff11 100644 --- a/src/internal/scheduler/AsapScheduler.ts +++ b/src/internal/scheduler/AsapScheduler.ts @@ -1,7 +1,10 @@ import { AsyncAction } from './AsyncAction'; import { AsyncScheduler } from './AsyncScheduler'; +import { SchedulerKind } from '../Scheduler'; export class AsapScheduler extends AsyncScheduler { + public kind = SchedulerKind.ASAP; + public flush(action?: AsyncAction): void { this.active = true; diff --git a/src/internal/scheduler/AsyncScheduler.ts b/src/internal/scheduler/AsyncScheduler.ts index cbbffac6bc..218c017769 100644 --- a/src/internal/scheduler/AsyncScheduler.ts +++ b/src/internal/scheduler/AsyncScheduler.ts @@ -1,7 +1,9 @@ -import { Scheduler } from '../Scheduler'; +import { Scheduler, SchedulerKind } from '../Scheduler'; import { AsyncAction } from './AsyncAction'; export class AsyncScheduler extends Scheduler { + public kind = SchedulerKind.ASYNC; + public actions: Array> = []; /** * A flag to indicate whether the Scheduler is currently executing a batch of diff --git a/src/internal/scheduler/QueueScheduler.ts b/src/internal/scheduler/QueueScheduler.ts index e9dab3de5d..005a4a0f86 100644 --- a/src/internal/scheduler/QueueScheduler.ts +++ b/src/internal/scheduler/QueueScheduler.ts @@ -1,4 +1,6 @@ import { AsyncScheduler } from './AsyncScheduler'; +import { SchedulerKind } from '../Scheduler'; export class QueueScheduler extends AsyncScheduler { + public kind = SchedulerKind.QUEUE; } diff --git a/src/internal/scheduler/VirtualTimeScheduler.ts b/src/internal/scheduler/VirtualTimeScheduler.ts index a542ec11bd..d100268aec 100644 --- a/src/internal/scheduler/VirtualTimeScheduler.ts +++ b/src/internal/scheduler/VirtualTimeScheduler.ts @@ -2,15 +2,17 @@ import { AsyncAction } from './AsyncAction'; import { Subscription } from '../Subscription'; import { AsyncScheduler } from './AsyncScheduler'; import { SchedulerAction } from '../types'; +import { SchedulerKind, Scheduler } from '../Scheduler'; export class VirtualTimeScheduler extends AsyncScheduler { + public kind = SchedulerKind.VIRTUAL; protected static frameTimeFactor: number = 10; public frame: number = 0; public index: number = -1; - constructor(SchedulerAction: typeof AsyncAction = VirtualAction as any, + constructor(SchedulerAction: typeof AsyncAction = VirtualAction, public maxFrames: number = Number.POSITIVE_INFINITY) { super(SchedulerAction, () => this.frame); } @@ -38,6 +40,11 @@ export class VirtualTimeScheduler extends AsyncScheduler { throw error; } } + + protected _schedule(originalScheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription { + const Action = this.SchedulerAction as typeof VirtualAction; + return new Action(this, work, undefined, originalScheduler).schedule(state, delay); + } } /** @@ -51,7 +58,8 @@ export class VirtualAction extends AsyncAction { constructor(protected scheduler: VirtualTimeScheduler, protected work: (this: SchedulerAction, state?: T) => void, - protected index: number = scheduler.index += 1) { + protected index: number = scheduler.index += 1, + protected originalScheduler: Scheduler = scheduler) { super(scheduler, work); this.index = scheduler.index = index; } diff --git a/src/internal/testing/NewTestScheduler.ts b/src/internal/testing/NewTestScheduler.ts new file mode 100644 index 0000000000..5a7c0d121c --- /dev/null +++ b/src/internal/testing/NewTestScheduler.ts @@ -0,0 +1,115 @@ +import { Observable } from '../Observable'; +import { Subscription } from '../Subscription'; +import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler'; +import { Scheduler, SchedulerKind } from '../Scheduler'; +import { Parser } from './parser'; + +import { + Event, NodeKind, Next, Error, Complete, TimeProgression, + DurationUnit, MacroTask, MicroTask +} from './ast'; + +export interface MarbleValues { + [key: string]: any[]; +} + +export function marbles(diagram: string, values: MarbleValues = {}, errorValue?: any): Event[] { + const parser = new Parser(); + const { events } = parser.parse(diagram); + + return events.reduce((results: any, event: any) => { + switch (event.kind) { + case NodeKind.NEXT_PLACEHOLDER: + if (event.id in values) { + const nexts = values[event.id].map(value => new Next(value)); + return results.concat(nexts); + } else { + return results.concat(new Next(event.id)); + } + + case NodeKind.ERROR_PLACEHOLDER: + return results.concat(new Error(errorValue)); + + default: + return results.concat(event); + } + }, []); +} + +export function run(stream: Observable): Event[] { + const scheduler = new NewTestScheduler(); + Scheduler.delegate = scheduler; + const events = scheduler.run(stream); + scheduler.flush(); + Scheduler.delegate = undefined; + return events; +} + +export function millisecondsToTimeProgression(ms: number) { + if (ms < 1000) { + return new TimeProgression(ms, DurationUnit.MILLISECONDS); + } else if (ms < 1000 * 60) { + return new TimeProgression(ms / 1000, DurationUnit.SECONDS); + } else { + return new TimeProgression(ms / 1000 / 60, DurationUnit.MINUTES); + } +} + +export class NewTestAction extends VirtualAction { + public schedule(state?: T, delay: number = 0): Subscription { + const { scheduler, originalScheduler } = this; + + if (!this.id) { + const events = (scheduler as NewTestScheduler).events; + + switch (originalScheduler.kind) { + case SchedulerKind.ASYNC: + if (delay === 0) { + events.push(new MacroTask()); + } else { + const event = millisecondsToTimeProgression(delay); + events.push(event); + } + break; + + case SchedulerKind.ASAP: + if (delay === 0) { + events.push(new MicroTask()); + } else { + const event = millisecondsToTimeProgression(delay); + events.push(event); + } + break; + + default: + throw new TypeError(`Unknown scheduler kind: ${originalScheduler.kind}`); + } + + return super.schedule(state, delay); + } + this.active = false; + const action = new NewTestAction(scheduler, this.work, undefined, originalScheduler); + this.add(action); + return action.schedule(state, delay); + } +} + +export class NewTestScheduler extends VirtualTimeScheduler { + public kind = SchedulerKind.NEW; + events: Event[] = []; + subscription?: Subscription; + + constructor() { + super(NewTestAction, Infinity); + } + + run(stream: Observable): Event[] { + this.subscription = stream.subscribe({ + next: value => this.events.push(new Next(value)), + error: value => this.events.push(new Error(value)), + complete: () => this.events.push(new Complete()) + }); + + return this.events; + } +} diff --git a/src/internal/testing/TestRunner.ts b/src/internal/testing/TestRunner.ts new file mode 100644 index 0000000000..000b624715 --- /dev/null +++ b/src/internal/testing/TestRunner.ts @@ -0,0 +1,5 @@ +export class TestRunner { + run(callback) { + callback(); + } +} diff --git a/src/internal/testing/TestScheduler.ts b/src/internal/testing/TestScheduler.ts index 1fba35203e..ef4495b149 100644 --- a/src/internal/testing/TestScheduler.ts +++ b/src/internal/testing/TestScheduler.ts @@ -6,6 +6,7 @@ import { TestMessage } from './TestMessage'; import { SubscriptionLog } from './SubscriptionLog'; import { Subscription } from '../Subscription'; import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler'; +import { SchedulerKind } from '../Scheduler'; const defaultMaxFrame: number = 750; @@ -19,6 +20,7 @@ export type observableToBeFn = (marbles: string, values?: any, errorValue?: any) export type subscriptionLogsToBeFn = (marbles: string | string[]) => void; export class TestScheduler extends VirtualTimeScheduler { + public kind = SchedulerKind.TEST; public readonly hotObservables: HotObservable[] = []; public readonly coldObservables: ColdObservable[] = []; private flushTests: FlushableTest[] = []; diff --git a/src/internal/testing/ast.ts b/src/internal/testing/ast.ts new file mode 100644 index 0000000000..edffe00587 --- /dev/null +++ b/src/internal/testing/ast.ts @@ -0,0 +1,96 @@ +export enum NodeKind { + MARBLE_DIAGRAM = 'MARBLE_DIAGRAM', + NEXT = 'NEXT', + NEXT_PLACEHOLDER = 'NEXT_PLACEHOLDER', + ERROR = 'ERROR', + ERROR_PLACEHOLDER = 'ERROR_PLACEHOLDER', + COMPLETE = 'COMPLETE', + SUBSCRIBE = 'SUBSCRIBE', + UNSUBSCRIBE = 'UNSUBSCRIBE', + MICRO_TASK = 'MICRO_TASK', + MACRO_TASK = 'MACRO_TASK', + QUEUE = 'QUEUE', + TIME_PROGRESSION = 'TIME_PROGRESSION', +} + +export interface Node { + kind: NodeKind; +} + +export class NextPlaceholder implements Node { + kind: NodeKind.NEXT_PLACEHOLDER = NodeKind.NEXT_PLACEHOLDER; + + constructor(public id: string) {} +} + +export class Next implements Node { + kind: NodeKind.NEXT = NodeKind.NEXT; + + constructor(public value: any) {} +} + +export class ErrorPlaceholder implements Node { + kind: NodeKind.ERROR_PLACEHOLDER = NodeKind.ERROR_PLACEHOLDER; +} + +export class Error implements Node { + kind: NodeKind.ERROR = NodeKind.ERROR; + + constructor(public value?: any) {} +} + +export class Complete implements Node { + kind: NodeKind.COMPLETE = NodeKind.COMPLETE; +} + +export class MacroTask implements Node { + kind: NodeKind.MACRO_TASK = NodeKind.MACRO_TASK; +} + +export class MicroTask implements Node { + kind: NodeKind.MICRO_TASK = NodeKind.MICRO_TASK; +} + +export class Queue implements Node { + kind: NodeKind.QUEUE = NodeKind.QUEUE; +} + +export class Subscribe implements Node { + kind: NodeKind.SUBSCRIBE = NodeKind.SUBSCRIBE; +} + +export class Unsubscribe implements Node { + kind: NodeKind.UNSUBSCRIBE = NodeKind.UNSUBSCRIBE; +} + +export enum DurationUnit { + MILLISECONDS = 'MILLISECONDS', + SECONDS = 'SECONDS', + MINUTES = 'MINUTES', +} + +export class TimeProgression implements Node { + kind: NodeKind.TIME_PROGRESSION = NodeKind.TIME_PROGRESSION; + + constructor(public duration: number,public unit: DurationUnit) {} +} + +export type Event + = NextPlaceholder + | Next + | ErrorPlaceholder + | Error + | Complete + | MacroTask + | MicroTask + | Queue + | Subscribe + | Unsubscribe + | TimeProgression + ; + +export class MarbleDiagram implements Node { + kind: NodeKind.MARBLE_DIAGRAM = NodeKind.MARBLE_DIAGRAM; + + constructor(public events: Event[]) {} +} diff --git a/src/internal/testing/parser/Parser.d.ts b/src/internal/testing/parser/Parser.d.ts new file mode 100644 index 0000000000..b08e4b6ded --- /dev/null +++ b/src/internal/testing/parser/Parser.d.ts @@ -0,0 +1,5 @@ +import { MarbleDiagram } from '../ast'; + +export class Parser { + parse(input: string): MarbleDiagram; +} diff --git a/src/internal/testing/parser/Parser.js b/src/internal/testing/parser/Parser.js new file mode 100644 index 0000000000..9c276f3ac9 --- /dev/null +++ b/src/internal/testing/parser/Parser.js @@ -0,0 +1,675 @@ +/* parser generated by jison 0.4.18 */ +/* + Returns a Parser object of the following structure: + + Parser: { + yy: {} + } + + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + + + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + + + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var Parser = (function(){ +var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,23],$V1=[1,24],$V2=[1,25],$V3=[1,14],$V4=[1,15],$V5=[1,16],$V6=[1,17],$V7=[1,18],$V8=[1,19],$V9=[1,20],$Va=[1,26],$Vb=[1,22],$Vc=[5,18,19,20,22,23,24,25,26,27,28,29,30],$Vd=[18,19,20,29]; +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"Diagram":3,"Diagram_repetition_plus0":4,"EOF":5,"Event":6,"Next":7,"Error":8,"Complete":9,"MacroTask":10,"MicroTask":11,"Queue":12,"Subscribe":13,"Unsubscribe":14,"TimeProgression":15,"TimeProgression_repetition_plus0":16,"TimeUnit":17,"ms":18,"s":19,"m":20,"Next_group0":21,"#":22,"|":23,"-":24,"~":25,"+":26,"^":27,"!":28,"NUMBER":29,"IDENTIFIER":30,"$accept":0,"$end":1}, +terminals_: {2:"error",5:"EOF",18:"ms",19:"s",20:"m",22:"#",23:"|",24:"-",25:"~",26:"+",27:"^",28:"!",29:"NUMBER",30:"IDENTIFIER"}, +productions_: [0,[3,2],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[15,2],[17,1],[17,1],[17,1],[7,1],[8,1],[9,1],[10,1],[11,1],[12,1],[13,1],[14,1],[4,1],[4,2],[16,1],[16,2],[21,1],[21,1],[21,1],[21,1]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1: + + this.$ = new ast.MarbleDiagram($$[$0-1]); + return this.$; + +break; +case 11: +this.$ = new ast.TimeProgression(parseFloat($$[$0-1]), $$[$0]); +break; +case 12: +this.$ = ast.DurationUnit.MILLISECONDS; +break; +case 13: +this.$ = ast.DurationUnit.SECONDS; +break; +case 14: +this.$ = ast.DurationUnit.MINUTES; +break; +case 15: +this.$ = new ast.NextPlaceholder($$[$0]); +break; +case 16: +this.$ = new ast.ErrorPlaceholder(); +break; +case 17: +this.$ = new ast.Complete(); +break; +case 18: +this.$ = new ast.MacroTask(); +break; +case 19: +this.$ = new ast.MicroTask(); +break; +case 20: +this.$ = new ast.Queue(); +break; +case 21: +this.$ = new ast.Subscribe(); +break; +case 22: +this.$ = new ast.Unsubscribe(); +break; +case 23: case 25: +this.$ = [$$[$0]]; +break; +case 24: case 26: +$$[$0-1].push($$[$0]); +break; +} +}, +table: [{3:1,4:2,6:3,7:4,8:5,9:6,10:7,11:8,12:9,13:10,14:11,15:12,16:21,18:$V0,19:$V1,20:$V2,21:13,22:$V3,23:$V4,24:$V5,25:$V6,26:$V7,27:$V8,28:$V9,29:$Va,30:$Vb},{1:[3]},{5:[1,27],6:28,7:4,8:5,9:6,10:7,11:8,12:9,13:10,14:11,15:12,16:21,18:$V0,19:$V1,20:$V2,21:13,22:$V3,23:$V4,24:$V5,25:$V6,26:$V7,27:$V8,28:$V9,29:$Va,30:$Vb},o($Vc,[2,23]),o($Vc,[2,2]),o($Vc,[2,3]),o($Vc,[2,4]),o($Vc,[2,5]),o($Vc,[2,6]),o($Vc,[2,7]),o($Vc,[2,8]),o($Vc,[2,9]),o($Vc,[2,10]),o($Vc,[2,15]),o($Vc,[2,16]),o($Vc,[2,17]),o($Vc,[2,18]),o($Vc,[2,19]),o($Vc,[2,20]),o($Vc,[2,21]),o($Vc,[2,22]),{17:29,18:[1,31],19:[1,32],20:[1,33],29:[1,30]},o($Vc,[2,27]),o($Vc,[2,28]),o($Vc,[2,29]),o($Vc,[2,30]),o($Vd,[2,25]),{1:[2,1]},o($Vc,[2,24]),o($Vc,[2,11]),o($Vd,[2,26]),o($Vc,[2,12]),o($Vc,[2,13]),o($Vc,[2,14])], +defaultActions: {27:[2,1]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + var error = new Error(str); + error.hash = hash; + throw error; + } +}, +parse: function parse(input) { + var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + var args = lstack.slice.call(arguments, 1); + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + for (var k in this.yy) { + if (Object.prototype.hasOwnProperty.call(this.yy, k)) { + sharedState.yy[k] = this.yy[k]; + } + } + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { + lexer.yylloc = {}; + } + var yyloc = lexer.yylloc; + lstack.push(yyloc); + var ranges = lexer.options && lexer.options.ranges; + if (typeof sharedState.yy.parseError === 'function') { + this.parseError = sharedState.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + _token_stack: + var lex = function () { + var token; + token = lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + }; + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push('\'' + this.terminals_[p] + '\''); + } + } + if (lexer.showPosition) { + errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; + } else { + errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected + }); + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(lexer.yytext); + lstack.push(lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }; + if (ranges) { + yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]; + } + r = this.performAction.apply(yyval, [ + yytext, + yyleng, + yylineno, + sharedState.yy, + action[1], + vstack, + lstack + ].concat(args)); + if (typeof r !== 'undefined') { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +}}; + + var ast = require('../ast'); +/* generated by jison-lex 0.3.4 */ +var lexer = (function(){ +var lexer = ({ + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input, yy) { + this.yy = yy || this.yy || {}; + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:/* skip whitespace */ +break; +case 1:return "-" +break; +case 2:return "~" +break; +case 3:return "+" +break; +case 4:return "^" +break; +case 5:return "!" +break; +case 6:return "|" +break; +case 7:return "#" +break; +case 8:return "ms" +break; +case 9:return "s" +break; +case 10:return "m" +break; +case 11:return "NUMBER" +break; +case 12:return "IDENTIFIER" +break; +case 13:return "EOF" +break; +case 14:return "INVALID" +break; +} +}, +rules: [/^(?:\s+)/,/^(?:-)/,/^(?:~)/,/^(?:\+)/,/^(?:\^)/,/^(?:!)/,/^(?:\|)/,/^(?:#)/,/^(?:ms\b)/,/^(?:s\b)/,/^(?:m\b)/,/^(?:[0-9]+(\.[0-9]+)?)/,/^(?:[a-zA-Z]([0-9a-zA-Z]+)?)/,/^(?:$)/,/^(?:.)/], +conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14],"inclusive":true}} +}); +return lexer; +})(); +parser.lexer = lexer; +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); + + +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = Parser; +exports.Parser = Parser.Parser; +exports.parse = function () { return Parser.parse.apply(Parser, arguments); }; +exports.main = function commonjsMain(args) { + if (!args[1]) { + console.log('Usage: '+args[0]+' FILE'); + process.exit(1); + } + var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); + return exports.parser.parse(source); +}; +if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); +} +} \ No newline at end of file diff --git a/src/internal/testing/parser/grammar.jison b/src/internal/testing/parser/grammar.jison new file mode 100644 index 0000000000..5148a17164 --- /dev/null +++ b/src/internal/testing/parser/grammar.jison @@ -0,0 +1,103 @@ +%{ + var ast = require('../ast'); +%} + +%lex +%% + +\s+ /* skip whitespace */ +"-" return "-" +"~" return "~" +"+" return "+" +"^" return "^" +"!" return "!" +"|" return "|" +"#" return "#" +"ms" return "ms" +"s" return "s" +"m" return "m" +[0-9]+(\.[0-9]+)? return "NUMBER" +[a-zA-Z]([0-9a-zA-Z]+)? return "IDENTIFIER" +<> return "EOF" +. return "INVALID" + +/lex + +%ebnf +%start Diagram + +%% + +Diagram + : Event+ EOF + { + $$ = new ast.MarbleDiagram($1); + return $$; + } + ; + +Event + : Next + | Error + | Complete + | MacroTask + | MicroTask + | Queue + | Subscribe + | Unsubscribe + | TimeProgression + ; + +TimeProgression + : "NUMBER"+ TimeUnit + -> new ast.TimeProgression(parseFloat($1), $2) + ; + +TimeUnit + : "ms" + -> ast.DurationUnit.MILLISECONDS + | "s" + -> ast.DurationUnit.SECONDS + | "m" + -> ast.DurationUnit.MINUTES + ; + +Next + : ("IDENTIFIER" | "ms"| "s" | "m") + -> new ast.NextPlaceholder($1) + ; + +Error + : "#" + -> new ast.ErrorPlaceholder() + ; + +Complete + : "|" + -> new ast.Complete() + ; + +MacroTask + : "-" + -> new ast.MacroTask() + ; + +MicroTask + : "~" + -> new ast.MicroTask() + ; + +Queue + : "+" + -> new ast.Queue() + ; + +Subscribe + : "^" + -> new ast.Subscribe() + ; + +Unsubscribe + : "!" + -> new ast.Unsubscribe() + ; diff --git a/src/internal/testing/parser/package.json b/src/internal/testing/parser/package.json new file mode 100644 index 0000000000..2627a80244 --- /dev/null +++ b/src/internal/testing/parser/package.json @@ -0,0 +1,6 @@ +{ + "name": "rxjs/internal/testing/parser", + "typings": "./Parser.d.ts", + "main": "./Parser.js", + "sideEffects": false +} diff --git a/src/testing/index.ts b/src/testing/index.ts index 4c23a723bb..3a34d9acc0 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -1 +1,2 @@ export { TestScheduler } from '../internal/testing/TestScheduler'; +export { run, marbles } from '../internal/testing/NewTestScheduler'; diff --git a/tools/make-umd-bundle.js b/tools/make-umd-bundle.js index e76faf6c23..42ec794ba6 100644 --- a/tools/make-umd-bundle.js +++ b/tools/make-umd-bundle.js @@ -3,6 +3,7 @@ var _ = require('lodash'); var rollup = require('rollup'); var rollupInject = require('rollup-plugin-inject'); var rollupNodeResolve = require('rollup-plugin-node-resolve'); +var rollupCommonJs = require('rollup-plugin-commonjs'); var fs = require('fs'); var path = require('path'); @@ -21,6 +22,9 @@ rollup.rollup({ return ['tslib', key]; }), }), + // This is needed because the jison Parser.js file is a generated CJS file + // and otherwise would not get included correctly in the UMD build + rollupCommonJs() ], }).then(function (bundle) { var result = bundle.generate({