Repositorio del curso CCOM4030 el semestre B91 del proyecto Artesanías con el Instituto de Cultura

TestScheduler.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import { Observable } from '../Observable';
  2. import { Notification } from '../Notification';
  3. import { ColdObservable } from './ColdObservable';
  4. import { HotObservable } from './HotObservable';
  5. import { TestMessage } from './TestMessage';
  6. import { SubscriptionLog } from './SubscriptionLog';
  7. import { Subscription } from '../Subscription';
  8. import { VirtualTimeScheduler, VirtualAction } from '../scheduler/VirtualTimeScheduler';
  9. import { AsyncScheduler } from '../scheduler/AsyncScheduler';
  10. const defaultMaxFrame: number = 750;
  11. export interface RunHelpers {
  12. cold: typeof TestScheduler.prototype.createColdObservable;
  13. hot: typeof TestScheduler.prototype.createHotObservable;
  14. flush: typeof TestScheduler.prototype.flush;
  15. expectObservable: typeof TestScheduler.prototype.expectObservable;
  16. expectSubscriptions: typeof TestScheduler.prototype.expectSubscriptions;
  17. }
  18. interface FlushableTest {
  19. ready: boolean;
  20. actual?: any[];
  21. expected?: any[];
  22. }
  23. export type observableToBeFn = (marbles: string, values?: any, errorValue?: any) => void;
  24. export type subscriptionLogsToBeFn = (marbles: string | string[]) => void;
  25. export class TestScheduler extends VirtualTimeScheduler {
  26. public readonly hotObservables: HotObservable<any>[] = [];
  27. public readonly coldObservables: ColdObservable<any>[] = [];
  28. private flushTests: FlushableTest[] = [];
  29. private runMode = false;
  30. constructor(public assertDeepEqual: (actual: any, expected: any) => boolean | void) {
  31. super(VirtualAction, defaultMaxFrame);
  32. }
  33. createTime(marbles: string): number {
  34. const indexOf: number = marbles.indexOf('|');
  35. if (indexOf === -1) {
  36. throw new Error('marble diagram for time should have a completion marker "|"');
  37. }
  38. return indexOf * TestScheduler.frameTimeFactor;
  39. }
  40. /**
  41. * @param marbles A diagram in the marble DSL. Letters map to keys in `values` if provided.
  42. * @param values Values to use for the letters in `marbles`. If ommitted, the letters themselves are used.
  43. * @param error The error to use for the `#` marble (if present).
  44. */
  45. createColdObservable<T = string>(marbles: string, values?: { [marble: string]: T }, error?: any): ColdObservable<T> {
  46. if (marbles.indexOf('^') !== -1) {
  47. throw new Error('cold observable cannot have subscription offset "^"');
  48. }
  49. if (marbles.indexOf('!') !== -1) {
  50. throw new Error('cold observable cannot have unsubscription marker "!"');
  51. }
  52. const messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode);
  53. const cold = new ColdObservable<T>(messages, this);
  54. this.coldObservables.push(cold);
  55. return cold;
  56. }
  57. /**
  58. * @param marbles A diagram in the marble DSL. Letters map to keys in `values` if provided.
  59. * @param values Values to use for the letters in `marbles`. If ommitted, the letters themselves are used.
  60. * @param error The error to use for the `#` marble (if present).
  61. */
  62. createHotObservable<T = string>(marbles: string, values?: { [marble: string]: T }, error?: any): HotObservable<T> {
  63. if (marbles.indexOf('!') !== -1) {
  64. throw new Error('hot observable cannot have unsubscription marker "!"');
  65. }
  66. const messages = TestScheduler.parseMarbles(marbles, values, error, undefined, this.runMode);
  67. const subject = new HotObservable<T>(messages, this);
  68. this.hotObservables.push(subject);
  69. return subject;
  70. }
  71. private materializeInnerObservable(observable: Observable<any>,
  72. outerFrame: number): TestMessage[] {
  73. const messages: TestMessage[] = [];
  74. observable.subscribe((value) => {
  75. messages.push({ frame: this.frame - outerFrame, notification: Notification.createNext(value) });
  76. }, (err) => {
  77. messages.push({ frame: this.frame - outerFrame, notification: Notification.createError(err) });
  78. }, () => {
  79. messages.push({ frame: this.frame - outerFrame, notification: Notification.createComplete() });
  80. });
  81. return messages;
  82. }
  83. expectObservable(observable: Observable<any>,
  84. subscriptionMarbles: string = null): ({ toBe: observableToBeFn }) {
  85. const actual: TestMessage[] = [];
  86. const flushTest: FlushableTest = { actual, ready: false };
  87. const subscriptionParsed = TestScheduler.parseMarblesAsSubscriptions(subscriptionMarbles, this.runMode);
  88. const subscriptionFrame = subscriptionParsed.subscribedFrame === Number.POSITIVE_INFINITY ?
  89. 0 : subscriptionParsed.subscribedFrame;
  90. const unsubscriptionFrame = subscriptionParsed.unsubscribedFrame;
  91. let subscription: Subscription;
  92. this.schedule(() => {
  93. subscription = observable.subscribe(x => {
  94. let value = x;
  95. // Support Observable-of-Observables
  96. if (x instanceof Observable) {
  97. value = this.materializeInnerObservable(value, this.frame);
  98. }
  99. actual.push({ frame: this.frame, notification: Notification.createNext(value) });
  100. }, (err) => {
  101. actual.push({ frame: this.frame, notification: Notification.createError(err) });
  102. }, () => {
  103. actual.push({ frame: this.frame, notification: Notification.createComplete() });
  104. });
  105. }, subscriptionFrame);
  106. if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) {
  107. this.schedule(() => subscription.unsubscribe(), unsubscriptionFrame);
  108. }
  109. this.flushTests.push(flushTest);
  110. const { runMode } = this;
  111. return {
  112. toBe(marbles: string, values?: any, errorValue?: any) {
  113. flushTest.ready = true;
  114. flushTest.expected = TestScheduler.parseMarbles(marbles, values, errorValue, true, runMode);
  115. }
  116. };
  117. }
  118. expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]): ({ toBe: subscriptionLogsToBeFn }) {
  119. const flushTest: FlushableTest = { actual: actualSubscriptionLogs, ready: false };
  120. this.flushTests.push(flushTest);
  121. const { runMode } = this;
  122. return {
  123. toBe(marbles: string | string[]) {
  124. const marblesArray: string[] = (typeof marbles === 'string') ? [marbles] : marbles;
  125. flushTest.ready = true;
  126. flushTest.expected = marblesArray.map(marbles =>
  127. TestScheduler.parseMarblesAsSubscriptions(marbles, runMode)
  128. );
  129. }
  130. };
  131. }
  132. flush() {
  133. const hotObservables = this.hotObservables;
  134. while (hotObservables.length > 0) {
  135. hotObservables.shift().setup();
  136. }
  137. super.flush();
  138. this.flushTests = this.flushTests.filter(test => {
  139. if (test.ready) {
  140. this.assertDeepEqual(test.actual, test.expected);
  141. return false;
  142. }
  143. return true;
  144. });
  145. }
  146. /** @nocollapse */
  147. static parseMarblesAsSubscriptions(marbles: string, runMode = false): SubscriptionLog {
  148. if (typeof marbles !== 'string') {
  149. return new SubscriptionLog(Number.POSITIVE_INFINITY);
  150. }
  151. const len = marbles.length;
  152. let groupStart = -1;
  153. let subscriptionFrame = Number.POSITIVE_INFINITY;
  154. let unsubscriptionFrame = Number.POSITIVE_INFINITY;
  155. let frame = 0;
  156. for (let i = 0; i < len; i++) {
  157. let nextFrame = frame;
  158. const advanceFrameBy = (count: number) => {
  159. nextFrame += count * this.frameTimeFactor;
  160. };
  161. const c = marbles[i];
  162. switch (c) {
  163. case ' ':
  164. // Whitespace no longer advances time
  165. if (!runMode) {
  166. advanceFrameBy(1);
  167. }
  168. break;
  169. case '-':
  170. advanceFrameBy(1);
  171. break;
  172. case '(':
  173. groupStart = frame;
  174. advanceFrameBy(1);
  175. break;
  176. case ')':
  177. groupStart = -1;
  178. advanceFrameBy(1);
  179. break;
  180. case '^':
  181. if (subscriptionFrame !== Number.POSITIVE_INFINITY) {
  182. throw new Error('found a second subscription point \'^\' in a ' +
  183. 'subscription marble diagram. There can only be one.');
  184. }
  185. subscriptionFrame = groupStart > -1 ? groupStart : frame;
  186. advanceFrameBy(1);
  187. break;
  188. case '!':
  189. if (unsubscriptionFrame !== Number.POSITIVE_INFINITY) {
  190. throw new Error('found a second subscription point \'^\' in a ' +
  191. 'subscription marble diagram. There can only be one.');
  192. }
  193. unsubscriptionFrame = groupStart > -1 ? groupStart : frame;
  194. break;
  195. default:
  196. // time progression syntax
  197. if (runMode && c.match(/^[0-9]$/)) {
  198. // Time progression must be preceeded by at least one space
  199. // if it's not at the beginning of the diagram
  200. if (i === 0 || marbles[i - 1] === ' ') {
  201. const buffer = marbles.slice(i);
  202. const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
  203. if (match) {
  204. i += match[0].length - 1;
  205. const duration = parseFloat(match[1]);
  206. const unit = match[2];
  207. let durationInMs: number;
  208. switch (unit) {
  209. case 'ms':
  210. durationInMs = duration;
  211. break;
  212. case 's':
  213. durationInMs = duration * 1000;
  214. break;
  215. case 'm':
  216. durationInMs = duration * 1000 * 60;
  217. break;
  218. default:
  219. break;
  220. }
  221. advanceFrameBy(durationInMs / this.frameTimeFactor);
  222. break;
  223. }
  224. }
  225. }
  226. throw new Error('there can only be \'^\' and \'!\' markers in a ' +
  227. 'subscription marble diagram. Found instead \'' + c + '\'.');
  228. }
  229. frame = nextFrame;
  230. }
  231. if (unsubscriptionFrame < 0) {
  232. return new SubscriptionLog(subscriptionFrame);
  233. } else {
  234. return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame);
  235. }
  236. }
  237. /** @nocollapse */
  238. static parseMarbles(marbles: string,
  239. values?: any,
  240. errorValue?: any,
  241. materializeInnerObservables: boolean = false,
  242. runMode = false): TestMessage[] {
  243. if (marbles.indexOf('!') !== -1) {
  244. throw new Error('conventional marble diagrams cannot have the ' +
  245. 'unsubscription marker "!"');
  246. }
  247. const len = marbles.length;
  248. const testMessages: TestMessage[] = [];
  249. const subIndex = runMode ? marbles.replace(/^[ ]+/, '').indexOf('^') : marbles.indexOf('^');
  250. let frame = subIndex === -1 ? 0 : (subIndex * -this.frameTimeFactor);
  251. const getValue = typeof values !== 'object' ?
  252. (x: any) => x :
  253. (x: any) => {
  254. // Support Observable-of-Observables
  255. if (materializeInnerObservables && values[x] instanceof ColdObservable) {
  256. return values[x].messages;
  257. }
  258. return values[x];
  259. };
  260. let groupStart = -1;
  261. for (let i = 0; i < len; i++) {
  262. let nextFrame = frame;
  263. const advanceFrameBy = (count: number) => {
  264. nextFrame += count * this.frameTimeFactor;
  265. };
  266. let notification: Notification<any>;
  267. const c = marbles[i];
  268. switch (c) {
  269. case ' ':
  270. // Whitespace no longer advances time
  271. if (!runMode) {
  272. advanceFrameBy(1);
  273. }
  274. break;
  275. case '-':
  276. advanceFrameBy(1);
  277. break;
  278. case '(':
  279. groupStart = frame;
  280. advanceFrameBy(1);
  281. break;
  282. case ')':
  283. groupStart = -1;
  284. advanceFrameBy(1);
  285. break;
  286. case '|':
  287. notification = Notification.createComplete();
  288. advanceFrameBy(1);
  289. break;
  290. case '^':
  291. advanceFrameBy(1);
  292. break;
  293. case '#':
  294. notification = Notification.createError(errorValue || 'error');
  295. advanceFrameBy(1);
  296. break;
  297. default:
  298. // Might be time progression syntax, or a value literal
  299. if (runMode && c.match(/^[0-9]$/)) {
  300. // Time progression must be preceeded by at least one space
  301. // if it's not at the beginning of the diagram
  302. if (i === 0 || marbles[i - 1] === ' ') {
  303. const buffer = marbles.slice(i);
  304. const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /);
  305. if (match) {
  306. i += match[0].length - 1;
  307. const duration = parseFloat(match[1]);
  308. const unit = match[2];
  309. let durationInMs: number;
  310. switch (unit) {
  311. case 'ms':
  312. durationInMs = duration;
  313. break;
  314. case 's':
  315. durationInMs = duration * 1000;
  316. break;
  317. case 'm':
  318. durationInMs = duration * 1000 * 60;
  319. break;
  320. default:
  321. break;
  322. }
  323. advanceFrameBy(durationInMs / this.frameTimeFactor);
  324. break;
  325. }
  326. }
  327. }
  328. notification = Notification.createNext(getValue(c));
  329. advanceFrameBy(1);
  330. break;
  331. }
  332. if (notification) {
  333. testMessages.push({ frame: groupStart > -1 ? groupStart : frame, notification });
  334. }
  335. frame = nextFrame;
  336. }
  337. return testMessages;
  338. }
  339. run<T>(callback: (helpers: RunHelpers) => T): T {
  340. const prevFrameTimeFactor = TestScheduler.frameTimeFactor;
  341. const prevMaxFrames = this.maxFrames;
  342. TestScheduler.frameTimeFactor = 1;
  343. this.maxFrames = Number.POSITIVE_INFINITY;
  344. this.runMode = true;
  345. AsyncScheduler.delegate = this;
  346. const helpers = {
  347. cold: this.createColdObservable.bind(this),
  348. hot: this.createHotObservable.bind(this),
  349. flush: this.flush.bind(this),
  350. expectObservable: this.expectObservable.bind(this),
  351. expectSubscriptions: this.expectSubscriptions.bind(this),
  352. };
  353. try {
  354. const ret = callback(helpers);
  355. this.flush();
  356. return ret;
  357. } finally {
  358. TestScheduler.frameTimeFactor = prevFrameTimeFactor;
  359. this.maxFrames = prevMaxFrames;
  360. this.runMode = false;
  361. AsyncScheduler.delegate = undefined;
  362. }
  363. }
  364. }